剑指offer部分题解

注:本文记录leetcode和牛客网的部分题解


链表

复杂链表的复制

题目:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向任意一个节点),返回结果为复制后复杂链表的head。

思路:首先对链表的每一个节点都复制一遍,并将原点和复制点放进HashMap中。然后对照着原点的random指针,将复制点的random指向对于的复制点。

    public RandomListNode Clone(RandomListNode pHead)
    {
        if(pHead == null) {
            return null;
        }
        HashMap<RandomListNode, RandomListNode> map = new HashMap<>();
        RandomListNode newHead = new RandomListNode(pHead.label);
        RandomListNode tmp = newHead;
        map.put(pHead, newHead);
        while(pHead.next != null) {
            pHead = pHead.next;
            tmp.next = new RandomListNode(pHead.label);
            tmp = tmp.next;
            map.put(pHead, tmp);
        }
        tmp.next = null;
        for(RandomListNode oldNode : map.keySet()) {
            map.get(oldNode).random = map.get(oldNode.random);
        }
        return newHead;
    }
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

思路2:可以不用哈希表的额外空间来保存复制的节点,我们可以通过复制每一个节点,将其对于的复制节点放到原节点后边。例如:1->2->3->null 变成 1->1->2->2->3->null。然后将复制节点对应的random赋值上后,使复制节点从链表从分离开。

    public RandomListNode Clone(RandomListNode pHead)
    {
        if(pHead == null) {
            return null;
        }
       RandomListNode copyNode;
       RandomListNode curNode = pHead;
        while(curNode != null) {
            copyNode = new RandomListNode(curNode.label);
            copyNode.next = curNode.next;
            curNode.next = copyNode;
            curNode = curNode.next.next;
        }
        curNode = pHead;
        while(curNode != null) {
            if(curNode.random != null)
                curNode.next.random = curNode.random.next;
            curNode = curNode.next.next;
        }
        RandomListNode newHead = pHead.next;
        curNode = pHead;         copyNode = newHead;
        while(curNode != null) {
            curNode.next = curNode.next.next;
            curNode = curNode.next;
            if(copyNode.next != null) {        // 判断复制节点后是否为空
                copyNode.next = copyNode.next.next;
                copyNode = copyNode.next;
            }
        }
        return newHead;
    }
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

 

 


 

双指针

链表中环的入口结点

题目:

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

 思路:设置快慢指针从链表头出发,快指针每次走两步,慢指针一次走一步,假如有环,一定相遇于环中某点。

假设链表头到环入口长度为为a,环入口到相遇点长度为b,相遇点到环入口长度为c

则快指针路程为a+(b+c)k+b ,k>=1  其中b+c为环的长度,k为绕环的圈数,且k ≥ 1;慢指针路程为 a+ b。

快指针走的路程是慢指针的两倍,所以:(a+b)*2=a+(b+c)k+b

化简得 a=(k-1)(b+c)+c ,即 链表头到环入口的距离=相遇点到环入口的距离+(k-1)圈环长度,其中由于k ≥ 1,

则有k - 1 ≥ 0。所以 如果两指针分别从链表头和相遇点出发,最后一定相遇于环入口。

 

    public ListNode EntryNodeOfLoop(ListNode pHead) {
        ListNode slow = pHead;
        ListNode fast = pHead;
        while(fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast) {
                break;
            }
        }
        if(fast == null || fast.next == null)
            return null;
        slow = pHead;
        while(slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }

 

两个链表中第一个公共节点

题目:

输入两个链表,找出它们的第一个公共节点。

如下面的两个链表

在节点 c1 开始相交。

 思路:假设两个链表长度分别为L1+C、L2+C, 其中C为公共部分的长度。设置两个指针a和b 分别从A B两个链表头开始走:

  • 如果两链表有相交部分:若L1 == L2,则当a走了L1,b指针走了L2后,即可相遇于第一个公共节点;而如果L1 != L2,则当a走了L1+C后,回到B起点走L2步,b指针走了L2+C后,回到A起点走L1步。这样两个指针都走了L1+L2+C,肯定会相遇于第一个公共节点。
  • 若两链表没有相交部分:当a走了L1+C后,回到B起点走L2步后变为空;b指针走了L2+C后,回到A起点走L1步后变为空。
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode lA = headA;
        ListNode lB = headB;
        while(lA != lB) {
            lA = lA == null ? headB : lA.next;
            lB = lB == null ? headA : lB.next;
        }
            return lA;
    }

 

链表中倒数第k个节点

题目:

输入一个链表,输出该链表中倒数第k个节点。若k为1,表示链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。

思路:设置快慢指针,首先快指针先走k步,然后快慢指针同时走直至快指针为空,此时慢指针所在点为倒数k节点。

    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode slow = head, fast = head;
        while(k > 0 && fast != null) {
            fast = fast.next;
            k--;
        }

        while(fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

 

数字在排序数组中出现的次数

题目:统计一个数字在排序数组中出现的次数。

思路:二分查找法

    public int GetNumberOfK(int [] array , int k) {
      if(array.length == 0) {
          return 0;
      }
      int firstPosition = getFirtstK(array, k, 0,array.length - 1);
      int lastPosition = getLastK(array, k, 0, array.length - 1);
        if(firstPosition != -1 && lastPosition != -1) {
            return lastPosition - firstPosition + 1;
        }
        return 0;
    }
    // 递归的二分查找
    int getFirtstK(int [] A , int k, int start, int end) {
        if(start > end) {
            return -1;
        }
        int mid = (start + end) / 2;
        if(A[mid] < k) {
            return getFirtstK(A, k, mid + 1, end);
        }else if(A[mid] > k) {
            return getFirtstK(A, k, start, mid - 1);
        }else if(mid > 0 && k == A[mid - 1]) {
            return getFirtstK(A, k, start, mid - 1);
        }else {
            return mid;
        }  
        
    }
     // 非递归的二分查找
     int getLastK(int [] A , int k, int start, int end) {
         int mid;
        while(start <= end) {
           mid = (start + end) / 2;
           if(A[mid] < k) {
               start = mid + 1;
           }else if(A[mid] > k) {
                end = mid - 1;
           }else if(mid < A.length - 1 && k == A[mid + 1]) {
                start = mid + 1;
            }else {
                return mid;
            }  
        }
         return -1;
    }

 

和为S的连续正数序列

题目:输入一个正整数 sum,输出所有和为 sum的连续正整数序列(至少含有两个数)。序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

思路:设立两个遍历变量left,right开始为1,将[left,right - 1]看成是一个滑动窗口,窗口里面的值就是所求的正整数序列。判断窗口和curSum与sum大小关系:

若curSum < sum,则需要将窗口的right边界右移,移动前curSum + right;

若curSum > sum,则需要将窗口的left边界右移,移动前curSum - left;

若curSum == sum,则[left,right - 1]为所求序列,放入数组好后,curSum - left,left++;

上面循环的终止条件是left <= sum / 2。因为大于1的正整数sum 总是小于 sum的中值 + 比中值大的数。比如9 = 5 + x,此时x是不可能比5大的。

 

    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
       ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer> >();
       int left = 1, right = 1, curSum = 0;
        while(left <= sum / 2 ) {
            if(curSum < sum) {
                curSum += right;
                ++right;
            }else if(curSum > sum) {
                curSum -= left;
                ++left;
            }else {
                ArrayList<Integer> tmp = new ArrayList<Integer>();
                for(int k = left; k < right; k++) {     //也可以用等差数列的求和公式
                    tmp.add(k);
                }
                res.add(tmp);
                curSum -= left;
                left++;
            }
        }   
        return res;
    }

 

