劍指offer解析-上(Java實現)

Anwen發表於2019-02-19

個人技術部落格:www.zhenganwen.top

以下題目按照牛客網線上程式設計排序,所有程式碼示例程式碼均已通過牛客網OJ。

二維陣列的查詢

題目描述

在一個二維陣列中(每個一維陣列的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函式,輸入這樣的一個二維陣列和一個整數,判斷陣列中是否含有該整數。

public boolean Find(int target, int [][] arr) {

}
複製程式碼

解析

暴力方法是遍歷一遍二維陣列,找到target就返回true,時間複雜度為O(M * N)(對於M行N列的二維陣列)。

由題可知輸入的資料樣本具有高度規律性(單獨一行的資料來看是有序的,單獨一列的資料來看也是有序的),因此考慮能否有一個比較基準在一次的比較中根據有序性淘汰不必再進行遍歷比較的數。有序查詢,由此不難聯想到二分查詢,我們可以借鑑二分查詢的思路,每次選出一個數作為比較基準進而淘汰掉一些不必比較的數。二分是選取陣列的中位數作為比較基準的,因此能夠保證每次都淘汰掉二分之一的數,那麼此題中有沒有這種特性的數呢?我們不妨舉例觀察一下:

image

不難發現上圖中對角線上的數是其所在行和所在列形成的序列的中位數,不妨選取右上角的數作為比較基準,如果不相等,那麼我們可以淘汰掉所有它左邊的數或者它所有下面的,比如對於target = 6,因為(0,3)位置上的4 < 6,因此(0,3)位置及其同一行的左邊的所有數都小於6因此可以直接淘汰掉,淘汰掉之後問題就變為了從剩下的三行中找target,這與原始問題是相似的,也就是說每一次都選取右上角的資料為比較基準然後淘汰掉一行或一列,直到某一輪被選取的數就是target或者已經淘汰得只剩下一個數的時候就一定能得出結果了,因此時間複雜度為被淘汰掉的行數和列數之和,即O(M + N),經過分析後不難寫出如下程式碼:

public boolean Find(int target, int [][] arr) {
    //input check
    if(arr == null || arr.length == 0 || arr[0] == null || arr[0].length == 0){
        return false;
    }
    int i = 0, j = arr[0].length - 1;
    while(i != arr.length - 1 && j != 0){
        if(target > arr[i][j]){
            i++;
        }else if(target < arr[i][j]){
            j--;
        }else{
            return true;
        }
    }

    return target == arr[i][j];
}
複製程式碼

值得注意的是每次選取的數都是第一行最後一個數,因此前提是第一行有數,那麼就對應著輸入檢查的arr[0] == null || arr[0].length == 0,這點比較容易忽略。

總結:經過分析其實不難發現,此題是在一維有序陣列使用二分查詢元素的一個變種,我們應該充分利用資料本身的規律性來尋找解題思路。

替換空格

題目描述

請實現一個函式,將一個字串中的每個空格替換成“%20”。例如,當字串為We Are Happy.則經過替換之後的字串為We%20Are%20Happy。

public String replaceSpace(StringBuffer str) {
    
}
複製程式碼

此題考查的是字串這個資料結構的陣列實現(對應的還有連結串列實現)的相關操作。

解析

String.replace簡單粗暴

如果可以使用API,那麼可以很容易地寫出如下程式碼:

public String replaceSpace(StringBuffer str) {
    //input check
    //null pointer
    if(str == null){
        return null;
    }
    //empty str or not exist blank
    if(str.length() == 0 || str.indexOf(" ") == -1){
        return str.toString();
    }

    for(int i = 0 ; i < str.length() ; i++){
        if(str.charAt(i) == ' '){
            str.replace(i, i + 1, "%20");
        }
    }

    return str.toString();
}
複製程式碼
時間O(n),空間O(n)

但是如果面試官告訴我們不許使用封裝好的替換函式,那麼目的就是在考查我們對字串陣列實現方式的相關操作。由於是連續空間儲存,因此需要在建立例項時指定大小,由於每個空格都使用%20替換,因此替換之後的字串應該比原串多出空格數 * 2個長度,實現如下:

public String replaceSpace(StringBuffer str) {
    //input check
    //null pointer
    if(str == null){
        return null;
    }
    //empty str or not exist blank
    if(str.length() == 0 || str.indexOf(" ") == -1){
        return str.toString();
    }

    char[] source = str.toString().toCharArray();
    int blankCount = 0;
    for(int i = 0 ; i < source.length ; i++){
        blankCount = (source[i] == ' ') ? blankCount + 1 : blankCount;
    }
    char[] dest = new char[source.length + blankCount * 2];
    for(int i = source.length - 1, j = dest.length - 1 ; i >=0 && j >=0 ; i--, j--){
        if(source[i] == ' '){
            dest[j--] = '0';
            dest[j--] = '2';
            dest[j] = '%';
            continue;
        }else{
            dest[j] = source[i];
        }
    }

    return new String(dest);
}
複製程式碼
時間O(n),空間O(1)

如果還要求不能有額外空間,那我們就要考慮如何複用輸入的字串,如果我們從前往後遇到空格就將空格及其之後的兩個位置替換為%20,勢必會覆蓋空格之後的兩個字元,比如hello world會被替換成hello%20rld,因此我們需要在長度被擴充套件後的新串中從後往前確定每個索引上的字元。比如使用一個originalIndex指向原串中的最後一個字元索引,使用newIndex指向新串的最後一個索引,每次將originalIndex上的字元複製到newIndex上並且兩個指標前移,如果originalIndex上的字元是空格,則將newIndex依次填充0,2,%,然後兩者再前移,直到兩者都到首索引位置。

image

public String replaceSpace(StringBuffer str) {
    //input check
    //null pointer
    if(str == null){
        return null;
    }
    //empty str or not exist blank
    if(str.length() == 0 || str.indexOf(" ") == -1){
        return str.toString();
    }

    int blankCount = 0;
    for(int i = 0 ; i < str.length() ; i++){
        blankCount = (str.charAt(i) == ' ') ? blankCount + 1 : blankCount;
    }
    int originalIndex = str.length() - 1, newIndex = str.length() - 1 + blankCount * 2;
    str.setLength(newIndex + 1); //需要重新設定一下字串的長度,否則會報越界錯誤
    while(originalIndex >= 0 && newIndex >= 0){
        if(str.charAt(originalIndex) == ' '){
            str.setCharAt(newIndex--, '0');
            str.setCharAt(newIndex--, '2');
            str.setCharAt(newIndex, '%');
        }else{
            str.setCharAt(newIndex, str.charAt(originalIndex));
        }
        originalIndex--;
        newIndex--;
    }

    return str.toString();
}
複製程式碼

總結:要把思維開啟,對於陣列的操作我們習慣性的以for(int i = 0 ; i < arr.length ; i++)的形式從頭到尾來運算元組,但是不要忽略了從尾到頭遍歷也有它的獨到之處。

反轉連結串列

題目描述

輸入一個連結串列,反轉連結串列後,輸出新連結串列的表頭。

public ListNode ReverseList(ListNode head) {
        
}
複製程式碼

解析

此題的難點在於無法通過一個單連結串列結點獲取其前驅結點,因此我們不僅要在反轉指標之前儲存當前結點的前驅結點,還要儲存當前結點的後繼結點,並在下一次反轉之前更新這兩個指標。

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public ListNode ReverseList(ListNode head) {
    if(head == null || head.next == null){
        return head;
    }
    ListNode pre = null, p = head, next;
    while(p != null){
        next = p.next;
        p.next = pre;
        pre = p;
        p = next;
    }

    return pre;
}
複製程式碼

從尾到頭列印連結串列

題目描述

輸入一個連結串列,按連結串列值從尾到頭的順序返回一個ArrayList。

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
      
}
複製程式碼

