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

zanwensicheng發表於2019-02-19

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

二叉搜尋樹與雙向連結串列

題目描述

輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。

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

解析

典型的二叉樹分解問題,我們可以定義一個黑盒transform,它的目的是將二叉樹轉換成雙向連結串列,那麼對於一個當前結點root,首先將其前驅結點(BST中前驅結點指中序序列的前一個數值,也就是當前結點的左子樹上最右的結點,如果左子樹為空則沒有前驅結點)和後繼結點(當前結點的右子樹上的最左結點,如果右子樹為空則沒有後繼結點),然後使用黑盒transform將左子樹和右子樹轉換成雙向連結串列,最後將當前結點和左子樹形成的連結串列鏈起來(通過之前儲存的前驅結點)和右子樹形成的連結串列鏈起來(通過之前儲存的後繼結點),整棵樹的轉換完畢。

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

    //head is the most left node
    TreeNode head = root;
    while(head.left != null){
        head = head.left;
    }
    transform(root);
    return head;
}

//transform a tree to a double-link list
public void transform(TreeNode root){
    if(root == null){
        return;
    }
    TreeNode pre = root.left, next = root.right;
    while(pre != null && pre.right != null){
        pre = pre.right;
    }
    while(next != null && next.left != null){
        next = next.left;
    }

    transform(root.left);
    transform(root.right);
    //asume the left and right has transformed and what's remaining is link the root
    root.left = pre;
    if(pre != null){
        pre.right = root;
    }
    root.right = next;
    if(next != null){
        next.left = root;
    }
}
複製程式碼

字串全排列

題目描述

輸入一個字串,按字典序列印出該字串中字元的所有排列。例如輸入字串abc,則列印出由字元a,b,c所能排列出來的所有字串abc,acb,bac,bca,cab和cba。

解析

定義一個遞迴體generate(char[] arr, int index, TreeSet<String> res),其中char[] arrindex組合表示上層狀態給當前狀態傳遞的資訊,即arr0 ~ index-1是已生成好的串,現在你(當前狀態)要確定index位置上應該放什麼字元(你可以從index ~ arr.length - 1上任選一個字元),然後將index + 1應該放什麼字元遞迴交給子過程處理,當某個狀態要確定arr.length上應該放什麼字元時說明0 ~ arr.length-1位置上的字元已經生成好了,因此遞迴終止,將生成好的字串記錄下來(這裡由於要求不能重複且按字典序排列,因此我們可以使用JDK中紅黑樹的實現TreeSet來做容器)

public ArrayList<String> Permutation(String str) {
    ArrayList<String> res = new ArrayList();
    if(str == null || str.length() == 0){
        return res;
    }
    TreeSet<String> set = new TreeSet();
    generate(str.toCharArray(), 0, set);
    res.addAll(set);
    return res;
}

public void generate(char[] arr, int index, TreeSet<String> res){
    if(index == arr.length){
        res.add(new String(arr));
    }
    for(int i = index ; i < arr.length ; i++){
        swap(arr, index, i);
        generate(arr, index + 1, res);
        swap(arr, index, i);
    }
}

public void swap(char[] arr, int i, int j){
    if(arr == null || arr.length == 0 || i < 0 || j > arr.length - 1){
        return;
    }
    char tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}
複製程式碼

注意:上述程式碼的第19行有個坑,筆者曾因忘記寫第19行而排錯許久,由於你任選一個index ~ arr.length - 1位置上的字元與index位置上的交換並將交換生成的結果交給了子過程(第17,18行),但你不應該影響後續選取其他字元放到index位置上而形成的結果,因此需要再交換回來(第19行)

陣列中出現次數超過一半的數

題目描述

陣列中有一個數字出現的次數超過陣列長度的一半,請找出這個數字。例如輸入一個長度為9的陣列{1,2,3,2,2,2,5,4,2}。由於數字2在陣列中出現了5次,超過陣列長度的一半,因此輸出2。如果不存在則輸出0。

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

解析

方法一:基於partition查詢陣列中第k大的數

如果我們將陣列排序,最快也要O(nlogn),排序後的中位數自然就是出現次數超過長度一半的數。

我們知道快排的partition操作能夠將陣列按照一個基準劃分成小於部分和大於等於部分並返回這個基準在陣列中的下標,雖然一次partition並不能使陣列整體有序,但是能夠返回隨機選擇的數在partition之後的下標index,這個下標標識了它是第index大的數,這也意味著我們要求陣列中第k大的數不一定要求陣列整體有序。

於是我們在首次對整個陣列partition之後將返回的indexn/2進行比較,並調整下一次partition的範圍直到index = n/2為止我們就找到了。

這個時間複雜度需要使用Master公式計算(計算過程參見 www.zhenganwen.top/62859a9a.ht…使用partition查詢陣列中第k大的數時間複雜度為O(n),最後不要忘了驗證一下index = n/2上的數出現的次數是否超過了長度的一半。

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

    int index = partition(arr, 0, arr.length - 1);
    int half = arr.length >> 1;// 0 <= half <= arr.length - 1
    while(index != half){
        index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1);
    }

    int count = 0;
    for(int i = 0 ; i < arr.length ; i++){
        count = (arr[i] == arr[index]) ? ++count : count;
    }

    return (count > arr.length / 2) ? arr[index] : 0;
}

public int partition(int[] arr, int start, int end){
    if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){
        throw new IllegalArgumentException();
    }
    if(start == end){
        return end;
    }
    int random = start + (int)(Math.random() * (end - start + 1));
    swap(arr, random, end);
    int small = start - 1;
    for(int i = start ; i < end ; i++){
        if(arr[i] < arr[end]){
            swap(arr, ++small, i);
        }
    }

    swap(arr, ++small, end);

    return small;
}

public void swap(int[] arr, int i, int j){
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}
複製程式碼
方法二
  1. 使用一個target記錄一個數,並使用count記錄它出現的次數
  2. 初始時target = arr[0]count = 1,表示arr[0]出現了1次
  3. 從第二個元素開始遍歷陣列,如果遇到的數不等於target就將count減1,否則加1
  4. 如果遍歷到某個數時,count為0了,那麼就將target設定為該數,並將count置1,繼續向後遍歷

如果存在出現次數超過一半的數,那麼必定是target最後一次被設定時的數。

public int MoreThanHalfNum_Solution(int [] arr) {
    if(arr == null || arr.length == 0){
        return 0;
    }
    //此題需要抓住出現次數超過陣列長度的一半這個點來想
    //使用一個計數器,如果這個數出現一次就自增,否則自減,如果自減為0則更新被記錄的數
    //如果存在出現次數大於一半的數,那麼最後一次被記錄的數就是所求之數
    int target = arr[0], count = 1;
    for(int i = 1 ; i < arr.length ; i++){
        if(count == 0){
            target = arr[i];
            count = 1;
        }else{
            count = (arr[i] == target) ? ++count : --count;
        }
    }

    if(count == 0){
        return 0;
    }

    //不要忘了驗證!!!
    count = 0;
    for(int i = 0 ; i < arr.length ; i++){
        count = (arr[i] == target) ? ++count : count;
    }

    return (count > arr.length / 2) ? target : 0;
}
複製程式碼

最小的k個數

題目描述

輸入n個整數,找出其中最小的K個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4,。

public ArrayList<Integer> GetLeastNumbers_Solution(int [] arr, int k) {
    
}
複製程式碼

解析

與上一題的求陣列第k大的數如出一轍,如果某次partition之後你得到了第k大的數的下標,那麼根據partitin規則該下標左邊的數均比該下標上的數小,最小的k個數自然就是此時的0~k-1下標上的數

public ArrayList<Integer> GetLeastNumbers_Solution(int [] arr, int k) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(arr == null || arr.length == 0 || k <= 0 || k > arr.length){
        //throw new IllegalArgumentException();
        return res;
    }

    int index = partition(arr, 0, arr.length - 1);
    k = k - 1;
    while(index != k){
        index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1);
    }

    for(int i = 0 ; i <= k ; i++){
        res.add(arr[i]);
    }

    return res;
}

public int partition(int[] arr, int start, int end){
    if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){
        throw new IllegalArgumentException();
    }
    if(start == end){
        return end;
    }

    int random = start + (int)(Math.random() * (end - start + 1));
    swap(arr, random, end);
    int small = start - 1;
    for(int i = start ; i < end ; i++){
        if(arr[i] < arr[end]){
            swap(arr, ++small, i);
        }
    }

    swap(arr, ++small, end);
    return small;
}

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

連續子陣列的最大和

題目描述

HZ偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全為正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和為8(從第0個開始,到第3個為止)。給一個陣列,返回它的最大連續子序列的和,你會不會被他忽悠住?(子向量的長度至少是1)

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

解析

暴力解

暴力法是找出所有子陣列,然後遍歷求和,時間複雜度為O(n^3)

public int FindGreatestSumOfSubArray(int[] arr) {
    if(arr == null || arr.length == 0){
        return 0;
    }
    int max = Integer.MIN_VALUE;

    //start
    for(int i = 0 ; i < arr.length ; i++){
        //end
        for(int j = i ; j < arr.length ; j++){
            //sum
            int sum = 0;
            for(int k = i ; k <= j ; k++){
                sum += arr[k];
            }
            max = Math.max(max, sum);
        }
    }

    return max;
}
複製程式碼
最優解

使用一個sum記錄累加和,初始時為0,遍歷陣列:

  1. 如果遍歷到i時,發現sum小於0,那麼丟棄這個累加和,將sum重置為0
  2. 將當前元素累加到sum上,並更新最大和maxSum
public int FindGreatestSumOfSubArray(int[] arr) {
    if(arr == null || arr.length == 0){
        return 0;
    }
    int sum = 0, max = Integer.MIN_VALUE;
    for(int i = 0 ; i < arr.length ; i++){
        if(sum < 0){
            sum = 0;
        }
        sum += arr[i];
        max = Math.max(max, sum);
    }

    return max;
}
複製程式碼