和为S的两个数字

题目:输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S

思路:和上题一样的双指针法,不同的是right指针初始时指向的是数组的末尾。

    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        if(array.length == 0)         return res;
        int left = 0, right = array.length - 1, curSum = 0;
        while(left < right) {
            curSum = array[left] + array[right];
            if(curSum < sum) {
                ++left;
            }else if(curSum > sum) {
                --right;
            }else {
                res.add(array[left]);
                res.add(array[right]);
                return res;
            }
        }
        return res;
    }

 

翻转单词顺序

题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。例如输入字符串"I am a student. ",则输出"student. a am I"。注意:输入字符串可能会在前面或者后面包含多余的空格,但是反转后的字符不能包括多余的空格。

思路:首先将字符串首尾空格去掉(trim()方法)两变量left,right都指向字符串尾部,left变量负责向左探索到空格的下标,[left + 1,right + 1]为一个单词。接着left变量再想向左探索到非空格的位置,将left赋值给right。重复上边操作直至left >= 0。以题目给的例子说明:开始时left,right指向student单词后便的. 。接着left向左位移直至到s左边的空格,然后student.是第一个单词。然后left接着向左位移直至遇到非空格,即单词a,right指向此位置,并重复上边动作。

    public String ReverseSentence(String str) {
        if(str.trim().equals("")) {                   //去除首尾空格
            return str;
        }
        StringBuilder  res = new StringBuilder();
        int left = str.length() - 1, right = left;
        while(left >= 0) {
            while(left >= 0 && str.charAt(left) != ' ') --left;
            // right + 1可能是空格
            res.append(str.substring(left + 1, right + 1) + " ");
            while(left >= 0 && str.charAt(left) == ' ') --left;
            right = left;
        }
        return res.toString().trim();
    }

 


栈和队列

滑动窗口的最大值

题目:

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。

例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口:

{[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

他们的最大值数组为{4,4,6,6,6,5}。

思路:设置一个存放元素下标的队列lList。

  1. 扫描数组中每一个元素,首先判断元素的值是否大于队列里存储的下标对应的值,若是 则将队列中的下标值出队。将元素值对应的下标放入队列。
  2. 判断队列中首元素是否处于正常滑动窗口范围以内,若不是,则出队。
  3. 如果此时已达到滑动窗口大小size,则将队列的头元素放入要返回的数组aList里。
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        int numLength = num.length;
         ArrayList<Integer> aList = new ArrayList<>();
        if(size <= 0 || numLength < size) {
            return aList;
        }
         LinkedList<Integer> lList = new LinkedList<>();
        for(int i = 0; i < numLength; i++) {
            while(!lList.isEmpty() && num[lList.peekLast()] <= num[i]) {       //(1)
                lList.pollLast();
            }
            lList.addLast(i);
            //(2)判断lList队列里的头元素是否处于正常范围内,即i - size + 1 到 i范围内
            if(lList.peekFirst() <= i - size) {             
                lList.pollFirst();
            }
            if(i + 1 >= size) {                // (3)
                aList.add(num[lList.peekFirst()]);
            }
        }
        return aList;
    }

 

按之字形顺序打印二叉树

题目:现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

例如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7
返回其层次遍历结果:

[
  [3],
  [20,9],
  [15,7]
]

思路:通过两个栈来存储每一行的树节点。

    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
       ArrayList<ArrayList<Integer>> list = new ArrayList<ArrayList<Integer>>();
       Stack<TreeNode> stack1 =  new Stack<TreeNode>();    // 存放奇数层
       Stack<TreeNode> stack2 =  new Stack<TreeNode>();     //存放偶数层
       ArrayList<Integer> tmpArray = new ArrayList<Integer>();
       TreeNode tmpNode = new TreeNode(0);    
       if(pRoot == null) {
           return list;
       }
        
       stack1.add(pRoot);
       while(!stack1.isEmpty() ||!stack2.isEmpty()) {
           if(!stack1.isEmpty()) { 
               tmpArray = new ArrayList<Integer>();
               while(!stack1.isEmpty()) {
                   tmpNode = stack1.pop();
                   tmpArray.add(tmpNode.val);
                   if(tmpNode.left != null) {
                       stack2.add(tmpNode.left);
                   }
                   if(tmpNode.right != null) {
                       stack2.add(tmpNode.right);
                   }
               }    
           }else if(!stack2.isEmpty()) {
               tmpArray = new ArrayList<Integer>();
               while(!stack2.isEmpty()) {
                   tmpNode = stack2.pop();
                   tmpArray.add(tmpNode.val);
                   if(tmpNode.right != null) {
                       stack1.add(tmpNode.right);
                   }
                   if(tmpNode.left != null) {
                       stack1.add(tmpNode.left);
                   }
              }  
           }
           list.add(tmpArray);
       }
       return list;
    }

 

 


二叉树

重建二叉树

题目:

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

思路:在先序遍历数组的第一个元素为根节点,再由中序遍历可得:4,7,2为根节点1的左子树,5,3,8,6为其右子树;我们可以通过递归的方式解决:将左子树的前序遍历{2, 4, 7},中序遍历{4, 7, 2}和右子树的前序遍历{3,5,6,8},中序遍历{5,3,8,6}分别构成树节点。重复上边操作直至前序遍历数组为空,即该节点为null,或者直至前序遍历数组长度为1,则该节点无子节点。

 

    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if(pre.length == 0) {
            return null;
        } else if(pre.length == 1) {
            return new TreeNode(pre[0]);
        }
        
        int rootVal = pre[0];
        int rootIndex = 0;
        for(int i = 0; i < in.length; i++) {
            if(rootVal == in[i]) {
                rootIndex = i;
                break;
            }
        }
        
        TreeNode root = new TreeNode(rootVal);
        root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, rootIndex + 1), 
                                        Arrays.copyOfRange(in, 0, rootIndex));
         root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, rootIndex + 1, pre.length), 
                                        Arrays.copyOfRange(in, rootIndex + 1, in.length));  
               return root;
    }

 

二叉树的下一个结点

题目:

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。树中的结点不仅包含左右子结点,同时包含指向父结点的指针next。