解析

此題的難點在於單連結串列只有指向後繼結點的指標,因此我們無法通過當前結點獲取前驅結點,因此不要妄想先遍歷一遍連結串列找到尾結點然後再依次從後往前列印。

遞迴,簡潔優雅

由於我們通常是從頭到尾遍歷連結串列的,而題目要求從尾到頭列印結點,這與前進後出的邏輯是相符的,因此你可以使用一個棧來儲存遍歷時走過的結點,再通過後進先出的特性實現從尾到頭列印結點,但是我們也可以利用遞迴來幫我們壓棧,由於遞迴簡潔不易出錯,因此面試中能用遞迴儘量用遞迴:只要當前結點不為空,就遞迴遍歷後繼結點,當後繼結點為空時,遞迴結束,在遞迴回溯時將“當前結點”依次新增到集合中

/**
*    public class ListNode {
*        int val;
*        ListNode next = null;
*
*        ListNode(int val) {
*            this.val = val;
*        }
*    }
*
*/
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> res = new ArrayList();
        //input check
        if(listNode == null){
            return res;
        }
        recursively(res, listNode);
        return res;
    }

    public void recursively(ArrayList<Integer> res, ListNode node){
        //base case
        if(node == null){
            return;
        }
        //node not null
        recursively(res, node.next);
        res.add(node.val);
        return;
    }
}
複製程式碼
反轉連結串列

還有一種方法就是將連結串列指標都反轉,這樣將反轉後的連結串列從頭到尾列印就是結果了。需要注意的是我們不應該在訪問使用者資料時更改儲存資料的結構,因此最後要記得反轉回來:

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> res = new ArrayList();
    //input check
    if(listNode == null){
        return res;
    }
    return unrecursively(listNode);
}

public ArrayList<Integer> unrecursively(ListNode node){
    ArrayList<Integer> res = new ArrayList<Integer>();
    ListNode newHead = reverse(node);
    ListNode p = newHead;
    while(p != null){
        res.add(p.val);
        p = p.next;
    }
    reverse(newHead);
    return res;
}

public ListNode reverse(ListNode node){
    ListNode pre = null, cur = node, next;
    while(cur != null){
        //save predecessor
        next = cur.next;
        //reverse pointer
        cur.next = pre;
        //move to next
        pre = cur;
        cur = next;
    }
    //cur is null
    return pre;
}
複製程式碼

總結:面試時能用遞迴就用遞迴,當然瞭如果面試官就是要考查你的指標功底那你也能just so so不是

重建二叉樹

題目描述

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,2,7,1,5,3,8,6},則重建二叉樹並返回。

public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
   
}
複製程式碼

解析

先序序列的特點是第一個數就是根結點而後是左子樹的先序序列和右子樹的先序序列,而中序序列的特點是先是左子樹的中序序列,然後是根結點,最後是右子樹的中序序列。因此我們可以通過先序序列得到根結點,然後通過在中序序列中查詢根結點的索引從而得到左子樹和右子樹的結點數。然後可以將兩序列都一分為三,對於其中的根結點能夠直接重建,然後根據對應子序列分別遞迴重建根結點的左子樹和右子樹。這是一個典型的將複雜問題劃分成子問題分步解決的過程。

image

遞迴體的定義,如上圖先序序列的左子樹序列是2,3,4對應下標1,2,3,而中序序列的左子樹序列是3,2,4對應下標0,1,2,因此遞迴體接收的引數除了儲存兩個序列的陣列之外,還需要指明需要遞迴重建的子序列分別在兩個陣列中的索引範圍:TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n)。然後遞迴體根據prei~j索引範圍形成的先序序列和inm~n索引範圍形成的中序序列重建一棵樹並返回根結點。

首先根結點就是先序序列的第一個數,即pre[i],因此TreeNode root = new TreeNode(pre[i])可以直接確定,然後通過在inm~n中查詢出pre[i]的索引index可以求得左子樹結點數leftNodes = index - m,右子樹結點數rightNodes = n - index,如果左(右)子樹結點數為0則表明左(右)子樹為null,否則通過root.left = rebuild(pre, i' ,j' ,in ,m' ,n')來重建左(右)子樹即可。

