算法工程师进化-算法编程
1 引言
对于每一个毕业生而言,只要面试互联网公司,手撕代码算是一个必备节目了。为了能够应对这种面试题,我们需要平时多刷题多积累。一般情况下,如果能够把剑指Offer的算法题搞懂,并且能够举一反三,就足以应对大多数的面试。
下面我们针对剑指Offer的题目逐一进行总结学习。
2 剑指Offer
2.1 二维数组中的查找
题目描述:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路:
- 这属于数组类型的题目,考察的是查找;
- 该题的关键在于把握数组的特点,基于数组排序的特点,当我们选择不同的起始点开始查找时,对下一步的查找会有不同的影响;
- 我们以右上角作为起始点,这样当遍历值大于或者小于target时,遍历的方向只有一种;
代码:
1 public boolean Find(int target, int [][] array) { 2 if(array.length<0) { 3 return false; 4 } 5 int i=0,j=array[0].length-1; 6 while(i<array.length&&j>=0) { 7 if(array[i][j]>target) { 8 j--; 9 } 10 else { 11 if(array[i][j]<target) { 12 i++; 13 } 14 else { 15 return true; 16 } 17 } 18 } 19 return false; 20 }
总结:
- 数组查找类型的题目,一方面要结合数组的特点,另一方面可以考虑二分查找的方法,这也是经常考察的点。
2.2 替换空格
题目描述:请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy,则经过替换之后的字符串围为We%20Are%20Happy。
思路:
- 该题属于字符串类型的题目,考察的是字符串替换;
- 既然是字符串替换,那么就需要有查找,然后才有替换,但是问题的难点在于字符串长度的扩展和字符的顺移。
- 关键点:一次遍历扩展字符串长度,另一次遍历替换字符(从后往前)
代码:
1 public String replaceSpace(StringBuffer str) { 2 int len=str.length(); 3 for(int i=0;i<len;i++) { 4 if(str.charAt(i)==' ') { 5 str=str.append(" "); 6 } 7 } 8 int j=str.length()-1; 9 for(int i=len-1;i>=0;i--) { 10 if(str.charAt(i)!=' ') { 11 str.setCharAt(j--, str.charAt(i)); 12 } 13 else { 14 str.setCharAt(j--, '0'); 15 str.setCharAt(j--, '2'); 16 str.setCharAt(j--, '%'); 17 } 18 } 19 return str.toString(); 20 }
总结:
- 对于字符串类型的题目,需要注意StringBuffer和String,String不可变,StringBuffer可扩展;
- 另外修改字符串中字符的方法:setCharAt(int,char);
- 对于字符串的替换问题,需要有逆向思维(逆向遍历);
3.3 从尾到头打印链表
题目描述:输入一个链表,从尾到头打印链表每个节点的值;
思路:
- 这是一道链表类型的题目,考察的是遍历;
- 链表的特性只能从头到尾进行遍历,但是题目要求从尾到头打印链表,那么我们可以考虑递归和非递归的思路进行解决;
- 非递归的思路,需要利用空间(栈),递归的思路往往在面试的过程中会是一个加分项。因为递归本身就比较抽象,可以说是装X利器了。
代码:
非递归思路:
1 public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { 2 ArrayList<Integer> res=new ArrayList<Integer>(); 3 Stack<ListNode> s=new Stack<ListNode>(); 4 while(listNode!=null) { 5 s.push(listNode); 6 listNode=listNode.next; 7 } 8 while(!s.isEmpty()) { 9 res.add(s.pop().val); 10 } 11 return res; 12 }
递归思路:
递归思路需要把握递归出口,递归之前的操作,递归之后的操作以及递归的限制条件,当然有时不一定都有;
1 public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { 2 ArrayList<Integer> res=new ArrayList<Integer>(); 3 print(listNode,res); 4 return res; 5 } 6 public void print(ListNode listNode,ArrayList<Integer> res) { 7 if(listNode==null) { 8 return ; 9 } 10 print(listNode.next,res); 11 res.add(listNode.val); 12 }
总结:
- 对于递归思路,需要理清打印操作是在递归之后还是在递归之前,很明显,对于当前函数,递归是以下一节点开始的子问题,而打印操作是打印当前节点,因此打印操作在递归之后;
- 该题可扩展到链表逆置,同样也有递归和非递归两种解法;
2.4 重建二叉树
题目描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。
思路:
- 这是一道二叉树类型的题目,基本会考虑递归的解法;
- 举例说明二叉树的重建,子问题为:以前序遍历的第一个节点为子树的根,然后从中序遍历中寻找该节点,这样中序遍历下该节点的左边所有节点为左子树,右边所有节点为右子树;
- 递归函数的参数:除了前序遍历和中序遍历的数组,还需要序列的preStart,preEnd,inStart和inEnd;
代码:
1 public TreeNode reConstructBinaryTree(int [] pre,int [] in) { 2 if(pre.length==0) { 3 return null; 4 } 5 return helper(pre,in,0,pre.length-1,0,in.length-1); 6 } 7 public TreeNode helper(int[] pre,int[] in,int preStart,int preEnd,int inStart,int inEnd) { 8 if(preStart>preEnd) { 9 return null; 10 } 11 TreeNode node=new TreeNode(pre[preStart]); 12 for(int i=inStart;i<=inEnd;i++) { 13 if(in[i]==pre[preStart]) { 14 int len=i-inStart; 15 node.left=helper(pre,in,preStart+1,preStart+len,inStart,i-1); 16 node.right=helper(pre,in,preStart+len+1,preEnd,i+1,inEnd); 17 break; 18 } 19 } 20 return node; 21 }
总结:
- 该题的关键在于发掘子问题,明确递归子问题的参数,然后递归出口,递归之前的操作(构建根节点),递归之后的操作(返回根节点),递归的限制条件;
2.5 用两个栈实现队列
题目描述:用两个栈来实现一个队列,完成队列的Push和Pop操作,队列中的元素为int类型。
思路:
- 该题属于数据结构的理解,包括栈和队列;
- 栈的特点为先进后出,队列的特点为先进先出;那么如何利用两个栈达到先进先出的效果?
- 方法1:push操作始终push到栈1中,pop操作首先判断栈2有没有元素,如果没有,就将栈1的元素全部依次push到栈2中,然后再pop,这样就实现了先进先出的特点;如果栈2有元素,直接pop即可;注意,只要栈2中有元素,就不要再将栈1中的元素依次push到栈2中,否则违反先进先出的原则。
代码:
1 Stack<Integer> stack1 = new Stack<Integer>(); 2 Stack<Integer> stack2 = new Stack<Integer>(); 3 4 public void push(int node) { 5 stack1.push(node); 6 } 7 8 public int pop() { 9 if(!stack2.isEmpty()) { 10 return stack2.pop(); 11 } 12 else { 13 while(!stack1.isEmpty()) { 14 stack2.push(stack1.pop()); 15 } 16 return stack2.pop(); 17 } 18 }
- 方法2:该方法专门用一个栈来push和pop,另一个栈专门用于过渡;我们以子问题的思路来进行思考,假设栈1的元素已经处理为先进先出,即假设我们按顺序1,2,3push元素到栈1中,那么栈1应该显示为1,2,3(从栈顶到栈底的顺序),这样就已经实现了先进先出;现在如果要push一个元素4到栈1中(4,1,2,3),那么必须把栈1调整为1,2,3,4;那么调整就可以利用到栈2,在push之前,先将栈1中元素全部依次push到栈2,然后push元素到栈1底部,最后再把栈2中的元素又依次push到栈1中;
代码:
1 Stack<Integer> stack1 = new Stack<Integer>(); 2 Stack<Integer> stack2 = new Stack<Integer>(); 3 4 public void push(int node) { 5 while(!stack1.isEmpty()) { 6 stack2.push(stack1.pop()); 7 } 8 stack1.push(node); 9 while(!stack2.isEmpty()) { 10 stack1.push(stack2.pop()); 11 } 12 } 13 14 public int pop() { 15 return stack1.pop(); 16 }
总结:
- 两个栈实现队列的两种方法:一种是以一个栈push,另一个栈pop;另一种,专门用一个栈push和pop,另一个栈专门用于过渡;
- 拓展:如果是两个队列实现栈:方法1好像不适用,按照方法2,以子问题的思路思考,假设按顺序1,2,3push元素到队列1中,那么队列1中应该显示为3,2,1(从队头到队尾),这样就实现了先进后出;现在如果push一个元素4到队列1中(3,2,1,4),如何调整为4,3,2,1;这样就考虑使用队列2,首先在push之前,先将队列1中元素依次push到队列2中,然后push新元素到队列1,最后再将队列2中元素依次push到队列1中。
2.6 Valid Parentheses
题目描述:给定一个字符串,字符串中包括小括号,中括号和大括号,判断该字符串的括号是否合法,类似于"([)]"就是不合法;
思路:
- 涉及到匹配,可以考虑使用数据结构-栈;
- 两种思路,第一种思路:直接push左括号,这种方法在pop的时候判断比较麻烦,需要判断三种情况;第二种思路:不直接push左括号,而是push左括号对应的右括号,这样push的时候需要判断三种情况,但是pop的时候就比较简单,只需要判断一次;
代码:
public boolean isValid(String s) { Stack<Character> stack=new Stack<Character>(); for (int i = 0; i < s.length(); i++){ if (s.charAt(i) == '('){ stack.push(')'); } else if (s.charAt(i) == '['){ stack.push(']'); } else if (s.charAt(i) == '{'){ stack.push('}'); } else if (!stack.isEmpty() && s.charAt(i) == stack.peek()){ stack.pop(); } else { return false; } } return stack.isEmpty(); }
总结:需要注意一点,就是在返回stack的栈顶元素时,需要判断该栈是否为空!
2.7 Generate Parentheses
题目描述:给定一个整数n,返回所有合法的括号序列
思路:
- 采用回溯的方法;
代码:
public List<String> generateParenthesis(int n) { List<String> res = new ArrayList<String>(); helper(0, 0, n, "", res); return res; } public void helper(int left, int right, int n, String s, List<String> res){ if (s.length() == 2*n){ res.add(s); return ; } if (left<n){ helper(left+1, right, n, s+"(", res); } if (left>right){ helper(left, right+1, n, s+")", res); } }
2.8 Symmetric Tree
题目描述:判断一棵树是否是对称树,需要明确对称树的概念,不是翻转的概念
思路:
- 左子树的左节点与右子树的右节点比较,左子树的右节点与左子树的左节点比较;
代码:
public boolean isSymmetric(TreeNode root) { if (root == null){ return true; } return helper(root.left, root.right); } public boolean helper(TreeNode left, TreeNode right){ if (left == null && right == null){ return true; } else { if (left ==null || right ==null){ return false; } if (left.val != right.val){ return false; } return helper(left.left, right.right) && helper(left.right, right.left); } }
2.9 二叉树的镜像
题目描述:给定一颗二叉树,将其变换为源二叉树的镜像
思路:
- 从根节点开始递归向下镜像;
代码:
public void Mirror(TreeNode root) { if (root == null){ return ; } TreeNode temp = root.left; root.left = root.right; root.right = temp; Mirror(root.left); Mirror(root.right); }
2.10 反转链表
题目描述:输入一个链表,反转链表后,输出新链表的表头
思路:
- 方法一:非递归
代码:
public ListNode ReverseList(ListNode head) { ListNode newhead = null; while (head !=null){ ListNode temp = head.next; head.next = newhead; newhead = head; head = temp; } return newhead; }
- 方法二:递归
- 递归的思路:明确递归的子问题,只需要改变node的next即可;
代码:
public ListNode ReverseList(ListNode head) { if (head == null || head.next == null){ return head; } ListNode newhead = ReverseList(head.next); head.next.next = head; head.next = null; return newhead; }
2.11 最小的k个数
题目描述:输入n个整数,找出其中最小的K个数
思路:
- 方法一:采用堆的思路来解决,最小的k个数,需要使用的是最大堆;空间复杂度O(K),时间复杂度O(n*logk),调整堆的时间复杂度为O(logk)
代码:
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) { ArrayList<Integer> res = new ArrayList<Integer>(); if (input.length < k){ return res; } PriorityQueue<Integer> q = new PriorityQueue<Integer>((o1, o2) -> o2-o1); for (int i = 0; i < input.length; i++){ q.offer(input[i]); if (q.size() > k){ q.poll(); } } while (!q.isEmpty()){ res.add(q.poll()); } return res; }
- 方法二:快速排序
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) { ArrayList<Integer> res = new ArrayList<Integer>(); if (input.length < k){ return res; } int lo = 0; int hi = input.length-1; while (lo <= hi){ int index = partition(input, lo, hi); if (index < k - 1){ lo = index + 1; } if (index > k - 1){ hi = index -1; } if (index == k-1){ for (int i = 0; i < k; i++){ res.add(input[i]); } break; } } return res; } public int partition(int[] input, int lo, int hi){ int key = input[lo]; while (lo < hi){ while (lo < hi && input[hi] >= key){ hi--; } input[lo] = input[hi]; while(lo < hi && input[lo] <= key){ lo++; } input[hi] = input[lo]; } input[lo] = key; return lo; }
2.12 无序数组求中位数
题目描述:给定一个数组,求该数组的中位数,如果数组长度为奇数,那么中位数在第(n+1)/2个,如果数组长度为偶数,那么中位数在第n/2个;
思路:利用快排的partition函数,二分的查找;
代码:
public static int partition(int[] input, int lo, int hi) { int key = input[lo]; while (lo < hi) { while (lo < hi && input[hi] >= key) { hi--; } input[lo] = input[hi]; while (lo < hi && input[lo] <= key) { lo++; } input[hi] = input[lo]; } input[lo] = key; return lo; } public static int getMid(int[] input, int lo, int hi) { int size = input.length; int mid; if (size % 2 == 0) { mid = size / 2 - 1; } else { mid = size / 2; } int index = partition(input, lo, hi); while (index != mid) { if (index < mid) { index = partition(input, index + 1, hi); } else { index = partition(input, lo, index - 1); } } return input[index]; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNextLine()) { String str = sc.nextLine(); String[] strArr = str.split(" "); int[] input = new int[strArr.length]; for (int i = 0;i < strArr.length; i++) { input[i] = Integer.parseInt(strArr[i]); } System.out.println(getMid(input, 0, input.length-1)); } }

浙公网安备 33010602011771号