整數中1出現的次數(從1到n整數中1出現的次數)

題目描述

求出1~13的整數中1出現的次數,並算出100~1300的整數中1出現的次數?為此他特別數了一下1~13中包含1的數字有1、10、11、12、13因此共出現6次,但是對於後面問題他就沒轍了。ACMer希望你們幫幫他,並把問題更加普遍化,可以很快的求出任意非負整數區間中1出現的次數(從1 到 n 中1出現的次數)。

解析

遍歷一遍不就完了嗎

當然,你可從1遍歷到n,然後將當前被遍歷的到的數中1出現的次數累加到結果中可以很容易地寫出如下程式碼:

public int NumberOf1Between1AndN_Solution(int n) {
    if(n < 1){
        return 0;
    }
    int res = 0;
    for(int i = 1 ; i <= n ; i++){
        res += count(i);
    }
    return res;
}

public int count(int n){
    int count = 0;
    while(n != 0){
        //取個位
        count = (n % 10 == 1) ? ++count : count;
        //去掉個位
        n /= 10;
    }
    return count;
}
複製程式碼

但n多大就會迴圈多少次,這並不是面試官所期待的,這時我們就需要找規律看是否有捷徑可走

不用數我也知道

51234這個數為例,我們可以先將51234劃分成1~1234(去掉最高位)和1235~51234兩部分來求解。下面先分析1235~51234這個區間的結果:

  1. 所有的數中,1在最高位(萬位)出現的次數

    對於1235~51234,最高位為1時(即萬位為1時)的數有10000~19999這10000個數,也就是說1在最高位(萬位)出現的次數為10000,因此我們可以得出結論:如果最高位大於1,那麼在最高位上1出現的次數為最高位對應的單位(本例中為一萬次);但如果最高位為1,比如1235~11234,那麼次數就為去掉最高位之後的數了,11234去掉最高位後是1234,即1在最高位上出現的次數為1234

  2. 所有的數中,1在非最高位上出現的次數

    我們可以進一步將1235~51234按照最高位的單位劃分成4個區間(能劃分成幾個區間由最高位上的數決定,這裡最高位為5,所以能劃分5個大小為一萬子區間):

    • 1235~11234
    • 11235~21234
    • 21235~31234
    • 31235~41234
    • 41235~51234

    而每個數不考慮萬位(因為1在萬位出現的總次數在步驟1中已統計好了),其餘四位(個、十、百、千)取一位放1(比如千位),剩下的3位從0~9中任意選(10 * 10 * 10),那麼僅統計1在千位上出現的次數之和就是:5(子區間數) * 10 * 10 * 10,還有百位、十位、個位,結果為:4 * 10 * 10 * 10 * 5

    因此非高位上1出現的總次數的計算通式為:(n-1) * 10^(n-2) * 十進位制最高位上的數(其中n為十進位制的總位數)

    於是1235 ~ 51234之間所有的數的所有的位上1出現的次數的綜合我們就計算出來了

剩下1 ~ 1234,你會發現這與1 ~ 51234的問題是一樣的,因此可以做遞迴處理,即子過程也會將1 ~ 1234也分成1 ~ 234235 ~ 1234兩部分,並計算235~1234而將1~234又進行遞迴處理。

而遞迴的終止條件如下:

  1. 如果1~n中的n1 <= n <= 9,那麼就可以直接返回1了,因為只有數1出現了一次1
  2. 如果n == 0,比如將10000劃分成的兩部分是0 ~ 0(10000去掉最高位後的結果)1 ~ 10000,那麼就返回0
public int NumberOf1Between1AndN_Solution(int n) {
    if(n < 1){
        return 0;
    }
    return process(n);
}

public int process(int n){
    if(n == 0){
        return 0;
    }
    if(n < 10 && n > 0){
        return 1;
    }
    int res = 0;
    //得到十進位制位數
    int bitCount = bitCount(n);
    //十進位制最高位上的數
    int highestBit = numOfBit(n, bitCount);
    //1、統計最高位為1時,共有多少個數
    if(highestBit > 1){
        res += powerOf10(bitCount - 1);
    }else{
        //highestBit == 1
        res += n - powerOf10(bitCount - 1) + 1;
    }
    //2、統計其它位為1的情況
    res += powerOf10(bitCount - 2) * (bitCount - 1) * highestBit;
    //3、剩下的部分交給遞迴
    res += process(n % powerOf10(bitCount - 1));
    return res;
}

//返回10的n次方
public int powerOf10(int n){
    if(n == 0){
        return 1;
    }
    boolean minus = false;
    if(n < 0){
        n = -n;
        minus = true;
    }
    int res = 1;
    for(int i = 1 ; i <= n ; i++){
        res *= 10;
    }
    return minus ? 1 / res : res;
}

public int bitCount(int n){
    int count = 1;
    while((n /= 10) != 0){
        count++;
    }
    return count;
}

public int numOfBit(int n, int bit){
    while(bit-- > 1){
        n /= 10;
    }
    return n % 10;
}
複製程式碼

筆者曾糾結,對於一個四位數,每個位上出現1時都統計了一遍會不會有重複,比如11111這個數在最高位為1時的10000 ~ 19999統計了一遍,在統計非最高位的其他位上為1時又統計了4次,總共被統計了5次,而這個數1出現的次數也確實是5次,因此沒有重複。

把陣列排成最小的數

題目描述

輸入一個正整數陣列,把陣列裡所有數字拼接起來排成一個數,列印能拼接出的所有數字中最小的一個。例如輸入陣列{3,32,321},則列印出這三個數字能排成的最小數字為321323。

解析

這是一個貪心問題,你發現將陣列按遞增排序之後依次連線起來的結果並不是最優的結果,於是需要尋求貪心策略,對於這類最小數和最小字典序的問題而言,貪心策略是:如果332相連的結果大於323相連的結果,那麼視作332大,最後我們需要按照按照這種策略將陣列進行升序排序,以得到首尾相連之後的結果是最小數字(最小字典序)。

public String PrintMinNumber(int [] numbers) {
    if(numbers == null || numbers.length == 0){
        return "";
    }
    List<Integer> list = new ArrayList();
    for(int num : numbers){
        list.add(num);
    }
    Collections.sort(list, new MyComparator());
    StringBuilder res = new StringBuilder("");
    for(Integer integer : list){
        res.append(integer.toString());
    }
    return res.toString();
}

class MyComparator implements Comparator<Integer>{
    public int compare(Integer i1, Integer i2){
        String s1 = i1.toString() + i2.toString();
        String s2 = i2.toString() + i1.toString();
        return Integer.parseInt(s1) - Integer.parseInt(s2);
    }
}
複製程式碼

醜數

題目描述

把只包含質因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因為它包含質因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。

解析

老實說,在《劍指offer》上看這道題的時候每太看懂,以至於第一遍在牛客網OJ這道題的時候都是背下來寫上去的,直到這第二遍總結時才弄清整個思路,思路的核心就是第一個醜數是1(題目給的),此後的每一個醜數都是由之前的某個醜數與2或3或5的乘積得來

image

public int GetUglyNumber_Solution(int index) {
    if(index < 1){
        //throw new IllegalArgumentException("index must bigger than one");
        return 0;
    }
    if(index == 1){
        return 1;
    }

    int[] arr = new int[index];
    arr[0] = 1;
    int indexOf2 = 0, indexOf3 = 0, indexOf5 = 0;
    for(int i = 1 ; i < index ; i++){
        arr[i] = Math.min(arr[indexOf2] * 2, Math.min(arr[indexOf3] * 3, arr[indexOf5] * 5));
        indexOf2 = (arr[indexOf2] * 2 <= arr[i]) ? ++indexOf2 : indexOf2;
        indexOf3 = (arr[indexOf3] * 3 <= arr[i]) ? ++indexOf3 : indexOf3;
        indexOf5 = (arr[indexOf5] * 5 <= arr[i]) ? ++indexOf5 : indexOf5;
    }

    return arr[index - 1];
}
複製程式碼

第一個只出現一次的字元

題目描述

在一個字串(0<=字串長度<=10000,全部由字母組成)中找到第一個只出現一次的字元,並返回它的位置, 如果沒有則返回 -1(需要區分大小寫).

解析

可以從頭遍歷字串,並使用一個表記錄每個字元第一次出現的位置(初始時表中記錄的位置均為-1),如果記錄當前被遍歷字元出現的位置時發現之前已經記錄過了(通過查表,該字元的位置不是-1而是大於等於0的一個有效索引),那麼當前字元不在答案的考慮範圍內,通過將表中該字元的出現索引標記為-2來標識。

遍歷一遍字串並更新表之後,再遍歷一遍字串,如果發現某個字元在表中對應的記錄是一個有效索引(大於等於0),那麼該字元就是整個串中第一個只出現一次的字元。

由於題目標註字串全都由字母組成,而字母可以使用ASCII碼錶示且ASCII範圍為0~255,因此使用了一個長度為256的陣列來實現這張表。用字母的ASCII值做索引,索引對應的值就是字母在字串中第一次出現的位置(初始時為-1,第一次遇到時設定為出現的位置,重複遇到時置為-2)。

public int FirstNotRepeatingChar(String str) {
    if(str == null || str.length() == 0){
        return -1;
    }
    //全部由字母組成
    int[] arr = new int[256];
    for(int i = 0 ; i < arr.length ; i++){
        arr[i] = -1;
    }
    for(int i = 0 ; i < str.length() ; i++){
        int ascii = (int)str.charAt(i);
        if(arr[ascii] == -1){
            //set index of first apearance
            arr[ascii] = i;
        }else if(arr[ascii] >= 0){
            //repeated apearance, don't care
            arr[ascii] = -2;
        }
        //arr[ascii] == -2 -> do not care
    }

    for(int i = 0 ; i < str.length() ; i++){
        int ascii = (int)str.charAt(i);
        if(arr[ascii] >= 0){
            return arr[ascii];
        }
    }

    return -1;
}
複製程式碼