這個題的難點也就在這裡,即i',j',m',n'的值的確定,筆者曾在此困惑許久,建議通過leftNodes,rightNodesi,j,m,n來確定:(這個時候了前往不要在腦子裡面想這些下標對應關係!!一定要在紙上畫,確保準確性和概括性)

image

於是容易得出如下程式碼:

if(leftNodes == 0){
    root.left = null;
}else{
    root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
}
if(rightNodes == 0){
    root.right = null;
}else{
    root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
}
複製程式碼

筆者曾以中序序列的根節點索引來確定i',j',m',n'的對應關係寫出如下錯誤程式碼

image

if(leftNodes == 0){
    root.left = null;
}else{
    root.left = rebuild(pre, i + 1, index, in, m, index - 1);
}
if(rightNodes == 0){
    root.right = null;
}else{
    root.right = rebuild(pre, index + 1, j, in, index + 1, n);
}
複製程式碼

這種對應關係乍一看沒錯,但是不具有概括性(即囊括所有情況),比如對序列2,3,43,2,4重建時:

image

你看這種情況,上述錯誤程式碼還適用嗎?原因就在於index是在inm~n中選取的,與陣列in是繫結的,和pre沒有直接的關係,因此如果用index來表示i',j'自然是不合理的。

此題的正確完整程式碼如下:

/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if(pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length){
            return null;
        }
        return rebuild(pre, 0, pre.length - 1, in, 0, in.length - 1);
    }
    
    public TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n){
        int rootVal = pre[i], index = findIndex(rootVal, in, m, n);
        if(index < 0){
            return null;
        }
        int leftNodes = index - m, rightNodes = n - index;
        TreeNode root = new TreeNode(rootVal);
        if(leftNodes == 0){
            root.left = null;
        }else{
            root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
        }
        if(rightNodes == 0){
            root.right = null;
        }else{
            root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
        }
        return root;
    }
    
    public int findIndex(int target, int arr[], int from, int to){
        for(int i = from ; i <= to ; i++){
            if(arr[i] == target){
                return i;
            }
        }
        return -1;
    }
}
複製程式碼

總結:

  1. 對於複雜問題,一定要劃分成若干子問題,逐一求解。比如二叉樹問題,我們通常將其劃分成頭結點、左子樹、右子樹。
  2. 對於遞迴過程的引數對應關係,儘量使用和資料樣本本身沒有直接關係的變數來表示。比如此題應該選取leftNodesrightNodes來計算i',j',m',n'而不應該使用頭結點在中序序列的下標index(它和in是繫結的,那麼可能對pre就不適用了)。

用兩個棧實現佇列

題目描述

用兩個棧來實現一個佇列,完成佇列的Push和Pop操作。 佇列中的元素為int型別。

Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();

public void push(int node) {

}

public int pop() {
    
}
複製程式碼

解析

這道題只要記住以下幾點即可:

  1. 一個棧(如stack1)只能用來存,另一個棧(如stack2)只能用來取
  2. 當取元素時首先檢查stack2是否為空,如果不空直接stack2.pop(),否則將stack1中的元素全部倒入stack2,如果倒入之後stack2仍為空則需要拋異常,否則stack2.pop()

程式碼示例如下:

import java.util.Stack;

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        stack1.push(node);
    }
    
    public int pop() {
        if(stack2.empty()){
            while(!stack1.empty()){
                stack2.push(stack1.pop());
            }
        }
        if(stack2.empty()){
            throw new IllegalStateException("no more element!");
        }
        return stack2.pop();
    }
}
複製程式碼

總結:只要取元素的棧不為空,取元素時直接彈出其棧頂元素即可,只有當其為空時才考慮將存元素的棧倒入進來,並且要一次性倒完。

旋轉陣列的最小數字

題目描述

把一個陣列最開始的若干個元素搬到陣列的末尾,我們稱之為陣列的旋轉。 輸入一個非減排序的陣列的一個旋轉,輸出旋轉陣列的最小元素。 例如陣列{3,4,5,1,2}為{1,2,3,4,5}的一個旋轉,該陣列的最小值為1。 NOTE:給出的所有元素都大於0,若陣列大小為0,請返回0。

public int minNumberInRotateArray(int [] arr) {
       
}
複製程式碼

解析

此題需先認真審題:

  1. 若干,涵蓋了一個元素都不搬的情況,此時陣列是一個非減排序序列,因此首元素就是陣列的最小元素。
  2. 非減排序,並不代表是遞增的,可能會出現若干相鄰元素相同的情況,極端的例子是整個陣列的所有元素都相同

由此不難得出如下input check

public int minNumberInRotateArray(int [] arr) {
    //input check
    if(arr == null || arr.length == 0){
        return 0;
    }
    //if only one element or no rotate
    if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
        return arr[0];
    }
    
    //TODO
}
複製程式碼

上述的arr[0] < arr[arr.length - 1]不能寫成arr[0] <= arr[arr.length - 1],比如可能會有[1,2,3,3,4] -> [3,4,1,2,3] 的情況,這時你不能返回arr[0]=3

如果走到了程式中的TODO,就可以考慮普遍情況下的推敲,陣列可以被分成兩部分:大於等於arr[0]的左半部分和小於等於arr[arr.length - 1]右半部分,我們不妨藉助兩個指標從陣列的頭、尾向中間靠近,這樣就能利用二分的思想快速移動指標從而淘汰一些不在考慮範圍之內的數。

image

如圖,我們不能直接通過arr[mid]arr[l](或arr[r])的比較(arr[mid] >= arr[l])來決定移動l還是rmid上,因為陣列可能存在若干相同且相鄰的數,因此我們還需要加上一個限制條件:arr[l + 1] >= arr[l] && arr[mid] >= arr[l](對於r來說則是arr[r - 1] <= arr[r] && arr[mid] <= arr[r]),即當左半部分(右半部分)不止一個數時,我們才可能去移動lr)指標。完整程式碼如下:

import java.util.ArrayList;
public class Solution {
    public int minNumberInRotateArray(int [] arr) {
         //input check
        if(arr == null || arr.length == 0){
            return 0;
        }
        //if only one element or no rotate
        if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
            return arr[0];
        }
         