思路:中序遍历即 先访问节点的左子树完毕后,再访问其节点,然后访问节点的右子树。因此,对于节点a而言:

  • 如果存在右子树,则访问右子节点b的最左节点,得到的节点为结果;
  • 若不存在右子树,则判断a是否是a的父节点parent的左子节点,若是则结果为parent;若不是,则不断访问节点的父节点,直至该节点是其父节点的左子节点。
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
         if(pNode == null) {
            return null;
        }
        
        if(pNode.right != null) {        //(1)
            TreeLinkNode tmp = pNode.right;
            while(tmp.left != null) {
                tmp = tmp.left;
            }
            return tmp;
        }
        while(pNode.next != null) {        //(2)
            TreeLinkNode parent = pNode.next;
            if(pNode == parent.left) {
                return parent;
            }
            pNode = parent;
        }
        return null;
    }

 

二叉搜索树的第k个结点

题目:给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8)    中,按结点数值大小顺序第三小结点的值为4。

思路:本题实际是二叉搜索树中序遍历第k个结果。用递归的做法:通过变量count来计数这是第几小的节点,判断当前节点是否为空,若不为空,则先判断左子树中是否存在第k小的节点,如果存在,则返回得到的节点;若不存在,则count加1,即判断当前节点是否是第k小,若是 则返回该节点;若不是 ,则判断右子树中是否存在第k小的节点。

public class Solution {
    int count = 0;
    TreeNode KthNode(TreeNode pRoot, int k) {
       if(pRoot != null) {
           TreeNode tmp =  KthNode(pRoot.left, k);
           if(tmp != null)
               return tmp;
           ++count;
           if(count == k) {
               return pRoot;
           }
           tmp =  KthNode(pRoot.right, k);
           if(tmp != null) {
               return tmp;
           }
       }
        return null;
    }
}

 

树的子结构

题目:输入两棵二叉树A,B,判断B是不是A的子结构。

例如:
给定的树 A:

     3
    / \
   4   5
  / \
 1   2

给定的树 B:

   4 
  /
 1

返回 true

思路:判断root2是否是root1的根节点,则可以 以root1为根节点,root1左子节点为根节点,root1右子结点为根节点,分别将root2与这三种去匹配。判断这三种匹配方式是否存在匹配成功。

匹配的标准是:若两节点相等,则判断root1的左节点与右节点是否和root2的左节点与右节点能成功匹配。如果root2的左节点或右节点为空,表示匹配完成,返回true。如果root1的左节点或右节点为空,表示匹配失败,返回false。

  public boolean fun(TreeNode root1,TreeNode root2) {
        if(root2 == null ) {
            return true;
        }else if(root1 == null) {
            return false;
        }
        if(root1.val == root2.val) {
            return fun(root1.left, root2.left) &&  fun(root1.right, root2.right);
        }else {
            return false;
        }
    }
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        if(root2 == null || root1 == null) {
            return false;
        }
        return fun(root1.left, root2) || fun(root1.right, root2) || fun(root1, root2);
    }

 

二叉搜索树的后序遍历序列

题目:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果

  5
    / \
   2   6
  / \
 1   3
示例 1:

输入: [1,6,3,2,5]
输出: false
示例 2:

输入: [1,3,2,6,5]
输出: true

 

思路:

搜索树的判断标准是左子树都小于根节点,右子树都大于根节点。对于后序遍历的数组,最后一个下标end的值为树的根节点,我们从从左往右找到第一个大于下标end的值,标记其下标rightIndex,它是该根节点的右子树中的节点。

数组中第一个下标start到rightIndex - 1区间的值为根节点的左子树,rightIndex到end - 1区间的值为其右子树。由于我们已经可以判断rightIndex之前的值肯定小于根节点,因此我们还需要判断rightIndex到end-1区间的值是否都大于end下标的值,判断方式为从rightIndex到end-1遍历判断,若不存在,则遍历的下标tmpIndex等于end;若存在,则会在遍历的过程中退出,不会等于end。

判断完本节点后,还需要判断左右子树的根节点是否也满足标准,可以通过递归的方式来往下判断,终止的条件是start >= end,即根节点数组范围里只有一个节点,返回true即可。

时间复杂度:O(N^2):每次调用fun减去一个根节点,递归占用O(N),最差情况下(树退化为链表),每次递归都需遍历树中所有节点,占用O(n) 

 空间复杂度:O(N) :最差情况下(树退化为链表),递归深度为N

    public boolean VerifySquenceOfBST(int [] A) {
        if(A == null || A.length == 0) {
            return false;
        }
       return fun(A, 0, A.length - 1);
    }
    public boolean fun(int [] A, int start, int end) {
        if(start >= end) {
            return true;
        }
       int tmpIndex = start, rightIndex;
       while(A[tmpIndex] < A[end]) {
           ++tmpIndex;
       }  
        rightIndex = tmpIndex;
        
       while(A[tmpIndex] > A[end]) {
           ++tmpIndex;
       } 
        if(tmpIndex != end) {
            return false;
        }else {
            return fun(A, start, rightIndex - 1) && fun(A, rightIndex, end - 1);
        }
     }

 

思路2:后序遍历的倒序为:根节点->右子树->左子树。我们可以遍历其数组的倒序[rn r(n-1) ... r1],如果ri > r (i + 1),则ri一定是r(i+1)节点的右子结点;如果遇到递减节点(ri < r (i + 1)),则如果是二叉搜索树,则[r(i-1) , r(i-2)  ... r1]区间的所有节点,均小于某根节点root,root是 r(i + 1)到rn中最后一个大于ri且距离ri最远的节点。

我们用栈来存储数组中的节点,直至遇到ri > 栈顶元素,将栈中大于ri的节点全部弹出,并标记最后一个弹出的节点为其根节点root,接下来判断ri之后的节点是否都小于root

 public boolean VerifySquenceOfBST(int [] A) {
        if(A.length == 0) {
            return false;
        }
        Stack<Integer> stack = new Stack<>();
        int root = Integer.MAX_VALUE;
        for(int i = A.length - 1; i >= 0; i--) {
            if(A[i] > root) return false;
            while(!stack.isEmpty() && stack.peek() > A[i]) {
               root =  stack.pop();
                
            }
            stack.push(A[i]);
        }
        return true;
    }

时间复杂度 O(N): 遍历所有节点,各节点均入栈 / 出栈一次,使用 O(N)时间。
空间复杂度 O(N) : 最差情况下,单调栈 stackstack 存储所有节点,使用 O(N)额外空间。

 

二叉树中和为某一值的路径

题目:输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。

思路:按照先序遍历来遍历树的节点,如果目标值减去树节点值后等于0且该节点是叶节点,则加入结果列表中。递归遍历左右子节点,在向上回溯前,需要将当前节点从路径中删除。

ArrayList<ArrayList<Integer>> list = new ArrayList<ArrayList<Integer>>();
    ArrayList<Integer> tmpList = new ArrayList<>();
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
       if(root == null) {
           return list;
       }
        target -= root.val;
        tmpList.add(root.val);
        if(target == 0 && root.left == null && root.right == null) {
            list.add(new ArrayList(tmpList));
        }
        FindPath(root.left, target);
        FindPath(root.right, target);
        tmpList.remove(tmpList.size() - 1);
        return list;
    }

 

二叉搜索树与双向链表