陣列中的逆序對

題目描述

在陣列中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個陣列,求出這個陣列中的逆序對的總數P。並將P對1000000007取模的結果輸出。 即輸出P%1000000007

public int InversePairs(int [] arr) {
    if(arr == null || arr.length <= 1){
        return 0;
    }
    return mergeSort(arr, 0, arr.length - 1).pairs;
}
複製程式碼

輸入描述

  1. 題目保證輸入的陣列中沒有相同的數字
  2. 資料範圍:對於%50的資料,size<=10^4;對於%75的資料,size<=10^5;對於%100的資料,size<=2*10^5

解析

藉助歸併排序的流程,將歸併流程中前一個陣列的數比後一個陣列的數小的情況記錄下來。

歸併的原始邏輯是根據輸入的無序陣列返回一個新建的排好序的陣列:

public int[] mergeSort(int[] arr, int start, int end){
    if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){
        throw new IllegalArgumentException();
    }
    if(start == end){
        return new int[]{ arr[end] };
    }

    int[] arr1 = mergeSort(arr, start, mid);
    int[] arr2 = Info right = mergeSort(arr, mid + 1, end);
    int[] copy = new int[arr1.length + arr2.length];
    int p1 = 0, p2 = 0, p = 0;

    while(p1 < arr1.length && p2 < arr2.length){
        if(arr1[p1] > arr2[p2]){
            copy[p++] = arr1[p1++];
        }else{
            copy[p++] = arr2[p2++];
        }
    }
    while(p1 < arr1.length){
        copy[p++] = arr1[p1++];
    }
    while(p2 < arr2.length){
        copy[p++] = arr2[p2++];
    }
    return copy;
}
複製程式碼

而我們需要再此基礎上對子狀態收集的資訊進行改造,假設左右兩半部分分別有序了,那麼進行merge的時候,不應是從前往後複製了,這樣當arr1[p1] > arr2[p2]的時候並不知道arr2p2後面還有多少元素是比arr1[p1]小的,要想一次比較就統計出arr2中所有比arr1[p1]小的數需要將p1,p2arr1,arr2的尾往前遍歷:

image

而將比較後較大的數移入輔助陣列的邏輯還是一樣。這樣當前遞迴狀態需要收集左半子陣列和右半子陣列的變成有序過程中記錄的逆序對數和自己merge記錄的逆序對數之和就是當前狀態要返回的資訊,並且merge後形成的有序輔助陣列也要返回。

public int InversePairs(int [] arr) {
    if(arr == null || arr.length <= 1){
        return 0;
    }
    return mergeSort(arr, 0, arr.length - 1).pairs;
}

class Info{
    int arr[];
    int pairs;
    Info(int[] arr, int pairs){
        this.arr = arr;
        this.pairs = pairs;
    }
}

public Info mergeSort(int[] arr, int start, int end){
    if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){
        throw new IllegalArgumentException();
    }
    if(start == end){
        return new Info(new int[]{arr[end]}, 0);
    }

    int pairs = 0;
    int mid = start + ((end - start) >> 1);
    Info left = mergeSort(arr, start, mid);
    Info right = mergeSort(arr, mid + 1, end);
    pairs += (left.pairs + right.pairs) % 1000000007;

    int[] arr1 = left.arr, arr2 = right.arr, copy = new int[arr1.length + arr2.length];
    int p1 = arr1.length - 1, p2 = arr2.length - 1, p = copy.length - 1;

    while(p1 >= 0 && p2 >= 0){
        if(arr1[p1] > arr2[p2]){
            pairs += (p2 + 1);
            pairs %= 1000000007;
            copy[p--] = arr1[p1--];
        }else{
            copy[p--] = arr2[p2--];
        }
    }

    while(p1 >= 0){
        copy[p--] = arr1[p1--];
    }
    while(p2 >= 0){
        copy[p--] = arr2[p2--];
    }

    return new Info(copy, pairs % 1000000007);
}
複製程式碼

兩個連結串列的第一個公共結點

題目描述

輸入兩個連結串列,找出它們的第一個公共結點。

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {

}
複製程式碼

解析

首先我們要分析兩個連結串列的組合狀態,根據有環、無環相互組合只可能會出現如下幾種情況:

image

於是我們首先要判斷兩個連結串列是否有環,判斷連結串列是否有環以及有環連結串列的入環結點在哪已有前人給我們總結好了經驗:

  1. 使用一個快指標和一個慢指標同時從首節點出發,快指標一次走兩步而慢指標一次走一步,如果兩指標相遇則說明有環,否則無環
  2. 如果兩指標相遇,先將快指標重新指向首節點,然後兩指標均一次走一步,再次相遇時的結點就是入環結點
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    //若其中一個連結串列為空則不存在相交問題
    if(pHead1 == null || pHead2 == null){
        return null;
    }
    ListNode ringNode1 = ringNode(pHead1);
    ListNode ringNode2 = ringNode(pHead2);
    //如果一個有環,另一個無環
    if((ringNode1 == null && ringNode2 != null) ||
       (ringNode1 != null && ringNode2 == null)){
        return null;
    }
    //如果兩者都無環,判斷是否共用尾結點
    else if(ringNode1 == null && ringNode2 == null){
        return firstCommonNode(pHead1, pHead2, null);
    }
    //剩下的情況就是兩者都有環了
    else{
        //如果入環結點相同,那麼第一個相交的結點肯定在入環結點之前
        if(ringNode1 == ringNode2){
            return firstCommonNode(pHead1, pHead2, ringNode1);
        }
        //如果入環結點不同,看能否通過ringNode1的後繼找到ringNode2
        else{
            ListNode p = ringNode1;
            while(p.next != ringNode1){
                p = p.next;
                if(p == ringNode2){
                    break;
                }
            }
            //如果能找到,那麼第一個相交的結點既可以是ringNode1也可以是ringNode2
            return (p == ringNode2) ? ringNode1 : null;
        }
    }
}

//查詢兩連結串列的第一個公共結點,如果兩連結串列無環,則傳入common=null,如果都有環且入環結點相同,那麼傳入common=入環結點
public ListNode firstCommonNode(ListNode pHead1, ListNode pHead2, ListNode common){
    ListNode p1 = pHead1, p2 = pHead2;
    int len1 = 1, len2 = 1, gap = 0;
    while(p1.next != common){
        p1 = p1.next;
        len1++;
    }
    while(p2.next != common){
        p2 = p2.next;
        len2++;
    }
    //如果是兩個無環連結串列,要判斷一下是否有公共尾結點
    if(common == null && p1 != p2){
        return null;
    }
    gap = len1 > len2 ? len1 - len2 : len2 - len1;
    //p1指向長連結串列,p2指向短連結串列
    p1 = len1 > len2 ? pHead1 : pHead2;
    p2 = len1 > len2 ? pHead2 : pHead1;
    while(gap-- > 0){
        p1 = p1.next;
    }
    while(p1 != p2){
        p1 = p1.next;
        p2 = p2.next;
    }
    return p1;
}

//判斷連結串列是否有環,沒有返回null,有則返回入環結點(整個連結串列是一個環時入環結點就是頭結點)
public ListNode ringNode(ListNode head){
    if(head == null){
        return null;
    }
    ListNode p1 = head, p2 = head;
    while(p1.next != null && p1.next.next != null){
        p1 = p1.next.next;
        p2 = p2.next;
        if(p1 == p2){
            break;
        }
    }

    if(p1.next == null || p1.next.next == null){
        return null;
    }

    p1 = head;
    while(p1 != p2){
        p1 = p1.next;
        p2 = p2.next;
    }
    //可能整個連結串列就是一個環,這時入環結點就是頭結點!!!
    return p1 == p2 ? p1 : head;
}
複製程式碼

數字在排序陣列中出現的次數

題目描述

統計一個數字在排序陣列中出現的次數。

public int GetNumberOfK(int [] array , int k) {

}
複製程式碼

解析

我們可以分兩步解決,先找出數值為k的連續序列的左邊界,再找右邊界。可以採用二分的方式,以查詢左邊界為例:如果arr[mid]小於k那麼移動左指標,否則移動右指標(初始時左指標指向-1,而右指標指向尾元素arr.length),當兩個指標相鄰時,左指標及其左邊的數均小於k而右指標及其右邊的數均大於或等於k,因此此時右指標就是要查詢的左邊界,同樣的方式可以求得右邊界。

值得注意的是,筆者曾將左指標初始化為0而右指標初始化為arr.length - 1,這與指標指向的含義是相悖的,因為左指標指向的元素必須是小於k的,而我們並不能保證arr[0]一定小於k,同樣的我們也不能保證arr[arr.length - 1]一定大於等於k

還有一點就是如果陣列中沒有k這個演算法是否依然會返回一個正確的值(0),這也是需要驗證的。

public int GetNumberOfK(int [] arr , int k) {
    if(arr == null || arr.length == 0){
        return 0;
    }
    if(arr.length == 1){
        return (arr[0] == k) ? 1 : 0;
    }

    int start, end, left, right;
    for(start = -1, end = arr.length ; end > start && end - start != 1 ;){
        int mid = start + ((end - start) >> 1);
        if(arr[mid] >= k){
            end = mid;
        }else{
            start = mid;
        }
    }
    left = end;
    for(start = -1, end = arr.length; end > start && end - start != 1 ;){
        int mid = start + ((end - start) >> 1);
        if(arr[mid] > k){
            end = mid;
        }else{
            start = mid;
        }
    }
    right = start;
    return right - left + 1;
}
複製程式碼

