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

我的技術博客:www.zhenganwen.tophtml

二叉搜索樹與雙向鏈表

題目描述

輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能建立任何新的結點,只能調整樹中結點指針的指向。java

public TreeNode Convert(TreeNode root) {
}
複製代碼

解析

典型的二叉樹分解問題,咱們能夠定義一個黑盒transform,它的目的是將二叉樹轉換成雙向鏈表,那麼對於一個當前結點root,首先將其前驅結點(BST中前驅結點指中序序列的前一個數值,也就是當前結點的左子樹上最右的結點,若是左子樹爲空則沒有前驅結點)和後繼結點(當前結點的右子樹上的最左結點,若是右子樹爲空則沒有後繼結點),而後使用黑盒transform將左子樹和右子樹轉換成雙向鏈表,最後將當前結點和左子樹造成的鏈表鏈起來(經過以前保存的前驅結點)和右子樹造成的鏈表鏈起來(經過以前保存的後繼結點),整棵樹的轉換完畢。node

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),排序後的中位數天然就是出現次數超過長度一半的數。app

咱們知道快排的partition操做可以將數組按照一個基準劃分紅小於部分和大於等於部分並返回這個基準在數組中的下標,雖然一次partition並不能使數組總體有序,可是可以返回隨機選擇的數在partition以後的下標index,這個下標標識了它是第index大的數,這也意味着咱們要求數組中第k大的數不必定要求數組總體有序。dom

因而咱們在首次對整個數組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的數字有一、十、十一、十二、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中已統計好了),其他四位(個、10、百、千)取一位放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時,共有多少個數
    if(highestBit > 1){
        res += powerOf10(bitCount - 1);
    }else{
        //highestBit == 1
        res += n - powerOf10(bitCount - 1) + 1;
    }
    //二、統計其它位爲1的狀況
    res += powerOf10(bitCount - 2) * (bitCount - 1) * highestBit;
    //三、剩下的部分交給遞歸
    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);
    }
}
複製代碼

醜數

題目描述

把只包含質因子二、3和5的數稱做醜數(Ugly Number)。例如六、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;
}
複製代碼
相關文章
相關標籤/搜索