题目:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。树的最小值(即最左节点)的左节点设置为空。要求不能创建任何新的结点,只能调整树中结点指针的指向。

思路:定义一个变量preNode 表示为中序遍历的前一个节点。

    TreeNode preNode = new TreeNode(0);
    TreeNode newHead = preNode;
    public void fun(TreeNode curNode) {
        if(curNode == null)   return;
        fun(curNode.left);
        preNode.right = curNode;
        curNode.left = preNode;
        preNode = curNode;
        fun(curNode.right);
    }
    
    public TreeNode Convert(TreeNode root) {
        if(root == null)     return null;
        fun(root);
        newHead = newHead.right;
        newHead.left = null;
        return newHead;
    }

时间复杂度 O(N) : N为二叉树的节点数,中序遍历需要访问所有节点。
空间复杂度 O(N): 最差情况下,即树退化为链表时,递归深度达到 N,系统使用 O(N)栈空间。

 

 

平衡二叉树

题目:判断一棵二叉树是否是平衡二叉树(左右子树高度差不大于1)。

思路:从低往上统计高度,避免了从上往下计算高度的重复计算。

    public boolean IsBalanced_Solution(TreeNode root) {
        return TreeDepth(root) != -1;
    }
    
   public int TreeDepth(TreeNode root) {
        if(root == null ){
            return 0;
        }
        int left = TreeDepth(root.left);
        if(left == -1) return -1;
        int right = TreeDepth(root.right);
        if(right == -1) return -1;
        return Math.abs(left - right) > 1 ? -1 : 1 + Math.max(left, right);
    }

 


字符串

正则表达式匹配

题目:实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配。

思路:首先考虑两种情况:

(1)当两个字符串都为空,返回true;当第一个字符串不空且第二个字符串为空,返回false;若第一个字符串空且第二个字符串不为空,则仍有可能匹配成功:例如第二个字符串为a*a*a*、

(2)开始匹配字符,判断下一个字符是否为 * ,若是,则判断字符是否匹配(匹配的标准是两字符是否相等 或 str字符为不空且pattern字符为.)。若匹配,则后续有3种匹配方式:

  1. pattern后移2个字符,即 x* 被忽略;
  2. str后移1个字符,pattern后移2个字符。即str中1个字符匹配pattern中的1个字符;
  3. str后移1个字符,pattern不变,即str中多个字符匹配pattern中的1个字符('*'表示它前面的字符可以出现任意次)。

(3)若若下一个字符不是*,则:判断两字符是否匹配,若是,则两字符串都后移1位;若不是,则返回false。

    public boolean match(char[] str, char[] pattern)
    {
        if(str == null || pattern == null) {
            return false;
        }
        return getRes(str, 0, pattern, 0);
    }
    
        public boolean getRes(char[] str, int strIndex, char[] pattern, int patIndex) {
            if(strIndex == str.length &&  patIndex == pattern.length) {
                return true;
            }
            if(strIndex != str.length &&  patIndex == pattern.length) {
                return false;
            }
            //pattern 下一个字符是 * 
            if(patIndex + 1 < pattern.length && pattern[patIndex + 1] == '*') {
                if((strIndex != str.length && pattern[patIndex] == str[strIndex]) || (pattern[patIndex] == '.' && strIndex != str.length)) {
                       return getRes(str, strIndex, pattern, patIndex + 2) 
                           || getRes(str, strIndex + 1, pattern, patIndex + 2) 
                           || getRes(str, strIndex + 1, pattern, patIndex);
                }else {
                       return getRes(str, strIndex, pattern, patIndex + 2);
                 }
            }else {        //pattern 下一个字符不是 * 
                if(strIndex != str.length && pattern[patIndex] == str[strIndex] 
                   || (pattern[patIndex] == '.' && strIndex != str.length)) {
                    return getRes(str, strIndex + 1, pattern, patIndex + 1);
                }else {
                    return false;
                }
            }
        }

 

字符流中第一个不重复的字符

题目:实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"

思路:通过队列来保存当前只出现1次的字符,由于字符出现的次数是变化的,因此从队列拿出字符前,应先判断该字符在当前是否只出现了1次。

int[] count = new int[128];
    LinkedList<Character> queue = new LinkedList<>();
    public void Insert(char ch)
    {
        if(count[ch]++ == 0) {
            queue.add(ch);
        }
    }
  //return the first appearence once char in current stringstream
    public char FirstAppearingOnce()
    {
        Character ch = '#';
        while((ch = queue.peek()) != null) {
            if(count[ch] != 1) {
                queue.remove();
            }else {
                return ch.charValue();
            }
        }
        return '#';
    }

 

字符串的排列

题目:输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

思路:对于没有重复的长度为n的字符串,其排列共有n * (n - 1) * (n - 2) ... * 2 * 1种方案,这是一种类似深度搜索的思想。在第一层深度中固定某个字符(n种情况),在固定第二个字符(n - 1种情况),直至固定n位字符(1种情况)。固定的方式是通过字符交换,比如在x到n的字符i中,若希望固定c[i],则可以交换c[i] 和c[x],然后进入x + 1的固定函数中,从x + 1函数中返回后需要恢复交换。

如果字符串中有重复字符时,则需要保证在某一层深度中固定该字符时,字符只能在此深度被固定一次,可以在每一层深度中用hashset来记录该深度已固定的字符,若遇到已重复字符则跳过。

“abc”字符串调用流程:

(1)dfs(0):此时x=0,进入循环:set有 a;  c[0] 和c[0]交换:abc  进入dfs(1)
(2)dfs(1):此时x=1,进入循环:i = 1,  set有 b;  c[1] 和c[1]交换:abc;  进入dfs(2)
(3)dfs(2)等于len - 1,添加结果abc。返回到(2)循环中,恢复交换 abc
(2)dfs(1):此时x=1,下一次循环,i = 2,set中有b, c;  c[1] 和c[2]交换,即 a c b;  进入dfs(2)
(4)dfs(2)等于len - 1,添加结果acb。返回到第2步的循环,恢复交换 a b c。再返回第1步的循环中, 恢复交换:abc.
(1)dfs(0):i = 1, set中有a, b;  c[0]和c[1]交换:bac;  进入 dfs(1);
  (5)  dfs(1):此时x=1,进入循环:i = 1, set有 a;  c[1] 和c[1]交换:bac;  进入dfs(2)
(3)dfs(2)等于len - 1,添加结果bac。返回到(5)中的循环,恢复交换 bac
  (5)  dfs(1):此时x = 1,下一次循环i = 2:set有 a c;  c[1] 和c[2]交换 b c a;  dfs(2)
  (6)dfs(2)等于len - 1,添加结果bca。返回到(2)中循环,恢复交换 b a c
  (7)  返回到(1)的循环,恢复交换 a b c