二叉樹的深度

題目描述

輸入一棵二叉樹,求該樹的深度。從根結點到葉結點依次經過的結點(含根、葉結點)形成樹的一條路徑,最長路徑的長度為樹的深度。

public int TreeDepth(TreeNode root) {
}
複製程式碼

解析

  1. TreeDepth看做一個黑盒,假設利用這個黑盒收集到了左子樹和右子樹的深度,那麼當前這棵樹的深度就是前面兩者的最大值加1
  2. base case,如果當前是一棵空樹,那麼深度為0
public class Solution {
    public int TreeDepth(TreeNode root) {
        if(root == null){
            return 0;
        }
        return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1;
    }
}
複製程式碼

平衡二叉樹

題目描述

輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。

public boolean IsBalanced_Solution(TreeNode root) {

}
複製程式碼

解析

判斷當前這棵樹是否是平衡二叉所需要收集的資訊:

  1. 左子樹、右子樹各自是平衡二叉樹嗎(需要收集子樹是否是平衡二叉樹)
  2. 如果1成立,還需要收集左子樹和右子樹的高度,如果高度相差不超過1那麼當前這棵樹才是平衡二叉樹(需要收集子樹的高度)
class Info{
    boolean isBalanced;
    int height;
    Info(boolean isBalanced, int height){
        this.isBalanced = isBalanced;
        this.height = height;
    }
}
複製程式碼

遞迴體的定義:(這裡高度之差不超過1中的left.height - right.height == 0容易被忽略)

public boolean IsBalanced_Solution(TreeNode root) {
    return process(root).isBalanced;
}

public Info process(TreeNode root){
    if(root == null){
        return new Info(true, 0);
    }
    Info left = process(root.left);
    Info right = process(root.right);
    if(!left.isBalanced || !right.isBalanced){
        //如果左子樹或右子樹不是平衡二叉樹,那麼當前這棵樹肯定也不是,樹高度資訊也就沒用了
        return new Info(false, 0);
    }
    //高度之差不超過1
    if(left.height - right.height == 1 || left.height - right.height == -1 ||
       left.height - right.height == 0){
        return new Info(true, Math.max(left.height, right.height) + 1);
    }
    return new Info(false, 0);
}
複製程式碼

陣列中只出現一次的數字

題目描述

一個整型陣列裡除了兩個數字之外,其他的數字都出現了偶數次。請寫程式找出這兩個只出現一次的數字。

解析

如果沒有解過類似的題目,思路比較難開啟。面試官可能會提醒你,如果是讓你求一個整型陣列裡只有一個數只出現了一次而其它數出現了偶數次呢?你應該聯想到:

  1. 偶數次相同的數異或的結果是0
  2. 任何數與0異或的結果是它本身

於是將陣列從頭到尾求異或和便可得知結果。那麼對於此題,能否將陣列分成這樣的兩部分呢:每個部分只有一個數出現了一次,其他的數都出現偶數次。

如果我們仍將整個陣列從頭到尾求異或和,那結果應該和這兩個只出現一次的數的異或結果相同,目前我們所能依仗的也就是這個結果了,能否靠這個結果將陣列分成想要的兩部分?

由於兩個只出現一次的數(用A和B表示)異或結果A ^ B肯定不為0,那麼A ^ B的二進位制表示中肯定包含數值為1的bit位,而這個位上的1肯定是由A或B提供的,也就是說我們能根據這個bit位上的數是否為1來區分A和B,那剩下的數呢?

由於剩下的數都出現偶數次,因此相同的數都會被分到一邊(按照某個bit位上是否為1來分)。

public void FindNumsAppearOnce(int [] arr,int num1[] , int num2[]) {
    if(arr == null || arr.length <= 1){
        return;
    }
    int xorSum = 0;
    for(int num : arr){
        xorSum ^= num;
    }
    //取xorSum二進位制表示中低位為1的bit位,將其它的bit位 置0
    //比如:xorSum = 1100,那麼 (1100 ^ 1011) & 1100 = 0100,只剩下一個為1的bit位
    xorSum = (xorSum ^ (xorSum - 1)) & xorSum;

    for(int num : arr){
        num1[0] = (num & xorSum) == 0 ? num1[0] ^ num : num1[0];
        num2[0] = (num & xorSum) != 0 ? num2[0] ^ num : num2[0];
    }
}
複製程式碼

和為S的連續正數序列

題目描述

小明很喜歡數學,有一天他在做數學作業時,要求計算出9~16的和,他馬上就寫出了正確答案是100。但是他並不滿足於此,他在想究竟有多少種連續的正數序列的和為100(至少包括兩個數)。沒多久,他就得到另一組連續正數和為100的序列:18,19,20,21,22。現在把問題交給你,你能不能也很快的找出所有和為S的連續正數序列? Good Luck!

public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {

}
複製程式碼

輸出描述

輸出所有和為S的連續正數序列。序列內按照從小至大的順序,序列間按照開始數字從小到大的順序

解析

1 ~ (S / 2 + 1)區間的數n依次加入到佇列中(因為從S/2 + 1之後的任意兩個正數之和都大於S):

  1. n加入到佇列queue中並將佇列元素之和queueSum更新,更新queueSum之後如果發現等於sum,那麼將此時的佇列快照加入到返回結果res中,並彈出隊首元素(保證下次入隊操作時佇列元素之和是小於sum的
  2. 更新queueSum之後如果發現大於sum,那麼迴圈彈出隊首元素直到queueSum <= Sum,如果迴圈彈出之後發現queueSum == sum那麼將佇列快照加入到res中,並彈出隊首元素(保證下次入隊操作時佇列元素之和是小於sum的);如果queueSum < sum那麼入隊下一個n

於是有如下程式碼:

public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
    ArrayList<ArrayList<Integer>> res = new ArrayList();
    if(sum <= 1){
        return res;
    }
    LinkedList<Integer> queue = new LinkedList();
    int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0;
    while(n <= halfSum){
        queue.addLast(n);
        queueSum += n;
        if(queueSum == sum){
            ArrayList<Integer> one = new ArrayList();
            one.addAll(queue);
            res.add(one);
            queueSum -= queue.pollFirst();
        }else if(queueSum > sum){
            while(queueSum > sum){
                queueSum -= queue.pollFirst();
            }
            if(queueSum == sum){
                ArrayList<Integer> one = new ArrayList();
                one.addAll(queue);
                res.add(one);
                queueSum -= queue.pollFirst();
            }
        }
        n++;
    }

    return res;
}
複製程式碼

我們發現11~1520~24行的程式碼是重複的,於是可以稍微優化一下:

public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
    ArrayList<ArrayList<Integer>> res = new ArrayList();
    if(sum <= 1){
        return res;
    }
    LinkedList<Integer> queue = new LinkedList();
    int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0;
    while(n <= halfSum){
        queue.addLast(n);
        queueSum += n;
        if(queueSum > sum){
            while(queueSum > sum){
                queueSum -= queue.pollFirst();
            }
        }
        if(queueSum == sum){
            ArrayList<Integer> one = new ArrayList();
            one.addAll(queue);
            res.add(one);
            queueSum -= queue.pollFirst();
        }
        n++;
    }

    return res;
}
複製程式碼

和為S的兩個數字

題目描述

輸入一個遞增排序的陣列和一個數字S,在陣列中查詢兩個數,使得他們的和正好是S,如果有多對數字的和等於S,輸出兩個數的乘積最小的。

public ArrayList<Integer> FindNumbersWithSum(int [] arr,int sum) {
    
}
複製程式碼

輸出描述

對應每個測試案例,輸出查詢到的兩個數,如果有多對,輸出乘積最小的兩個。

解析

使用指標l,r,初始時l指向首元素,r指向尾元素,當兩指標元素之和不等於sumr指標在l指標右側時迴圈:

  1. 如果兩指標元素之和大於sum,那麼將r指標左移,試圖減小兩指標之和
  2. 如果兩指標元素之和小於sum,那麼將l右移,試圖增大兩指標之和
  3. 如果兩指標元素之和等於sum那麼就可以返回了,或者r跑到了l的左邊表名沒有和sum的兩個數,也可以返回了。
public ArrayList<Integer> FindNumbersWithSum(int [] arr,int sum) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(arr == null || arr.length <= 1 ){
        return res;
    }
    int l = 0, r = arr.length - 1;
    while(arr[l] + arr[r] != sum && r > l){
        if(arr[l] + arr[r] > sum){
            r--;
        }else{
            l++;
        }
    }
    if(arr[l] + arr[r] == sum){
        res.add(arr[l]);
        res.add(arr[r]);
    }
    return res;
}
複製程式碼

旋轉字串

題目描述

組合語言中有一種移位指令叫做迴圈左移(ROL),現在有個簡單的任務,就是用字串模擬這個指令的運算結果。對於一個給定的字元序列S,請你把其迴圈左移K位後的序列輸出。例如,字元序列S=”abcXYZdef”,要求輸出迴圈左移3位後的結果,即“XYZdefabc”。是不是很簡單?OK,搞定它!

public String LeftRotateString(String str,int n) {
    
}
複製程式碼

解析

將開頭的一段子串移到串尾:將開頭的子串翻轉一下、將剩餘的子串翻轉一下,最後將整個子串翻轉一下。按理來說應該輸入char[] str的,這樣的話這種演算法不會使用額外空間。

public String LeftRotateString(String str,int n) {
    if(str == null || str.length() == 0 || n <= 0){
        return str;
    }
    char[] arr = str.toCharArray();
    reverse(arr, 0, n - 1);
    reverse(arr, n, arr.length - 1);
    reverse(arr, 0, arr.length - 1);
    return new String(arr);
}