        //has rotate, left part is big than right part
        int l = 0, r = arr.length - 1, mid;
        //l~r has more than 3 elements
        while(r > l && r - l != 1){
            //r-l >= 2	->	mid > l
            mid = l + ((r - l) >> 1);
            if(arr[l + 1] >= arr[l] && arr[mid] >= arr[l]){
                l = mid;
            }else{
                r = mid;
            }
        }
         
        return arr[r];
    }
}
複製程式碼

總結:審題時要充分考慮資料樣本的極端情況,以寫出魯棒性較強的程式碼。

斐波那契數列

題目描述

大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項為0)。n<=39

public int Fibonacci(int n) {
      
}
複製程式碼

解析

遞迴方式

對於公式f(n) = f(n-1) + f(n-2),明顯就是一個遞迴呼叫,因此根據f(0) = 0f(1) = 1我們不難寫出如下程式碼:

public int Fibonacci(int n) {
    if(n == 0 || n == 1){
        return n;
    }
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}
複製程式碼
動態規劃

在上述遞迴過程中,你會發現有很多計算過程是重複的:

image

動態規劃就在使用遞迴呼叫自上而下分析過程中發現有很多重複計算的子過程,於是採用自下而上的方式將每個子狀態快取下來,這樣對於上層而言只有當需要的子過程結果不在快取中時才會計算一次,因此每個子過程都只會被計算一次

public int Fibonacci(int n) {
    if(n == 0 || n == 1){
        return n;
    }
    //n1 -> f(n-1), n2 -> f(n-2)
    int n1 = 1, n2 = 0;
    //從f(2)開始算起
    int N = 2, res = 0;
    while(N++ <= n){
        //每次計算後更新快取,當然你也可以使用一個一維陣列儲存每次的計算結果,只額外空間複雜度就變為O(n)了
        res = n1 + n2;
        n2 = n1;
        n1 = res;
    }
    return res;
}
複製程式碼

上述程式碼很多人都能寫出來,只是沒有意識到這就是動態規劃。

總結:當你自上而下分析遞迴時發現有很多子過程被重複計算,那麼就應該考慮能否通過自下而上將每個子過程的計算結果快取下來。

跳臺階

題目描述

一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先後次序不同算不同的結果)。

public int JumpFloor(int target) {
       
}
複製程式碼

解析

遞迴版本

將複雜問題分解:複雜問題就是不斷地將target減1或減2(對應跳一級和跳兩級臺階)直到target變為1或2(對應只剩下一層或兩層臺階)時我們能夠很容易地得出結果。因此對於當前的青蛙而言,它能夠選擇的就是跳一級或跳二級,剩下的臺階有多少種跳法交給子過程來解決:

public int JumpFloor(int target) {
    //input check
    if(target <= 0){
        return 0;
    }
    //base case
    if(target == 1){
        return 1;
    }
    if(target == 2){
        return 2;
    }
    return JumpFloor(target - 1) + JumpFloor(target - 2);
}
複製程式碼

你會發現這其實就是一個斐波那契數列,只不過是從f(1) = 1,f(2) = 2開始的斐波那契數列罷了。自然你也應該能夠寫出動態規劃版本。

進階問題

一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

解析

遞迴版本

本質上還是分解,只不過上一個是分解成兩步,而這個是分解成n步:

public int JumpFloorII(int target) {
    if(target <= 0){
        return 0;
    }
    //base case,當target=0時表示某個分解分支跳完了所有臺階,這個分支就是一種跳法
    if(target == 0){
        return 1;
    }
    
    //本過程要收集的跳法的總數
    int res = 0;
    for(int i = 1 ; i <= target ; i++){
         //本次選擇,選擇跳i階臺階,剩下的臺階交給子過程,每個選擇就代表一個分解分支
        res += JumpFloorII(target - i);
    }
    return res;
}
複製程式碼
動態規劃

這個動態規劃就有一點難度了,首先我們要確定快取目標,斐波那契數列中由於f(n)只依賴於f(n-1)f(n-2)因此我們僅用兩個快取變數實現了動態規劃,但是這裡f(n)依賴的是f(0),f(1),f(2),...,f(n-1),因此我們需要通過長度量級為n的表快取前n個狀態(int arr[] = new int[target + 1]arr[target]表示f(n))。然後根據遞迴版本(通常是base case)確定哪些狀態的值是可以直接確定的,比如由if(target == 0){ return 1 }可知arr[0] = 1,從f(N = 1)開始的所有狀態都需要依賴之前(f(n < N))的所有狀態:

int res = 0;
for(int i = 1 ; i <= target ; i++){
    res += JumpFloorII(target - i);
}
return res
複製程式碼

因此我們可以據此自下而上計算出每個子狀態的值:

public int JumpFloorII(int target) {
    if(target <= 0){
        return 0;
    }

    int arr[] = new int[target + 1];
    arr[0] = 1;
    for(int i = 1 ; i < arr.length ; i++){
        for(int j = 0 ; j < i ; j++){
            arr[i] += arr[j];
        }
    }

    return arr[target];
}
複製程式碼

但這仍不是最優解,因為觀察迴圈體你會發現,每次f(n)的計算都要從f(0)累加到f(n-1),我們完全可以將這個累加值快取起來preSum,每計算出一次f(N)之後都將快取更新為preSum += f(N)。如此得到最優解:

public int JumpFloorII(int target) {
    if(target <= 0){
        return 0;
    }

    int arr[] = new int[target + 1];
    arr[0] = 1;
    int preSum = arr[0];
    for(int i = 1 ; i < arr.length ; i++){
        arr[i] = preSum;
        preSum += arr[i];
    }

    return arr[target];
}
複製程式碼

矩形覆蓋

題目描述

我們可以用2*1的小矩形橫著或者豎著去覆蓋更大的矩形。請問用n個2*1的小矩形無重疊地覆蓋一個2*n的大矩形,總共有多少種方法?

public int RectCover(int target) {
        
}
複製程式碼