(1)dfs(0):i = 2, set中有a, b, c   c[0]和c[2]交换:cba;   进入dfs(1);
(9)dfs(1),此时x = 1, 进入循环:i = 1,set有b;  c[1]和c[1]交换:cba; 进入dfs(2)
(10)  dfs(2)等于len - 1,添加结果cba。返回第9步的循环中.恢复交换 cba
  (9) dfs(1),此时x = 1, 下一次循环:i = 2 set有b a ; c[1]和c[2]交换:cab; 进入dfs(2)  
  (12) dfs(2)等于len - 1,添加结果cab。返回第9步的循环中.恢复交换 cab。
  (13)返回到(1)中的循环
(1)恢复交换  a b c 

 

ArrayList<String> res = new ArrayList<String>();
   char[] chArray;
    public ArrayList<String> Permutation(String str) {
        chArray = str.toCharArray();
        dfs(0);
        Collections.sort(res);
        return res;
    }
    
    public void dfs(int x) {
        if(x == chArray.length - 1) {
            res.add(String.valueOf(chArray));
            return;
        }
        HashSet<Character> set = new HashSet<>();
        for(int i = x; i < chArray.length; i++) {
            if(set.contains(chArray[i])) continue;
            set.add(chArray[i]);
            swap(i, x);
            dfs(x + 1);
            swap(x, i);
        }
        
    }
    
    void swap(int a, int b) {
        char tmp = chArray[a];
        chArray[a] = chArray[b];
        chArray[b] = tmp;
    }

 

第一个只出现一次的字符

题目:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).

思路:用哈希表存储遍历字符串的每个字符。

    public int FirstNotRepeatingChar(String str) {
        if(str == null || str == "") {
            return -1;
        } 
        HashMap<Character, Boolean> map = new HashMap<>();
        for(int i = 0; i < str.length(); i++) {
            map.put(str.charAt(i), map.containsKey(str.charAt(i)));
        }
        for(int i = 0; i < str.length(); i++) {
            if(map.get(str.charAt(i)) == false) {
                return i;
            }
        }   
           return -1;
    }

 

左位移字符串

题目:对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。

思路:首先新建一个用于存储原字符串中[n,str.length - 1]的新字符串字符串res,最后将原字符串[0,n]中的字符放入res后。此处的新字符串res可用String或StringBuilder。

    public String LeftRotateString(String str,int n) {
        if(str.length() == 0)  return "";
        String res = "";
        for(int i = n; i < n + str.length(); i++) {
            res += str.charAt(i % str.length());
        }
        return res;
    }

 

把字符串转换成整数

题目:将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0

思路:遍历整个字符串即可:首字符可能有符号要考虑到;其次int类型的范围是[-2147483648, 2147483647],要考虑可能会超出该范围。

    public int StrToInt(String str) {
       if(str.length() == 0 || str.equals(""))        return 0;
       char[] chArray = str.toCharArray();
       int sign = 1, i = 1;
       long res = 0;
        if(chArray[0] == '-')  {
            sign = -1;
        } else if(chArray[0] != '+')  {
            i = 0;
        }
        for(; i < chArray.length; i++) {
            if(chArray[i] < '0' || chArray[i] > '9')
                return 0;
            res = res * 10 + chArray[i] - '0';
            if(res * sign > Integer.MAX_VALUE || res * sign < Integer.MIN_VALUE) 
                  return  0;
        }
        return sign * (int)res;
    }

 


数组

构建乘积数组

题目:给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]

思路:B[i] 等于A[i]左边的总乘积 乘上 A[i]右边的总乘积。因此,我们用res数组保存总乘积,首先从左到右遍历累乘,结果保存在res数组里,此时res[i]表示 A[i]左边的总乘积。然后从右往左遍历累乘,两次遍历后得到的即为两边的总乘积。

以上例说明:第一次遍历后,res数组的值为1,1,2,6,24。第二次遍历的结果为1*120,1 * 60,2 * 20,6 * 5 ,24 * 1

    public int[] multiply(int[] A) {
       int[] res = new int[A.length];
       int leftRes = 1;
        for(int i = 0; i < A.length; i++) {
            res[i] = leftRes;
            leftRes *= A[i];
        }
        int rightRes = 1;
        for(int i = A.length - 1; i >= 0; i--) {
            res[i] *= rightRes;
            rightRes *= A[i];
        }
        return res;
    }

 

调整数组顺序使奇数位于偶数前面

题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。例如[1,2,3,4,5,6,7]调整后的顺序为:[1,3,5,6,7,2,4,6]

思路:若要保证奇数和偶数间的相对位置不变,则在移动时 应把偶数到奇数之间的偶数往后移一位,空出来的那一位由奇数占用。例如[1,2,4,3,5,6,7] 第一次调整为:[1, 3, 2, 4, 5, 6, 7 ],第二次调整为:[1, 3, 5, 2, 4,  6, 7 ],第三次调整为:[1, 3, 5, 7,2, 4,  6,]。

    public void reOrderArray(int [] array) {
        if(array == null || array.length == 0) {
            return;
        }
        int left = 0, right = 0, tmp, i;
        while(left < array.length) {
            while(left < array.length && array[left] % 2 != 0) {
                ++left;
            }
            right = left + 1;
            while(right < array.length && array[right] % 2 == 0) {
                ++right;
            }
            if(right < array.length ) {
                tmp = array[right];
                for(i = right - 1; i >= left; i--) {
                    array[i + 1] = array[i];
                }
                array[left] = tmp;
                ++left;
            }else {
                break;
            }
        }
    }

 

顺时针打印矩阵

题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

思路:打印矩阵分为循环的四步:从左到右,从上到下,从右到左,从下到上。我们用变量left(0),right(列长),up(0),down(行长)分别定义矩阵的四个角。

  1. 当从左往右打印时,遍历的是left到right下标的值,然后up加1,表示上边界向下压缩一行,若up大于down,则表示打印完成。
  2. 当从上往下打印时,遍历的是up到down下标的值,然后right减1,表示右边界向左位移一行,若left大于right,则表示打印完成。
  3. 当从右往左打印时,遍历的是right到left下标的值,然后down减1,表示下边界向上位移一行,若up大于down,则表示打印完成。
  4. 当从下往上打印时,遍历的是down到up下标的值,然后left减1,表示左边界向右位移一行,若left大于right,则表示打印完成。
    public ArrayList<Integer> printMatrix(int [][] A) {
        ArrayList<Integer> array = new ArrayList<>();
       if(A == null) {
           return array;
       }
        int row = A.length - 1, col =  A[0].length - 1;
        int up = 0, right = col, down = row, left = 0, i;
        while(true) {
            for(i = left; i <= right; i++) {        //左往右
                array.add(A[up][i]);
            }
            if(++up > down) break;
            for(i = up; i <= down; i++) {            //上到下
                array.add(A[i][right]);
            }
            if(--right < left)   break;
            for(i = right; i >= left; i--) {        //右往左
                array.add(A[down][i]);
            }
            if(-- down < up) break;
            for(i = down; i >= up; i--) {           //上到下
                array.add(A[i][left]);
            }
            if(++left > right) break;
         }
             return array;
    }

 

数组中出现次数超过一半的数字

题目:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

