劍指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;
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章