解析

遞迴版本

有了之前的歷練,我們能很快的寫出遞迴版本:先豎著放一個或者先橫著放兩個,剩下的交給遞迴處理:

//target 大矩形的邊長,也是剩餘小矩形的個數
public int RectCover(int target) {
    if(target <= 0){
        return 0;
    }
    if(target == 1 || target == 2){
        return target;
    }
    return RectCover(target - 1) + RectCover(target - 2);
}
複製程式碼
動態規劃

這仍然是個以f(1)=1,f(2)=2開頭的斐波那契數列:

//target 大矩形的邊長,也是剩餘小矩形的個數
public int RectCover(int target) {
    if(target <= 0){
        return 0;
    }
    if(target == 1 || target == 2){
        return target;
    }
    //n_1->f(n-1), n_2->f(n-2),從f(N=3)開始算起
    int n_1 = 2, n_2 = 1, N = 3, res = 0;
    while(N++ <= target){
        res = n_1 + n_2;
        n_2 = n_1;
        n_1 = res;
    }

    return res;
}
複製程式碼

二進位制中1的個數

題目描述

輸入一個整數,輸出該數二進位制表示中1的個數。其中負數用補碼錶示。

public int NumberOf1(int n) {
       
}
複製程式碼

解析

題目已經給我們降低了難度:負數用補碼(取反加1)表示表明輸入的引數為均為正數,我們只需統計其二進位制表示中1的個數、運算時只考慮無符號移位即可。

典型的判斷某個二進位制位上是否為1的方法是將該二進位制數右移至該二進位制位為最低位然後與1相與&,由於1的二進位制表示中只有最低位為1其餘位均為0,因此相與後的結果與該二進位制位上的數相同。據此不難寫出如下程式碼:

public int NumberOf1(int n) {
    int count = 0;
    for(int i = 0 ; i < 32 ; i++){
        count += ((n >> i) & 1);
    }
    return count;
}
複製程式碼

當然了,還有一種比較秀的解法就是利用n = n & (n - 1)n的二進位制位中為1的最低位置為0(只要n不為0就說明含有二進位制為1的位,如此這樣的操作能做多少次就說明有多少個二進位制位為1的位):

public int NumberOf1(int n) {
    int count = 0;
    while(n != 0){
        count++;
        n &= (n - 1);
    }
    return count;
}
複製程式碼

數值的整數次方

題目描述

給定一個double型別的浮點數base和int型別的整數exponent。求base的exponent次方。

public double Power(double base, int exponent) {
        
}
複製程式碼

解析

這是一道充滿危險色彩的題,求職者可能會內心竊喜不假思索的寫出如下程式碼:

public double Power(double base, int exponent) {
    double res = 1;
    for(int i = 1 ; i <= exponent ; i++){
        res *= base;
    }
	return res;
}
複製程式碼

但是你有沒有想過底數base和冪exponent都是可正、可負、可為0的。如果冪為負數,那麼底數就不能為0,否則應該丟擲算術異常:

//是否是負數
boolean minus = false;
//如果存在分母
if(exponent < 0){
    minus = true;
    exponent = -exponent;
    if(base == 0){
        throw new ArithmeticException("/ by zero");
    }
}
複製程式碼

如果冪為0,那麼根據任何不為0的數的0次方為1,0的0次方未定義,應該有如下判斷:

//如果指數為0
if(exponent == 0){
    if(base != 0){
        return 1;
    }else{
        throw new ArithmeticException("0^0 is undefined");
    }
}
複製程式碼

剩下的就是計算乘方結果,但是不要忘了如果冪為負需要將結果取倒數:

//指數不為0且分母也不為0,正常計算並返回整數或分數
double res = 1;
for(int i = 1 ; i <= exponent ; i++){
    res *= base;
}

if(minus){
    return 1/res;
}else{
    return res;
}
複製程式碼

也許你還可以錦上添花為冪乘方的計算引入二分計算(當冪為偶數時2^n = 2^(n/2) * 2^(n/2)):

public double binaryPower(double base, int exp){
    if(exp == 1){
        return base;
    }
    double res = 1;
    res *= (binaryPower(base, exp/2) * binaryPower(base, exp/2));
    return exp % 2 == 0 ? res : res * base;
}
複製程式碼

調整陣列順序使奇數位於偶數前面

題目描述

輸入一個整數陣列,實現一個函式來調整該陣列中數字的順序,使得所有的奇數位於陣列的前半部分,所有的偶數位於陣列的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變

public void reOrderArray(int [] arr) {
      
}
複製程式碼

解析

讀題之後發現這個跟快排的partition思路很像,都是選取一個比較基準將陣列分成兩部分,當然你也可以以arr[i] % 2 == 0為基準將奇數放前半部分,將偶數放有半部分,但是雖然只需O(n)的時間複雜度但不能保證調整後奇數之間、偶數之間的相對位置:

public void reOrderArray(int [] arr) {
    if(arr == null || arr.length == 0){
        return;
    }

    int odd = -1;
    for(int i = 0 ; i < arr.length ; i++){
        if(arr[i] % 2 == 1){
            swap(arr, ++odd, i);
        }
    }
}

public void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
複製程式碼

涉及到排序穩定性,我們自然能夠想到插入排序,從陣列的第二個元素開始向後依次確定每個元素應處的位置,確定的邏輯是:將該數與前一個數比較,如果比前一個數小則與前一個數交換位置並在交換位置後繼續與前一個數比較直到前一個數小於等於該數或者已達陣列首部停止。

此題不過是將比較的邏輯由數值的大小改為:當前的數是否是奇數並且前一個數是偶數,是則遞迴向前交換位置。程式碼示例如下:

public void reOrderArray(int [] arr) {
    if(arr == null || arr.length == 0){
        return;
    }

    int odd = -1;
    for(int i = 1 ; i < arr.length ; i++){
        for(int j = i ; j >= 1 ; j--){
            if(arr[j] % 2 == 1 && arr[j - 1] % 2 == 0){
                swap(arr, j, j - 1);
            }
        }
    }
}
複製程式碼