思路:若数组中存在众数x,即x出现的次数一定大于其他数出现次数的总和。因此可以定义变量note来统计当前数值出现的次数,若当前数值已出现过一次,则note++,否则减1。若存在众数,则note一定大于0。

    public int MoreThanHalfNum_Solution(int [] array) {
        if(array.length == 0) return 0;
        int note = 0, x = array[0];
        for(int i = 1; i < array.length; i++) {
            if(note == 0) {
                x = array[i];
            }
            note += (array[i] == x) ? 1 : (-1);
        }
        // 数组中可能不存在众数,因此需要判断x是否为众数
        int count = 0;
        for(int num : array) {
            if(num == x)
                count++;
        }
        return (count > array.length / 2) ? x :0;
    }

 

最小的K个数

题目:输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,(顺序不定)。

思路1:快速排序思想,即把某数(默认为数组首位)调整位置为其左边的数都比他小,右边的数都比他大。若该下标index恰好为k-1(下标,第k小的数,下标为k-1),则该下标及其前面的数为所求结果;若index 小于k - 1,则去遍历[index + 1, end],否则遍历[start, index- 1]。

 
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
         ArrayList<Integer> res = new ArrayList<Integer>();
        if(input.length == 0 || k == 0 || input.length < k) {
            return res;
        }
        quickSearh(input, 0, input.length - 1, k - 1) ;
        for(int i = 0; i < k; i++) 
            res.add(input[i]);
        return res;
    }
     void quickSearh(int[] input, int start, int end, int k) {
         if(start < end) {
             int index = partition(input, start, end);
             if(index == k) {
                 return ;
             }else if(index < k){
                 quickSearh(input, index + 1, end, k);
                
             }else {
                 quickSearh(input, start, index - 1, k);
             }
         }
     }
    
    int partition(int[] input, int start, int end) {
        int pivot = input[start], tmp;
        while(start < end) {
            while(start < end && input[end] >= pivot) {
                --end;
            }
            input[start] = input[end];
            while(start < end && input[start] <= pivot) {
                ++start;
            }
            input[end] = input[start];
        }
        input[start] = pivot;
       return start;
    }

时间复杂度:每次调用partition()函数遍历的元素数目都是上一次遍历的1/2,因此复杂度为 n +n/2 + .. + n/n = 2n。O(N)

 

思路2:最大堆,即根节点大于等于 左右子树的节点值,我们用最大堆存储数组里前k-1个数(前k小的数,即最后下标为k-1)。之后遍历数组后面的数i,若i 小于根节点,则将根节点的值替换为i。

    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        if(k > input.length || k ==0) {
            return res;
        }
        //默认为最小堆,若要实现大根堆需要重写一下比较器。
       PriorityQueue<Integer> MaxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                //若o2大于o1,返回1,小于则返回-1。
                return o2.compareTo(o1);
            }
       });
        for(int i = 0; i < input.length; i++) {
            if(MaxHeap.size() < k) {
                MaxHeap.offer(input[i]);
            }else if(MaxHeap.peek() > input[i]) {
                 MaxHeap.poll();
                 MaxHeap.offer(input[i]);
            }
        }
        for (Integer integer : MaxHeap) {
            res.add(integer);
        }
        return res;
    }

时间复杂度:0(Nlogk)

 

连续子数组的最大和

题目:输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

思路:设置动态规划列表dp,dp[i]表示以array[i]结尾的连续子数组和。dp[0]=array[0]。若dp[i-1]<= 0,表明如果dp[i]为dp[i-1] + array[i],会产生负效果。不如array[i]本身一个元素大;若dp[i-1] >  0,则直接加上array[i]作为dp[i]。最后的结果即为dp列表中最大值

  public int FindGreatestSumOfSubArray(int[] array) {
        int res = array[0];
        for(int i = 1; i < array.length; i++) {
            array[i] += Math.max(array[i - 1], 0);
            res = Math.max(array[i], res);
        }
        return res;
    }

 

把数组排成最小的数

题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

思路:快速排序的思想。对于数组{3, 32}而言有两种排列方式:332, 323。显然323符合要求。我们需要自定义一种排序标准:对于x,y两个字符串而言:

  • 若x + y < y + x(此处的+是字符串拼接的意思),说明xy拼接而成的字符串数比yx拼接成的字符串数要小,因此我们希望x在y前面(因为这样可以式拼接的数更小),可以判定为 x 小于y。
  • 若x + y > y + x(此处的+是字符串拼接的意思),说明xy拼接而成的字符串数比yx拼接成的字符串数要大,因此我们希望x在y后面,可以判定为 x 大于y。

明确了排序的标准后,我们可以使用各种排序方法来将字符串调整为从小到大的顺序,此处我们用快排的方式来实现:

    public String PrintMinNumber(int [] nums) {
        String[] numStrArray = new String[nums.length];
        for(int i = 0; i < nums.length; i++) {
            numStrArray[i] = String.valueOf(nums[i]);
        }
        quickSearch(0, nums.length - 1, numStrArray);
        StringBuilder res = new StringBuilder();
        for(String s : numStrArray)
            res.append(s);
        return res.toString();


    }
    public void quickSearch(int start, int end, String[] numStrArray) {
        if(start >= end )  return;
        String posiv = numStrArray[start];
        int left = start, right = end;
        while(left < right) {
            while(left < right && 
                  (numStrArray[right] + posiv).compareTo(posiv + numStrArray[right]) >= 0) {
                --right;
            }
            numStrArray[left] = numStrArray[right];
            while(left < right && 
                  (numStrArray[left] + posiv).compareTo(posiv + numStrArray[left]) <= 0) {
                ++left;
            }
            numStrArray[right] = numStrArray[left];
        }
        numStrArray[right] = posiv;
        quickSearch(start, right - 1, numStrArray);
        quickSearch(right + 1, end, numStrArray);
    }

 

扑克牌中的顺子

题目:从扑克牌中随机抽5张牌,判断这5张牌是不是连续的。大小王共有4张,它们被视为0,可以被看成任何数。注意:这5张牌可能有重复的数,而重复的数构成的顺子不能视为正确(例如1,2,2,2,3)。

思路:对于不重复的5张牌而言,忽略掉大小王,其中最大的牌maxNum - 最小牌minNum +1是[minNum,maxNum]区间的连续数,如果这个区间的值大于5,则不能构成连续数;若小于等于5,则可以构成。

此题也可以将数组排序后,遍历数组:判断nums[i]到nums[i + 1]之间的差值val,即val = nums[i+1]-nums[i] - 1;若遍历到的nums[i]为0,则差值val ++。最后判断val是否大于等于0。

    public boolean isContinuous(int [] nums) {
        if(nums.length == 0) {
            return false;
        }
        boolean[] IsReapeat = new boolean[15];
        int minNum = 14, maxNum = 0;
        for(int i = 0; i < nums.length; i++) {
            if(nums[i] == 0) {
                continue;
            }   
            if(IsReapeat[nums[i]] == true) {    // 有重复的牌出现
                return false;        
            }
            IsReapeat[nums[i]] = true;
            minNum = Math.min(nums[i], minNum);
            maxNum = Math.max(nums[i], maxNum);
        }
        return (maxNum - minNum + 1 <= 5);
    }

 

 