public void reverse(char[] str, int start, int end){
    if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){
        return;
    }
    for(int i = start, j = end ; j > i ; i++, j--){
        char tmp = str[i];
        str[i] = str[j];
        str[j] = tmp;
    }
}
複製程式碼

翻轉單詞順序列

題目描述

牛客最近來了一個新員工Fish,每天早晨總是會拿著一本英文雜誌,寫些句子在本子上。同事Cat對Fish寫的內容頗感興趣,有一天他向Fish借來翻看,但卻讀不懂它的意思。例如,“student. a am I”。後來才意識到,這傢伙原來把句子單詞的順序翻轉了,正確的句子應該是“I am a student.”。Cat對一一的翻轉這些單詞順序可不在行,你能幫助他麼?

public String LeftRotateString(String str,int n) {
    
}
複製程式碼

解析

先將整個字串翻轉,最後按照標點符號或空格一次將句中的單詞翻轉。注意:由於最後一個單詞後面沒有空格,因此需要單獨處理!!!

public String ReverseSentence(String str) {
    if(str == null || str.length() <= 1){
        return str;
    }
    char[] arr = str.toCharArray();
    reverse(arr, 0, arr.length - 1);
    int start = -1;
    for(int i = 0 ; i < arr.length ; i++){
        if(arr[i] != ' '){
            //初始化start
            start = (start == -1) ? i : start;
        }else{
            //如果是空格,不用擔心start>i-1,reverse會忽略它
            reverse(arr, start, i - 1);
            start = i + 1;
        }
    }
    //最後一個單詞,這裡比較容易忽略!!!
    reverse(arr, start, arr.length - 1);

    return new String(arr);
}

public void reverse(char[] str, int start, int end){
    if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){
        return ;
    }
    for(int i = start, j = end ; j > i ; i++, j--){
        char tmp = str[i];
        str[i] = str[j];
        str[j] = tmp;
    }
}
複製程式碼

撲克牌順子

題目描述

LL今天心情特別好,因為他去買了一副撲克牌,發現裡面居然有2個大王,2個小王(一副牌原本是54張^_^)...他隨機從中抽出了5張牌,想測測自己的手氣,看看能不能抽到順子,如果抽到的話,他決定去買體育彩票,嘿嘿!!“紅心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是順子.....LL不高興了,他想了想,決定大\小 王可以看成任何數字,並且A看作1,J為11,Q為12,K為13。上面的5張牌就可以變成“1,2,3,4,5”(大小王分別看作2和4),“So Lucky!”。LL決定去買體育彩票啦。 現在,要求你使用這幅牌模擬上面的過程,然後告訴我們LL的運氣如何, 如果牌能組成順子就輸出true,否則就輸出false。為了方便起見,你可以認為大小王是0。

解析

先將陣列排序(5個元素排序時間複雜O(1)),然後遍歷陣列統計王的數量和相鄰非王牌之間的缺口數(需要用幾個王來填)。還有一點值得注意:如果發現兩種相同的非王牌,則不可能組成五張不同的順子。

public boolean isContinuous(int [] arr) {
    if(arr == null || arr.length != 5){
        return false;
    }
    //5 numbers -> O(1)
    Arrays.sort(arr);
    int zeroCount = 0, slots = 0;
    for(int i = 0 ; i < arr.length ; i++){
        //如果遇到兩張相同的非王牌則不可能組成順子,這點很容易忽略!!!
        if(i > 0 && arr[i - 1] != 0){
            if(arr[i] == arr[i - 1]){
                return false;
            }else{
                slots += arr[i] - arr[i - 1] - 1;
            }

        }
        zeroCount = (arr[i] == 0) ? ++zeroCount : zeroCount;
    }

    return zeroCount >= slots;
}
複製程式碼

孩子們的遊戲(圓圈中剩下的數)

題目描述

每年六一兒童節,牛客都會準備一些小禮物去看望孤兒院的小朋友,今年亦是如此。HF作為牛客的資深元老,自然也準備了一些小遊戲。其中,有個遊戲是這樣的:首先,讓小朋友們圍成一個大圈。然後,他隨機指定一個數m,讓編號為0的小朋友開始報數。每次喊到m-1的那個小朋友要出列唱首歌,然後可以在禮品箱中任意的挑選禮物,並且不再回到圈中,從他的下一個小朋友開始,繼續0...m-1報數....這樣下去....直到剩下最後一個小朋友,可以不用表演,並且拿到牛客名貴的“名偵探柯南”典藏版(名額有限哦!!^_^)。請你試著想下,哪個小朋友會得到這份禮品呢?(注:小朋友的編號是從0到n-1)

解析

  1. 報數時,在報到m-1之前,可通過報數求得報數的結點編號:

    image

  2. 在某個結點(小朋友)出列後的重新編號過程中,可通過新編號求結點的就編號

    image

    因此在某輪重新編號時,我們能在已知新編號x的情況下通過公式y = (x + S + 1) % n求得結點重新標號之前的舊編號,上述兩步分析的公式整理如下:

    1. 某一輪報數出列前:編號 = (報數 - 1)% 出列前結點個數
    2. 某一輪報數出列後:舊編號 = (新編號 + 出列編號 + 1)% 出列前結點個數,因為出列結點是因為報數m才出列的,所以有:出列編號 = (m - 1)% 出列前結點個數
    3. 由2可推出:舊編號 = (新編號 + (m - 1)% 出列前結點個數 + 1)% 出列前結點個數 ,若用n表示出列後結點個數:y = (x + (m - 1) % n + 1) % n = (x + m - 1) % n + 1

經過上面3步的複雜分析之後,我們得出這麼一個通式:舊編號 = (新編號 + m - 1 )% 出列前結點個數 + 1,於是我們就可以自下而上(用連結串列模擬出列過程是自上而下),求出**最後一輪重新編號為1**的小朋友(只剩他一個了)在倒數第二輪重新編號時的舊編號,自下而上可倒推出這個小朋友在第一輪編號時(這時還沒有任何一個小朋友出列過)的原始編號,即目標答案。

注意:式子y = (x + m - 1) % n + 1的計算結果不可能為0,因此我們可以按小朋友從1開始編號,將最後的計算結果應題目的要求(小朋友從0開始編號)減一個1即可。

public int LastRemaining_Solution(int n, int m) {
    if(n <= 0){
        //throw new IllegalArgumentException();
        return -1;
    }
    //最後一次重新編號:最後一個結點編號為1,出列前結點數為2
    return orginalNumber(2, 0, n, m);
}

//根據出列後的重新編號(newNumber)推匯出列前的舊編號(返回值)
//n:出列前有多少小朋友,N:總共有多少個小朋友
public int orginalNumber(int n, int newNumber, int N, int m){
    int lastNumber = (newNumber + m - 1) % n + 1;
    if(n == N){
        return lastNumber;
    }
    return orginalNumber(n + 1, lastNumber, N, m);
}
複製程式碼

求1+2+3+…+n

題目描述

求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等關鍵字及條件判斷語句(A?B:C)。

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

解析

遞迴輕鬆解決

既然不允許遍歷求和,不如將計算分解,如果知道了f(n - 1)f(n)則可以通過f(n - 1) + n算出:

public int Sum_Solution(int n) {
    if(n == 1){
        return 1;
    }
    return n + Sum_Solution(n - 1);
}
複製程式碼

不用加減乘除做加法

題目描述

寫一個函式,求兩個整數之和,要求在函式體內不得使用+、-、*、/四則運算子號。

解析

不要忘了加減乘除是人類熟悉的運算方法,而計算機只知道位運算哦!

我們可以將兩數的二進位制表示寫出來,然後按位與得出進位資訊、按位或得出非進位資訊,如果進位資訊不為0,則迴圈計算直到進位資訊為0,此時異或資訊就是兩數之和:

image

public int Add(int num1,int num2) {
    if(num1 == 0 || num2 == 0){
        return num1 == 0 ? num2 : num1;
    }
    int and = 0, xor = 0;
    do{
        and = num1 & num2;
        xor = num1 ^ num2;
        num1 = and << 1;
        num2 = xor;
    }while(and != 0);

    return xor;
}
複製程式碼

把字串轉換成整數

題目描述

將一個字串轉換成一個整數(實現Integer.valueOf(string)的功能,但是string不符合數字要求時返回0),要求不能使用字串轉換整數的庫函式。 數值為0或者字串不是一個合法的數值則返回0。

public int StrToInt(String str) {
    
}
複製程式碼

輸入描述

輸入一個字串,包括數字字母符號,可以為空

輸出描述

如果是合法的數值表達則返回該數字,否則返回0

示例

輸入:+2147483647,輸出:2147483647 輸入:1a33,輸出0

解析

  1. 只有第一個位置上的字元可以是+-或數字,其他位置上的字元必須是數字
  2. 如果第一個字元是-,返回結果必須是負數
  3. 如果字串只有一個字元,且為+-,這情況很容易被忽略
  4. 在對字串解析轉換時,如果發現溢位(包括正數向負數溢位,負數向正數溢位),必須有所處理(此時可以和麵試官交涉),但不能視而不見
public int StrToInt(String str) {
    if(str == null || str.length() == 0){
        return 0;
    }
    boolean minus = false;
    int index = 0;
    if(str.charAt(0) == '-'){
        minus = true;
        index = 1;
    }else if(str.charAt(0) == '+'){
        index = 1;
    }
    //如果只有一個正負號
    if(index == str.length()){
        return 0;
    }

    if(checkInteger(str, index, str.length() - 1)){
        return transform(str, index, str.length() - 1, minus);
    }

    return 0;
}

public boolean checkInteger(String str, int start, int end){
    if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){
        return false;
    }
    for(int i = start ; i <= end ; i++){
        if(str.charAt(i) < '0' || str.charAt(i) > '9'){
            return false;
        }
    }
    return true;
}