連結串列中倒數第K個結點

題目描述

輸入一個連結串列,輸出該連結串列中倒數第k個結點。

public ListNode FindKthToTail(ListNode head,int k) {
    
}
複製程式碼

解析

倒數,這又是一個從尾到頭的遍歷邏輯,而連結串列對從尾到頭遍歷是敏感的,前面我們有通過壓棧/遞迴、反轉連結串列的方式實現這個遍歷邏輯,自然對於此題同樣適用,但是那樣未免太麻煩了,我們可以通過兩個間距為(k-1)個結點的連結串列指標來達到此目的。

public ListNode FindKthToTail(ListNode head,int k) {
    //input check
    if(head == null || k <= 0){
        return null;
    }
    ListNode tmp = new ListNode(0);
    tmp.next = head;
    ListNode p1 = tmp, p2 = tmp;
    while(k > 0 && p1.next != null){
        p1 = p1.next;
        k--;
    }
    //length < k
    if(k != 0){
        return null;
    }
    while(p1 != null){
        p1 = p1.next;
        p2 = p2.next;
    }
    
    tmp = null; //help gc

    return p2;
}
複製程式碼

這裡使用了一個技巧,就是建立一個臨時結點tmp作為兩個指標的初始指向,以模擬p1先走k步之後,p2才開始走,沒走時停留在初始位置的邏輯,有利於幫我們梳理指標在對應位置上的意義,這樣當p1走到頭時(p1=null),p2就是倒數第k個結點。

這裡還有一個坑就是,筆者層試圖為了簡化程式碼將上述的9 ~ 12行寫成如下偷懶模式而導致排錯許久:

while(k-- > 0 && p1.next != null){
        p1 = p1.next;
}
複製程式碼

原因是將k--寫在while()中,無論判斷是否通過都會執行k = k - 1,因此程式碼總是會在if(k != 0)處返回null,希望讀者不要和筆者一樣粗心。

總結:當遇到複雜的指標操作時,我們不妨試圖多引入幾個指標或者臨時結點,以方便梳理我們的思路,加強程式碼的邏輯化,這些空間複雜度O(1)的操作通常也不會影響效能。

合併兩個排序的連結串列

題目描述

輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則。

public ListNode Merge(ListNode list1,ListNode list2) {
    
}
複製程式碼

解析

image

public ListNode Merge(ListNode list1,ListNode list2) {
    if(list1 == null || list2 == null){
        return list1 == null ? list2 : list1;
    }
    ListNode newHead = list1.val < list2.val ? list1 : list2;
    ListNode p1 = (newHead == list1) ? list1.next : list1;
    ListNode p2 = (newHead == list2) ? list2.next : list2;
    ListNode p = newHead;
    while(p1 != null && p2 != null){
        if(p1.val <= p2.val){
            p.next = p1;
            p1 = p1.next;
        }else{
            p.next = p2;
            p2 = p2.next;
        }
        p = p.next;
    }

    while(p1 != null){
        p.next = p1;
        p = p.next;
        p1 = p1.next;
    }
    while(p2 != null){
        p.next = p2;
        p = p.next;
        p2 = p2.next;
    }

    return newHead;
}
複製程式碼

樹的子結構

題目描述

輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}*/
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
    if(root1 == null || root2 == null){
        return false;
    }

    return process(root1, root2);
}
複製程式碼

解析

這是一道典型的分解求解的複雜問題。典型的二叉樹分解:遍歷頭結點、遍歷左子樹、遍歷右子樹。首先按照root1root2的值是否相等劃分為兩種情況:

  1. 兩個頭結點的值相等,並且root2.left也是roo1.left的子結構(遞迴)、root2.right也是root1.right的子結構(遞迴),那麼可返回true
  2. 否則,要看只有當root2root1.left的子結構或者root2root1.right的子結構時,才能返回true

據上述兩點很容易得出如下遞迴邏輯:

if(root1.val == root2.val){
    if(process(root1.left, root2.left) && process(root1.right, root2.right)){
        return true;
    }
}

return process(root1.left, root2) || process(root1.right, root2);
複製程式碼

接下來確定遞迴的終止條件,如果某個子過程root2=null那麼說明在自上而下的比較過程中root2的結點已被羅列比較完了,這時無論root1是否為null,該子過程都應該返回true

image

if(root2 == null){
    return true;
}
複製程式碼

但是如果root2 != nullroot1 = null,則應返回false

image

if(root1 == null && root2 != null){
    return false;
} 
複製程式碼

完整程式碼如下:

public class Solution {
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        if(root1 == null || root2 == null){
            return false;
        }

        return process(root1, root2);
    }

    public boolean process(TreeNode root1, TreeNode root2){
        if(root2 == null){
            return true;
        }
        if(root1 == null && root2 != null){
            return false;
        }  

        if(root1.val == root2.val){
            if(process(root1.left, root2.left) && process(root1.right, root2.right)){
                return true;
            }
        }

        return process(root1.left, root2) || process(root1.right, root2);
    }
}
複製程式碼

二叉樹的映象

題目描述

操作給定的二叉樹,將其變換為源二叉樹的映象。

image

public void Mirror(TreeNode root) {
        
}
複製程式碼

解析

由圖可知獲取二叉樹的映象就是將原樹的每個結點的左右孩子交換一下位置(這個規律一定要會找),也就是說我們只需遍歷每個結點並交換left,right的引用指向就可以了,而我們有成熟的先序遍歷:

public void Mirror(TreeNode root) {
    if(root == null){
        return;
    }

    TreeNode tmp = root.left;
    root.left = root.right;
    root.right = tmp;
    Mirror(root.left);
    Mirror(root.right);
}
複製程式碼

順時針列印矩陣

題目描述

輸入一個矩陣,按照從外向裡以順時針的順序依次列印出每一個數字,例如,如果輸入如下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.

public ArrayList<Integer> printMatrix(int [][] matrix) {
        
}
複製程式碼

解析

image