二分法

旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

示例 :

输入:[2,2,2,0,1]
输出:0

思路:我们将旋转后的数组分为左数组和右数组,由于旋转前的数组是递增的,因此左数组任意一元素大于等于右数组任意一元素。

我们采用二分法来解决问题:设置left,right指针分别指向nums数组的左右两端,mid = (left + right) / 2,此处left ≤ mid < right。算出来的mid有三种情况:

  • nums[mid] > nums[right] :由于左数组任意一元素一定大于等于右数组任意一元素,因此 mid在左数组中,而旋转点则在 [mid + 1, right]中。然后执行left = mid + 1;
  • nums[mid] < nums[right] :可得mid一定在右数组中,旋转点在[left, mid]中。然后执行right = mid。
  • nums[mid] == nums[right] :无法判断mid在哪个数组里,例如:[1,0,1,1,1]中的旋转点为1(下标),此处的mid在右数组中; 再比如 [1, 1, 1, 0, 1]中,旋转点为3,此处的mid在左数组中。因此我们需要将right--,这样仍可确保旋转点x在[left, right]中:        (1)当mid在右数组中时,由于nums[mid] == nums[right],因此[mid, right]中所有元素相等,执行right-- 只是丢掉一个重复值而已;

       (2)当mid在左数组中时,旋转点的元素值nums[x] ≤ nums[j] == nums[m]。

为什么不用nums[left]来代替nums[right] 而与 nums[mid]比较?

在nums[mid] > nums[left]无法判断mid在哪个数组里。right初始值肯定是在右数组中,而left初始值不确定在哪个数组里。例如:

  • [1,2,3,4,5]数组中,旋转点为0,mid在右数组中;
  • [3,4,5,1,2] 数组中,旋转点为3,mid在左数组中。
    public int minNumberInRotateArray(int [] array) {
       int left = 0, right = array.length - 1, mid;
       while(left < right) {
           mid = (left +right) / 2;
           if(array[mid] < array[right]) {
               right = mid;
           }else if(array[right] < array[mid]) {
               left = mid +1;
           }else {
               --right;
           }  
       }
        return array[left];
    }

 

数值的整数次方

题目:给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

思路:

x^n = x^(n / 2) * x^(n / 2) = (x^2)^(n / 2),而n / 2的结果分为奇数和偶数:

  • 结果为奇数时,x^n =  x * (x^2)^(n / 2);,会多出一个x
  • 结果为偶数时,x^n = (x^2)^(n / 2);

每次对n除2直至n为0,设置一个变量res = 1,在循环n / 2时,若结果为奇数,则多出来的x乘上res。最终可得到x^0 * res,最后返回该结果即可。

    public double Power(double base, int exponent) {
        long n = exponent;
        double res = 1.0;
        if(n < 0) {            // 要考虑n小于0的情况,需要将x倒过来并取n的正数。
            n = -n;
            base = 1 / base;
        }
        while(n > 0) {
            if(n % 2 != 0) {
                res *= base;
            }
            n /= 2;
            base *= base;
        }
        return res;
  }

 

数组中的逆序对

题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

思路1:可以用归并排序递增的思路。每次将已排序好的左数组和右数组合并时,遍历整个数组,判断左数组的值是否大于右数组的值,若大于,即在左数组里 下标i的值及后面的值 都大于右数组当前下标j的值,数量有mid - i + 1。例如100 160 290 5 8 200。每次归并后都可以统计出一个区间的逆序对。

    public int InversePairs(int[] A) {
        if(A.length < 2) {
            return 0;
        }
        int[] tmpArr = new int[A.length];
        return Sort(A, 0, A.length - 1, tmpArr) % 1000000007;
    }

    
    public int Sort(int[] A, int start, int end, int[] tmpArr){
        if(start >= end) {
            return 0;
        }
        int mid = (start + end) / 2;
        int leftRes = Sort(A, start, mid, tmpArr) % 1000000007;
        int rightRes = Sort(A, mid + 1, end, tmpArr) % 1000000007;
        if(A[mid] <= A[mid + 1]) {
            return (leftRes + rightRes) % 1000000007;
        }else {
            return (leftRes + rightRes + Merge(A, start, end, mid, tmpArr)) % 1000000007;
        }
        
    }
    
    public int Merge(int[] A, int start, int end, int mid, int[] tmpArr) {
        int res = 0;
        for(int i = start; i <= end; i++) {
            tmpArr[i] = A[i];
        }
        for(int i = start, j = mid + 1, k = start; k <= end; k++) {
            if(i > mid) {
                A[k] = tmpArr[j++]; 
            }else if(j > end || tmpArr[i] <= tmpArr[j]){
                A[k] = tmpArr[i++];
            }else {
                A[k] = tmpArr[j++]; 
                res += mid - i + 1;
                if(res >= 1000000007) {
                    res %= 1000000007;
                }
            }
        }
        return res % 1000000007;
    }

 

 


斐波那契数列

斐波那契数列

题目:写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(从0开始,第0项为0,第1项为1)。

思路:通过变量sum存储变量前一个数a和前两个数b的和。

    public int Fibonacci(int n) {
        int a = 0 , b = 1, sum = 0;
        for(int i = 1; i <= n; i++) {
            sum = a + b;
            b = a;
            a = sum;
        }
        return a;
    }

 

跳台阶

题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

思路:同样是斐波那契数列的思路,假设跳上n级台阶有f(n)种跳法,跳上n级台阶的最后一步有两种跳法:跳上1级台阶,则之前剩下n-1级台阶,有f(n-1)种跳法;跳上2级台阶,则之前剩下n-2级台阶,有f(n-2)种跳法;由此可得f(n) = f(n -1) + f(n - 2)

    public int JumpFloor(int target) {
       int a =  1, b = 1, sum;
        for(int i = 0; i < target; i++) {
            sum = a + b;
            a = b;
            b = sum;
        }
        return a;
    }

 


动态规划

丑数

题目:把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