public int transform(String str, int start, int end, boolean minus){
    if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){
        throw new IllegalArgumentException();
    }
    int res = 0;
    for(int i = start ; i <= end ; i++){
        int num = str.charAt(i) - '0';
        res = minus ? (res * 10 - num) : (res * 10 + num);
        if((minus && res > 0) || (!minus && res < 0)){
            throw new ArithmeticException("the str is overflow int");
        }
    }
    return res;
}
複製程式碼

陣列中重複的數字

題目描述

在一個長度為n的陣列裡的所有數字都在0到n-1的範圍內。 陣列中某些數字是重複的,但不知道有幾個數字是重複的。也不知道每個數字重複幾次。請找出陣列中任意一個重複的數字。 例如,如果輸入長度為7的陣列{2,3,1,0,2,5,3},那麼對應的輸出是第一個重複的數字2。

// Parameters:
//    numbers:     an array of integers
//    length:      the length of array numbers
//    duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
//                  Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
//    這裡要特別注意~返回任意重複的一個,賦值duplication[0]
// Return value:       true if the input is valid, and there are some duplications in the array number
//                     otherwise false
public boolean duplicate(int numbers[],int length,int [] duplication) {
	
}
複製程式碼

解析

認真審題發現輸入資料是有特徵的,即陣列長度為n,陣列中的元素都在0~n-1範圍內,如果陣列中沒有重複的元素,那麼排序後每個元素和其索引值相同,這就意味著陣列中如果有重複的元素,那麼陣列排序後肯定有元素和它對應的索引是不等的。

順著這個思路,我們可以將每個元素放到與它相等的索引上,如果某次放之前發現對應的索引上已有了和索引相同的元素,那麼說明這個元素是重複的,由於每個元素最多會被調整兩次,因此時間複雜O(n)

public boolean duplicate(int arr[],int length,int [] duplication) {
    if(arr == null || arr.length == 0){
        return false;
    }
    int index = 0;
    while(index < arr.length){
        if(arr[index] == arr[arr[index]]){
            if(index != arr[index]){
                duplication[0] = arr[index];
                return true;
            }else{
                index++;
            }
        }else{
            int tmp = arr[index];
            arr[index] = arr[tmp];
            arr[tmp] = tmp;
        }
    }

    return false;
}
複製程式碼

構建乘積陣列

題目描述

給定一個陣列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]。不能使用除法。

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

分析

規律題:

image

public int[] multiply(int[] arr) {
    if(arr == null || arr.length == 0){
        return arr;
    }
    int len = arr.length;
    int[] arr1 = new int[len], arr2 = new int[len];
    arr1[0] = 1;
    arr2[len - 1] = 1;
    for(int i = 1 ; i < len ; i++){
        arr1[i] = arr1[i - 1] * arr[i - 1];
        arr2[len - 1 - i] = arr2[len - i] * arr[len - i];
    }
    int[] res = new int[len];
    for(int i = 0 ; i < len ; i++){
        res[i] = arr1[i] * arr2[i];
    }

    return res;
}
複製程式碼

正規表示式匹配

題目描述

請實現一個函式用來匹配包括'.'和''的正規表示式。模式中的字元'.'表示任意一個字元,而''表示它前面的字元可以出現任意次(包含0次)。 在本題中,匹配是指字串的所有字元匹配整個模式。例如,字串"aaa"與模式"a.a"和"abaca"匹配,但是與"aa.a"和"ab*a"均不匹配

public boolean match(char[] str, char[] pattern){
    
}
複製程式碼

解析

使用p1指向str中下一個要匹配的字元,使用p2指向pattern中剩下的模式串的首字元

  1. 如果p2 >= pattern.length,表示模式串消耗完了,這時如果p1仍有字元要匹配那麼返回false否則返回true
  2. 如果p1 >= str.length,表示要匹配的字元都匹配完了,但模式串還沒消耗完,這時剩下的模式串必須符合a*b*c*這樣的正規化以能夠作為空串處理,否則返回false
  3. p1p2都未越界,按照p2後面是否是*來討論
    1. p2後面如果是*,又可按照pattern[p2]是否能夠匹配str[p1]分析:
      1. pattern[p2] == ‘.’ || pattern[p2] == str[p1],這時可以選擇匹配一個str[p1]並繼續向後匹配(不用跳過p2和其後面的*),也可以選擇將pattern[p2]和其後面的*作為匹配空串處理,這時要跳過p2和 其後面的*
      2. pattern[p2] != str[p1],只能作為匹配空串處理,跳過p2
    2. p2後面如果不是*
      1. pattern[p2] == str[p1] || pattern[p2] == ‘.’p1,p2同時後移一個繼續匹配
      2. pattern[p2] == str[p1],直接返回false
public boolean match(char[] str, char[] pattern){
    if(str == null || pattern == null){
        return false;
    }
    if(str.length == 0 && pattern.length == 0){
        return true;
    }
    return matchCore(str, 0, pattern, 0);
}

public boolean matchCore(char[] str, int p1, char[] pattern, int p2){
    //模式串用完了
    if(p2 >= pattern.length){
        return p1 >= str.length;
    }
    if(p1 >= str.length){
        if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){
            return matchCore(str, p1, pattern, p2 + 2);
        }else{
            return false;
        }
    }

    //如果p2的後面是“*”
    if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){
        if(pattern[p2] == '.' || pattern[p2] == str[p1]){
            //匹配一個字元,接著還可以向後匹配;或者將當前字元和後面的星合起來做空串
            return matchCore(str, p1 + 1, pattern, p2) || matchCore(str, p1, pattern, p2 + 2);
        }else{
            return matchCore(str, p1, pattern, p2 + 2);
        }
    }
    //如果p2的後面不是*
    else{
        if(pattern[p2] == '.' || pattern[p2] == str[p1]){
            return matchCore(str, p1 + 1, pattern, p2 + 1);
        }else{
            return false;
        }
    }
}
複製程式碼

表示數值的字串

題目描述

請實現一個函式用來判斷字串是否表示數值(包括整數和小數)。例如,字串"+100","5e2","-123","3.1416"和"-1E-16"都表示數值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。

public boolean isNumeric(char[] str) {

}
複製程式碼

解析

由題式可得出如下約束:

  1. 正負號只能出現在第一個位置或者e/E後一個位置
  2. e/E後面有且必須有整數
  3. 字串中只能包含數字、小數點、正負號、e/E,其它的都是非法字元
  4. e/E的前面最多隻能出現一次小數點,而e/E的後面不能出現小數點
public boolean isNumeric(char[] str) {
    if(str == null || str.length == 0){
        return false;
    }

    boolean signed = false;        //標識是否以正負號開頭
    boolean decimal = false;       //標識是否有小數點
    boolean existE = false;        //是否含有e/E
    int start = -1;                //一段連續數字的開頭
    int index = 0;                 //從0開始遍歷字元

    if(existSignAtIndex(str, 0)){
        signed = true;
        index++;
    }

    while(index < str.length){
        //以下按照index上可能出現的字元進行分支判斷
        if(str[index] >= '0' && str[index] <= '9'){
            start = (start == -1) ? index : start;
            index++;

        }else if(str[index] == '+' || str[index] == '-'){
            //首字元的+-我們已經判斷過了,因此+-只可能出現在e/E的後面
            if(!existEAtIndex(str, index - 1)){
                return false;
            }
            index++;

        }else if(str[index] == '.'){
            //小數點只可能出現在e/E前面,且只可能出現一次
            //如果出現過小數點了,或者小數點前一段連續數字的前面是e/E
            if(decimal || existEAtIndex(str, start - 1)
               || existEAtIndex(str, start - 2) ){
                return false;
            }
            decimal = true;//出現了小數點
            index++;
            //下一段連續數字的開始
            start = index;

        }else if(existEAtIndex(str, index)){
            if(existE){
                //如果已出現過e/E
                return false;
            }
            existE = true;
            index++;
            //由於e/E後面可能是正負號也可能是數字,所以下一段連續數字的開始不確定
            start = !existSignAtIndex(str, index) ? index : index + 1;

        }else{
            return false;
        }
    }

    //如果最後一段連續數字的開始不存在 -> e/E後面沒有數字
    if(start >= str.length){
        return false;
    }

    return true;
}

//在index上的字元是否是e或者E
public boolean existEAtIndex(char[] str, int index){
    if(str == null || str.length == 0 || index < 0 || index > str.length - 1){
        return false;
    }
    return str[index] == 'e' || str[index] == 'E';
}

//在index上的字元是否是正負號
public boolean existSignAtIndex(char[] str, int index){
    if(str == null || str.length == 0 || index < 0 || index > str.length - 1){
        return false;
    }
    return str[index] == '+' || str[index] == '-';
}
複製程式碼

字元流中第一個只出現一次的字元

題目描述

請實現一個函式用來找出字元流中第一個只出現一次的字元。例如,當從字元流中只讀出前兩個字元"go"時,第一個只出現一次的字元是"g"。當從該字元流中讀出前六個字元“google"時,第一個只出現一次的字元是"l"。

輸出描述

如果當前字元流沒有存在出現一次的字元,返回#字元。

解析

首先要選取一個容器來儲存字元,並且要記錄字元進入容器的順序。如果不考慮中文字元,那麼可以使用一張大小為256(對應ASCII碼值範圍)的表來儲存字元,用字元的ASCII碼值作為索引,用字元進入容器的次序作為索引對應的記錄,表內部維護了一個計數器position,每當有字元進入時以該計數器的值作為該字元的次序(初始時,每個字元對應的次序為-1),如果設定該字元的次序時發現之前已設定過(次序不為-1,而是大於等於0),那麼將該字元的次序置為-2,表示以後從容器取第一個只出現一次的字元時不考慮該字元。

當從容器取第一個只出現一次的字元時,考慮次序大於等於0的字元,在這個前提下找出次序最小的字元並返回。