只要分析清楚了列印思路(左上角和右下角即可確定一條列印軌跡)後,此題主要考查條件控制的把握。只要給我一個左上角的點(i,j)和右下角的點(m,n),就可以將這一圈的列印分解為四步:

image

但是如果左上角和右下角的點在一行或一列上那就沒必要分解,直接列印改行或該列即可,列印的邏輯如下:

public void printEdge(int[][] matrix, int i, int j, int m, int n, ArrayList<Integer> res){
    if(i == m && j == n){
        res.add(matrix[i][j]);
        return;
    }

    if(i == m || j == n){
        //only one while will be execute
        while(i < m){
            res.add(matrix[i++][j]);
        }
        while(j < n){
            res.add(matrix[i][j++]);
        }
        res.add(matrix[m][n]);
        return;
    }

    int p = i, q = j;
    while(q < n){
        res.add(matrix[p][q++]);
    }
    //q == n
    while(p < m){
        res.add(matrix[p++][q]);
    }
    //p == m
    while(q > j){
        res.add(matrix[p][q--]);
    }
    //q == j
    while(p > i){
        res.add(matrix[p--][q]);
    }
    //p == i
}
複製程式碼

接著我們將每個圈的左上角和右下角傳入該函式即可:

public ArrayList<Integer> printMatrix(int [][] matrix) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){
        return res;
    }
    int i = 0, j = 0, m = matrix.length - 1, n = matrix[0].length - 1;
    while(i <= m && j <= n){
        printEdge(matrix, i++, j++, m--, n--, res);
    }
    return res;
}
複製程式碼

包含min函式的棧

題目描述

定義棧的資料結構,請在該型別中實現一個能夠得到棧中所含最小元素的min函式(時間複雜度應為O(1))。

public class Solution {

    
    public void push(int node) {
        
    }
    
    public void pop() {
        
    }
    
    public int top() {
        
    }
    
    public int min() {
        
    }
}
複製程式碼

解析

最直接的思路是使用一個變數儲存棧中現有元素的最小值,但這隻對只存不取的棧有效,當彈出的值不是最小值時還沒什麼影響,但當彈出最小值後我們就無法獲取當前棧中的最小值。解決思路是使用一個最小值棧,棧頂總是儲存當前棧中的最小值,每次資料棧存入資料時最小值棧就要相應的將存入後的最小值壓入棧頂:

private Stack<Integer> dataStack = new Stack();
private Stack<Integer> minStack = new Stack();

public void push(int node) {
    dataStack.push(node);
    if(!minStack.empty() && minStack.peek() < node){
        minStack.push(minStack.peek());
    }else{
        minStack.push(node);
    }
}

public void pop() {
    if(!dataStack.empty()){
        dataStack.pop();
        minStack.pop();
    }
}

public int top() {
    if(!dataStack.empty()){
        return dataStack.peek();
    }
    throw new IllegalStateException("stack is empty");
}

public int min() {
    if(!dataStack.empty()){
        return minStack.peek();
    }
    throw new IllegalStateException("stack is empty");
}
複製程式碼

棧的壓入、彈出序列

題目描述

輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序列。(注意:這兩個序列的長度是相等的)

public boolean IsPopOrder(int [] arr1,int [] arr2) {
     
}
複製程式碼

解析

可以使用兩個指標i,j,初始時i指向壓入序列的第一個,j指向彈出序列的第一個,試圖將壓入序列按照順序壓入棧中:

  1. 如果arr1[i] != arr2[j],那麼將arr1[i]壓入棧中並後移i(表示arr1[i]還沒到該它彈出的時刻)
  2. 如果某次後移i之後發現arr1[i] == arr2[j],那麼說明此刻的arr1[i]被壓入後應該被立即彈出才會產生給定的彈出序列,於是不壓入arr1[i](表示壓入並彈出了)並後移ij也要後移(表示彈出序列的arr2[j]記錄已產生,接著產生或許的彈出記錄即可)。
  3. 因為步驟2和3都會後移i,因此迴圈的終止條件是i到達arr1.length,此時若棧中還有元素,那麼從棧頂到棧底形成的序列必須與arr2j之後的序列相同才能返回true
public boolean IsPopOrder(int [] arr1,int [] arr2) {
    //input check
    if(arr1 == null || arr2 == null || arr1.length != arr2.length || arr1.length == 0){
        return false;
    }
    Stack<Integer> stack = new Stack();
    int length = arr1.length;
    int i = 0, j = 0;
    while(i < length && j < length){
        if(arr1[i] != arr2[j]){
            stack.push(arr1[i++]);
        }else{
            i++;
            j++;
        }
    }

    while(j < length){
        if(arr2[j] != stack.peek()){
            return false;
        }else{
            stack.pop();
            j++;
        }
    }

    return stack.empty() && j == length;
}
複製程式碼

從上往下列印二叉樹

題目描述

從上往下列印出二叉樹的每個節點,同層節點從左至右列印。

public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
       
}
複製程式碼

解析

使用一個佇列來儲存當前遍歷結點的孩子結點,首先將根節點加入佇列中,然後進行佇列非空迴圈:

  1. 從佇列頭取出一個結點,將該結點的值列印
  2. 如果取出的結點左孩子不空,則將其左孩子放入佇列尾部
  3. 如果取出的結點右孩子不空,則將其右孩子放入佇列尾部
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(root == null){
        return res;
    }
    LinkedList<TreeNode> queue = new LinkedList();
    queue.addLast(root);
    while(queue.size() > 0){
        TreeNode node = queue.pollFirst();
        res.add(node.val);
        if(node.left != null){
            queue.addLast(node.left);
        }
        if(node.right != null){
            queue.addLast(node.right);
        }
    }

    return res;
}
複製程式碼

二叉搜尋樹的後序遍歷序列

題目描述

輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的陣列的任意兩個數字都互不相同。

public boolean VerifySquenceOfBST(int [] sequence) {
        
}
複製程式碼

解析

對於二叉樹的後序序列,我們能夠確定最後一個數就是根結點,還能確定的是前一半部分是左子樹的後序序列,後一部分是右子樹的後序序列。

