劍指offer——Java刷題總結【一】

Note

  • 題解彙總:劍指offer題解彙總
  • 代碼地址:Github 劍指offer Java實現彙總
  • 點擊目錄中的題名鏈接可直接食用題解~
  • 有些解法博文中未實現,不代表一定很難,可能只是因爲博主太懶```(Orz)
  • 如果博文中有明顯錯誤或者某些題目有更加優雅的解法請指出,謝謝~

目錄

題號 題目名稱
1 二維數組中的查找
2 替換空格
3 從尾到頭打印鏈表
4 重建二叉樹
5 用兩個棧實現隊列
6 旋轉數組的最小數字
7 斐波那契數列
8 跳臺階
9 變態跳臺階
10 矩形覆蓋

正文

1、二維數組中的查找

題目描述

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

題目分析

解法一: 我們可以利用該二維數組的性質:每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。也就是說,對於數組左下角的值 m,m 是該行最小的數,同時也是該列最大的數。我們只需要每次將 m 和目標值 target 進行比較:
1、當 m < target 時,由於 m 是當前列的最大元素,想要得到更大的數只能對列進行右移;
2、當 m > target 時,由於 m 是當前行的最小元素,想要得到更小的數只能對行進行上移;
3、當 m = target 時,找到該值,返回 true。
用某行最小或某列最大與 target 比較,每次可剔除一整行或一整列。故時間複雜度是O(m+n),使用兩層循環暴力破解需要時間複雜度爲O(n²)。

代碼實現

解法一:

public boolean Find(int target, int [][] array) {
    if (array == null || array.length == 0) return false;
    if (array[0].length == 0) return false;
    int row = array.length - 1;
    int col = 0;
    do {
        if (array[row][col] < target) {
            col++;
        } else if (array[row][col] > target) {
            row--;
        } else {
            return true;
        }
    } while (row >= 0 && col < array[0].length);
    return false;
}

2、替換空格

題目描述

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

題目分析

解法一: 用Java自帶的替換函數,str.toString().replace(" “,”%20")。
解法二: 在當前字符串上進行替換。
1、先計算替換後的字符串需要多大的空間,並對原字符串空間進行擴容;
2、從後往前替換字符串的話,每個字符串只需要移動一次;
3、如果從前往後,每個字符串需要多次移動,效率較低。
解法三: 開闢一個新的字符串。與解法二基本一致,只是不需要對字符進行移動,而是將需要替換的字符直接添加到新的字符串上,需要整個字符串的額外空間。

代碼實現

解法一:

public String replaceSpace(StringBuffer str) {
    if (str == null || str.length() == 0) return "";
    return str.toString().replace(" ","%20");
}

解法二:

public static String replaceSpace1(StringBuffer str) {
    if (str == null || str.length() == 0) return "";
    int count = 0;
    for (int i = 0; i < str.length(); i++) {
        if (str.charAt(i) == ' ') {
            count++;
        }
    }
    int oldLen = str.length();
    str.setLength(oldLen + 2 * count);
    for (int i = oldLen - 1; i >= 0; i--) {
        if (str.charAt(i) != ' ') {
            str.setCharAt(i + 2 * count, str.charAt(i));
        } else {
            str.setCharAt(i + 2 * count, '0');
            str.setCharAt(i + 2 * count - 1, '2');
            str.setCharAt(i + 2 * count - 2, '%');
            count--;
        }
    }
    return str.toString();
}

解法三:

public String replaceSpace(StringBuffer str) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (c == ' ') {
            sb.append("%20");
        } else {
            sb.append(c);
        }
    }
    return sb.toString();
}

3、從尾到頭打印鏈表

題目描述

輸入一個鏈表,按鏈表從尾到頭的順序返回一個ArrayList。

題目分析

解法一: 依次遍歷鏈表並將每個鏈表元素插入到ArrayList中,最後使用Collection.reverse對ArrayList進行翻轉並返回。
解法二: ArrayList中有個方法是add(index,value),可以指定index位置插入value值。依次遍歷鏈表並將每個鏈表元素插入到list的0位置,最後返回ArrayList即可得到逆序鏈表。
解法三: 利用遞歸,藉助系統的棧將元素壓棧,依次出棧並將元素添加至ArrayList中即可完成逆序。

代碼實現

解法一:

public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ListNode head = listNode;
    ArrayList<Integer> list = new ArrayList<>();
    while (head != null) {
        list.add(head.val);
        head = head.next;
    }
    Collections.reverse(list);
    return list;
}

解法二:

public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> list = new ArrayList<>();
    ListNode tmp = listNode;
    while(tmp != null){
        list.add(0, tmp.val);
        tmp = tmp.next;
    }
    return list;
}

解法三:

ArrayList<Integer> list = new ArrayList();
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    if (listNode != null) {
        printListFromTailToHead(listNode.next);
        list.add(listNode.val);
    }
    return list;
}

4、重建二叉樹

題目描述

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

題目分析

解法一:
根據中序遍歷和前序遍歷可以確定二叉樹,具體過程爲:

  1. 根據前序序列第一個元素確定根結點; 遍歷中序序列,查找前序序列第一個節點在中序序列所在位置;
  2. 根據根結點在中序序列中的位置分割出左右兩個子序列,分別代表左子樹和右子樹的中序遍歷結果;
  3. 根據左右子樹的中序遍歷長度,計算出左右子樹的前序遍歷結果;
  4. 現已獲得左右子樹的前序和中序遍歷結果,對左右子樹按照1-4步驟進行遞歸重構。

例如對於前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6}而言,我們的步驟是:

  1. 根據前序遍歷第一個元素,確定1爲根節點;
  2. 遍歷中序序列,查找到第四個元素爲根節點1,因此對於根節點1而言,左子樹的中序遍歷序列爲{4,7,2},右子樹的中序遍歷序列爲{5,3,8,6};
  3. 根據左子樹的中序遍歷長度爲3,可以知道在前序遍歷序列中,{2,4,7}爲左子樹的前序遍歷序列,{3,5,6,8}爲右子樹的前序遍歷序列;
  4. 根據左子樹的前序和中序遍歷序列分別爲{2,4,7}和{4,7,2},可以按照1-3步驟遞歸重構出一顆樹,並將它作爲1的左子樹;
  5. 根據右子樹的前序和中序遍歷序列分別爲{3,5,6,8}和{5,3,8,6},可以按照1-3步驟遞歸重構出一顆樹,並將它作爲1的右子樹;
  6. 直至前序序列或中序序列長度爲0,說明到達葉子節點。返回空,結束遞歸。
代碼實現

解法一:

public TreeNode reConstructBinaryTree(int [] pre, int [] in) {
    if (pre.length == 0 || in.length == 0) {
        return null;
    }
    TreeNode h = new TreeNode(pre[0]);
    int index = -1;
    for (int i = 0; i < in.length; i++) {
        if (in[i] == pre[0]) {
            index = i;
        }
    }
    int[] in1 = Arrays.copyOfRange(in, 0, index);
    int[] in2 = Arrays.copyOfRange(in, index + 1, in.length);
    int[] p1 = Arrays.copyOfRange(pre, 1, in1.length + 1);
    int[] p2 = Arrays.copyOfRange(pre, in1.length + 1, pre.length);
    h.left = reConstructBinaryTree(p1, in1);
    h.right = reConstructBinaryTree(p2, in2);
    return h;
}

5、用兩個棧實現隊列

題目描述

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

題目分析

解法一: 定義兩個棧,棧A和棧B。棧A只負責正常的push;當棧B有元素時則直接pop,當棧B沒有元素時,將棧A中的元素全部、一次性地pop到棧B,然後對棧B進行pop。

代碼實現

解法一:

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()) {
            return stack2.pop();
        } else {
            return -1;
        }
    }
}

6、旋轉數組的最小數字

題目描述

把一個數組最開始的若干個元素搬到數組的末尾,我們稱之爲數組的旋轉。輸入一個非遞減排序的數組的一個旋轉,輸出旋轉數組的最小元素。例如數組{3,4,5,1,2}爲{1,2,3,4,5}的一個旋轉,該數組的最小值爲1。

NOTE:給出的所有元素都大於0,若數組大小爲0,請返回0。

題目分析

解法一: 一次遍歷。數組在發生旋轉後,以最小元素爲中心的左右兩端數組都是升序的,因此我們可以對數組進行遍歷,當某個元素比它前一個元素小,則說明該元素是數組中的最小元素。
解法二: 二分查找。二分查找用於查找有序的數組中的值,題目所給數組在兩段範圍內有序,我們可以將給定數組分爲兩種情況:

  1. 其實並沒有旋轉,例如 {1,2,3,4,5},旋轉後也是 {1,2,3,4,5},這樣可以直接使用二分查找;
  2. 如題所示,旋轉了一部分,例如 {1,2,3,4,5},旋轉後爲 {3,4,5,1,2},需要限定特殊條件後使用二分查找。

當數組如情況 1,有個鮮明的特徵,即數組左邊元素 < 數組右邊元素,這時我們直接返回首元素即可;
當數組如情況 2,此時有三種可能找到最小值:

  1. 下標爲 n+1 的值小於下標爲 n 的值,則下標爲 n+1 的值肯定是最小元素;
  2. 下標爲 n 的值小於下標爲 n-1 的值,則下標爲 n 的值肯定是最小元素;
  3. 由於不斷查找,數組查找範圍內的值已經全爲非降序(退化爲情況1)。

再討論每次二分查找時範圍的變化,由於情況數組的情況 1 能直接找到最小值,需要變化範圍的肯定是情況 2:

  1. 當下標爲 n 的值大於下標爲 0 的值,從 0 到 n 這一段肯定是升序,由於是情況 2,最小值肯定在後半段;
  2. 當下標爲 n 的值小於下標爲 0 的值,從 0 到 n 這一段不是升序,最小值肯定在這一段。
代碼實現

解法一: O(n)

public int minNumberInRotateArray(int [] array) {
    if (array.length == 0) return 0;
    int index = 0;
    for (int i = 1; i < array.length; i++) {
        if (array[i] < array[i - 1]) {
            index = i;
            break;
        }
    }
    return array[index];
}

解法二: O(logn)

public int minNumberInRotateArray(int [] array) {
    if (array.length == 1) return array[0];
    int l = 0;
    int r = array.length - 1;
    while (l < r) {
        int m = l + ((r - l) >> 1);
        if (array[l] < array[r]) {
            return array[l];
        }
        if (array[m] > array[m + 1]) {
            return array[m + 1];
        }
        if (array[m] < array[m - 1]) {
            return array[m];
        }
        if (array[m] > array[0]) {
            l = m + 1;
        } else {
            r = m - 1;
        }
    }
    return 0;
}

7、斐波那契數列

題目描述

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

題目分析

解法一: 遞歸。根據F(n)=F(n-1)+F(n-2)遞歸調用即可。
解法二: 動態規劃。可以選擇一個長度爲n的數組用於保存每次計算的值,遍歷數組,根據數組之前的元素得到當前元素,在該情況下空間複雜度爲O(n)。由於在遍歷過程中,實際上只用到n-1和n-2兩個位置的值,因此可以只使用兩個變量對歷史的計算結果進行存儲,將空間複雜度優化到O(1)。

代碼實現

解法一: O(2^n)

public int Fibonacci(int n) {
    if (n <= 1) return n;
    return Fibonacci(n-1) + Fibonacci(n-2);
}

解法一: O(n)

public int Fibonacci(int n) {
    if (n == 0) return 0;
    if (n <= 2) return 1;
    int pre1 = 1;
    int pre2 = 1;
    int res = 0;
    for (int i = 3; i <= n; i++) {
        res = pre1 + pre2;
        pre1 = pre2;
        pre2 = res;
    }
    return res;
}

8、跳臺階

題目描述

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

題目分析

解法一: 動態規劃。斐波那契數列的變種,上一題的優化解法需要三個額外變量,該解法只需要兩個額外變量,sum用來存儲當前值,pre用來存儲前一個值,但是空間複雜度仍爲O(1)。

代碼實現

解法一: O(n)

public int JumpFloor(int target) {
    if (target < 2) return 1;
    int pre = 1;
    int sum = 1;
    for (int i = 2; i <= target; i++) {
        sum = sum + pre;
        pre = sum - pre;
    }
    return sum;
}

9、變態跳臺階

題目描述

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

題目分析

解法一: 動態規劃。斐波那契數列的變種。由於青蛙每次可以跳n級臺階,因此青蛙跳上n級臺階的跳法爲f(n)=f(n-1)+f(n-2)+……f(1)+f(0)。經過分析,發現結果呈規律性,f(n)以2的n次冪形式增長,f(0)=1,f(1)=1,f(2)=2…從而可以歸納得到計算表達式:f(n)=2^(n-1)。

代碼實現

解法一: O(1)

public int JumpFloorII(int target) {
    return target == 0 ? 1 : (int) Math.pow(2, target - 1);
}

10、矩形覆蓋

題目描述

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

題目分析

解法一: 動態規劃。斐波那契數列的變種。其實和第9題是一樣的,f(n)=f(n-1)+f(n-2)。因爲每次多加1*2的矩形時,無非就是f(n-1)種然後加上一條豎着的矩陣,或者f(n-2)種然後加上兩條橫着的矩陣。在該解法中使用的是遞歸方法求解,時間複雜度極高。在這僅僅是給出遞歸解的實現,建議使用時間複雜度爲O(n)、空間複雜度爲O(1)的最優解法(參考第7-9題)。

代碼實現

解法一: O(2^n)

public int RectCover(int target) {
    if (target == 0) return 0;
    if (target == 1) return 1;
    if (target == 2) return 2;
    return RectCover(target - 1) + RectCover(target - 2);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章