思路:第一个丑数是1,之后的丑数序列都是2,3,5的倍数,或者说是2,3,5的倍数中最小的那个数。我们定义一个丑数序列dp,dp[n]表示下标为n的丑数(也是第n+1个丑数)。假设我们已知长度为n的丑数序列dp[n],则dp[n + 1]有如下取值可能:

  1. dp[n + 1] = dp[a] * 2,其中a为[1, n]的数。
  2. dp[[n + 1] = dp[b]  * 3,其中b为[1, n]的数。
  3. dp[[n + 1] = dp[c]* 5,其中c为[1, n]的数。

其中a,b,c需满足如下条件,即dp[n + 1] = min{dp[a] * 2,dp[b] * 3,dp[c] * 5}

  1. dp[a - 1] * 2 ≤ dp[n] < dp[a] * 2
  2. dp[b - 1] * 2 ≤ dp[n] < dp[b] * 3
  3. dp[c - 1] * 2 ≤ dp[n] < dp[c] * 5
    public int GetUglyNumber_Solution(int index) {
        if(index == 0) {
            return 0;
        }
        int[] dp = new int[index];
        int xa = 0, xb = 0, xc = 0;
        int ta, tb, tc;
        dp[0] = 1;
        for(int i = 1; i < index; i++) {
            ta = dp[xa] * 2;    tb = dp[xb] * 3; tc = dp[xc] * 5;
            dp[i] = Math.min(Math.min(ta, tb), tc);
            if(dp[i] == ta)       xa++;
            if(dp[i] == tb)       xb++;
            if(dp[i] == tc)       xc++;
        }
        return dp[index - 1];
    }

 

 

 


 

数学

从1到n整数中1出现的次数

题目:输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

思路:自定义函数f(n)来统计1出现的次数,将n分为两部分:最高位high和剩余位last。我们分两种情况考虑:

(1)最高位high是1:以1234为例,high=1, last=234, pow=1000。将1~1234分为两部分:1~999 和 1000~1234。在1~999范围中1的次数是f(pow-1);在1000~1234范围中,对于千分位是1的次数(只考虑千分位,其他位不考虑)是 234 + 1,即last + 1。对于其他位是1的次数,即234中出现的个数,为f(234)。 将这几部分结果相加即 f(pow-1) + last + 1 + f(last)。

(2)最高位不是1:以3234为例,high=3, pow=1000, last=234。将n分为多部份:1~999,1000~1999,2000~2999和3000~3234。

1~999范围中1出现次数为f(pow - 1);1000~1999范围中1出现的个数分为两部分情况:对于千分位是1的次数(只考虑千分位,其他位不考虑)是pow,其他位(即999)出现1的次数是f(pow-1);2000~2999范围1的次数是f(pow-1);3000~3234范围1的次数是f(last)。全部相加得 pow + high*f(pow-1) + f(last)

    public int NumberOf1Between1AndN_Solution(int n) {
         return f(n);
    }
    
    public int f(int n) {
        if(n <= 0)     return 0;
        String nStr = String.valueOf(n);
        int high = nStr.charAt(0) - '0';    //n的最高位
        int pow = (int)Math.pow(10, nStr.length() - 1);  //n的最高位名
        int last = n - pow * high;
        if(high == 1) {
            return f(pow - 1) + last + 1 + f(last);
        }else {
             return pow + high * f(pow-1) + f(last);
        }
    }

 

圆圈中最后剩下的数字

题目:

0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

思路:以N=8,M=3中的数字的例子说明,将字母代替成数字来方便观看

将N = 7反推到N=8:将被淘汰的C加回来,向右移m位,溢出的部分在前面补充。这样就变回N = 8的排列。此时幸存者C由下标6变回了下标2,即由F(7,3)变成了f(8,3)。

由此可以推出公式:f(8, 3) = [f(7, 3) + 3] % 8。即:f(n, m) = [f(n - 1, m) + m] % n,此处的n为当前队列的长度。而当n = 1时,f(1, 3)等于0,即幸存者下标为0。

    public int LastRemaining_Solution(int n, int m) {
        int pos = 0;
        if(n == 0 && m == 0) {
            return -1;
        }
        for(int i = 2; i <= n; i++) {
            pos = (pos + m) % i;    // i为当前队列长度,pos为当前幸存者在当前队列的下标
        }
        return pos;
    }

 


位运算

数组中数字出现的次数

题目:一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。找出这两个只出现一次的数字并放在给定的两个数组num1和num2。要求时间复杂度是O(n),空间复杂度是O(1)。

思路:异或:相同为0,不同为1,且a ^ a = 0; a ^ 0 = 0。定义一个初值为0的变量s,去一一异或数组里的每个值,得到的结果s是数组里只出现1次的两个数 异或的结果。s的二进制中必定有1(a和b是不同的,因此必定存在多个位置i,a和b的二进制在位置i中的值是不同的),因此我们需要找到其中的一个1。我们可以通过 s & (-s)来找到s二进制中的最后一个1,其余位全弄为0。在这个位置中,a和b的数是不同的,即一个为1,一个为0。

举个例子:[1,2,1,3,2,5],s得到的结果是3 & 5 = 0x0110 = 6。我们通过k = s & (-s) 即 0000 0110 & 1111 1010得到0000 0010。然后我们通过k再去一一去异或数组里的每个值,将异或的不同结果分别存放在两个数组里。若异或到两个相同的值,它们会被放在同一个数组里,且之间异或的值为0;若异或到的是a和b,由于在k二进制中的位置i中,一个是与它相同的,另一个则是不同的,因此他们会被划分到不同的数组里。某一组为:1,1,5,另一组为:2,3,2。最后分别各自异或数组里的值,得到的结果就是两个不同的值a,b。

    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int s = 0;
        for(int i = 0; i < array.length; i++) {
            s ^= array[i];
        }
        int k = s & (-s);
        for(int i = 0; i < array.length; i++) {
            if((k & array[i]) != 0) {    // 同为1,不同为0
                num1[0] ^= array[i];
            }else {
                 num2[0] ^= array[i];
            }        
        }
    }

若要求找到的数只有1个,也可以按此此思路。

    public int singleNumber(int[] nums) {
        int res = nums[0];
        for(int i = 1; i < nums.length; i++) 
          res = res ^ nums[i];

        return res;
    }

 

二进制中1的个数

题目:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

思路:对于一个数n,n - 1的二进制值相当于将最右边的1变成0,此1的右边的0都变为1,因此 n & (n - 1)将n最右边的1变为0,其余位不变。这样不断循环直至n为0。

    public int NumberOf1(int n) {
         int cnt = 0;
         while(n != 0) {
             n = n & (n - 1);
             ++cnt;
         }   
        return cnt;
    }

 


 

深度优先搜索

矩阵中的路径

题目:设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如

a b c e 
s f c  s
a d e e

矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

此处给定矩阵是一个一维数组

思路:遍历矩阵中所有字符为起点:DFS通过递归,朝上下左右方向分别搜,返回是否可行。

    boolean dfs(char[] matrix, char[] str, int rows, int cols, int i, int j, int k) {
        if(i < 0 || i >= rows || j < 0 || j >= cols || matrix[i * cols + j] != str[k]) {
            return false;
        }
        if(k == str.length - 1) {
            return true;
        }
        char tmp = matrix[i * cols + j];
        matrix[i * cols + j] = '/';
        boolean curRes = 
               dfs(matrix, str, rows, cols, i + 1, j, k + 1) 
            || dfs(matrix, str, rows, cols, i, j + 1, k + 1) 
            || dfs(matrix, str, rows, cols, i, j - 1, k + 1) 
            || dfs(matrix, str, rows, cols, i - 1, j, k + 1);
  
        matrix[i * cols + j] = tmp;
        return curRes;
    }
    
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
    {
         int[] flag = new int[matrix.length];
           for(int i = 0; i < rows; i++) {
               for(int j = 0; j < cols; j++) {
                   if(dfs(matrix, str, rows, cols, i, j, 0)) {
                       return true;
                   }
               }
           }
        return false;
    }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值