遇到這種複雜問題,我們仍能採用三步走戰略(根結點、左子樹、右子樹):

  1. 如果當前根結點的左子樹是BST且其右子樹也是BST,那麼才可能是BST
  2. 在1的條件下,如果左子樹的最大值小於根結點且右子樹的最小值大於根結點,那麼這棵樹就是BST

據此我們需要定義一個遞迴體,該遞迴體需要收集的資訊如下:下層需要向我返回其最大值、最小值、以及是否是BST

class Info{
    boolean isBST;
    int max;
    int min;
    Info(boolean isBST, int max, int min){
        this.isBST = isBST;
        this.max = max;
        this.min = min;
    }
}
複製程式碼

遞迴體的定義如下:

public Info process(int[] arr, int start, int end){
    if(start < 0 || end > arr.length - 1 || start > end){
        throw new IllegalArgumentException("invalid input");
    }
    //base case : only one node
    if(start == end){
        return new Info(true, arr[end], arr[end]);
    }

    int root = arr[end];
    Info left, right;
    //not exist left child
    if(arr[start] > root){
        right = process(arr, start, end - 1);
        return new Info(root < right.min && right.isBST, 
                        Math.max(root, right.max), Math.min(root, right.min));
    }
    //not exist right child
    if(arr[end - 1] < root){
        left = process(arr, start, end - 1);
        return new Info(root > left.max && left.isBST, 
                        Math.max(root, left.max), Math.min(root, left.min));
    }

    int l = 0, r = end - 1;
    while(r > l && r - l != 1){
        int mid = l + ((r - l) >> 1);
        if(arr[mid] > root){
            r = mid;
        }else{
            l = mid;
        }
    }
    left = process(arr, start, l);
    right = process(arr, r, end - 1);
    return new Info(left.isBST && right.isBST && root > left.max && root < right.min, 
                    right.max, left.min);
}
複製程式碼

總結:二叉樹相關的資訊收集問題分步走:

  1. 分析當前狀態需要收集的資訊
  2. 根據下層傳來的資訊加工出當前狀態的資訊
  3. 確定遞迴終止條件

二叉樹中和為某一值的路徑

題目描述

輸入一顆二叉樹的跟節點和一個整數,列印出二叉樹中結點值的和為輸入整數的所有路徑。路徑定義為從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。(注意: 在返回值的list中,陣列長度大的陣列靠前)

public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        
}
複製程式碼

解析

審題可知,我們需要有一個自上而下從根結點到每個葉子結點的遍歷思路,而先序遍歷剛好可以拿來用,我們只需在來到當前結點時將當前結點值加入到棧中,在離開當前結點時再將棧中儲存的當前結點的值彈出即可使用棧模擬儲存自上而下經過的結點,從而實現在來到每個葉子結點時只需判斷棧中數值之和是否為target即可。

public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
    ArrayList<ArrayList<Integer>> res = new ArrayList();
    if(root == null){
        return res;
    }
    Stack<Integer> stack = new Stack<Integer>();
    preOrder(root, stack, 0, target, res);
    return res;
}

public void preOrder(TreeNode root, Stack<Integer> stack, int sum, int target, 
                     ArrayList<ArrayList<Integer>> res){
    if(root == null){
        return;
    }

    stack.push(root.val);
    sum += root.val;
    //leaf node
    if(root.left == null && root.right == null && sum == target){
        ArrayList<Integer> one = new ArrayList();
        one.addAll(stack);
        res.add(one);
    }

    preOrder(root.left, stack, sum, target, res);
    preOrder(root.right, stack, sum, target, res);

    sum -= stack.pop();
}
複製程式碼

複雜連結串列的複製

題目描述

輸入一個複雜連結串列(每個節點中有節點值,以及兩個指標,一個指向下一個節點,另一個特殊指標指向任意一個節點),返回結果為複製後複雜連結串列的head。(注意,輸出結果中請不要返回引數中的節點引用,否則判題程式會直接返回空)

/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
*/
public class Solution {
    public RandomListNode Clone(RandomListNode pHead)
    {
        
    }
}
複製程式碼

解析

此題主要的難點在於random指標的處理。

方法一:使用雜湊表,額外空間O(n)

可以將連結串列中的結點都複製一份,用一個雜湊表來儲存,key是源結點,value就是副本結點,然後遍歷key取出每個對應的value將副本結點的next指標和random指標設定好:

public RandomListNode Clone(RandomListNode pHead){
    if(pHead == null){
        return null;
    }
    HashMap<RandomListNode, RandomListNode> map = new HashMap();
    RandomListNode p = pHead;
    //copy
    while(p != null){
        RandomListNode cp = new RandomListNode(p.label);
        map.put(p, cp);
        p = p.next;
    }
    //link
    p = pHead;
    while(p != null){
        RandomListNode cp = map.get(p);
        cp.next = (p.next == null) ? null : map.get(p.next);
        cp.random = (p.random == null) ? null : map.get(p.random);
        p = p.next;
    }

    return map.get(pHead);
}
複製程式碼
方法二:追加結點,額外空間O(1)

首先將每個結點複製一份並插入到對應結點之後,然後遍歷連結串列將副本結點的random指標設定好,最後將源結點和副本結點分離成兩個連結串列

public RandomListNode Clone(RandomListNode pHead){
    if(pHead == null){
        return null;
    }

    RandomListNode p = pHead;
    while(p != null){
        RandomListNode cp = new RandomListNode(p.label);
        cp.next = p.next;
        p.next = cp;
        p = p.next.next;
    }

    //more than two node
    //link random pointer
    p = pHead;
    RandomListNode cp;
    while(p != null){
        cp = p.next;
        cp.random = (p.random == null) ? null : p.random.next;
        p = p.next.next;
    }

    //split source and copy
    p = pHead;
    RandomListNode newHead = p.next;
    //p != null -> p.next != null
    while(p != null){
        cp = p.next;
        p.next = p.next.next;
        p = p.next;
        cp.next = (p == null) ? null : p.next;
    }

    return newHead;
}
複製程式碼

相關文章