算法工程师进化-算法编程

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 }
View Code

  总结:

  • 数组查找类型的题目,一方面要结合数组的特点,另一方面可以考虑二分查找的方法,这也是经常考察的点。

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     }
View Code

   总结:

  • 对于字符串类型的题目,需要注意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     }
View Code

  递归思路:

  递归思路需要把握递归出口,递归之前的操作,递归之后的操作以及递归的限制条件,当然有时不一定都有;

 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     }
View Code

  总结:

  • 对于递归思路,需要理清打印操作是在递归之后还是在递归之前,很明显,对于当前函数,递归是以下一节点开始的子问题,而打印操作是打印当前节点,因此打印操作在递归之后;
  • 该题可扩展到链表逆置,同样也有递归和非递归两种解法;

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     }
View Code

  总结:

  • 该题的关键在于发掘子问题,明确递归子问题的参数,然后递归出口,递归之前的操作(构建根节点),递归之后的操作(返回根节点),递归的限制条件;

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     }
View Code
  • 方法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     }
View Code

  总结:

  • 两个栈实现队列的两种方法:一种是以一个栈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();
    }
View Code

  总结:需要注意一点,就是在返回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);
        }
    }
View Code

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);
        }
    }
View Code

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);
    }
View Code

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;
    }
View Code
  • 方法二:递归
  • 递归的思路:明确递归的子问题,只需要改变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;
    }
View Code

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;
    }
View Code
  • 方法二:快速排序
    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;
    }
View Code

 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));
        }
    }
View Code

 

posted @ 2018-06-11 21:45  新月尘封  阅读(399)  评论(0)    收藏  举报