//不算中文,儲存所有ascii碼對應的字元只需256位元組,記錄ascii碼為index的字元首次出現的位置
int[] arr = new int[256];
int position = 0;
{
    for(int i = 0 ; i < arr.length ; i++){
        //初始時所有字元的首次出現的位置為-1
        arr[i] = -1;
    }
}
//Insert one char from stringstream
public void Insert(char ch){
    int ascii = (int)ch;
    if(arr[ascii] == -1){
        arr[ascii] = position++;
    }else if(arr[ascii] >= 0){
        arr[ascii] = -2;
    }
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce(){
    int minPosi = Integer.MAX_VALUE;
    char res = '#';
    for(int i = 0 ; i < arr.length ; i++){
        if(arr[i] >= 0 && arr[i] < minPosi){
            minPosi = arr[i];
            res = (char)i;
        }
    }

    return res;
}
複製程式碼

刪除連結串列中重複的結點

題目描述

在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5

public ListNode deleteDuplication(ListNode pHead){
    
}
複製程式碼

解析

此題處理起來棘手的有兩個地方:

  1. 如果某個結點的後繼結點與其重複,那麼刪除該結點的一串連續重複的結點之後如何刪除該結點本身,這就要求我們需要保留當前遍歷結點的前驅指標。

    但是如果從頭結點開始就出現一連串的重複呢?我們又如何刪除刪除頭結點,因此我們需要新建一個輔助結點作為頭結點的前驅結點。

  2. 在遍歷過程中如何區分當前結點是不重複的結點,還是在刪除了它的若干後繼結點之後最後也要刪除它本身的重複結點?這就需要我們使用一個布林變數記錄是否開啟了刪除模式(deleteMode

經過上述兩步分析,我們終於可以安心遍歷結點了:

public ListNode deleteDuplication(ListNode pHead){
    if(pHead == null){
        return null;
    }
    ListNode node = new ListNode(Integer.MIN_VALUE);
    node.next = pHead;
    ListNode pre = node, p = pHead;
    boolean deletedMode = false;
    while(p != null){
        if(p.next != null && p.next.val == p.val){
            p.next = p.next.next;
            deletedMode = true;
        }else if(deletedMode){
            pre.next = p.next;
            p = pre.next;
            deletedMode = false;
        }else{
            pre = p;
            p = p.next;
        }
    }

    return node.next;
}
複製程式碼

二叉樹的下一個結點

題目描述

給定一個二叉樹和其中的一個結點,請找出中序遍歷順序的下一個結點並且返回。注意,樹中的結點不僅包含左右子結點,同時包含指向父結點的指標。

解析

由於中序遍歷來到某個結點後,首先會接著遍歷它的右子樹,如果它沒有右子樹則會回到祖先結點中將它當做左子樹上的結點的那一個,因此有如下分析:

  1. 如果當前結點有右子樹,那麼其後繼結點就是其右子樹上的最左結點
  2. 如果當前結點沒有右子樹,那麼其後繼結點就是其祖先結點中,將它當做左子樹上的結點的那一個。
public TreeLinkNode GetNext(TreeLinkNode pNode){
    if(pNode == null){
        return null;
    }
    //如果有右子樹,後繼結點是右子樹上最左的結點
    if(pNode.right != null){
        TreeLinkNode p = pNode.right;
        while(p.left != null){
            p = p.left;
        }
        return p;
    }else{
        //如果沒有右子樹,向上查詢第一個當前結點是父結點的左孩子的結點
        TreeLinkNode p = pNode.next;
        while(p != null && pNode != p.left){
            pNode = p;
            p = p.next;
        }

        if(p != null && pNode == p.left){
            return p;
        }
        return null;
    }
}
複製程式碼

對稱的二叉樹

題目描述

請實現一個函式,用來判斷一顆二叉樹是不是對稱的。注意,如果一個二叉樹同此二叉樹的映象是同樣的,定義其為對稱的。

boolean isSymmetrical(TreeNode pRoot){
    
}
複製程式碼

解析

判斷一棵樹是否是映象二叉樹,只需將經典的先序遍歷序列和變種的先根再右再左的先序遍歷序列比較,如果相同則為映象二叉樹。

boolean isSymmetrical(TreeNode pRoot){
    if(pRoot == null){
        return true;
    }
    StringBuffer str1 = new StringBuffer("");
    StringBuffer str2 = new StringBuffer("");
    preOrder(pRoot, str1);
    preOrder2(pRoot, str2);
    return str1.toString().equals(str2.toString());
}

public void preOrder(TreeNode root, StringBuffer str){
    if(root == null){
        str.append("#");
        return;
    }
    str.append(String.valueOf(root.val));
    preOrder(root.left, str);
    preOrder(root.right, str);
}

public void preOrder2(TreeNode root, StringBuffer str){
    if(root == null){
        str.append("#");
        return;
    }
    str.append(String.valueOf(root.val));
    preOrder2(root.right, str);
    preOrder2(root.left, str);
}
複製程式碼

按之字形列印二叉樹

題目描述

請實現一個函式按照之字形列印二叉樹,即第一行按照從左到右的順序列印,第二層按照從右至左的順序列印,第三行按照從左到右的順序列印,其他行以此類推。

解析

注意下述程式碼的第14行,筆者曾寫為stack2 = stack1 == empty ? stack1 : stack2,你能發現錯誤在哪兒嗎?

public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
    ArrayList<ArrayList<Integer>> res = new ArrayList();
    if(pRoot == null){
        return res;
    }

    Stack<TreeNode> stack1 = new Stack();
    Stack<TreeNode> stack2 = new Stack();
    stack1.push(pRoot);
    boolean flag = true;//先加左孩子,再加右孩子
    while(!stack1.empty() || !stack2.empty()){
        Stack<TreeNode> empty = stack1.empty() ? stack1 : stack2;
        stack1 = stack1 == empty ? stack2 : stack1;
        stack2 = empty;
        ArrayList<Integer> row = new ArrayList();
        while(!stack1.empty()){
            TreeNode p = stack1.pop();
            row.add(p.val);
            if(flag){
                if(p.left != null){
                    stack2.push(p.left);
                }
                if(p.right != null){
                    stack2.push(p.right);
                }
            }else{
                if(p.right != null){
                    stack2.push(p.right);
                }
                if(p.left != null){
                    stack2.push(p.left);
                }
            }
        }
        res.add(row);
        flag = !flag;
    }

    return res;
}
複製程式碼

序列化二叉樹

題目描述

請實現兩個函式,分別用來序列化和反序列化二叉樹

解析

怎麼序列化的,就怎麼反序列化。這裡deserialize反序列化時對於序列化到String[] arr的哪個結點值來了的變數index有兩個坑(都是筆者親自踩的):

  1. index宣告為成員的intJava中函式呼叫時不會改變基本型別引數的值的,因此不要企圖使用int表示當前序列化哪個結點的值來了
  2. 之後筆者想用Integer代替,但是IntegerString一樣,都是不可變物件,所有的值更改操作在底層都是拆箱和裝箱生成新的Integer,因此也不要使用Integer做序列化到哪一個結點數值來了的計數器
  3. 最好使用陣列或者自定義的類(在類中宣告一個int變數)
String Serialize(TreeNode root) {
    if(root == null){
        return "#_";
    }
    //處理頭結點、左子樹、右子樹
    String res = root.val + "_";
    res += Serialize(root.left);
    res += Serialize(root.right);
    return res;
}

TreeNode Deserialize(String str) {
    if(str == null || str.length() == 0){
        return null;
    }
    Integer index = 0;
    return deserialize(str.split("_"), new int[]{0});
}

//怎麼序列化的,就怎麼反序列化
TreeNode deserialize(String[] arr, int[] index){
    if("#".equals(arr[index[0]])){
        index[0]++;
        return null;
    }
    //頭結點、左子樹、右子樹
    TreeNode root = new TreeNode(Integer.parseInt(arr[index[0]]));
    index[0]++;
    root.left = deserialize(arr, index);
    root.right = deserialize(arr, index);
    return root;
}
複製程式碼

二叉搜尋樹的第k個結點

題目描述

給定一棵二叉搜尋樹,請找出其中的第k小的結點。例如, (5,3,7,2,4,6,8) 中,按結點數值大小順序第三小結點的值為4。

TreeNode KthNode(TreeNode pRoot, int k){
    
}
複製程式碼

解析

二叉搜尋樹的特點是,它的中序序列是有序的,因此我們可以藉助中序遍歷在遞迴體中第二次來到當前結點時更新一下計數器,直到遇到第k個結點儲存並返回即可。

值得注意的地方是:

  1. 由於計數器在遞迴中傳來傳去,因此你需要保證每個遞迴引用的是同一個計數器,這裡使用的是一個int[]的第一個元素來儲存
  2. 我們寫中序遍歷是不需要返回值的,可以在找到第k小的結點時將其儲存在傳入的陣列中以返回給呼叫方
TreeNode KthNode(TreeNode pRoot, int k){
    if(pRoot == null){
        return null;
    }
    TreeNode[] res = new TreeNode[1];
    inOrder(pRoot, new int[]{ k }, res);
    return res[0];
}

public void inOrder(TreeNode root, int[] count, TreeNode[] res){
    if(root == null){
        return;
    }
    inOrder(root.left, count, res);
    count[0]--;
    if(count[0] == 0){
        res[0] = root;
        return;
    }
    inOrder(root.right, count, res);
}
複製程式碼

如果可以利用我們熟知的演算法,比如本題中的中序遍歷。管它三七二十一先將熟知方法寫出來,然後再按具體的業務需求對其進行改造(包括返回值、引數列表,但一般不會更改遍歷演算法的返回值)

資料流的中位數

題目描述

如何得到一個資料流中的中位數?如果從資料流中讀出奇數個數值,那麼中位數就是所有數值排序之後位於中間的數值。如果從資料流中讀出偶數個數值,那麼中位數就是所有數值排序之後中間兩個數的平均值。我們使用Insert()方法讀取資料流,使用GetMedian()方法獲取當前讀取資料的中位數。

public void Insert(Integer num) {
    
}

public Double GetMedian() {
    
}
複製程式碼

解析

由於中位數只與排序後位於陣列中間的一個數或兩個數相關,而與陣列兩邊的其它數無關,因此我們可以用一個大根堆儲存陣列左半邊的數的最大值,用一個小根堆儲存陣列右半邊的最小值,插入元素O(logn),取中位數O(1)

public class Solution {

    //小根堆、大根堆
    PriorityQueue<Integer> minHeap = new PriorityQueue(new MinRootHeadComparator());
    PriorityQueue<Integer> maxHeap = new PriorityQueue(new MaxRootHeadComparator());
    int count = 0;

    class MaxRootHeadComparator implements Comparator<Integer>{
        //返回值大於0則認為邏輯上i2大於i1(無關物件包裝的數值)
        public int compare(Integer i1, Integer i2){
            return i2.intValue() - i1.intValue();
        }
    }

    class MinRootHeadComparator implements Comparator<Integer>{
        public int compare(Integer i1, Integer i2){
            return i1.intValue() - i2.intValue();
        }
    }

    public void Insert(Integer num) {
        count++;//當前這個數是第幾個進來的
        //編號是奇數就放入小根堆(右半邊),否則放入大根堆
        if(count % 2 != 0){
            //如果要放入右半邊的數比左半邊的最大值要小則需調整左半邊的最大值放入右半邊並將當前這個數放入左半邊,這樣才能保證右半邊的數都比左半邊的大
            if(maxHeap.size() > 0 && num < maxHeap.peek()){
                maxHeap.add(num);
                num = maxHeap.poll();
            }
            minHeap.add(num);
        }else{
            if(minHeap.size() > 0 && num > minHeap.peek()){
                minHeap.add(num);
                num = minHeap.poll();
            }
            maxHeap.add(num);
        }
    }

    public Double GetMedian() {
        if(count == 0){
            return 0.0;
        }
        if(count % 2 != 0){
            return minHeap.peek().doubleValue();
        }else{
            return (minHeap.peek().doubleValue() + maxHeap.peek().doubleValue()) / 2;
        }
    }
}
複製程式碼

滑動視窗的最大值

題目描述

給定一個陣列和滑動視窗的大小,找出所有滑動視窗裡數值的最大值。例如,如果輸入陣列{2,3,4,2,6,2,5,1}及滑動視窗的大小3,那麼一共存在6個滑動視窗,他們的最大值分別為{4,4,6,6,6,5}; 針對陣列{2,3,4,2,6,2,5,1}的滑動視窗有以下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]}。

public ArrayList<Integer> maxInWindows(int [] num, int size){
    
}
複製程式碼

解析

使用一個單調非增佇列,隊頭儲存當前視窗的最大值,後面儲存在視窗移動過程中導致隊頭失效(出視窗)後的從而晉升為視窗最大值的候選值。

public ArrayList<Integer> maxInWindows(int [] num, int size){
    ArrayList<Integer> res = new ArrayList();
    if(num == null || num.length == 0 || size <= 0 || size > num.length){
        return res;
    }

    //用隊頭元素儲存視窗最大值,佇列中元素只能是單調遞減的,視窗移動可能導致隊頭元素失效
    LinkedList<Integer> queue = new LinkedList();
    int start = 0, end = size - 1;
    for(int i = start ; i <= end ; i++){
        addLast(queue, num[i]);
    }
    res.add(queue.getFirst());
    //移動視窗
    while(end < num.length - 1){
        addLast(queue, num[++end]);
        if(queue.getFirst() == num[start]){
            queue.pollFirst();
        }
        start++;
        res.add(queue.getFirst());
    }

    return res;
}

public void addLast(LinkedList<Integer> queue, int num){
    if(queue == null){
        return;
    }
    //加元素之前要確保該元素小於等於隊尾元素
    while(queue.size() != 0 && num > queue.getLast()){
        queue.pollLast();
    }
    queue.addLast(num);
}
複製程式碼

矩形中的路徑

題目描述

請設計一個函式,用來判斷在一個矩陣中是否存在一條包含某字串所有字元的路徑。路徑可以從矩陣中的任意一個格子開始,每一步可以在矩陣中向左,向右,向上,向下移動一個格子。如果一條路徑經過了矩陣中的某一個格子,則之後不能再次進入這個格子。 例如 a b c e s f c s a d e e 這樣的3 X 4 矩陣中包含一條字串"bcced"的路徑,但是矩陣中不包含"abcb"路徑,因為字串的第一個字元b佔據了矩陣中的第一行第二個格子之後,路徑不能再次進入該格子。

解析

定義一個黑盒hasPathCorechar(matrix, rows, cols, int i, int j, str, index),表示從rowscols列的矩陣matrix中的(i,j)位置開始走是否能走出一條與str的子串index ~ str.length-1相同的路徑。那麼對於當前位置(i,j),需要關心的只有一下三點:

  1. (i,j)是否越界了
  2. (i,j)上的字元是否和str[index]匹配
  3. (i,j)是否已在之前走過的路徑上

如果通過了上面三點檢查,那麼認為(i,j)這個位置是可以走的,剩下的就是(i,j)上下左右四個方向能否走出strindex+1 ~ str.length-1,這個交給黑盒就好了。

還有一點要注意,如果確定了可以走當前位置(i,j),那麼需要將該位置的visited標記為true,表示該位置在已走過的路徑上,而退出(i,j)的時候(對應下面第32行)又要將他的visited重置為false

public boolean hasPath(char[] matrix, int rows, int cols, char[] str){
    if(matrix == null || matrix.length != rows * cols || str == null){
        return false;
    }
    boolean[] visited = new boolean[matrix.length];
    for(int i = 0 ; i < rows ; i++){
        for(int j = 0 ; j < cols ; j++){
            //以矩陣中的每個點作為起點嘗試走出str對應的路徑
            if(hasPathCore(matrix, rows, cols, i, j, str, 0, visited)){
                return true;
            }
        }
    }
    return false;
}

//當前在矩陣的(i,j)位置上
//index -> 匹配到了str中的第幾個字元
private boolean hasPathCore(char[] matrix, int rows, int cols, int i, int j, 
                            char[] str, int index, boolean[] visited){
    if(index == str.length){
        return true;
    }
    //越界或字元不匹配或該位置已在路徑上
    if(!match(matrix, rows, cols, i, j, str[index]) || visited[i * cols + j] == true){
        return false;
    }
    visited[i * cols + j] = true;
    boolean res = hasPathCore(matrix, rows, cols, i + 1, j, str, index + 1, visited) ||
        hasPathCore(matrix, rows, cols, i - 1, j, str, index + 1, visited) ||
        hasPathCore(matrix, rows, cols, i, j + 1, str, index + 1, visited) ||
        hasPathCore(matrix, rows, cols, i, j - 1, str, index + 1, visited);
    visited[i * cols + j] = false;
    return res;
}

//矩陣matrix中的(i,j)位置上是否是c字元
private boolean match(char[] matrix, int rows, int cols, int i, int j, char c){
    if(i < 0 || i > rows - 1 || j < 0 || j > cols - 1){
        return false;
    }
    return matrix[i * cols + j] == c;
}
複製程式碼

機器人的運動範圍

題目描述

地上有一個m行和n列的方格。一個機器人從座標0,0的格子開始移動,每一次只能向左,右,上,下四個方向移動一格,但是不能進入行座標和列座標的數位之和大於k的格子。 例如,當k為18時,機器人能夠進入方格(35,37),因為3+5+3+7 = 18。但是,它不能進入方格(35,38),因為3+5+3+8 = 19。請問該機器人能夠達到多少個格子?

解析

定義一個黑盒walk(int threshold, int rows, int cols, int i, int j, boolean[] visited),它表示統計從rowscols列的矩陣中的(i,j)開始所能到達的格子並返回,對於當前位置(i,j)有如下判斷:

  1. (i,j)是否越界矩陣了
  2. (i,j)是否已被統計過了
  3. (i,j)的行座標和列座標的數位之和是否大於k

如果通過了上面三重檢查,則認為(i,j)是可以到達的(res=1),並標記(i,j)visitedtrue表示已被統計過了,然後對(i,j)的上下左右的格子呼叫黑盒進行統計。

這裡要注意的是,與上一題不同,visited不會在遞迴計算完子狀態後被重置回false,因為每個格子只能被統計一次。visited的含義不一樣

public int movingCount(int threshold, int rows, int cols){
    if(threshold < 0 || rows < 0 || cols < 0){
        return 0;
    }
    boolean[] visited = new boolean[rows * cols];
    return walk(threshold, rows, cols, 0, 0, visited);
}

private int walk(int threshold, int rows, int cols, int i, int j, boolean[] visited){
    if(!isLegalPosition(rows, cols, i, j) || visited[i * cols + j] == true
       || bitSum(i) + bitSum(j) > threshold){
        return 0;
    }
    int res = 1;
    visited[i * cols + j] = true;
    res += walk(threshold, rows, cols, i + 1, j, visited) +
        walk(threshold, rows, cols, i - 1, j, visited) +
        walk(threshold, rows, cols, i, j + 1, visited) +
        walk(threshold, rows, cols, i, j - 1, visited);
    return res;
}

private boolean isLegalPosition(int rows, int cols, int i, int j){
    if(i < 0 || j < 0 || i > rows - 1 || j > cols - 1){
        return false;
    }
    return true;
}

public int bitSum(int num){
    int res = num % 10;
    while((num /= 10) != 0){
        res += num % 10;
    }
    return res;
}
複製程式碼

相關文章