leetcode-常見算法總結


目錄

快速冪

50. Pow(x, n)

實現 pow(x, n) ,即計算 xn 次冪函數(即,xn )。

快速冪解析(分治法角度):
快速冪實際上是分治思想的一種應用。

image-20220105144211979

觀察發現,當 n 為奇數時,二分后會多出一項 x 。

  • 冪結果獲取:
    image-20220105144234932

  • 轉化為位運算:
    向下整除 n // 2等價於 右移一位 n>>1 ;
    取余數 n%2 等價於 判斷二進制最右位 n&1 ;

代碼如下:

public double myPow(double x, int n) {
    // 快速冪,二分分治法
    if (n == 0) {
        return (double)1;
    }
    // 因為n是[-2147483648,2147483647],所以如果k<0,轉成正數之后就超出了范圍,所以轉成long類型
    long k = n;
    double res = 1.0;
    if (k < 0) {
        k = -k;
        x = 1/x;
    }
    while (k > 0) {
        if ((k & 1) == 1) {
            res *= x;
        }
        x *= x;
        k = k >> 1;
    }

    return res;
}

矩陣

73. 矩陣置零

給定一個 *m* x *n* 的矩陣,如果一個元素為 0 ,則將其所在行和列的所有元素都設為 0 。請使用 原地 算法

  • 第一個思路是遇到0把該行該列所有不是0的都變成唯一的額外值,然后遍歷完之后把所有的變成0即可;但是這里的是整形不好轉成一個額外的唯一值
  • 第二個思路:先二重循環通過hashSet記錄0所在的行和列,然后把這些行和列的值都變成0,空間復雜度O(m+n)
  • 第三個思路:通過第一行和第一列來當做標記數組,通過兩個標記位標記第一個行第一列是否有0,然后遍歷第一行第一列之外的行列,如果對應有0,反向對標技數組標記位0,然后再利用標記數組對對應的行列賦值

我使用的第二個思路:

代碼如下:

    public void setZeroes(int[][] matrix) {
        // 第一個思路是遇到0把該行該列所有不是0的都變成唯一的額外值*,然后遍歷完之后把所有的*變成0即可;但是這里的是整形不好轉成一個額外的唯一值
        // 第二個思路:先二重循環通過hashSet記錄0所在的行和列,然后把這些行和列的值都變成0,空間復雜度O(m+n)
        // 第三個思路:通過第一行和第一列來當做標記數組,通過兩個標記位標記第一個行第一列是否有0,然后遍歷第一行第一列之外的行列,如果對應有0,反向對標技數組標記位0,然后再利用標記數組對對應的行列賦值
        int m = matrix.length;
        int n = matrix[0].length;
        Set<Integer> rowSet = new HashSet<>();
        Set<Integer> colSet = new HashSet<>();
        // 這里利用第二個思路:先二重循環通過hashSet記錄0所在的行和列,然后把這些行和列的值都變成0,空間復雜度O(m+n)
        for (int i = 0;i < m;i++) {
            for (int j = 0;j < n;j++) {
                if (matrix[i][j] == 0) {
                    rowSet.add(i);
                    colSet.add(j);
                }
            }

        }
        for (int row:rowSet) {
            Arrays.fill(matrix[row],0);
        }
        for (int col:colSet) {
            for (int k = 0;k < m;k++) {
                matrix[k][col] = 0;
            }
        }
        
    }

79. 單詞搜索

給定一個 m x n 二維字符網格 board 和一個字符串單詞 word 。如果 word 存在於網格中,返回 true ;否則,返回 false 。

單詞必須按照字母順序,通過相鄰的單元格內的字母構成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母不允許被重復使用。

思路:

回溯,DFS,利用visited數組標記已訪問的位置,防止重復訪問

代碼如下:

class Solution {
    boolean[][] visited;
    char[] wordCopy;
    char[][] boardCopy;
    int[][] dierction = {{-1,0},{0,1},{1,0},{0,-1}};
    int m,n;
    public boolean exist(char[][] board, String word) {
        m = board.length;
        n = board[0].length;
        int index = 0;
        visited = new boolean[m][n];
        wordCopy = word.toCharArray();
        boardCopy = board;
        for (int i = 0;i < m;++i) {
            for (int j = 0;j < n;++j) {
                if (backTrack(i,j,0)) {
                    return true;
                }
            }
        }

        return false;
    }
    // 利用回溯算法,begin:當前指向的word的位置
    private boolean backTrack(int x,int y,int begin) {
        // 遞歸結束條件:指向到了word的末尾
        if (begin == wordCopy.length - 1) {
            return boardCopy[x][y] == wordCopy[begin];
        }
        if (boardCopy[x][y] != wordCopy[begin]) {
            return false;
        }
        // 通過visited數組標記已訪問過,防止重復訪問
        visited[x][y] = true;
        // 遍歷四個方向的選擇
        for (int i = 0;i < dierction.length;i++) {
            int newX = x + dierction[i][0];
            int newY = y + dierction[i][1];
            if (!isValid(newX,newY,m,n) || visited[newX][newY]) {
                continue;
            }
            if (backTrack(newX,newY,begin+1)) {
                return true;
            }
        }
        // 訪問過后如果找不到正確的路線,重新標記為未訪問過
        visited[x][y] = false;
        return false;
    }
    // 判斷是否越界
    private boolean isValid(int x,int y,int m,int n) {
        return ((x >= 0 && x < m) && (y >= 0 && y < n));
    }
}

48. 旋轉圖像

給定一個 n × n 的二維矩陣 matrix 表示一個圖像。請你將圖像順時針旋轉 90 度。

你必須在 原地 旋轉圖像,這意味着你需要直接修改輸入的二維矩陣。請不要 使用另一個矩陣來旋轉圖像。

思路:

先沿着對角線反轉,然后反轉每一行,就相當於順時針翻轉矩陣

代碼如下:

    public void rotate(int[][] matrix) {
        // 思路:先沿着對角線反轉,然后反轉每一行,就相當於順時針翻轉矩陣
        int n = matrix.length;
        if (n == 1) {
            return;
        }
        // 先沿着對角線反轉
        for (int i = 0;i < n;i++) {
            for (int j = i;j < n;j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }

        // 翻轉每一行
        for (int[] row:matrix) {
            reverse(row);
        }

    }

    private void reverse(int[] row) {
        int left = 0,right = row.length-1;
        while (left < right) {
            int temp = row[left];
            row[left] = row[right];
            row[right] = temp;
            left++;
            right--;
        }
    }

378. 有序矩陣中第 K 小的元素

給你一個 n x n 矩陣 matrix ,其中每行和每列元素均按升序排序,找到矩陣中第 k 小的元素。
請注意,它是 排序后 的第 k 小元素,而不是第 k 個 不同 的元素。

思路(二分查找)

總體思路:二分查找;矩陣左上角的值是最小值,矩陣右下角的值是最大值,取中間值,然后判斷該中間值左上角的數的個數有沒有超過k,如果超過,想要的數就在中間值的左上角,繼續二分,否則就在中間值的右下角

判斷該中間值左上角的數的個數有沒有超過k的時候每次從左下角開始判斷比較

代碼如下:

   public int kthSmallest(int[][] matrix, int k) {
        // 總體思路:二分查找;矩陣左上角的值是最小值,矩陣右下角的值是最大值,取中間值,然后判斷該中間值左上角的數的個數有沒有超過k,如果超過,想要的數就在中間值的左上角,繼續二分,否則就在中間值的右下角
        int n = matrix.length;
        // 矩陣左上角的值是最小值,矩陣右下角的值是最大值
        int left = matrix[0][0];
        int right = matrix[n - 1][n - 1];
        while (left < right) {
            int mid = left + (right - left) / 2;
            // 判斷該中間值左上角的數的個數有沒有超過k
            if (check(matrix,mid,k,n)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    private boolean check(int[][] matrix,int mid,int k,int n) {
        // 判斷該中間值左上角的數的個數有沒有超過k
        int num = 0;
        // 每次從左下角開始判斷比較
        int i = n - 1;
        int j = 0;
        while(i >= 0 && j < n) {
            // 如果當前值小於等於mid,那么該數值對應列的上邊都比mid小
            if (matrix[i][j] <= mid) {
                num += i + 1;
                j++;
            } else {
                // 如果當前值比mid大,那么就要看該數值上邊的行做判斷比較
                i--;
            }
        }
        return num >= k;
    }

大頂堆,小頂堆

一個小頂堆可以搜索前k大的數,一個大頂堆可以搜索前k小的數

347. 前 K 個高頻元素

給你一個整數數組 nums 和一個整數 k ,請你返回其中出現頻率前 k 高的元素。你可以按 任意順序 返回答案。

思路:

利用小頂堆,因為要統計最大前k個元素,只有小頂堆每次將最小的元素彈出,最后小頂堆里積累的才是前k個最大元素。先利用map統計頻數,然后把map元素遍歷放入到小頂堆里,這里要對PriorityQueue自定義排序規則,根據map的value正序排序

代碼如下:

    public int[] topKFrequent(int[] nums, int k) {
        /**
            利用小頂堆,因為要統計最大前k個元素,只有小頂堆每次將最小的元素彈出,最后小頂堆里積累的才是前k個最大元素。
         */
        Map<Integer,Integer> map = new HashMap<>();
        // 先利用map統計每個數出現的頻率
        for (int i = 0;i<nums.length;i++) {
            map.put(nums[i],map.getOrDefault(nums[i],0)+1);
        }
        Set<Map.Entry<Integer,Integer> > entries = map.entrySet();
        // 小頂堆,// 根據map的value值正序排,相當於一個小頂堆
        PriorityQueue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<>((o1,o2) -> o1.getValue() - o2.getValue());
        for (Map.Entry<Integer,Integer> entry:entries) {
            pq.offer(entry);
            if (pq.size() > k) {
                pq.poll();
            }
        }
        int[] res = new int[k];
        for (int i = 0;i<k;i++) {
            res[i] = pq.poll().getKey();
        }
        return res;
    }

4. 尋找兩個正序數組的中位數

給定兩個大小分別為 m 和 n 的正序(從小到大)數組 nums1 和 nums2。請你找出並返回這兩個正序數組的 中位數 。

算法的時間復雜度應該為 O(log (m+n)) 。

思路:

通過兩個優先級隊列實現,大頂堆存放的數據要小於小頂堆存放的數據,只要保證小頂堆頂部的數據(最大值)小於小頂堆頂部的數據(最小值)就可以了;

同時要滿足兩個堆的數量差不能大於1,所以在插入的時候要判斷K,如果是偶數,插入到小頂堆(先插入到大頂堆,然后poll出大頂堆的頂部數據,添加到小頂堆中);如果k是奇數,插入到大頂堆(先插入到小頂堆,然后poll出小頂堆的頂部數據,添加到大頂堆中),最后根據數組長度之和是否是偶數,如果是偶數取兩個堆的頂部數據的一半,如果是奇數,取小頂堆的頂部數據,因為在最后一次遍歷的時候k是偶數,然后k++之后k才是奇數退出循環,所以最后一次添加到的是小頂堆right

代碼如下:

   public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 通過兩個優先級隊列實現,大頂堆存放的數據要小於小頂堆存放的數據,只要保證小頂堆頂部的數據(最大值)小於小頂堆頂部的數據(最小值)就可以了
        // 同時要滿足兩個堆的數量差不能大於1,所以在插入的時候要判斷K,如果是偶數,插入到小頂堆(先插入到大頂堆,然后poll出大頂堆的頂部數據,添加到小頂堆中);如果k是奇數,插入到大頂堆(先插入到小頂堆,然后poll出小頂堆的頂部數據,添加到大頂堆中),最后根據數組長度之和是否是偶數,如果是偶數取兩個堆的頂部數據的一半,如果是奇數,取小頂堆的頂部數據,因為在最后一次遍歷的時候k是偶數,然后k++之后k才是奇數退出循環,所以最后一次添加到的是小頂堆right
        // 大頂堆
        PriorityQueue<Integer> left = new PriorityQueue<Integer>((o1,o2) ->(o2 - o1));
        // 小頂堆,初始化默認是小頂堆
        PriorityQueue<Integer> right = new PriorityQueue<Integer>();
        int K = 0;
        for (int i = 0;i < nums1.length;i++) {
            // 如果是偶數,插入到小頂堆(先插入到大頂堆,然后poll出大頂堆的頂部數據,添加到小頂堆中)
            if (K % 2 == 0) {
                left.add(nums1[i]);
                right.add(left.poll());
            } else {
                // 如果k是奇數,插入到大頂堆(先插入到小頂堆,然后poll出小頂堆的頂部數據,添加到大頂堆中)
                right.add(nums1[i]);
                left.add(right.poll());
            }
            K++;
        }
        for (int i = 0;i < nums2.length;i++) {
            if (K % 2 == 0) {
                left.add(nums2[i]);
                right.add(left.poll());
            } else {
                right.add(nums2[i]);
                left.add(right.poll());
            }
            K++;
        }

        if (K % 2 == 0) {
            return (double) (left.peek() + right.peek()) / 2;
        }
        // 因為在最后一次遍歷的時候k是偶數,然后k++之后k才是奇數退出循環,所以最后一次添加到的是小頂堆right
        return right.peek();
    }

378. 有序矩陣中第 K 小的元素

給你一個 n x n 矩陣 matrix ,其中每行和每列元素均按升序排序,找到矩陣中第 k 小的元素。
請注意,它是 排序后 的第 k 小元素,而不是第 k 個 不同 的元素。

思路

總體思路:利用小頂堆,先將每一行的第一個數存放到小頂堆里,然后每次從小頂堆里取堆頂的最小值,然后將該行的下一列的值放入隊列里,直到拿到K-1個數,最后第K個數就是小頂堆的堆頂的數

代碼如下:

class Solution {
    public int kthSmallest(int[][] matrix, int k) {
        // 總體思路:利用小頂堆,先將每一行的第一個數存放到小頂堆里,然后每次從小頂堆里取堆頂的最小值,然后將該行的下一列的值放入隊列里,直到拿到K-1個數,最后第K個數就是小頂堆的堆頂的數
        int m = matrix.length;
        int n = matrix[0].length;
        // 優先級隊列里的數組array:array[0]存放數值,array[1]存放在矩陣中的行數,array[2]存放在矩陣中的列數
        PriorityQueue<int[] > pq = new PriorityQueue<>((int[] a,int[] b) -> {
            return a[0] - b[0];
        });
        for (int i = 0;i < m;i++) {
            pq.offer(new int[]{matrix[i][0],i,0});
        }
        // 每次從小頂堆里取堆頂的最小值,然后將該行的下一列的值放入隊列里,直到拿到K-1個數,最后第K個數就是小頂堆的堆頂的數,直接返回堆頂的數
        for (int i = 0;i < k - 1;i++) {
            int[] now = pq.poll();
            // 如果該行已經到了最右邊,就不往后移動不用考慮該行了
            if (now[2] != n - 1) {
                int nowI = now[1];
                int nowJ = now[2];
                pq.offer(new int[]{matrix[nowI][nowJ + 1],nowI,nowJ + 1});
            }
        }
        // 最后第K個數就是小頂堆的堆頂的數,直接返回堆頂的數
        return pq.poll()[0];
    }
}

回文子串

5. 最長回文子串

雙指針寫法

代碼如下:

    public String longestPalindrome(String s) {
        // 雙指針寫法
        if (s.length() <= 1) {
            return s;
        }
        int n = s.length();
        String res = "";
        for (int i = 0;i < n;i++) {
            // 以 s[i] 為中心的最長回文子串
            String s1 = palindrome(s,i,i);
            // 以 s[i] 和 s[i+1] 為中心的最長回文子串
            String s2 = palindrome(s,i,i+1);
            // 獲取最長的
            res = res.length() < s1.length() ?s1:res;
            res = res.length() < s2.length() ?s2:res;
        }

        return res;
    }

    // 返回以 s[left] 和 s[right] 為中心的最長回文串
    private String palindrome(String s,int left,int right) {
        // 利用雙指針防止索引越界
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            // 向兩邊展開
            left--;
            right++;
        }

        return s.substring(left+1,right);
    }

動態規划寫法

代碼如下:

    public String longestPalindrome(String s) {
        // 動態規划方法
        if (s.length() <= 1) {
            return s;
        }
        int n = s.length();
        // dp[i][j] 表示i...j區間是否是回文子串
        boolean[][] dp = new boolean[n][n];
        for (int i = 0;i < n;i++) {
            dp[i][i] = true;
        }
        // 因為dp[i][j] 會依賴於dp[i+1][j-1],所以要從下往上,從左往右遍歷
        for (int i = n-2;i >= 0;i--) {
            for (int j = i+1;j < n;j++) {
                // 如果i和j位置的字符相同,判斷i和j是否相鄰或者中間間隔一個元素,這樣的情況i...j可以為回文子串,如果不滿足前面的情況,但是i+1...j-1時回文子串,那i...j也是回文子串
                if (s.charAt(i) == s.charAt(j)) {
                    if ((j == i + 1) || (j == i + 2) || (j > i + 2 && dp[i+1][j-1])) {
                        dp[i][j] = true;
                    }
                }
            }
        }

        int left = 0;
        int right = 0;
        // 比較計算最長的回文子串
        for (int i = 0;i < n-1;i++) {
            for (int j = i+1;j < n;j++) {
                if (dp[i][j]) {
                    if (j - i > right - left) {
                        right = j;
                        left = i;
                    }
                }
            }
        }

        return s.substring(left,right+1);
    }

雙指針

75. 顏色分類

給定一個包含紅色、白色和藍色,一共 n 個元素的數組,原地對它們進行排序,使得相同顏色的元素相鄰,並按照紅色、白色、藍色順序排列。

此題中,我們使用整數 0、 1 和 2 分別表示紅色、白色和藍色。

思路:

雙指針解法,時間復雜度O(n),p0表示指向0放置的最后位置的指針,p1表示指向1放置的最后位置的指針;

  • 遇到1,將nums[i]和p1位置上的數值交換
  • 遇到0,將nums[i]和p0位置上的數值交換;如果p0<p1,那么p0當前的位置上的值原來可能是1,上邊把1交換到了nums[i]的位置上了,要把nums[i]上的1交換到p1的位置上,無論p0是否<p1,兩個指針都要后移

代碼如下:

    public void sortColors(int[] nums) {
        // Arrays.sort(nums);
        // 雙指針解法,時間復雜度O(n),p0表示指向0放置的最后位置的指針,p1表示指向1放置的最后位置的指針
        int p0 = 0,p1 = 0;
        for (int i = 0;i < nums.length;i++) {
            // 遇到1,將nums[i]和p1位置上的數值交換
            if (nums[i] == 1) {
                int temp = nums[i];
                nums[i] = nums[p1];
                nums[p1] = temp;
                p1++;
            } else if (nums[i] == 0) {
                // 遇到0,將nums[i]和p0位置上的數值交換
                int temp = nums[i];
                nums[i] = nums[p0];
                nums[p0] = temp;
                // 如果p0<p1,那么p0當前的位置上的值原來可能是1,上邊把1交換到了nums[i]的位置上了,要把nums[i]上的1交換到p1的位置上
                if (p0 < p1) {
                    temp = nums[i];
                    nums[i] = nums[p1];
                    nums[p1] = temp;
                }
                // 無論p0是否<p1,兩個指針都要后移
                p0++;
                p1++;
            }
        }
    }

三指針

88. 合並兩個有序數組

給你兩個按 非遞減順序 排列的整數數組 nums1 和 nums2,另有兩個整數 m 和 n ,分別表示 nums1 和 nums2 中的元素數目。

請你 合並 nums2 到 nums1 中,使合並后的數組同樣按 非遞減順序 排列。

思路:

思路的重點一個是從后往前確定兩組中該用哪個數字;

三指針:分別從后往前遍歷兩個數組有真實數值的位置,如果nums1[i]大,就將nums1[i]的值放到nums[k]中,並且i--,如果nums2[j]的值大,將nums2[j]的值放到nums[k]中,j--;兩種情況k都要減一,k表示nums1數組當前放置真實正確數值的指針

如果上述過程結束之后nums2的數組沒有遍歷完,這個時候此時nums1的i已經遍歷到頭了,還要將nums2的數組放置到nums1中

代碼如下:

    public void merge(int[] nums1, int m, int[] nums2, int n) {
        // 三指針:分別從后往前遍歷兩個數組有真實數值的位置,如果nums1[i]大,就將nums1[i]的值放到nums[k]中,並且i--,如果nums2[j]的值大,將nums2[j]的值放到nums[k]中,j--;兩種情況k都要減一,k表示nums1數組當前放置真實正確數值的指針
        int i = m  - 1;
        int j = n - 1;
        int k = m + n - 1;
        while (i >= 0 && j >= 0) {
            if (nums1[i] < nums2[j]) {
                nums1[k] = nums2[j];
                j--;
            } else {
                nums1[k] = nums1[i];
                i--;
            }
            k--;
        }
        // 如果nums2的數組沒有遍歷完,還要將nums2的數組放置到nums1中,此時nums1的i已經遍歷到頭了
        if (j >= 0) {
            while (j >= 0) {
                nums1[k] = nums2[j];
                j--;
                k--;
            }
        }

        // int i = 0,j = 0;
        // while (i < m && j < n ) {
        //     // 遇到nums1[i] > nums2[j]的情況,將i,j位置的數值交換,然后將j位置的數值一直往后移動,保證nums2的非遞減順序
        //     if (nums1[i] > nums2[j]) {
        //         int k = i;
        //         int t = j;
        //         // 將i,j位置的數值交換
        //         int temp = nums1[i];
        //         nums1[i] = nums2[j];
        //         nums2[j] = temp;
        //         t++;
        //         // 將j位置的數值一直往后移動,保證nums2的非遞減順序,這里要不停的交換數值保證非遞減順序
        //         while (t < n && temp > nums2[t]) {
        //             int temp2 = nums2[t - 1];
        //             nums2[t - 1] = nums2[t];
        //             nums2[t] = temp2;
        //             t++;
        //         }
        //         i++;
        //     } else {
        //         i++;
        //     }
            
        // }
        
        // i = m;
        // while (i < m + n) {
        //     nums1[i] = nums2[i - m];
        //     i++;
        // }
    }

原地哈希

劍指 Offer 03. 數組中重復的數字

找出數組中重復的數字。

在一個長度為 n 的數組 nums 里的所有數字都在 0~n-1 的范圍內。數組中某些數字是重復的,但不知道有幾個數字重復了,也不知道每個數字重復了幾次。請找出數組中任意一個重復的數字。

思路:

數組元素的 索引 和 值 是 一對多 的關系。
因此,可遍歷數組並通過交換操作,使元素的 索引 與 值 一一對應(即 nums[i] = i)。因而,就能通過索引映射對應的值,起到與字典等價的作用。

image-20220107192100657

原地哈希:將nums[i]作為索引的位置上的數值和nums[i]互換,保證nums[i]位置上的值是nums[i],這樣最后如果有重復的,肯定會有nums[nums[i]] == nums[i]同時nums[i]不等於i,這樣的nums[i]就是重復值

代碼如下:

    public int findRepeatNumber(int[] nums) {
        // 原地哈希:將nums[i]作為索引的位置上的數值和nums[i]互換,保證nums[i]位置上的值是nums[i],這樣最后如果有重復的,肯定會有nums[nums[i]] == nums[i]同時nums[i]不等於i,這樣的nums[i]就是重復值
        for (int i = 0;i < nums.length;i++) {
            while (nums[nums[i]] != nums[i]) {
                swap(nums,nums[i],i);
            }
            if (nums[i] != i && nums[nums[i]] == nums[i]) {
                return nums[i];
            }
        }

        return nums[nums.length - 1];
    }
    private void swap(int[] nums,int i,int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

448. 找到所有數組中消失的數字

給你一個含 n 個整數的數組 nums ,其中 nums[i] 在區間 [1, n] 內。請你找出所有在 [1, n] 范圍內但沒有出現在 nums 中的數字,並以數組的形式返回結果。

思路:

其實這道題和上邊的題目類似,因為消失的數字就是用重復的數字來填充的,所以找到重復的數字所在的索引位置就找到了消失的數字

原地哈希:將nums[i]放置在nums[i] - 1作為索引的位置上,然后從頭遍歷,如果遇到nums[i] - 1 != i的情況,i+1就是消失的數字

代碼如下:

    public List<Integer> findDisappearedNumbers(int[] nums) {
        // 原地哈希:將nums[i]放置在nums[i] - 1作為索引的位置上,然后從頭遍歷,如果遇到nums[i] - 1 != i的情況,i+1就是消失的數字
        List<Integer> res = new ArrayList<>();
        for (int i = 0;i < nums.length;i++) {
            while (nums[nums[i] - 1] != nums[i]) {
                swap(nums,nums[i] - 1,i);
            }
        }

        for (int i = 0;i < nums.length;i++) {
            if (nums[i] - 1 != i) {
                res.add(i + 1);
            }
        }

        return res;
    }

    private void swap(int[] nums,int i,int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

41. 缺失的第一個正數

給你一個未排序的整數數組 nums ,請你找出其中沒有出現的最小的正整數。

請你實現時間復雜度為 O(n) 並且只使用常數級別額外空間的解決方案。

思路:

原地哈希法:位置i的數值nums[i]應該放在索引位置為nums[i] - 1的位置上,對於數值為負數或者大於數組長度的不用交換直接跳過,在放置完之后,從頭遍歷,第一個不滿足上述條件的位置+1就是缺失的第一個正數,相當於把每個數值哈希映射到對應的位置上去了

代碼如下:

    public int firstMissingPositive(int[] nums) {
        // 原地哈希法:位置i的數值nums[i]應該放在索引位置為nums[i] - 1的位置上,在放置完之后,從頭遍歷,第一個不滿足上述條件的位置+1就是缺失的第一個正數,相當於把每個數值哈希映射到對應的位置上去了
        // nums = [3,4,-1,1]
        for (int i = 0;i < nums.length;i++) {
            while (nums[i] > 0 && nums[i] <= nums.length && nums[nums[i] - 1] != nums[i]) {
                // 滿足在指定范圍內、並且沒有放在正確的位置上,才交換
                // 例如:數值 3 應該放在索引 2 的位置上
                swap(nums,nums[i] - 1,i);
            }
        }

        // [1, -1, 3, 4]
        for (int i = 0;i < nums.length;i++) {
            if (nums[i] - 1 != i) {
                return i + 1;
            }
        }

        // 都正確則返回數組長度 + 1
        return nums.length + 1;
    }

    private void swap(int[] nums,int i,int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

排序

148. 排序鏈表

給你鏈表的頭結點 head ,請將其按 升序 排列並返回 排序后的鏈表

進階:你可以在 O(n log n) 時間復雜度和常數級空間復雜度下,對鏈表進行排序嗎?

插入排序時間復雜度O(n的平方),時間復雜度為O(N log N)的排序算法有:堆排序,歸並排序,快速排序。

這里采用自頂向下的歸並排序要不停遞歸,需要棧空間,空間復雜度為O(log N),所以采用自底向上的歸並排序不用遞歸,空間復雜度可以滿足O(N log N)

思路:

首先求得鏈表的長度 length,然后將鏈表拆分成子鏈表進行合並。

具體做法如下。

  • 用 subLength 表示每次需要排序的子鏈表的長度,初始時 subLength=1。

  • 每次將鏈表拆分成若干個長度為 subLength 的子鏈表(最后一個子鏈表的長度可以小於 subLength),按照每兩個子鏈表一組進行合並,合並后即可得到若干個長度為subLength×2 的有序子鏈表(最后一個子鏈表的長度可以小於 subLength×2)。合並兩個子鏈表仍然使用「21. 合並兩個有序鏈表」的做法。

  • 將 subLength 的值加倍,重復第 2 步,對更長的有序子鏈表進行合並操作,直到有序子鏈表的長度大於或等於 length,整個鏈表排序完畢。

代碼如下:

    public ListNode sortList(ListNode head) {
        // 自底向上歸並排序,先把鏈表分割成一個一個節點(分),然后兩個節點兩個節點通過(合並兩個有序鏈表)進行合並,然后分割成兩個兩個節點,一次增大分割節點數到鏈表長度為止
        if (head == null || head.next == null) {
            return head;
        }
        int len = 0;
        ListNode node = head;
        // 先記錄鏈表長度
        while (node != null) {
            len++;
            node = node.next;
        }
        // dummy節點用來標記最終鏈表的頭結點
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode preNode = dummy;
        // 分割節點數從1開始遞增,一直遞增到鏈表總長度為止
        for (int subLen = 1;subLen < len;subLen <<= 1) {
            preNode = dummy;
            ListNode curNode = dummy.next;
            // 在while循環里先分割成兩個鏈表,然后合並兩個鏈表,接入到之前的pre節點,然后curNode指向合並后的鏈表下一個節點,用來往后繼續兩兩分割並合並,直到最末尾
            while (curNode != null) {
                ListNode head1 = curNode;
                // 分割第一個有subLen個節點數的鏈表
                for (int i = 1;i < subLen && curNode != null && curNode.next != null;i++) {
                    curNode = curNode.next;
                }
                ListNode head2 = curNode.next;
                // 將第一個分割后的鏈表切斷,用於合並
                curNode.next = null;
                curNode = head2;
                // 分割第一個有subLen個節點數的鏈表
                for(int i = 1;i < subLen && curNode != null && curNode.next != null;i++) {
                    curNode = curNode.next;
                }
                ListNode next = curNode;
                if (curNode != null) {
                    // 這里next用於記錄下一趟分割的首節點
                    next = curNode.next;
                    // 將第二個分割后的鏈表切斷,用於合並
                    curNode.next = null;
                }
                // 合並兩個有序鏈表
                ListNode mergeList = mergeTwo(head1,head2);
                // 接入到pre節點的下一節點
                preNode.next = mergeList;
                // pre節點往后遍歷到當前合並過的鏈表的最后一個節點,用於拼接后邊的合並鏈表
                while (preNode.next != null) {
                    preNode = preNode.next;
                }
                // next用於記錄下一趟分割的首節點,這里curNode指向該next節點用於下一趟分割合並
                curNode = next;
            }
        }

        return dummy.next;
    }
    // 合並兩個有序鏈表
    private ListNode mergeTwo(ListNode headA,ListNode headB) {
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        while (headA != null && headB != null) {
            if (headA.val < headB.val) {
                cur.next = headA;
                headA = headA.next;
            } else {
                cur.next = headB;
                headB = headB.next;
            }
            cur = cur.next;
        }
        if (headA != null) {
            cur.next = headA;
        }
        if (headB != null) {
            cur.next = headB;
        }

        return dummy.next;
    }

二叉樹經典難題

124. 二叉樹中的最大路徑和

路徑 被定義為一條從樹中任意節點出發,沿父節點-子節點連接,達到任意節點的序列。同一個節點在一條路徑序列中 至多出現一次 。該路徑 至少包含一個 節點,且不一定經過根節點。路徑和 是路徑中各節點值的總和。給你一個二叉樹的根節點 root ,返回其 最大路徑和 。

image-20220113224834649

思路:

題目的意思的路徑不只是父節點到子節點的路徑,還包括左子節點到父節點右子節點

如果不包括(左子節點到父節點右子節點),可以直接用遞歸,這里要考慮這種情況,就要用全局最大值記錄判斷這里的情況

二叉樹 abc,a 是根結點(遞歸中的 root),bc 是左右子結點(代表其遞歸后的最優解)。
最大的路徑,可能的路徑情況:

​ a

​ / \

b c

1.b + a + c。
2.b + a + a 的父結點。
3.a + c + a 的父結點。
其中情況 1,表示如果不聯絡父結點的情況,或本身是根結點的情況。
這種情況是沒法遞歸的,但是結果有可能是全局最大路徑和。

情況 2 和 3,遞歸時計算 a+b 和 a+c,選擇一個更優的方案返回,也就是上面說的遞歸后的最優解啦。

另外結點有可能是負值,最大和肯定就要想辦法舍棄負值(max(0, x))(max(0,x))。
但是上面 3 種情況,無論哪種,a 作為聯絡點,都不能夠舍棄。

所要做的就是遞歸,遞歸時記錄好全局最大和,返回聯絡最大和。

代碼如下:

   int maxVal;
    public int maxPathSum(TreeNode root) {
        // 對於a父節點,有b作為左子節點,c作為右子節點,存在以下三種情況
        // 1、b + a + c;2、b + a + a的父節點;3、c + a + a的父節點。
        // 對第一種情況,無法做遞歸,所以需要全局的maxVal記錄三種情況最大值
        // 對第二三種情況,這里的b代表的是遞歸后的最優解,所以遞歸左子節點,遞歸右子節點然后判斷兩個大小
        maxVal = Integer.MIN_VALUE;
        getMaxPathSum(root);
        return maxVal;
    }

    private int getMaxPathSum(TreeNode root) {
        if (root == null) {
            return 0;
        }
        // 遞歸左子節點,得到左子樹的最優解
        int leftVal = getMaxPathSum(root.left);
        // 遞歸右子節點,得到右子樹的最優解
        int rightVal = getMaxPathSum(root.right);
        // 比較左右子節點的最優解哪個大,然后加到父節點a上,然后返回給上一層去
        int leftOrRight = root.val + Math.max(0,Math.max(leftVal,rightVal));
        // 這里針對第一種情況,直接左子節點最優解加右子節點最優解加父節點,然后通過全局最大值記錄
        int mid = root.val + Math.max(0,leftVal) + Math.max(0,rightVal);
        maxVal = Math.max(maxVal,Math.max(leftOrRight,mid));
        // 返回的是第二三種情況的最大值
        return leftOrRight;
    }
}

摩爾投票法

169. 多數元素

給定一個大小為 n 的數組,找到其中的多數元素。多數元素是指在數組中出現次數 大於 ⌊ n/2 ⌋ 的元素。你可以假設數組是非空的,並且給定的數組總是存在多數元素。

思路

摩爾投票法:

核心就是對拼消耗。

玩一個諸侯爭霸的游戲,假設你方人口超過總人口一半以上,並且能保證每個人口出去干仗都能一對一同歸於盡。最后還有人活下來的國家就是勝利。

那就大混戰唄,最差所有人都聯合起來對付你(對應你每次選擇作為計數器的數都是眾數),或者其他國家也會相互攻擊(會選擇其他數作為計數器的數),但是只要你們不要內斗,最后肯定你贏。

最后能剩下的必定是自己人。

從第一個數開始count=1,遇到相同的就加1,遇到不同的就減1,減到0就重新換個數開始計數,總能找到最多的那個

代碼如下:

class Solution {
    public int majorityElement(int[] nums) {
        //摩爾投票法,先假設第一個數過半數並設cnt=1;遍歷后面的數如果相同則cnt+1,不同則減一,當cnt為0時則更換新的數字為候選數(成立前提:有出現次數大於n/2的數存在)
        if (nums.length <= 1) {
            return nums[0];
        }
        int count = 1;
        int res = nums[0];
        for (int i = 1;i < nums.length;i++) {
            if (res == nums[i]) {
                count++;
            } else {
                if (count > 0) {
                    count--;
                } else {
                    count = 1;
                    // 更換新的數值作為候選數
                    res = nums[i];
                }
            }
        }

        return res;
    }
}

匹配問題

44. 通配符匹配

給定一個字符串 (s) 和一個字符模式 (p) ,實現一個支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何單個字符。
'*' 可以匹配任意字符串(包括空字符串)。
兩個字符串完全匹配才算匹配成功。

說明:

s 可能為空,且只包含從 a-z 的小寫字母。
p 可能為空,且只包含從 a-z 的小寫字母,以及字符 ? 和 *。

思路

利用動態規划:dp[i] [j] 表示s的前i個字符能否匹配p的前j個字符

image-20220114180053432

image-20220114180107709

這里如果p[j]是 * 號,還有一種情況,考慮該 * 號匹配s中多個字符,具體實現看代碼

代碼如下:

public boolean isMatch(String s, String p) {
    int m = s.length();
    int n = p.length();
    if (n <= 0) {
        // 兩個都為空返回true,p為空s不為空返回false
        return m <= 0;
    }
    if (m <= 0) {
        // s為空,如果p全是*字符,返回true
        boolean flag = true;
        for (char c:p.toCharArray()) {
            if (c != '*') {
                flag = false;
            }
        }
        return flag;
    }
    // dp[i][j] 表示s的前i個字符能否匹配p的前j個字符
    boolean[][] dp = new boolean[m + 1][n + 1];
    // 初始化
    dp[0][0] = true;
    if (p.charAt(0) == '*') {
        // 如果p前邊的有很多連續的*,都要賦值true
        int i = 0;
        while (i < n && p.charAt(i) == '*') {
            dp[0][i + 1] = true;
            i++;
        }
    }
    for (int i = 0;i < m;i++) {
        for (int j = 0;j < n;j++) {
            char c = s.charAt(i);
            char t = p.charAt(j);
            if (t == '*') {
                // 該*號什么都不匹配
                if (dp[i + 1][j]) {
                    dp[i + 1][j + 1] = true;
                }
                // 該*號匹配c,那就看前邊的是否能匹配上
                if (dp[i][j]) {
                    dp[i + 1][j + 1] = true;
                }
                // 該*號匹配c及其之前的多個字符,那就要對s往前遍歷,看在i之前有沒有一段子串(0...m ,m < i)可以匹配t之前的字符串,如果可以的話,那么*號就匹配[m+1,i]之間的子串
                if (!dp[i + 1][j + 1]) {
                    int index = i - 1;
                    while (index + 1 >= 0) {
                        if (dp[index + 1][j]) {
                            dp[i +1][j + 1] = true;
                            break;
                        }
                        index--;
                    }

                }


            } else if (t == '?') {
                // 該?號匹配c,那就看前邊的是否能匹配上
                if (dp[i][j]) {
                    dp[i + 1][j + 1] = true;
                }
            } else {
                // c與t匹配,如果之前的能匹配,那么dp[i + 1][j + 1] = true
                if (c == t) {
                    if (dp[i][j]) {
                        dp[i + 1][j + 1] = true;
                    }
                }
            }
        }
    }

    return dp[m][n];
}

深度優先遍歷(DFS)和廣度優先遍歷(BFS)

算法解析

深度優先遍歷(Depth First Search, 簡稱 DFS) 與廣度優先遍歷(Breath First Search)是圖論中兩種非常重要的算法,生產上廣泛用於拓撲排序,尋路(走迷宮),搜索引擎,爬蟲等

深度優先遍歷

主要思路是從圖中一個未訪問的頂點 V 開始,沿着一條路一直走到底,然后從這條路盡頭的節點回退到上一個節點,再從另一條路開始走到底...,不斷遞歸重復此過程,直到所有的頂點都遍歷完成,它的特點是不撞南牆不回頭,先走完一條路,再換一條路繼續走。

樹是圖的一種特例(連通無環的圖就是樹),接下來我們來看看樹用深度優先遍歷該怎么遍歷。

image-20220115170157015

1、我們從根節點 1 開始遍歷,它相鄰的節點有 2,3,4,先遍歷節點 2,再遍歷 2 的子節點 5,然后再遍歷 5 的子節點 9。

image-20220115170221228

2、上圖中一條路已經走到底了(9是葉子節點,再無可遍歷的節點),此時就從 9 回退到上一個節點 5,看下節點 5 是否還有除 9 以外的節點,沒有繼續回退到 2,2 也沒有除 5 以外的節點,回退到 1,1 有除 2 以外的節點 3,所以從節點 3 開始進行深度優先遍歷,如下:

image-20220115185103926

3、同理從 10 開始往上回溯到 6, 6 沒有除 10 以外的子節點,再往上回溯,發現 3 有除 6 以外的子點 7,所以此時會遍歷 7。

image-20220115185126792

3、從 7 往上回溯到 3, 1,發現 1 還有節點 4 未遍歷,所以此時沿着 4, 8 進行遍歷,這樣就遍歷完成了。

完整的節點的遍歷順序如下(節點上的的藍色數字代表):

image-20220115185148955

廣度優先遍歷

廣度優先遍歷,指的是從圖的一個未遍歷的節點出發,先遍歷這個節點的相鄰節點,再依次遍歷每個相鄰節點的相鄰節點。

上文所述樹的廣度優先遍歷動圖如下,每個節點的值即為它們的遍歷順序。所以廣度優先遍歷也叫層序遍歷,先遍歷第一層(節點 1),再遍歷第二層(節點 2,3,4),第三層(5,6,7,8),第四層(9,10)。

img

深度優先遍歷用的是,而廣度優先遍歷要用隊列來實現例如:二叉樹的層次遍歷.

BFS框架:
// 計算從起點 start 到終點 target 的最近距離
int BFS(Node start, Node target) {
    Queue<Node> q; // 核心數據結構
    Set<Node> visited; // 避免走回頭路

    q.offer(start); // 將起點加入隊列
    visited.add(start);
    int step = 0; // 記錄擴散的步數

    while (q not empty) {
        int sz = q.size();
        /* 將當前隊列中的所有節點向四周擴散 */
        for (int i = 0; i < sz; i++) {
            Node cur = q.poll();
            /* 划重點:這里判斷是否到達終點 */
            if (cur is target)
                return step;
            /* 將 cur 的相鄰節點加入隊列 */
            for (Node x : cur.adj())
                if (x not in visited) {
                    q.offer(x);
                    visited.add(x);
                }
        }
        /* 划重點:更新步數在這里 */
        step++;
    }
}

隊列 q 就不說了,BFS 的核心數據結構;cur.adj() 泛指 cur 相鄰的節點,比如說二維數組中,cur 上下左右四面的位置就是相鄰節點;visited 的主要作用是防止走回頭路,大部分時候都是必須的,但是像一般的二叉樹結構,沒有子節點到父節點的指針,不會走回頭路就不需要 visited。

127. 單詞接龍

字典 wordList 中從單詞 beginWord 和 endWord 的 轉換序列 是一個按下述規格形成的序列:

序列中第一個單詞是 beginWord 。
序列中最后一個單詞是 endWord 。
每次轉換只能改變一個字母。
轉換過程中的中間單詞必須是字典 wordList 中的單詞。
給你兩個單詞 beginWord 和 endWord 和一個字典 wordList ,找到從 beginWord 到 endWord 的 最短轉換序列 中的 單詞數目 。如果不存在這樣的轉換序列,返回 0。

思路:

「轉換」意即:兩個單詞對應位置只有一個字符不同,例如 "hit" 與 "hot",這種轉換是可以逆向的,因此,根據題目給出的單詞列表,可以構建出一個無向(無權)圖;

image-20220115183527595

如果一開始就構建圖,每一個單詞都需要和除它以外的另外的單詞進行比較,復雜度是O(NwordLen),這里 N是單詞列表的長度;
為此,我們在遍歷一開始,把所有的單詞列表放進一個哈希表中,然后在遍歷的時候構建圖,每一次得到在單詞列表里可以轉換的單詞,復雜度是 O(26×wordLen),借助哈希表,找到鄰居與 NN 無關;
使用 BFS 進行遍歷,需要的輔助數據結構是:

  • 隊列;
  • visited 集合。說明:可以直接在 wordSet (由 wordList 放進集合中得到)里做刪除。但更好的做法是新開一個哈希表,遍歷過的字符串放進哈希表里。這種做法具有普遍意義。絕大多數在線測評系統和應用場景都不會在意空間開銷。
單向廣度優先遍歷

廣度優先遍歷(BFS),一層一層的將隊列里的字符串出隊,然后遍歷它可以修改字符后的字符串,查看能否變成endWord,如果不能就入隊,並標記已訪問

代碼如下:

public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    // 廣度優先遍歷(BFS),一層一層的將隊列里的字符串出隊,然后遍歷它可以修改字符后的字符串,查看能否變成endWord,如果不能就入隊,並標記已訪問
    // 轉換成set便於快速判斷是否存在
    Set<String> wordSet = new HashSet<>(wordList);
    if (wordSet.size() <= 0 || !wordSet.contains(endWord)) {
        return 0;
    }
    wordSet.remove(beginWord);
    // 廣度優先遍歷一定要有隊列和visited標記數組
    Set<String> visited = new HashSet<>();
    Queue<String> queue = new LinkedList<>();

    // 先把第一個開始字符串加進來,標記已訪問
    queue.offer(beginWord);
    visited.add(beginWord);

    int step = 1;
    while (!queue.isEmpty()) {
        // 記錄好目前隊列長度,先把當前一層的遍歷完,遍歷過程會往隊列里入隊新字符串
        int size = queue.size();
        for (int i = 0;i < size;i++) {
            String curWord = queue.poll();
            // 取出隊列里的字符串,然后進入下邊的方法判斷該字符串修改一個字符是否能變成endWord,如果是,直接返回
            if (changeWordEveryOneLetter(curWord,endWord,queue,visited,wordSet)) {
                return step + 1;
            }
        }

        step++;
    }

    return 0;

}
// 判斷curWord修改一個字符能否變成endword,如果能,返回true,不能,就把修改過后的字符串入隊,並且標記已訪問
private boolean changeWordEveryOneLetter(String curWord,String endWord,Queue<String> queue,Set<String> visited,Set<String> wordSet) {
    char[] charArray = curWord.toCharArray();
    // 對endWord的每一個位置對應搞curWord上來修改,看是否可以變成endWord
    for (int i = 0;i < endWord.length();i++) {
        char originChar = charArray[i];
        for (char k = 'a';k <= 'z';k++) {
            // 修改第i位,如果是本身的字符,跳過
            if (originChar == k) {
                continue;
            }
            charArray[i] = k;
            String newStr = String.valueOf(charArray);
            // 判斷修改字符過后的字符串是否在哈希表里
            if (wordSet.contains(newStr)) {
                // 如果修改字符過后的字符串和endWord相等,直接返回true
                if (newStr.equals(endWord)) {
                    return true;
                }
                // 如果修改字符過后的字符串和endWord不相等,如果沒訪問過,就入隊,並且標記為已訪問
                if (!visited.contains(newStr)) {
                    queue.offer(newStr);
                    visited.add(newStr);
                }
            }
        }
        // 最后恢復該位置的字符,進行下一個位置的修改
        charArray[i] = originChar;
    }

    return false;
}
雙向廣度優先遍歷
  • 已知目標頂點的情況下,可以分別從起點和目標頂點(終點)執行廣度優先遍歷,直到遍歷的部分有交集。這種方式搜索的單詞數量會更小一些;
  • 更合理的做法是,每次從單詞數量小的集合開始擴散
  • 這里 beginVisited 和 endVisited 交替使用,等價於單向 BFS 里使用隊列,每次擴散都要加到總的 visited 里。

image-20220115184450513

代碼如下:

// 雙向廣度優先遍歷
public int ladderLength2(String beginWord, String endWord, List<String> wordList) {
    // 雙向廣度優先遍歷(BFS)分別從起點和目標頂點(終點)執行廣度優先遍歷,直到遍歷的部分有交集。這種方式搜索的單詞數量會更小一些;
    // 更合理的做法是,每次從單詞數量小的集合開始擴散;
    // 第 1 步:先將 wordList 放到哈希表里,便於判斷某個單詞是否在 wordList 里
    Set<String> wordSet = new HashSet<>(wordList);
    if (wordSet.size() <= 0 || !wordSet.contains(endWord)) {
        return 0;
    }

    // 這里 beginVisited 和 endVisited 交替使用,等價於單向 BFS 里使用隊列,每次擴散都要加到總的 visited 里。
    // 第 2 步:已經訪問過的 word 添加到 visited 哈希表里
    Set<String> visited = new HashSet<>();
    // 分別用左邊和右邊擴散的哈希表代替單向 BFS 里的隊列,它們在雙向 BFS 的過程中交替使用
    Set<String> beginVisited = new HashSet<>();
    beginVisited.add(beginWord);
    Set<String> endVisited = new HashSet<>();
    endVisited.add(endWord);

    // 第 3 步:執行雙向 BFS,左右交替擴散的步數之和為所求
    int step = 1;
    while (!beginVisited.isEmpty() && !endVisited.isEmpty()) {
        // 優先選擇小的哈希表進行擴散,考慮到的情況更少,這里是把兩個集合互換,這樣就只用考慮beginVisited集合就可以了,保證beginVisited集合始終是最小的
        if (beginVisited.size() > endVisited.size()) {
            Set<String> temp = endVisited;
            endVisited = beginVisited;
            beginVisited = temp;
        }
        // 邏輯到這里,保證 beginVisited 是相對較小的集合,nextLevelVisited 在擴散完成以后,會成為新的 beginVisited
        Set<String> nextLevelVisited = new HashSet<>();
        for (String curWord:beginVisited) {
            if (changeWordEveryOneLetter(curWord,endVisited,visited,wordSet,nextLevelVisited)) {
                return step + 1;
            }
        }
        // 原來的 beginVisited 廢棄,從 nextLevelVisited 開始新的雙向 BFS
        beginVisited = nextLevelVisited;
        step++;
    }
    return 0;

}
/**
 * 嘗試對 word 修改每一個字符,看看是不是能落在 endVisited 中,擴展得到的新的 word 添加到 nextLevelVisited 里
 *
 * @param curWord
 * @param endVisited
 * @param visited
 * @param wordSet
 * @param nextLevelVisited
 * @return
 */
private boolean changeWordEveryOneLetter(String curWord,Set<String> endVisited,Set<String> visited,Set<String> wordSet,Set<String> nextLevelVisited) {
    char[] charArray = curWord.toCharArray();
    for (int i = 0;i < charArray.length;i++) {
        char originChar = charArray[i];
        for (char c = 'a';c <= 'z';c++) {
            if (originChar == c) {
                continue;
            }
            charArray[i] = c;
            String newStr = String.valueOf(charArray);
            if (wordSet.contains(newStr)) {
                // 這里判斷的是修改字符后的字符串是否落入endVisited里,如果是,說明出現了交集,直接返回true
                if (endVisited.contains(newStr)) {
                    return true;
                }
                if (!visited.contains(newStr)) {
                    // nextLevelVisited記錄curWord可以一步變成的字符串,最后用來更新beginVisited集合
                    nextLevelVisited.add(newStr);
                    visited.add(newStr);
                }
            }
        }
        // 恢復,下次再用
        charArray[i] = originChar;
    }
    return false;
}

752.打開轉盤鎖

題目描述:
你有一個帶有四個圓形撥輪的轉盤鎖。每個撥輪都有10個數字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每個撥輪可以自由旋轉:例如把 '9' 變為 '0','0' 變為 '9' 。每次旋轉都只能旋轉一個撥輪的一位數字。

鎖的初始數字為 '0000' ,一個代表四個撥輪的數字的字符串。

列表 deadends 包含了一組死亡數字,一旦撥輪的數字和列表里的任何一個元素相同,這個鎖將會被永久鎖定,無法再被旋轉。

字符串 target 代表可以解鎖的數字,你需要給出最小的旋轉次數,如果無論如何不能解鎖,返回 -1。

代碼題解:
class Solution {
    public int openLock(String[] deadends, String target) {
        Queue<String> q = new LinkedList<>();
        q.offer("0000");
        // 記錄轉動過的數字,防止走回頭路
        Set<String> visited = new HashSet<>();
        // 記錄需要跳過的死亡密碼
        Set<String> deads = new HashSet<>();
        for(String s:deadends) {
            deads.add(s);
        }
        visited.add("0000");
        // 從起點開始啟動廣度優先搜索
        int dep = 0;
        while(!q.isEmpty()) {
            int size = q.size();
            /* 將當前隊列中的所有節點向周圍擴散 */
            for(int i=0; i<size;i++) {
                String cur = q.poll();
                if(cur.equals(target)) {
                    return dep;
                }
                if(deads.contains(cur)) {
                    continue;
                }
                /* 將一個節點的未遍歷相鄰的八個節點加入隊列 */
                for(int j=0;j<4;j++) {
                    String up = plusOne(cur,j);
                    if(!visited.contains(up)) {
                        visited.add(up);
                        q.offer(up);
                    }
                    String down = minusOne(cur,j);
                    if(!visited.contains(down)) {
                        visited.add(down);
                        q.offer(down);
                    }
                }
            }
            dep++;
        }
        return -1;
    }

    String plusOne(String s,int j) {
        //往上波動
        char[] ch = s.toCharArray();
        if(ch[j] == '9') {
            ch[j] ='0';
        }else {
            ch[j] +=1; 
        }
        return new String(ch);
    }

    String minusOne(String s,int j) {
        //向下波動
        char[] ch = s.toCharArray();
        if(ch[j] == '0') {
            ch[j] = '9';
        }else {
            ch[j]-=1;
        }
        return new String(ch);
    }
}

雙向BFS解法:

class Solution {
    public int openLock(String[] deadends, String target) {
        //雙向BFS
        // 用集合不用隊列,可以快速判斷元素是否存在
        Set<String> q1 = new HashSet<>();
        Set<String> q2 = new HashSet<>();
        Set<String> visited = new HashSet<>();
        Set<String> deads = new HashSet<>();
        for(String s:deadends) {
            deads.add(s);
        }
        //起點
        q1.add("0000");
        // 終點
        q2.add(target);
        int dep = 0;
        while(!q1.isEmpty()&&!q2.isEmpty()) {
            // 哈希集合在遍歷的過程中不能修改,用 temp 存儲擴散結果
            Set<String> temp = new HashSet<>();
             /* 將 q1 中的所有節點向周圍擴散 */
            for (String cur : q1) {
                if(deads.contains(cur)) {
                    continue;
                }
                //這里判斷雙隊列是否有交集,如果有交集,則找到最短路徑
                if(q2.contains(cur)) {
                    return dep;
                }
                visited.add(cur);
                /* 將一個節點的未遍歷相鄰節點加入集合 */
                for(int j=0;j<4;j++) {
                    String up = plusOne(cur,j);
                    if(!visited.contains(up)) {
                        temp.add(up);
                    }
                    String down = minusOne(cur,j);
                    if(!visited.contains(down)) {
                        temp.add(down);
                    }
                }
            }
            //交換q1和q2
            // temp 相當於 q1
            // 這里交換 q1 q2,下一輪 while 就是擴散 q2
            q1=q2;
            q2=temp;
            dep++;

        }

        return -1;
    }

    String plusOne(String s,int j) {
        //往上波動
        char[] ch = s.toCharArray();
        if(ch[j] == '9') {
            ch[j] ='0';
        }else {
            ch[j] +=1; 
        }
        return new String(ch);
    }

    String minusOne(String s,int j) {
        //向下波動
        char[] ch = s.toCharArray();
        if(ch[j] == '0') {
            ch[j] = '9';
        }else {
            ch[j]-=1;
        }
        return new String(ch);
    }


}

111.二叉樹的最小深度

題目描述
給定一個二叉樹,找出其最小深度。

最小深度是從根節點到最近葉子節點的最短路徑上的節點數量。

說明:葉子節點是指沒有子節點的節點。

代碼題解:
class Solution {
    public int minDepth(TreeNode root) {
        if(root ==null) {
            return 0;
        }
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        int dep =1;
        while(!q.isEmpty()) {
            int size = q.size();
            /* 將當前隊列中的所有節點向四周擴散 */
            for(int i = 0;i<size;i++) {
                TreeNode t = q.poll();
                if(t.left==null&&t.right==null) {
                    return dep;
                }
                /* 將 t 的相鄰節點加入隊列 */
                if(t.left!=null) {
                    q.offer(t.left);
                }
                if(t.right!=null) {
                    q.offer(t.right);
                }
            }
            dep++;
        }
        return dep;
    }
}

網格類問題的 DFS 遍歷方法

https://leetcode-cn.com/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/

們所熟悉的 DFS(深度優先搜索)問題通常是在樹或者圖結構上進行的。而我們今天要討論的 DFS 問題,是在一種「網格」結構中進行的。島嶼問題是這類網格 DFS 問題的典型代表。

網格問題是由 m \times nm×n 個小方格組成一個網格,每個小方格與其上下左右四個方格認為是相鄰的,要在這樣的網格上進行某種搜索。

島嶼問題是一類典型的網格問題。每個格子中的數字可能是 0 或者 1。我們把數字為 0 的格子看成海洋格子,數字為 1 的格子看成陸地格子,這樣相鄰的陸地格子就連接成一個島嶼。

image-20220118223105827

在這樣一個設定下,就出現了各種島嶼問題的變種,包括島嶼的數量、面積、周長等。不過這些問題,基本都可以用 DFS 遍歷來解決。

DFS 的基本結構

網格結構要比二叉樹結構稍微復雜一些,它其實是一種簡化版的圖結構。要寫好網格上的 DFS 遍歷,我們首先要理解二叉樹上的 DFS 遍歷方法,再類比寫出網格結構上的 DFS 遍歷。我們寫的二叉樹 DFS 遍歷一般是這樣的:

void traverse(TreeNode root) {
    // 判斷 base case
    if (root == null) {
        return;
    }
    // 訪問兩個相鄰結點:左子結點、右子結點
    traverse(root.left);
    traverse(root.right);
}

可以看到,二叉樹的 DFS 有兩個要素:「訪問相鄰結點」和「判斷 base case」。

第一個要素是訪問相鄰結點。二叉樹的相鄰結點非常簡單,只有左子結點和右子結點兩個。二叉樹本身就是一個遞歸定義的結構:一棵二叉樹,它的左子樹和右子樹也是一棵二叉樹。那么我們的 DFS 遍歷只需要遞歸調用左子樹和右子樹即可。

第二個要素是 判斷 base case。一般來說,二叉樹遍歷的 base case 是 root == null。這樣一個條件判斷其實有兩個含義:一方面,這表示 root 指向的子樹為空,不需要再往下遍歷了。另一方面,在 root == null 的時候及時返回,可以讓后面的 root.left 和 root.right 操作不會出現空指針異常。

對於網格上的 DFS,我們完全可以參考二叉樹的 DFS,寫出網格 DFS 的兩個要素:

首先,網格結構中的格子有多少相鄰結點?答案是上下左右四個。對於格子 (r, c) 來說(r 和 c 分別代表行坐標和列坐標),四個相鄰的格子分別是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)。換句話說,網格結構是「四叉」的。

image-20220118223300155

其次,網格 DFS 中的 base case 是什么?從二叉樹的 base case 對應過來,應該是網格中不需要繼續遍歷、grid[r][c] 會出現數組下標越界異常的格子,也就是那些超出網格范圍的格子。

我們得到了網格 DFS 遍歷的框架代碼:

void dfs(int[][] grid, int r, int c) {
    // 判斷 base case
    // 如果坐標 (r, c) 超出了網格范圍,直接返回
    if (!inArea(grid, r, c)) {
        return;
    }
    // 訪問上、下、左、右四個相鄰結點
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判斷坐標 (r, c) 是否在網格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

如何避免重復遍歷
網格結構的 DFS 與二叉樹的 DFS 最大的不同之處在於,遍歷中可能遇到遍歷過的結點。這是因為,網格結構本質上是一個「圖」,我們可以把每個格子看成圖中的結點,每個結點有向上下左右的四條邊。在圖中遍歷時,自然可能遇到重復遍歷結點。

這時候,DFS 可能會不停地「兜圈子」,永遠停不下來,如下圖所示:

DFS 遍歷可能會兜圈子(動圖)

如何避免這樣的重復遍歷呢?答案是標記已經遍歷過的格子。以島嶼問題為例,我們需要在所有值為 1 的陸地格子上做 DFS 遍歷。每走過一個陸地格子,就把格子的值改為 2,這樣當我們遇到 2 的時候,就知道這是遍歷過的格子了。也就是說,每個格子可能取三個值:

0 —— 海洋格子
1 —— 陸地格子(未遍歷過)
2 —— 陸地格子(已遍歷過)
我們在框架代碼中加入避免重復遍歷的語句:

void dfs(int[][] grid, int r, int c) {
    // 判斷 base case
    if (!inArea(grid, r, c)) {
        return;
    }
    // 如果這個格子不是島嶼,直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 將格子標記為「已遍歷過」
    
    // 訪問上、下、左、右四個相鄰結點
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
   }

// 判斷坐標 (r, c) 是否在網格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

標記已遍歷的格子

這樣,我們就得到了一個島嶼問題、乃至各種網格問題的通用 DFS 遍歷方法。

695. 島嶼的最大面積

給你一個大小為 m x n 的二進制矩陣 grid 。

島嶼 是由一些相鄰的 1 (代表土地) 構成的組合,這里的「相鄰」要求兩個 1 必須在 水平或者豎直的四個方向上 相鄰。你可以假設 grid 的四個邊緣都被 0(代表水)包圍着。

島嶼的面積是島上值為 1 的單元格的數目。

計算並返回 grid 中最大的島嶼面積。如果沒有島嶼,則返回面積為 0 。

代碼如下:

class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        int res = 0;
        int m = grid.length;
        int n = grid[0].length;
        // 遇到'1'了就深度優先遍歷網格,每一次的深度優先遍歷都代表一個島嶼,所以res++
        for (int i = 0;i < m;i++) {
            for (int j = 0;j < n;j++) {
                if (grid[i][j] == 1) {
                    int area = dfs(grid,i,j);
                    res = Math.max(res,area);
                }
            }
        }

        return res;
    }

    private int dfs(int[][] grid,int i,int j) {
        // 深度優先遍歷,判斷是否到達了邊界
        if (!inArea(grid,i,j)) {
            return 0;
        }
        // 如果當前格子不是島嶼就直接返回,沒必要往非島嶼周圍dfs了
        if (grid[i][j] != 1) {
            return 0;
        }
        grid[i][j] = 2;
        return 1 + dfs(grid,i - 1,j) + dfs(grid,i,j+1) + dfs(grid,i+1,j) + dfs(grid,i,j-1);
    }
    private boolean inArea(int[][] grid,int i,int j) {
        if (i >= 0 && i < grid.length && j >= 0 && j < grid[0].length) {
            return true;
        }
        return false;
    }
}

200. 島嶼數量

給你一個由 '1'(陸地)和 '0'(水)組成的的二維網格,請你計算網格中島嶼的數量。

島嶼總是被水包圍,並且每座島嶼只能由水平方向和/或豎直方向上相鄰的陸地連接形成。

此外,你可以假設該網格的四條邊均被水包圍。

思路

遇到'1'了就深度優先遍歷網格,遍歷過程把島嶼'0'變成2,標記為已訪問的島嶼,每一次的深度優先遍歷都代表一個島嶼,所以dfs一次就加一。

代碼如下:

class Solution {
    public int numIslands(char[][] grid) {
        int res = 0;
        int m = grid.length;
        int n = grid[0].length;
        // 遇到'1'了就深度優先遍歷網格,每一次的深度優先遍歷都代表一個島嶼,所以res++
        for (int i = 0;i < m;i++) {
            for (int j = 0;j < n;j++) {
                if (grid[i][j] == '1') {
                    dfs(grid,i,j);
                    res++;
                }
            }
        }

        return res;
    }

    private void dfs(char[][] grid,int i,int j) {
        // 深度優先遍歷,判斷是否到達了邊界
        if (!inArea(grid,i,j)) {
            return;
        }
        // 如果當前格子不是島嶼就直接返回,沒必要往非島嶼周圍dfs了
        if (grid[i][j] != '1') {
            return;
        }
        // 訪問過了直接標記為2表示已訪問過
        grid[i][j] = '2';
        // 對當前格子的鄰近四個格子進行dfs
        dfs(grid,i-1,j);
        dfs(grid,i,j+1);
        dfs(grid,i+1,j);
        dfs(grid,i,j-1);
    }

    private boolean inArea(char[][] grid,int i,int j) {
        if (i >= 0 && i < grid.length && j >= 0 && j < grid[0].length) {
            return true;
        }
        return false;
    }
}

212. 單詞搜索 II

給定一個 m x n 二維字符網格 board 和一個單詞(字符串)列表 words,找出所有同時在二維網格和字典中出現的單詞。

單詞必須按照字母順序,通過 相鄰的單元格 內的字母構成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母在一個單詞中不允許被重復使用。

思路:dfs + 前綴樹

前綴樹(字典樹)是一種樹形數據結構,用於高效地存儲和檢索字符串數據集中的鍵。前綴樹可以用 O(|S|)O(∣S∣) 的時間復雜度完成如下操作,其中 |S|∣S∣ 是插入字符串或查詢前綴的長度:

  • 向前綴樹中插入字符串 word;

  • 查詢前綴串prefix 是否為已經插入到前綴樹中的任意一個字符串 word 的前綴;

根據題意,我們需要逐個遍歷二維網格中的每一個單元格;然后搜索從該單元格出發的所有路徑,找到其中對應 words 中的單詞的路徑。因為這是一個回溯的過程,所以我們有如下算法:

  • 遍歷二維網格中的所有單元格。

  • 深度優先搜索所有從當前正在遍歷的單元格出發的、由相鄰且不重復的單元格組成的路徑。因為題目要求同一個單元格內的字母在一個單詞中不能被重復使用;所以我們在深度優先搜索的過程中,每經過一個單元格,都將該單元格的字母臨時修改為特殊字符(例如 #),以避免再次經過該單元格。

  • 如果當前路徑是 words 中的單詞,則將其添加到結果集中。如果當前路徑是 words 中任意一個單詞的前綴,則繼續搜索;反之,如果當前路徑不是 wordswords 中任意一個單詞的前綴,則剪枝。我們可以將 words 中的所有字符串先添加到前綴樹中,而后用 O(∣S∣) 的時間復雜度查詢當前路徑是否為 words 中任意一個單詞的前綴。

注意:

在回溯的過程中,我們不需要每一步都判斷完整的當前路徑是否是 wordswords 中任意一個單詞的前綴;而是可以記錄下路徑中每個單元格所對應的前綴樹結點,每次只需要判斷新增單元格的字母是否是上一個單元格對應前綴樹結點的子結點即可。

代碼如下:

class Solution {
    // 前綴樹
    private class Trie {
        String word;
        boolean isEnd;
        Map<Character,Trie> children;
        public Trie() {
            word = "";
            children = new HashMap<>();
        }

        public void insert(String word) {
            Trie cur = this;
            for (char c:word.toCharArray()) {
                if (!cur.children.containsKey(c)) {
                    cur.children.put(c,new Trie());
                }
                cur = cur.children.get(c);
            }
            // 標記為葉子節點的word為整個路徑的字符串
            cur.word = word;
        }
    }
    private int[][] direction ={{-1,0},{0,1},{1,0},{0,-1}};
    public List<String> findWords(char[][] board, String[] words) {
        Set<String> res = new HashSet<>();
        Trie trie = new Trie();
        // 先把所有單詞都插入到前綴樹里
        for (String word:words) {
            trie.insert(word);
        }
        for (int i = 0;i < board.length;i++) {
            for (int j = 0;j < board[0].length;j++) {
                dfs(board,i,j,trie,res);
            }
        }
        
        return new ArrayList<String>(res);
    }

    private void dfs(char[][] board,int i,int j,Trie now,Set<String> res) {
        // 如果當前前綴樹的子節點不包含該字符就返回
        if (!now.children.containsKey(board[i][j])) {
            return;
        }
        char ch = board[i][j];
        // 獲取當前前綴樹節點的字符對應的樹節點
        now = now.children.get(ch);
        // 如果樹節點到了葉子節點,說明有了一個完整的word的路徑,添加單詞進去
        if (!"".equals(now.word)) {
            res.add(now.word);
        }
        // 標記為已訪問狀態
        board[i][j] = '#';
        // 對當前單元格往四周前進
        for (int k = 0;k < direction.length;k++) {
            if (inArea(board,i + direction[k][0],j + direction[k][1])) {
                dfs(board,i + direction[k][0],j + direction[k][1],now,res);
            }
        }
        // 還原單元格的字符
        board[i][j] = ch;

    }

    private boolean inArea(char[][] board,int i,int j) {
        if (i >= 0 && i < board.length && j >= 0 && j < board[0].length) {
            return true;
        }
        return false;
    }
}

前綴樹

https://mp.weixin.qq.com/s/hGrTUmM1zusPZZ0nA9aaNw

208. 實現 Trie (前綴樹)

Trie(發音類似 "try")或者說 前綴樹 是一種樹形數據結構,用於高效地存儲和檢索字符串數據集中的鍵。這一數據結構有相當多的應用情景,例如自動補完和拼寫檢查。

請你實現 Trie 類:

Trie() 初始化前綴樹對象。
void insert(String word) 向前綴樹中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前綴樹中,返回 true(即,在檢索之前已經插入);否則,返回 false 。
boolean startsWith(String prefix) 如果之前已經插入的字符串 word 的前綴之一為 prefix ,返回 true ;否則,返回 false

思路

類似於多叉樹,這里是26叉樹,每個子節點包含26個字符中的一個,並且通過isEnd標記是否是葉子節點,每次插入字符串就遍歷字符串的每個字符然后依次插入到對應的數組的節點位置,如果本來為null就新建個樹到對應數組的位置上。

代碼如下:

public class Trie {
    class TrieNode {
        // 標記是否是葉子節點
        private boolean isEnd;

        TrieNode[] next;
        public TrieNode() {
            isEnd = false;
            // 每個節點下都有26個節點,通過數組表示,每個節點表示一個字母,不為空說明該節點有對應字母出現
            next = new TrieNode[26];
        }
    }

    private TrieNode root;
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String word) {
        TrieNode trieNode = root;
        // 插入就是遍歷每個字符,然后新建到對應數組的節點上
        for (char c:word.toCharArray()) {
            if (trieNode.next[c - 'a'] == null) {
                trieNode.next[c - 'a'] = new TrieNode();
            }
            trieNode = trieNode.next[c - 'a'];
        }
        trieNode.isEnd = true;
    }

    public boolean search(String word) {
        TrieNode trieNode = root;
        // 查詢,要遍歷字符串每個字符,直到能搜索到最后一個字符,並且最后一個字符是葉子節點
        for (char c:word.toCharArray()) {
            trieNode = trieNode.next[c - 'a'];
            if (trieNode == null) {
                return false;
            }
        }
        if (trieNode.isEnd) {
            return true;
        }
        return false;
    }

    public boolean startsWith(String prefix) {
        TrieNode trieNode = root;
        // 遍歷字符串每個字符,直到能搜索到最后一個字符,同時不必要最后一個字符時葉子節點,因為是搜索前綴
        for (char c:prefix.toCharArray()) {
            trieNode = trieNode.next[c - 'a'];
            if (trieNode == null) {
                return false;
            }
        }
        return true;
    }

648. 單詞替換

在英語中,我們有一個叫做 詞根(root) 的概念,可以詞根后面添加其他一些詞組成另一個較長的單詞——我們稱這個詞為 繼承詞(successor)。例如,詞根an,跟隨着單詞 other(其他),可以形成新的單詞 another(另一個)。

現在,給定一個由許多詞根組成的詞典 dictionary 和一個用空格分隔單詞形成的句子 sentence。你需要將句子中的所有繼承詞用詞根替換掉。如果繼承詞有許多可以形成它的詞根,則用最短的詞根替換它。

你需要輸出替換之后的句子。

代碼:

class Solution {
    // 前綴樹
    class TrieNode {
        String word;
        Map<Character,TrieNode> children;
        public TrieNode() {
            children = new HashMap<>();
        }
    }
    public String replaceWords(List<String> dictionary, String sentence) {
        String[] words = sentence.split(" ");
        StringBuilder sb = new StringBuilder();
        TrieNode trie = new TrieNode();
        for (String dict:dictionary) {
            TrieNode  cur = trie;
            for (char c:dict.toCharArray()) {
                if (!cur.children.containsKey(c)) {
                    TrieNode now = new TrieNode();
                    cur.children.put(c,now);
                }
                cur = cur.children.get(c);
            }
            cur.word = dict;
        }

        for (int i = 0;i < words.length;i++) {
            TrieNode cur = trie;
            for (char c:words[i].toCharArray()) {
                // 如果當前字符不存在當前前綴樹節點中,或者已經在前綴樹中找到了一個符合的前綴了就退出
                if (!cur.children.containsKey(c) || cur.word != null) {
                    break;
                }
                cur = cur.children.get(c);
            }
            // 如果找到了符合的前綴就拼接前綴,否則拼接原字符串
            sb.append(cur.word == null?words[i]:cur.word);
            if (i < words.length - 1) {
                sb.append(" ");
            }
        }

        return sb.toString();
    }
}

拓撲排序

207. 課程表

你這個學期必須選修 numCourses 門課程,記為 0 到 numCourses - 1 。

在選修某些課程之前需要一些先修課程。 先修課程按數組 prerequisites 給出,其中 prerequisites[i] = [ai, bi] ,表示如果要學習課程 ai 則 必須 先學習課程 bi 。

例如,先修課程對 [0, 1] 表示:想要學習課程 0 ,你需要先完成課程 1 。
請你判斷是否可能完成所有課程的學習?如果可以,返回 true ;否則,返回 false 。

思路

本題可約化為: 課程安排圖是否是 有向無環圖(DAG)。即課程間規定了前置條件,但不能構成任何環路,否則課程前置條件將不成立。
思路是通過 拓撲排序 判斷此課程安排圖是否是 有向無環圖(DAG) 。 拓撲排序原理: 對 DAG 的頂點進行排序,使得對每一條有向邊 (u, v)(u,v),均有 uu(在排序記錄中)比 vv 先出現。亦可理解為對某點 vv 而言,只有當 vv 的所有源點均出現了,vv 才能出現。
通過課程前置條件列表 prerequisites 可以得到課程安排圖的 鄰接表 adjacency

方法一:入度表(廣度優先遍歷)

算法流程:
1、統計課程安排圖中每個節點的入度,生成 入度表 indegrees。
2、借助一個隊列 queue,將所有入度為 00 的節點入隊。
3、當 queue 非空時,依次將隊首節點出隊,在課程安排圖中刪除此節點 pre:

  • 並不是真正從鄰接表中刪除此節點 pre,而是將此節點對應所有鄰接節點 cur 的入度 -1−1,即 indegrees[cur] -= 1。
  • 當入度 -1−1后鄰接節點 cur 的入度為 00,說明 cur 所有的前驅節點已經被 “刪除”,此時將 cur 入隊。

4、在每次 pre 出隊時,執行 numCourses--;

  • 若整個課程安排圖是有向無環圖(即可以安排),則所有節點一定都入隊並出隊過,即完成拓撲排序。換個角度說,若課程安排圖中存在環,一定有節點的入度始終不為 00。
  • 因此,拓撲排序出隊次數等於課程個數,返回 numCourses == 0 判斷課程是否可以成功安排。
  • 0210.gif

復雜度分析:

  • 時間復雜度 O(N + M)O(N+M): 遍歷一個圖需要訪問所有節點和所有臨邊,NN 和 MM 分別為節點數量和臨邊數量;
  • 空間復雜度 O(N + M)O(N+M): 為建立鄰接表所需額外空間,adjacency 長度為 NN ,並存儲 MM 條臨邊的數據

代碼如下:

/**
 * 廣度優先遍歷方式:BFS
 * @param numCourses
 * @param prerequisites
 * @return
 */
 public boolean canFinish(int numCourses, int[][] prerequisites) {
     // 課程鄰接表
     List<List<Integer> > adjacency = new ArrayList<>();
     // 入度數組,inDegrees[i]表示課程i的前序課程的數量
     int[] inDegrees = new int[numCourses];
     for (int i = 0;i < numCourses;i++) {
         adjacency.add(new ArrayList<>());
     }
     for (int i = 0;i < prerequisites.length;i++) {
         // 后序課程的入度加一
         inDegrees[prerequisites[i][1]] += 1;
         // 在鄰接表中記錄兩個課程的前后序關系
         adjacency.get(prerequisites[i][0]).add(prerequisites[i][1]);
     }
     Queue<Integer> qu = new LinkedList<>();
     // 把入度為0的課程先加入到隊列里
     for (int i = 0;i < numCourses;i++) {
         if (inDegrees[i] == 0) {
             qu.add(i);
         }
     }
     while (!qu.isEmpty()) {
         // 獲取隊列頭結點,然后課程數目減一
         int cur = qu.poll();
         numCourses--;
         // 獲得該課程的后序課程的列表
         List<Integer> nextList = adjacency.get(cur);
         // 遍歷該課程的后序課程,把每個課程的后序課程的入度減一
         for (int next:nextList) {
             inDegrees[next]--;
             // 入度減一之后,如果后序課程的入度為0,加入到隊列里
             if (inDegrees[next] == 0) {
                 qu.add(next);
             }
         }
     }
     // 最后如果所有課程都經過了合法的驗證,就是合法的課程表
     return numCourses == 0;
 }

方法二:深度優先遍歷

原理是通過 DFS 判斷圖中是否有環。

算法流程:
1、借助一個標志列表 flags,用於判斷每個節點 i (課程)的狀態:

  • 未被 DFS 訪問:i == 0;
  • 已被其他節點啟動的 DFS 訪問:i == -1;
  • 已被當前節點啟動的 DFS 訪問:i == 1。

2、對 numCourses 個節點依次執行 DFS,判斷每個節點起步 DFS 是否存在環,若存在環直接返回 FalseFalse。DFS 流程;

2.1 終止條件:

  • 當 flag[i] == -1,說明當前訪問節點已被其他節點啟動的 DFS 訪問,無需再重復搜索,直接返回 TrueTrue。
  • 當 flag[i] == 1,說明在本輪 DFS 搜索中節點 i 被第 22 次訪問,即 課程安排圖有環 ,直接返回 FalseFalse。

2.2 將當前訪問節點 i 對應 flag[i] 置 11,即標記其被本輪 DFS 訪問過;

2.3 遞歸訪問當前節點 i 的所有鄰接節點 j,當發現環直接返回 False;

2.4 當前節點所有鄰接節點已被遍歷,並沒有發現環,則將當前節點 flag 置為 -1−1 並返回 True。

3.若整個圖 DFS 結束並未發現環,返回 TrueTru**e

復雜度分析:

  • 時間復雜度 O(N + M)O(N+M): 遍歷一個圖需要訪問所有節點和所有臨邊,NN 和 MM 分別為節點數量和臨邊數量;
  • 空間復雜度 O(N + M)O(N+M): 為建立鄰接表所需額外空間,adjacency 長度為 NN ,並存儲 MM 條臨邊的數據。

代碼如下:

/**
 * 深度優先遍歷方式:dfs
 * @param numCourses
 * @param prerequisites
 * @return
 */
public boolean canFinish(int numCourses, int[][] prerequisites) {
    // 課程鄰接表
    List<List<Integer>> adjacency = new ArrayList<>();
    // flags[i] == -1標記該課程是已經確定后邊的課程都是沒有環的
    // flags[i] == 1標記在對i課程進行遍歷后續課程的時候又遇到了該課程,說明遇到了環
    int[] flags = new int[numCourses];
    for (int i = 0;i < numCourses;i++) {
        adjacency.add(new ArrayList<>());
    }
    for (int i = 0;i < prerequisites.length;i++) {
        // 在鄰接表中記錄兩個課程的前后序關系
        adjacency.get(prerequisites[i][0]).add(prerequisites[i][1]);
    }
    // 遍歷每一個課程,進行dfs查看該課程是否后續的課程存在環
    for (int i = 0;i < numCourses;i++) {
        if (!dfs(adjacency,flags,i)) {
            return false;
        }
    }
    return true;
}

private boolean dfs(List<List<Integer> > adjacency,int[] flags,int cur) {
    // 如果當前課程的flag為1,說明在上一層遞歸中或者更上幾層中,已經把該課程置為了1,即在本課程的dfs過程中又遇到了該課程,遇到了環,返回false
    if (flags[cur] == 1) {
        return false;
    }
    // 當前課程的flag為-1,說明該課程已經遍歷了所有后續課程,並且沒有出現環,直接返回true,不用再搜索了
    if (flags[cur] == -1) {
        return true;
    }
    // 先將當前課程flag標記為1,然后搜索該課程所有的后續課程,查看是否有環
    flags[cur] = 1;
    for (int next:adjacency.get(cur)) {
        if (!dfs(adjacency,flags,next)) {
            return false;
        }
    }
    // 搜索該課程所有的后續課程結束之后,沒有出現環,標記該課程flag為-1,后邊操作在遇到該課程不用再對該課程進行搜索了,直接返回true
    flags[cur] = -1;
    return true;
}

210. 課程表 II

現在你總共有 numCourses 門課需要選,記為 0 到 numCourses - 1。給你一個數組 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在選修課程 ai 前 必須 先選修 bi 。

例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示:[0,1] 。
返回你為了學完所有課程所安排的學習順序。可能會有多個正確的順序,你只要返回 任意一種 就可以了。如果不可能完成所有課程,返回 一個空數組 。

思路

整體還是和上邊的廣度優先遍歷思路一樣,只不過在每次出隊的時候把出隊的課程放入到res列表里去

算法流程:

1、在開始排序前,掃描對應的存儲空間(使用鄰接表),將入度為 00 的結點放入隊列。

2、只要隊列非空,就從隊首取出入度為 00 的結點,將這個結點輸出到結果集中,並且將這個結點的所有鄰接結點(它指向的結點)的入度減 11,在減 11 以后,如果這個被減 11 的結點的入度為 00 ,就繼續入隊。

3、當隊列為空的時候,檢查結果集中的頂點個數是否和課程數相等即可。

(思考這里為什么要使用隊列?如果不用隊列,還可以怎么做,會比用隊列的效果差還是更好?)

在代碼具體實現的時候,除了保存入度為 00 的隊列,我們還需要兩個輔助的數據結構:

4、鄰接表:通過結點的索引,我們能夠得到這個結點的后繼結點;

5、入度數組:通過結點的索引,我們能夠得到指向這個結點的結點個數。

這個兩個數據結構在遍歷題目給出的鄰邊以后就可以很方便地得到。

代碼如下:

List<Integer> res;
public int[] findOrder(int numCourses, int[][] prerequisites) {
    res = new LinkedList<>();
    // [1,0] 0 -> 1
    // 入度數組,inDegrees[i]表示課程i的前序課程的數量
    int[] inDegrees = new int[numCourses];

    // 課程鄰接表
    List<List<Integer> > adjacency = new ArrayList<>();
    for (int i = 0;i < numCourses;i++) {
        adjacency.add(new ArrayList<>());
    }
    for (int i = 0;i < prerequisites.length;i++) {
        // 后序課程的入度加一
        inDegrees[prerequisites[i][0]] += 1;
        // 在鄰接表中記錄兩個課程的前后序關系
        adjacency.get(prerequisites[i][1]).add(prerequisites[i][0]);
    }

    Queue<Integer> qu = new LinkedList<>();

    // 把入度為0的課程先加入到隊列里
    for (int i = 0;i < numCourses;i++) {
        if (inDegrees[i] == 0) {
            qu.add(i);
        }
    }
    int noCircle = 0;
    while (!qu.isEmpty()) {
        // 獲取隊列頭結點,然后noCircle加一,並且添加進res列表,因為該課程沒有了前邊的課程所以可以添加進res列表
        int cur = qu.poll();
        noCircle += 1;
        res.add(cur);
        // 獲得該課程的后序課程的列表
        for (int next:adjacency.get(cur)) {
            // 遍歷該課程的后序課程,把每個課程的后序課程的入度減一
            inDegrees[next] -= 1;
            // 入度減一之后,如果后序課程的入度為0,加入到隊列里
            if (inDegrees[next] == 0) {
                qu.add(next);
            }
        }

    }
    if (noCircle != numCourses) {
        return new int[]{};
    }
    return res.stream().mapToInt(Integer::intValue).toArray();
}

歸並排序

劍指 Offer 51. 數組中的逆序對

在數組中的兩個數字,如果前面一個數字大於后面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數。

思路

利用歸並排序,每一次在分治的治階段,將兩個分別排好序的數組進行比較,如果前面的數字比右邊的當前數字大,那么逆序對的個數就等於mid - i + 1。

代碼如下:

    int[] temp;
    public int reversePairs(int[] nums) {
        // 利用歸並排序,每一次在分治的治階段,將兩個分別排好序的數組進行比較,如果前面的數字比右邊的當前數字大,那么逆序對的個數就等於mid - i + 1
        temp = new int[nums.length];
        return mergeSort(nums,0,nums.length - 1);
    }

    private int mergeSort(int[] nums,int left,int right) {
        int res = 0;
        if (left >= right) {
            return 0;
        }
        int mid = (left + right) / 2;
        // 分階段,拆分為兩個數組
        int leftCount = mergeSort(nums,left,mid);
        int rightCount = mergeSort(nums,mid + 1,right);
        int mergeCount = merge(nums,left,mid,right);
        return leftCount + rightCount + mergeCount;
    }
    private int merge(int[] nums,int left,int mid,int right) {
        int res = 0;
        int index = 0;
        int i = left;
        int j = mid + 1;
        while (i <= mid && j <= right) {
            if (nums[i] <= nums[j]) {
                temp[index] = nums[i];
                i++;
            } else {
                // 遇到nums[i] > nums[j]的情況,說明i...mid之間的數字都比nums[j]大,所以逆序對個數加mid - i + 1
                temp[index] = nums[j];
                res += mid - i + 1;
                j++;
            }
            index++;
        }
        while (i <= mid) {
            temp[index] = nums[i];
            i++;
            index++;
        }
        while (j <= right) {
            temp[index] = nums[j];
            j++;
            index++;
        }
        index = 0;
        for (int k = left;k <= right;k++) {
            nums[k] = temp[index];
            index++;
        }
        return res;
    }

23. 合並K個升序鏈表

給你一個鏈表數組,每個鏈表都已經按升序排列。

請你將所有鏈表合並到一個升序鏈表中,返回合並后的鏈表。

思路

用分治的方法進行合並。將k個鏈表分成兩組,然后遞歸再分別分成兩組,直到最后剩下一個鏈表作為一組,然后再和另一個鏈表合並(mergeTwoList),然后返回遞歸上一層接着進行合並操作

代碼如下:

   public ListNode mergeKLists(ListNode[] lists) {
                /**
         * 用分治的方法進行合並。
         * 將k個鏈表分成兩組,然后遞歸再分別分成兩組,直到最后剩下一個鏈表作為一組,然后再和另一個鏈表合並(mergeTwoList),然后返回遞歸上一層接着進行合並操作
         */
        int len = lists.length;
        // ListNode dummy = new ListNode(-1);
        // ListNode curNode = dummy;
        // boolean flag = false;
        // ListNode[] nodes = new ListNode[len];
        // for (int i = 0;i < len;i++) {
        //     nodes[i] = lists[i];
        // }
        // int minVal = Integer.MAX_VALUE;
        // while (!flag) {
        //     minVal = Integer.MAX_VALUE;
        //     int minIndex = -1;
        //     for (int i = 0;i < len;i++) {
        //         if (nodes[i] != null && nodes[i].val < minVal) {
        //             minIndex = i;
        //             minVal = nodes[i].val;
        //         }
        //     }
        //     if (minIndex == -1) {
        //         break;
        //     }
        //     ListNode newNode = new ListNode(nodes[minIndex].val);
        //     curNode.next = newNode;
        //     curNode = curNode.next;
        //     nodes[minIndex] = nodes[minIndex].next;
        // }
        return merge(lists,0,len - 1);
    }

    private ListNode merge(ListNode[] lists,int left,int right) {
        // 分治歸並排序
        if (left == right) {
            return lists[left];
        }
        if (left > right) {
            return null;
        }
        int mid = (left + right) / 2;
        return mergeTwoLists(merge(lists,left,mid),merge(lists,mid + 1,right));
    }

    private ListNode mergeTwoLists(ListNode list1,ListNode list2) {
        ListNode dummy = new ListNode(0);
        ListNode curNode = dummy;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                curNode.next = list1;
                list1 = list1.next;
            } else {
                curNode.next = list2;
                list2 = list2.next;
            }
            curNode = curNode.next;
        }
        if (list1 != null) {
            curNode.next = list1;
        }
        if (list2 != null) {
            curNode.next = list2;
        }
        return dummy.next;
    }

單調棧

739. 每日溫度

請根據每日 氣溫 列表,重新生成一個列表。對應位置的輸出為:要想觀測到更高的氣溫,至少需要等待的天數。如果氣溫在這之后都不會升高,請在該位置用 0 來代替。

例如,給定一個列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的輸出應該是 [1, 1, 4, 2, 1, 1, 0, 0]。

思路

通常是一維數組,要尋找任一個元素的右邊或者左邊第一個比自己大或者小的元素的位置,此時我們就要想到可以用單調棧了

時間復雜度為$O(n)$。

例如本題其實就是找找到一個元素右邊第一個比自己大的元素。

此時就應該想到用單調棧了。

單調棧的本質是空間換時間,因為在遍歷的過程中需要用一個棧來記錄右邊第一個比當前元素的元素,優點是只需要遍歷一次。

在使用單調棧的時候首先要明確如下幾點:

  1. 單調棧里存放的元素是什么?

單調棧里只需要存放元素的下標i就可以了,如果需要使用對應的元素,直接T[i]就可以獲取。

  1. 單調棧里元素是遞增呢? 還是遞減呢?

注意一下順序為 從棧頭到棧底的順序,因為單純的說從左到右或者從前到后,不說棧頭朝哪個方向的話,大家一定會越看越懵。

這里我們要使用遞增循序(再強調一下是指從棧頭到棧底的順序),因為只有遞增的時候,加入一個元素i,才知道棧頂元素在數組中右面第一個比棧頂元素大的元素是i。

使用單調棧主要有三個判斷條件。

  • 當前遍歷的元素T[i]小於棧頂元素T[st.top()]的情況
  • 當前遍歷的元素T[i]等於棧頂元素T[st.top()]的情況
  • 當前遍歷的元素T[i]大於棧頂元素T[st.top()]的情況

接下來我們用temperatures = [73, 74, 75, 71, 71, 72, 76, 73]為例來逐步分析,輸出應該是 [1, 1, 4, 2, 1, 1, 0, 0]。

首先先將第一個遍歷元素加入單調棧

image-20220204195506169

加入T[1] = 74,因為T[1] > T[0](當前遍歷的元素T[i]大於棧頂元素T[st.top()]的情況),而我們要保持一個遞增單調棧(從棧頭到棧底),所以將T[0]彈出,T[1]加入,此時result數組可以記錄了,result[0] = 1,即T[0]右面第一個比T[0]大的元素是T[1]。

image-20220204195557777

加入T[2],同理,T[1]彈出

image-20220204195706982

加入T[3],T[3] < T[2] (當前遍歷的元素T[i]小於棧頂元素T[st.top()]的情況),加T[3]加入單調棧。

image-20220204195827301

加入T[4],T[4] == T[3] (當前遍歷的元素T[i]等於棧頂元素T[st.top()]的情況),此時依然要加入棧,不用計算距離,因為我們要求的是右面第一個大於本元素的位置,而不是大於等於!

image-20220204195856333

加入T[5],T[5] > T[4] (當前遍歷的元素T[i]大於棧頂元素T[st.top()]的情況),將T[4]彈出,同時計算距離,更新result

image-20220204195936029

T[4]彈出之后, T[5] > T[3] (當前遍歷的元素T[i]大於棧頂元素T[st.top()]的情況),將T[3]繼續彈出,同時計算距離,更新result

image-20220204200007202

直到發現T[5]小於T[st.top()],終止彈出,將T[5]加入單調棧

image-20220204200034010

加入T[6],同理,需要將棧里的T[5],T[2]彈出

image-20220204200102531

同理,繼續彈出

image-20220204200117084

此時棧里只剩下了T[6]

image-20220204200149315

加入T[7], T[7] < T[6] 直接入棧,這就是最后的情況,result數組也更新完了。

image-20220204200222176

其實定義result數組的時候,就應該直接初始化為0,如果result沒有更新,說明這個元素右面沒有更大的了,也就是為0。

以上在圖解的時候,已經把,這三種情況都做了詳細的分析。

  • 情況一:當前遍歷的元素T[i]小於棧頂元素T[st.top()]的情況
  • 情況二:當前遍歷的元素T[i]等於棧頂元素T[st.top()]的情況
  • 情況三:當前遍歷的元素T[i]大於棧頂元素T[st.top()]的情況

代碼如下:

/**
     * 單調棧,棧內從棧頂到棧底順序要么從大到小 要么從小到大,本題從棧頂到棧底從小到大
     * <p>
     * 入站元素要和當前棧內棧首元素進行比較
     * 若大於棧首則 則與元素下標做差
     * 若小於等於則放入
     *
     * @param temperatures
     * @return
     */
    public static int[] dailyTemperatures(int[] temperatures) {
        Stack<Integer> stack = new Stack<>();
        int[] res = new int[temperatures.length];
        for (int i = 0; i < temperatures.length; i++) {
            /**
             * 取出下標進行元素值的比較
             */
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                int preIndex = stack.pop();
                res[preIndex] = i - preIndex;
            }
            /**
             * 注意 放入的是元素位置
             */
            stack.push(i);
        }
        return res;
    }

496.下一個更大元素 I

給你兩個 沒有重復元素 的數組 nums1 和 nums2 ,其中nums1 是 nums2 的子集。

請你找出 nums1 中每個元素在 nums2 中的下一個比其大的值。

nums1 中數字 x 的下一個更大元素是指 x 在 nums2 中對應位置的右邊的第一個比 x 大的元素。如果不存在,對應位置輸出 -1 。

思路

在 739. 每日溫度 中是求每個元素下一個比當前元素大的元素的位置。

本題則是說nums1 是 nums2的子集,找nums1中的元素在nums2中下一個比當前元素大的元素。

看上去和 739. 每日溫度就如出一轍了。

從題目示例中我們可以看出最后是要求nums1的每個元素在nums2中下一個比當前元素大的元素,那么就要定義一個和nums1一樣大小的數組result來存放結果。

在遍歷nums2的過程中,我們要判斷nums2[i]是否在nums1中出現過,因為最后是要根據nums1元素的下標來更新result數組。

注意題目中說是兩個沒有重復元素 的數組 nums1 和 nums2

沒有重復元素,我們就可以用map來做映射了。根據數值快速找到下標,還可以判斷nums2[i]是否在nums1中出現過。

預處理代碼如下:

unordered_map<int, int> umap; // key:下標元素,value:下標
for (int i = 0; i < nums1.size(); i++) {
    umap[nums1[i]] = i;
}

使用單調棧,首先要想單調棧是從大到小還是從小到大。

本題和739. 每日溫度是一樣的。

棧頭到棧底的順序,要從小到大,也就是保持棧里的元素為遞增順序。只要保持遞增,才能找到右邊第一個比自己大的元素。

接下來就要分析如下三種情況,一定要分析清楚。

  1. 情況一:當前遍歷的元素T[i]小於棧頂元素T[st.top()]的情況

此時滿足遞增棧(棧頭到棧底的順序),所以直接入棧。

  1. 情況二:當前遍歷的元素T[i]等於棧頂元素T[st.top()]的情況

如果相等的話,依然直接入棧,因為我們要求的是右邊第一個比自己大的元素,而不是大於等於!

  1. 情況三:當前遍歷的元素T[i]大於棧頂元素T[st.top()]的情況

此時如果入棧就不滿足遞增棧了,這也是找到右邊第一個比自己大的元素的時候。

判斷棧頂元素是否在nums1里出現過,(注意棧里的元素是nums2的元素),如果出現過,開始記錄結果。

此時棧頂元素在nums2中右面第一個大的元素是nums2[i]即當前遍歷元素。

代碼如下:

while (!st.empty() && nums2[i] > nums2[st.top()]) {
    if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在這個元素
        int index = umap[nums2[st.top()]]; // 根據map找到nums2[st.top()] 在 nums1中的下標
        result[index] = nums2[i];
    }
    st.pop();
}
st.push(i);

代碼如下:

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        Stack<Integer> temp = new Stack<>();
        int[] res = new int[nums1.length];
        Arrays.fill(res,-1);
        HashMap<Integer, Integer> hashMap = new HashMap<>();
        for (int i = 0 ; i< nums1.length ; i++){
            hashMap.put(nums1[i],i);
        }
        temp.add(0);
        for (int i = 1; i < nums2.length; i++) {
            if (nums2[i] <= nums2[temp.peek()]) {
                temp.add(i);
            } else {
                while (!temp.isEmpty() && nums2[temp.peek()] < nums2[i]) {
                    if (hashMap.containsKey(nums2[temp.peek()])){
                        Integer index = hashMap.get(nums2[temp.peek()]);
                        res[index] = nums2[i];
                    }
                    temp.pop();
                }
                temp.add(i);
            }
        }

        return res;
    }
}

503.下一個更大元素II

給定一個循環數組(最后一個元素的下一個元素是數組的第一個元素),輸出每個元素的下一個更大元素。數字 x 的下一個更大的元素是按數組遍歷順序,這個數字之后的第一個比它更大的數,這意味着你應該循環地搜索它的下一個更大的數。如果不存在,則輸出 -1。

思路:

這道題和 739. 每日溫度 也幾乎如出一轍。

不同的時候本題要循環數組了。

如何處理循環數組。

在遍歷的過程中模擬走了兩邊nums。

代碼如下:

class Solution {
    public int[] nextGreaterElements(int[] nums) {
        //邊界判斷
        if(nums == null || nums.length <= 1) {
            return new int[]{-1};
        }
        int size = nums.length;
        int[] result = new int[size];//存放結果
        Arrays.fill(result,-1);//默認全部初始化為-1
        Stack<Integer> st= new Stack<>();//棧中存放的是nums中的元素下標
        for(int i = 0; i < 2*size; i++) {
            while(!st.empty() && nums[i % size] > nums[st.peek()]) {
                result[st.peek()] = nums[i % size];//更新result
                st.pop();//彈出棧頂
            }
            st.push(i % size);
        }
        return result;
    }
}

42. 接雨水

給定 n 個非負整數表示每個寬度為 1 的柱子的高度圖,計算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

image-20220204211826343

  • 輸入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
  • 輸出:6
  • 解釋:上面是由數組 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度圖,在這種情況下,可以接 6 個單位的雨水(藍色部分表示雨水)。

思路:

  1. 首先單調棧是按照行方向來計算雨水,如圖:

image-20220205192037411

  1. 使用單調棧內元素的順序

從大到小還是從小到大呢?

從棧頭(元素從棧頭彈出)到棧底的順序應該是從小到大的順序。

因為一旦發現添加的柱子高度大於棧頭元素了,此時就出現凹槽了,棧頭元素就是凹槽底部的柱子,棧頭第二個元素就是凹槽左邊的柱子,而添加的元素就是凹槽右邊的柱子。

如圖:

image-20220205192110878

  1. 遇到相同高度的柱子怎么辦。

遇到相同的元素,更新棧內下標,就是將棧里元素(舊下標)彈出,將新元素(新下標)加入棧中。

例如 5 5 1 3 這種情況。如果添加第二個5的時候就應該將第一個5的下標彈出,把第二個5添加到棧中。

因為我們要求寬度的時候 如果遇到相同高度的柱子,需要使用最右邊的柱子來計算寬度

如圖所示:

image-20220205192135438

  1. 棧里要保存什么數值

是用單調棧,其實是通過 長 * 寬 來計算雨水面積的。

長就是通過柱子的高度來計算,寬是通過柱子之間的下標來計算,

那么棧里有沒有必要存一個pair<int, int>類型的元素,保存柱子的高度和下標呢。

其實不用,棧里就存放int類型的元素就行了,表示下標,想要知道對應的高度,通過height[stack.top()] 就知道彈出的下標對應的高度了。

所以棧的定義如下:

stack<int> st; // 存着下標,計算的時候用下標對應的柱子高度

明確了如上幾點,我們再來看處理邏輯。

單調棧處理邏輯

先將下標0的柱子加入到棧中,st.push(0);

然后開始從下標1開始遍歷所有的柱子,for (int i = 1; i < height.size(); i++)

如果當前遍歷的元素(柱子)高度小於棧頂元素的高度,就把這個元素加入棧中,因為棧里本來就要保持從小到大的順序(從棧頭到棧底)。

代碼如下:

if (height[i] < height[st.top()])  st.push(i);

如果當前遍歷的元素(柱子)高度等於棧頂元素的高度,要跟更新棧頂元素,因為遇到相相同高度的柱子,需要使用最右邊的柱子來計算寬度。

代碼如下:

if (height[i] == height[st.top()]) { // 例如 5 5 1 7 這種情況
  st.pop();
  st.push(i);
}

如果當前遍歷的元素(柱子)高度大於棧頂元素的高度,此時就出現凹槽了,如圖所示:

image-20220205192626583

取棧頂元素,將棧頂元素彈出,這個就是凹槽的底部,也就是中間位置,下標記為mid,對應的高度為height[mid](就是圖中的高度1)。

此時的棧頂元素st.top(),就是凹槽的左邊位置,下標為st.top(),對應的高度為height[st.top()](就是圖中的高度2)。

當前遍歷的元素i,就是凹槽右邊的位置,下標為i,對應的高度為height[i](就是圖中的高度3)。

此時大家應該可以發現其實就是棧頂和棧頂的下一個元素以及要入棧的三個元素來接水!

那么雨水高度是 min(凹槽左邊高度, 凹槽右邊高度) - 凹槽底部高度,代碼為:int h = min(height[st.top()], height[i]) - height[mid];

雨水的寬度是 凹槽右邊的下標 - 凹槽左邊的下標 - 1(因為只求中間寬度),代碼為:int w = i - st.top() - 1 ;

當前凹槽雨水的體積就是:h * w

求當前凹槽雨水的體積代碼如下:

while (!st.empty() && height[i] > height[st.top()]) { // 注意這里是while,持續跟新棧頂元素
    int mid = st.top();
    st.pop();
    if (!st.empty()) {
        int h = min(height[st.top()], height[i]) - height[mid];
        int w = i - st.top() - 1; // 注意減一,只求中間寬度
        sum += h * w;
    }
}

代碼如下:

class Solution {
    public int trap(int[] height){
        int size = height.length;

        if (size <= 2) return 0;
        
        // in the stack, we push the index of array
        // using height[] to access the real height
        Stack<Integer> stack = new Stack<Integer>();
        stack.push(0);

        int sum = 0;
        for (int index = 1; index < size; index++){
            int stackTop = stack.peek();
            if (height[index] < height[stackTop]){
                stack.push(index);
            }else if (height[index] == height[stackTop]){
                // 因為相等的相鄰牆,左邊一個是不可能存放雨水的,所以pop左邊的index, push當前的index
                stack.pop();
                stack.push(index);
            }else{
                //pop up all lower value
                int heightAtIdx = height[index];
                while (!stack.isEmpty() && (heightAtIdx > height[stackTop])){
                    int mid = stack.pop();
                    
                    if (!stack.isEmpty()){
                        int left = stack.peek();

                        int h = Math.min(height[left], height[index]) - height[mid];
                        int w = index - left - 1;
                        int hold = h * w;
                        if (hold > 0) sum += hold;
                        stackTop = stack.peek();
                    }
                }
                stack.push(index);
            }
        }
        
        return sum;
    }
}

84.柱狀圖中最大的矩形

給定 n 個非負整數,用來表示柱狀圖中各個柱子的高度。每個柱子彼此相鄰,且寬度為 1 。

求在該柱狀圖中,能夠勾勒出來的矩形的最大面積。

image-20220205193559375

思路:

  1. 接雨水是找每個柱子左右兩邊第一個大於該柱子高度的柱子,而本題是找每個柱子左右兩邊第一個小於該柱子的柱子。

這里就涉及到了單調棧很重要的性質,就是單調棧里的順序,是從小到大還是從大到小

那么因為本題是要找每個柱子左右兩邊第一個小於該柱子的柱子,所以從棧頭(元素從棧頭彈出)到棧底的順序應該是從大到小的順序!

我來舉一個例子,如圖:

image-20220205193850626

只有棧里從大到小的順序,才能保證棧頂元素找到左右兩邊第一個小於棧頂元素的柱子。

所以本題單調棧的順序正好與接雨水反過來。

此時大家應該可以發現其實就是棧頂和棧頂的下一個元素以及要入棧的三個元素組成了我們要求最大面積的高度和寬度

剩下就是分析清楚如下三種情況:

  • 情況一:當前遍歷的元素heights[i]小於棧頂元素heights[st.top()]的情況
  • 情況二:當前遍歷的元素heights[i]等於棧頂元素heights[st.top()]的情況
  • 情況三:當前遍歷的元素heights[i]大於棧頂元素heights[st.top()]的情況

代碼如下:

單調棧

class Solution {
    int largestRectangleArea(int[] heights) {
        Stack<Integer> st = new Stack<Integer>();
        
        // 數組擴容,在頭和尾各加入一個元素
        int [] newHeights = new int[heights.length + 2];
        newHeights[0] = 0;
        newHeights[newHeights.length - 1] = 0;
        for (int index = 0; index < heights.length; index++){
            newHeights[index + 1] = heights[index];
        }

        heights = newHeights;
        
        st.push(0);
        int result = 0;
        // 第一個元素已經入棧,從下標1開始
        for (int i = 1; i < heights.length; i++) {
            // 注意heights[i] 是和heights[st.top()] 比較 ,st.top()是下標
            if (heights[i] > heights[st.peek()]) {
                st.push(i);
            } else if (heights[i] == heights[st.peek()]) {
                st.pop(); // 這個可以加,可以不加,效果一樣,思路不同
                st.push(i);
            } else {
                while (heights[i] < heights[st.peek()]) { // 注意是while
                    int mid = st.peek();
                    st.pop();
                    int left = st.peek();
                    int right = i;
                    int w = right - left - 1;
                    int h = heights[mid];
                    result = Math.max(result, w * h);
                }
                st.push(i);
            }
        }
        return result;
    }
}

劍指 Offer 33. 二叉搜索樹的后序遍歷序列

輸入一個整數數組,判斷該數組是不是某二叉搜索樹的后序遍歷結果。如果是則返回 true,否則返回 false。假設輸入的數組的任意兩個數字都互不相同。

思路:

二叉搜索樹的后序遍歷倒序: [ 根節點 | 右子樹 | 左子樹 ] 。類似 先序遍歷的鏡像 ,即先序遍歷為 “根、左、右” 的順序,而后序遍歷的倒序為 “根、右、左” 順序。

image-20220214231552955

根據以上特點,考慮借助單調棧 實現:
1、借助一個單調棧 stack存儲值遞增的節點;
2、每當遇到值遞減的節點 ri,則通過出棧來更新節點 ri的父節點root;

3、每輪判斷ri和root的值關系:

(1)如果ri>root,則說明不滿足二叉搜索樹定義,直接返回false

(2)如果ri<root,則說明滿足二叉搜索樹定義,則繼續遍歷

代碼如下:

class Solution {
    public boolean verifyPostorder(int[] postorder) {
        // 整體思路:搜索樹的后序遍歷的倒敘類似於:根節點、右子樹、左子樹這樣的結構
        // 所以遍歷過程中一定會遇到先升序后降序的過程,設計一個單調棧,棧頂到棧底遞減,如果遇到小於棧頂的節點,
        // 說明到了左子樹的部分,這個時候不斷的出棧,找到大於這個節點的數中最小的數(不斷的出棧直到為空,這個數就是當前節點的父節點),最后再把當前節點入棧,之后再每次遍歷過程中都要判斷左子樹的值是否比父節點小,如果不滿足就返回false
        Stack<Integer> st = new Stack<>();
        // 初始化單調棧,父節點值 root =+∞ (初始值為正無窮大,可把樹的根節點看為此無窮大節點的左孩子);
        int rootValue = Integer.MAX_VALUE;
        // 倒敘遍歷該中序數組
        for (int i = postorder.length - 1;i >= 0;i--) {
            if (postorder[i] > rootValue) {
                return false;
            }
            while (!st.isEmpty() && st.peek() > postorder[i]) {
                rootValue = st.pop();
            }
            st.push(postorder[i]);
        }
        return true;
    }
}

區間DP

486. 預測贏家

給你一個整數數組 nums 。玩家 1 和玩家 2 基於這個數組設計了一個游戲。

玩家 1 和玩家 2 輪流進行自己的回合,玩家 1 先手。開始時,兩個玩家的初始分值都是 0 。每一回合,玩家從數組的任意一端取一個數字(即,nums[0] 或 nums[nums.length - 1]),取到的數字將會從數組中移除(數組長度減 1 )。玩家選中的數字將會加到他的得分上。當數組中沒有剩余數字可取時,游戲結束。

如果玩家 1 能成為贏家,返回 true 。如果兩個玩家得分相等,同樣認為玩家 1 是游戲的贏家,也返回 true 。你可以假設每個玩家的玩法都會使他的分數最大化。

思路:

狀態定義:dp[i] [j] 表示作為先手,在區間 nums[i..j] 里進行選擇可以獲得的 相對分數。相對分數的意思是:當前自己的選擇得分為正,對手的選擇得分為負。

相對分數 說成 凈勝分 ,語義會更強一些。

甲乙比賽,甲先手面對區間[i...j]時,dp[i] [j]表示甲對乙的凈勝分。

最終求的就是,甲先手面對區間[0...n-1]時,甲對乙的凈勝分dp[0] [n-1]是否>=0。

甲先手面對區間[i...j]時,

如果甲拿nums[i],那么變成乙先手面對區間[i+1...j],這段區間內乙對甲的凈勝分為dp[i+1] [j];那么甲對乙的凈勝分就應該是nums[i] - dp[i+1] [j]。

如果甲拿nums[j],同理可得甲對乙的凈勝分為是nums[j] - dp[i] [j-1]。

以上兩種情況二者取大即可。

image-20220311164838171

因為區間i要小於等於j,所以只用遍歷右上角的就行,同時dp[i][j]依賴於dp[i+1] [j]和dp[i] [j-1],所以從右下角開始逐步往上,同時j從i+1往后遍歷

image-20220311164920555

代碼如下:

    public boolean PredictTheWinner(int[] nums) {
        /**
        狀態定義:dp[i][j] 表示作為先手,在區間 nums[i..j] 里進行選擇可以獲得的 相對分數。相對分數的意思是:當前自己的選擇得分為正,對手的選擇得分為負。

        相對分數 說成 凈勝分 ,語義會更強一些。

        甲乙比賽,甲先手面對區間[i...j]時,dp[i][j]表示甲對乙的凈勝分。

        最終求的就是,甲先手面對區間[0...n-1]時,甲對乙的凈勝分dp[0][n-1]是否>=0。

        甲先手面對區間[i...j]時,

        如果甲拿nums[i],那么變成乙先手面對區間[i+1...j],這段區間內乙對甲的凈勝分為dp[i+1][j];那么甲對乙的凈勝分就應該是nums[i] - dp[i+1][j]。
        如果甲拿nums[j],同理可得甲對乙的凈勝分為是nums[j] - dp[i][j-1]。
        以上兩種情況二者取大即可。
         */
        int len = nums.length;
        // dp[i][j]:作為先手,在區間 nums[i..j] 里進行選擇可以獲得的相對分數
        // 狀態轉移方程:dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])
        int[][] dp = new int[len][len];
        // 區間[i,i]內的相對分數是nums[i]
        for (int i = 0;i < len;i++) {
            dp[i][i] = nums[i];
        }
        // 因為區間i要小於等於j,所以只用遍歷右上角的就行,同時dp[i][j]依賴於dp[i+1][j]和dp[i][j-1],所以從右下角開始逐步往上,同時j從i+1往后遍歷
        for (int i = len - 2;i >= 0;i--) {
            for (int j = i + 1;j < len;j++) {
                dp[i][j] = Math.max(nums[i] - dp[i + 1][j],nums[j] - dp[i][j - 1]);
            }
        }
        return dp[0][len - 1] >= 0;
    }

877. 石子游戲

Alice 和 Bob 用幾堆石子在做游戲。一共有偶數堆石子,排成一行;每堆都有 正 整數顆石子,數目為 piles[i] 。

游戲以誰手中的石子最多來決出勝負。石子的 總數 是 奇數 ,所以沒有平局。

Alice 和 Bob 輪流進行,Alice 先開始 。 每回合,玩家從行的 開始 或 結束 處取走整堆石頭。 這種情況一直持續到沒有更多的石子堆為止,此時手中 石子最多 的玩家 獲勝 。

假設 Alice 和 Bob 都發揮出最佳水平,當 Alice 贏得比賽時返回 true ,當 Bob 贏得比賽時返回 false 。

思路:

狀態定義:dp[i] [j] 表示作為先手,在區間 nums[i..j] 里進行選擇可以獲得的 相對分數。相對分數的意思是:當前自己的選擇得分為正,對手的選擇得分為負。

相對分數 說成 凈勝分 ,語義會更強一些。

甲乙比賽,甲先手面對區間[i...j]時,dp[i] [j]表示甲對乙的凈勝分。

最終求的就是,甲先手面對區間[0...n-1]時,甲對乙的凈勝分dp[0] [n-1]是否>=0。

甲先手面對區間[i...j]時,

如果甲拿nums[i],那么變成乙先手面對區間[i+1...j],這段區間內乙對甲的凈勝分為dp[i+1] [j];那么甲對乙的凈勝分就應該是nums[i] - dp[i+1] [j]。

如果甲拿nums[j],同理可得甲對乙的凈勝分為是nums[j] - dp[i] [j-1]。

以上兩種情況二者取大即可。

image-20220311164838171

因為區間i要小於等於j,所以只用遍歷右上角的就行,同時dp[i][j]依賴於dp[i+1] [j]和dp[i] [j-1],所以從右下角開始逐步往上,同時j從i+1往后遍歷

image-20220311164920555

代碼如下:

   public boolean stoneGame(int[] piles) {
        /**
        狀態定義:dp[i][j] 表示作為先手,在區間 piles[i..j] 里進行選擇可以獲得的 相對分數。相對分數的意思是:當前自己的選擇得分為正,對手的選擇得分為負。

        相對分數 說成 凈勝分 ,語義會更強一些。

        甲乙比賽,甲先手面對區間[i...j]時,dp[i][j]表示甲對乙的凈勝分。

        最終求的就是,甲先手面對區間[0...n-1]時,甲對乙的凈勝分dp[0][n-1]是否>0。

        甲先手面對區間[i...j]時,

        如果甲拿piles[i],那么變成乙先手面對區間[i+1...j],這段區間內乙對甲的凈勝分為dp[i+1][j];那么甲對乙的凈勝分就應該是piles[i] - dp[i+1][j]。
        如果甲拿piles[j],同理可得甲對乙的凈勝分為是piles[j] - dp[i][j-1]。
        以上兩種情況二者取大即可。
         */
        int len = piles.length;
        int[][] dp = new int[len][len];

        // 區間[i,i]內的相對分數是piles[i]
        for (int i = 0;i < len;i++) {
            dp[i][i] = piles[i];
        }

        // 因為區間i要小於等於j,所以只用遍歷右上角的就行,同時dp[i][j]依賴於dp[i+1][j]和dp[i][j-1],所以從右下角開始逐步往上,同時j從i+1往后遍歷
        for (int i = len - 2;i >= 0;i--) {
            for (int j = i + 1;j < len;j++) {
                dp[i][j] = Math.max(piles[i] - dp[i + 1][j],piles[j] - dp[i][j - 1]);
            }
        }

        return dp[0][len - 1] > 0;
    }

前綴和

前綴和+哈希表 用到這個思想的的題目:

  1. 連續數組
  2. 連續的子數組和
  3. 元素和為目標值的子矩陣數量
  4. 和為k的子數組
  5. 每個元音包含最長的子字符串
  6. 統計「優美子數組」 LCP 19:秋葉收藏集
  7. 矩形區域不超過 K 的最大數值和

560. 和為 K 的子數組

給你一個整數數組 nums 和一個整數 k ,請你統計並返回 該數組中和為 k 的子數組的個數

思路:

子數組求和的經典技巧就是前綴和,原理就是對數組進行預處理,計算前綴和數組,從而在 O(1) 時間計算子數組和。

nt n = nums.length;
// 前綴和數組
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
    preSum[i + 1] = preSum[i] + nums[i];

preSum[i] 就是 nums[0..i-1] 的和,如果想求 nums[i..j] 的和,只需要 preSum[j+1] - preSum[i] 即可:

對於這道題,把前綴和和哈希表結合起來,時間復雜度為 O(N)

維護一個 前綴和-> 前綴和出現的次數 的map,直接記錄下有幾個sum[j]和sum[i]-k相等,直接更新結果,就避免了內層的 for 循環

代碼:

class Solution {
    public int subarraySum(int[] nums, int k) {
        int n = nums.length;
        // 前綴和-> 前綴和出現的次數
        Map<Integer,Integer> preSum = new HashMap<>();
        // base case
        preSum.put(0,1);
        int ans = 0,sum_i=0;
        // 直接記錄下有幾個sum[j]和sum[i]-k相等,直接更新結果,就避免了內層的 for 循環
        for (int i = 0;i<n;i++) {
            sum_i += nums[i];
            // 這是想找的前綴和nums[0..j]
            int sum_j = sum_i - k;
            // 如果前面有這個前綴和,則直接更新答案
            if (preSum.containsKey(sum_j)) {
                ans += preSum.get(sum_j);
            }
            // 把前綴和nums[0..i] 加入並記錄出現次數
            preSum.put(sum_i,preSum.getOrDefault(sum_i,0)+1);
        }
        return ans;
    }
}

1074. 元素和為目標值的子矩陣數量

給出矩陣 matrix 和目標值 target,返回元素總和等於目標值的非空子矩陣的數量。

子矩陣 x1, y1, x2, y2 是滿足 x1 <= x <= x2 且 y1 <= y <= y2 的所有單元 matrix[x][y] 的集合。

如果 (x1, y1, x2, y2) 和 (x1', y1', x2', y2') 兩個子矩陣中部分坐標不同(如:x1 != x1'),那么這兩個子矩陣也不同。

思路:

枚舉子矩陣的上下邊界,並計算出該邊界內每列的元素和,則原問題轉換成了如下一維問題:

給定一個整數數組和一個整數target,計算該數組中子數組和等於 target 的子數組個數。

對於每列的元素和 sum 的計算,我們在枚舉子矩陣上邊界 i時,初始下邊界 j 為 i,此時 sum 就是矩陣第 i 行的元素。每次向下延長下邊界 j 時,我們可以將矩陣第 j 行的元素累加到 sum 中。

利用一維數組的前綴和的思路,這里分別對第一行,第一行加第二行,第一行加第二行加第三行,第二行,第二行加第三行,第三行這些一維數組進行處理

代碼:

class Solution {
    public int numSubmatrixSumTarget(int[][] matrix, int target) {
        int m = matrix.length;
        int n = matrix[0].length;
        int res = 0;
        // 利用一維數組的前綴和的思路,這里分別對第一行,第一行加第二行,第一行加第二行加第三行,第二行,第二行加第三行,第三行這些一維數組進行處理
        for (int i = 0;i < m;i++) {
            // sum數組記錄某個子矩陣的每一列的和,創建成一個一維數組
            int[] sum = new int[n];
            for (int j = i;j < m;j++) {
                for (int c = 0;c < n;c++) {
                    sum[c] += matrix[j][c];
                }
                res += processSubArr(sum,target);
            }
        }

        return res;
    }
    // 對前綴和數組sum尋找和為target的子數組的個數
    public int processSubArr(int[] sum,int target) {
        // 記錄前綴和出現的次數
        Map<Integer,Integer> map = new HashMap<>();
        // 默認前綴和為0先添加進來
        map.put(0,1);
        int preSum = 0;
        int res = 0;

        for (int x:sum) {
            preSum += x;
            // 如果存在preSum - target的前綴和,說明preSum - 這個前綴和剛好等於target,中間存在一個區間和為target
            if (map.containsKey(preSum - target)) {
                res += map.get(preSum - target);
            }
            // 記錄前綴和出現的個數
            map.put(preSum,map.getOrDefault(preSum,0) + 1);
        }

        return res;
    }
}

525. 連續數組

給定一個二進制數組 nums , 找到含有相同數量的 01 的最長連續子數組,並返回該子數組的長度。

思路:

維護一個變量 counter 存儲 newNums 的前綴和即可。具體做法是,遍歷數組nums,當遇到元素 11 時將 counter 的值加 1,當遇到元素 0 時將 counter 的值減 1,遍歷過程中使用哈希表存儲每個前綴和第一次出現的下標。

規定空的前綴的結束下標為 −1,由於空的前綴的元素和為 0,因此在遍歷之前,首先在哈希表中存入鍵值對 (0,−1)。遍歷過程中,對於每個下標 i,進行如下操作:

如果 counter 的值在哈希表中已經存在,則取出 counter 在哈希表中對應的下標 prevIndex,nums 從下標 prevIndex+1 到下標 i 的子數組中有相同數量的 0 和 1,該子數組的長度為i−prevIndex,使用該子數組的長度更新最長連續子數組的長度;

如果 counter 的值在哈希表中不存在,則將當前余數和當前下標 i 的鍵值對存入哈希表中。

由於哈希表存儲的是 counter 的每個取值第一次出現的下標,因此當遇到重復的前綴和時,根據當前下標和哈希表中存儲的下標計算得到的子數組長度是以當前下標結尾的子數組中滿足有相同數量的 0 和 1 的最長子數組的長度。遍歷結束時,即可得到 nums 中的有相同數量的 0 和 1 的最長子數組的長度。

代碼

class Solution {
    public int findMaxLength(int[] nums) {
        int n = nums.length;
        // key為前綴和,value為第一次遇到前綴和的坐標
        Map<Integer,Integer> countMap = new HashMap<>();
        // 前綴和為0默認坐標為-1,表示在數組索引為0的前一個位置
        countMap.put(0,-1);
        // 計算前綴和
        int counter = 0;
        int maxLen = 0;

        for (int i = 0;i < nums.length;i++) {
            if (nums[i] == 0) {
                counter--;
            } else {
                counter++;
            }
            // 如果遇到重復的前綴和,此時相同數量0和1的子數組長度為i-preIndex
            if (countMap.containsKey(counter)) {
                int preIndex = countMap.get(counter);
                maxLen = Math.max(maxLen,i - preIndex);
            } else {
                countMap.put(counter,i);
            }
        }

        
        return maxLen;
    }
}

930. 和相同的二元子數組

給你一個二元數組 nums ,和一個整數 goal ,請你統計並返回有多少個和為 goal非空 子數組。

子數組 是數組的一段連續部分。

代碼

class Solution {
    public int numSubarraysWithSum(int[] nums, int goal) {
        int n = nums.length;
        // 記錄前綴和
        int[] preSum = new int[n + 1];
        // 記錄前綴和出現的次數
        Map<Integer,Integer> countMap = new HashMap<>();
        // 可能某個前綴和就滿足等於goal,所以此時這個前綴和-goal剛好等於0,所以首先記錄<0,1>鍵值對
        countMap.put(0,1);
        int res = 0;

        for (int i = 0;i < n;i++) {
            preSum[i + 1] = preSum[i] + nums[i];
            // 當preSum[i+1] - goal的值在前綴和里存在(假設為x)的時候,此時preSum[i + 1] - x = goal,此時存在子數組滿足和為goal
            if (countMap.containsKey(preSum[i + 1] - goal)) {
                res += countMap.get(preSum[i + 1] - goal);
            }

            countMap.put(preSum[i + 1],countMap.getOrDefault(preSum[i + 1],0) + 1);
        }

        return res;
    }
}

523. 連續的子數組和

給你一個整數數組 nums 和一個整數 k ,編寫一個函數來判斷該數組是否含有同時滿足下述條件的連續子數組:

子數組大小 至少為 2 ,且
子數組元素總和為 k 的倍數。
如果存在,返回 true ;否則,返回 false 。

如果存在一個整數 n ,令整數 x 符合 x = n * k ,則稱 x 是 k 的一個倍數。0 始終視為 k 的一個倍數。

思路:

思路:前綴和+哈希表+同余定理:a對k的余數和b對k的余數相同,那么a-b對k的余數為0,也就是(a-b)是k的倍數

代碼:

class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        // 思路:前綴和+哈希表+同余定理:a對k的余數和b對k的余數相同,那么a-b對k的余數為0,也就是(a-b)是k的倍數
        int n = nums.length;
        int[] preSum = new int[n + 1];
        // 求前綴和
        for (int i = 1;i < n + 1;i++) {
            preSum[i] = preSum[i - 1] + nums[i - 1];
        }

        // map存放每一個前綴和對於k的余數作為key,當前的坐標作為value,這里的坐標選取最小的坐標
        Map<Integer,Integer> countMap= new HashMap<>();
        // 對於坐標為0的前綴和為0,把它的value置為坐標0
        countMap.put(0,0);
        for (int i = 1;i < n + 1;i++) {
            // 計算當前前綴和對k的余數
            int num = preSum[i] % k;
            if (countMap.containsKey(num)) {
                // 如果之前存在某個前綴和對k的余數相同,並且坐標差大於2(滿足子數組長達大於等於2)就返回 true 
                if (i - countMap.get(num) >= 2) {
                    return true;
                }
                
            }
            // 這里如果已經存在這個余數就不做更新
            countMap.put(num,countMap.getOrDefault(num,i));
        }

        return false;
    }
}

974. 和可被 K 整除的子數組

給定一個整數數組 nums 和一個整數 k ,返回其中元素之和可被 k 整除的(連續、非空) 子數組 的數目。

子數組 是數組的 連續 部分。

思路:

前綴和+哈希表+同余定理:a對k的余數和b對k的余數相同,那么a-b對k的余數為0,也就是(a-b)是k的倍數

代碼:

class Solution {
    public int subarraysDivByK(int[] nums, int k) {
        // 思路:前綴和+哈希表+同余定理:a對k的余數和b對k的余數相同,那么a-b對k的余數為0,也就是(a-b)是k的倍數
        int n = nums.length;
        int[] preSum = new int[n + 1];
        // 求前綴和
        for (int i = 1;i < n + 1;i++) {
            preSum[i] = preSum[i - 1] + nums[i - 1];
        }
        Map<Integer,Integer> countMap = new HashMap<>();
        countMap.put(0,1);
        int res = 0;

        for (int i = 1;i < n + 1;i++) {
            // 計算當前前綴和對k的余數這里有負數,所以要通過(preSum[i] % k + k) % k把余數轉化為正數
            int num = (preSum[i] % k + k) % k;
            if (countMap.containsKey(num)) {
                res += countMap.get(num);
            }
            countMap.put(num,countMap.getOrDefault(num,0) + 1);
        }

        return res;
    }
}

滑動窗口

76. 最小覆蓋子串

給你一個字符串 s 、一個字符串 t 。返回 s 中涵蓋 t 所有字符的最小子串。如果 s 中不存在涵蓋 t 所有字符的子串,則返回空字符串 ""

代碼:

   public String minWindow(String s, String t) {
        Map<Character,Integer> need = new HashMap<Character,Integer>();
        int satisCount = 0;
        Map<Character,Integer> window = new HashMap<>();

        int left = 0;
        int right = 0;
        int minLen = Integer.MAX_VALUE;
        int resLeft = 0;
        int resRight = 0;

        for (char c:t.toCharArray()) {
            need.put(c,need.getOrDefault(c,0) + 1);
        }

        while (right < s.length()) {
            char cRight = s.charAt(right);
            if (need.containsKey(cRight)) {
                window.put(cRight,window.getOrDefault(cRight,0) + 1);
                // 這里注意Integer的值超過127會生成不同的對象,用==來判斷結果會為false,用equals作比較
                if (window.get(cRight).equals(need.get(cRight))) {
                    satisCount++;
                }
            }
            

            while (satisCount == need.size()) {
                // 窗口縮小
                if (right - left + 1 < minLen) {
                    resLeft = left;
                    resRight = right;
                    minLen = right - left + 1;
                }
                
                char cLeft = s.charAt(left);
                if (need.containsKey(cLeft)) {
                    //這里做判斷是因為在第一次遇到某一類字符時satisCount-1就可以了,防止后邊再遇到這類字符(window.get(d)<(need.get(d)))再減一就發生錯誤了
                    if (window.get(cLeft).equals(need.get(cLeft))) {
                        satisCount--;
                    }
                    window.put(cLeft,window.get(cLeft) - 1);
                }
                
                left++;
            }


            right++;
        }

        if (minLen == Integer.MAX_VALUE) {
            return "";
        }
        return s.substring(resLeft,resRight + 1);
    }

424. 替換后的最長重復字符

給你一個字符串 s 和一個整數 k 。你可以選擇字符串中的任一字符,並將其更改為任何其他大寫英文字符。該操作最多可執行 k 次。

在執行上述操作后,返回包含相同字母的最長子字符串的長度。

代碼:

   public int characterReplacement(String s, int k) {
        /**
        這里有個優化,不需要每次都去重新更新max_count。比如說"AAABCDEDFG" k=2,這個case,一開始A出現3次,max_count=3,但是當指針移到D時發現不行了,要移動left指針了。此時count['A']-=1,但是不需要把max_count更新為2。為什么呢? 因為根據我們的算法,當max_count和k一定時,區間最大長度也就定了。當我們找到一個max_count之后,我們就能說我們找到了一個長度為d=max_count+k的合法區間,所以最終答案一定不小於d。所以,當發現繼續向右擴展right不合法的時候,我們不需要不斷地右移left,只需要保持區間長度為d向右滑動即可。如果有某個合法區間大於d,一定在某個時刻存在count[t]+1>max_count,這時再去更新max_count即可。
         */

         // 滑動窗口:記錄窗口中出現次數最多的字符的出現次數,然后該數值 + k就為當前替換k次之后包含相同字母的子串最長長度,通過不斷的滑動窗口來更新結果
        int left = 0;
        int right = 0;
        // 記錄位於窗口中的每個字符出現的次數
        int[] countMap = new int[26];
        // 歷史中位於窗口中的子串中出現次數最多的字符的次數
        int maxSameLen = 0;

        while (right < s.length()) {
            char cRight = s.charAt(right);
            int index = cRight - 'A';
            // 更新窗口中該字符出現的次數
            countMap[index]++;
            // 更新窗口中子串中出現次數最多的字符的次數
            maxSameLen = Math.max(maxSameLen,countMap[index]);

            // 整個過程要保證窗口大小一定要保證等於歷史和當前窗口中出現最多的次數 + k,如果小於就right++ 來擴張窗口,否則就滑動窗口:left++,right++;
            if (right - left + 1 > maxSameLen + k) {
                char cLeft = s.charAt(left);
                countMap[cLeft - 'A']--;
                left++;
            }
            right++;
        }

        // 因為窗口大小始終會等於歷史中窗口出現字符最多的次數 + k,而且窗口的大小不會減小只會增大,所以最后直接返回該窗口大小即可
        return right - left;
    }

1004. 最大連續1的個數 III

給定一個二進制數組 nums 和一個整數 k,如果可以翻轉最多 k0 ,則返回 數組中連續 1 的最大個數

思路和424那一題一樣

代碼:

   public int longestOnes(int[] nums, int k) {
        int left = 0;
        int right = 0;
        int maxLen = 0;
        int oneCount = 0;
        int maxOneLen = 0;

        while (right < nums.length) {
            if (nums[right] == 1) {
                oneCount++;
            }
            maxOneLen = Math.max(maxOneLen,oneCount);

            if (right - left + 1 > maxOneLen + k) {
                if (nums[left] == 1) {
                    oneCount--;
                }
                left++;
            }

            right++;
        }

        return right - left;
    }

劍指 Offer 59 - I. 滑動窗口的最大值

給定一個數組 nums 和滑動窗口的大小 k,請找出所有滑動窗口里的最大值。

代碼:

    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length <= 0) {
            return new int[]{};
        }
        // 創建一個單調遞減的單調棧這里用雙端隊列()
        Deque<Integer> st = new LinkedList<>();
        int[] res = new int[nums.length - k + 1];
        // 先取前k個數值放啊如到單調棧中,滿足遞減,棧底就是最大值
        for (int i = 0;i < k;i++) {
            while (!st.isEmpty() && st.peekLast() < nums[i]) {
                st.pollLast();
            }
            st.offerLast(nums[i]);
        }
        for (int i = 0;i < nums.length - k;i++) {
            // 直接取棧底的最大值
            res[i] = st.peekFirst();
            // 如果nums[i] == 棧底的最大值,就要彈出棧底的最大值
            if (nums[i] == st.peekFirst()) {
                st.pollFirst();
            }
            // 獲取下一個nums數組的值
            int nextNum = nums[i + k];
            // 更新單調棧
            while (!st.isEmpty() && st.peekLast() < nextNum) {
                st.pollLast();
            }
            st.offerLast(nextNum);
        }
        // 這里對nums數組最后一個值沒有更新,因為在上次遍歷中只是對最后一個值對單調棧進行了更新,但是res數組最后一個值還是空的
        res[res.length - 1] = st.peekFirst();

        return res;
    }

劍指 Offer 48. 最長不含重復字符的子字符串

從字符串中找出一個最長的不包含重復字符的子字符串,計算該最長子字符串的長度。

代碼:

    public int lengthOfLongestSubstring(String s) {
        int maxLen = 0;
        int left = 0;
        int right = 0;
        Map<Character,Integer> countMap = new HashMap<>();

        while (right < s.length()) {
            char cRight = s.charAt(right);
            countMap.put(cRight,countMap.getOrDefault(cRight,0) + 1);

            while (countMap.get(cRight) > 1) {
                char cLeft = s.charAt(left);
                countMap.put(cLeft,countMap.get(cLeft) - 1);
                left++;
            }
            maxLen = Math.max(maxLen,right - left + 1);

            right++;
        }

        return maxLen;
    }

395. 至少有 K 個重復字符的最長子串

給你一個字符串 s 和一個整數 k ,請你找出 s 中的最長子串, 要求該子串中的每一字符出現次數都不少於 k 。返回這一子串的長度。

代碼:

   public int longestSubstring(String s, int k) {
        int maxLen = 0;

        // 限制字符種類數量,從1-26
        for (int category = 1;category <= 26;category++) {
            int left = 0;
            int right = 0;
            // 記錄窗口中出現的字符種類個數
            int total = 0;
            // 記錄窗口內出現的滿足出現次數大於等於k的字符的個數
            int perfectTotal = 0;

            int cnt[] = new int[26];

            while (right < s.length()) {
                char c = s.charAt(right);
                cnt[c - 'a']++;
                if (cnt[c - 'a'] == 1) {
                    total++;
                }
                if (cnt[c - 'a'] == k) {
                    perfectTotal++;
                }
                // 如果窗口內出現的字符種類個數超過了限定的category,進行以下操作,窗口左邊界右移
                while (total > category) {
                    char cLeft = s.charAt(left);
                    cnt[cLeft - 'a']--;
                    if (cnt[cLeft - 'a'] == 0) {
                        total--;
                    }
                    if (cnt[cLeft - 'a'] == k - 1) {
                        perfectTotal--;
                    }
                    // 右移窗口左邊界
                    left++;
                }

                // 如果窗口內的字符種類個數剛好等於滿足條件的字符種類個數,說明窗口內所有字符都滿足條件,進行比較判斷更新
                if (total == perfectTotal) {
                    maxLen = Math.max(maxLen,right - left + 1);
                }
                right++;

            }
        }

        return maxLen;
    }

992. K 個不同整數的子數組

給定一個正整數數組 nums和一個整數 k ,返回 num 中 「好子數組」 的數目。

如果 nums 的某個子數組中不同整數的個數恰好為 k,則稱 nums 的這個連續、不一定不同的子數組為 「好子數組 」。

例如,[1,2,3,1,2] 中有 3 個不同的整數:1,2,以及 3。
子數組 是數組的 連續 部分。

代碼:

class Solution {
    public int subarraysWithKDistinct(int[] nums, int k) {
        // 思路:維護兩個窗口,和普通的滑動窗口解法的不同之處在於,我們需要記錄兩個左指針 left1與 left2來表示左端點區間[left1,left2)。第一個左指針表示極大的包含 k 個不同整數的區間的左端點,第二個左指針則表示極大的包含 k-1 個不同整數的區間的左端點。
        /**
        假設區間 [l_1,r]和 [l_2,r] 為滿足條件的數組(不失一般性,設 l_1≤l_2)。現在我們設存在一個 l 滿足 l_1≤l≤l_2,那么區間 [l,r]作為 [l_1,r]的子數組,其中的不同整數數量必然不超過 k。同理,區間 [l,r]作為 [l_2,r]的父數組,其中的不同整數數量必然不少於 k。那么可知區間 [l,r]中的不同整數數量即為 k。
        用一個區間 [l_1,l_2]來代表能夠與右端點 r 對應的左端點們。
        和普通的滑動窗口解法的不同之處在於,我們需要記錄兩個左指針 left1與 left2來表示左端點區間[left1,left2)。第一個左指針表示極大的包含 k 個不同整數的區間的左端點,第二個左指針則表示極大的包含 k-1 個不同整數的區間的左端點。
         */
        int tot1 = 0,tot2 = 0;
        // 第一個左指針表示極大的包含 k 個不同整數的區間的左端點,第二個左指針則表示極大的包含 k-1 個不同整數的區間的左端點。
        int left1 = 0,left2 = 0,right = 0;
        int n = nums.length;
        int[] nums1 = new int[n + 1];
        int[] nums2 = new int[n + 1];
        int res = 0;

        while (right < nums.length) {
            if (nums1[nums[right]] == 0) {
                tot1++;
            }
            nums1[nums[right]]++;
            if (nums2[nums[right]] == 0) {
                tot2++;
            }
            nums2[nums[right]]++;

            while (tot1 > k) {
                nums1[nums[left1]]--;
                if (nums1[nums[left1]] == 0) {
                    tot1--;
                }
                left1++;
            }

            while (tot2 > k - 1) {
                nums2[nums[left2]]--;
                if (nums2[nums[left2]] == 0) {
                    tot2--;
                }
                left2++;
            }

            res += left2 - left1;
            right++;
        }

        return res;
    }
}

1658. 將 x 減到 0 的最小操作數

給你一個整數數組 nums 和一個整數 x 。每一次操作時,你應當移除數組 nums 最左邊或最右邊的元素,然后從 x 中減去該元素的值。請注意,需要 修改 數組以供接下來的操作使用。

如果可以將 x 恰好 減到 0 ,返回 最小操作數 ;否則,返回 -1 。

代碼

class Solution {
    public int minOperations(int[] nums, int x) {
        // 若每次都從數組的開頭和結尾處取值,則剩余未取的數在數組中是連續的,且其總和為數組總和sum-x。
        // 從開頭和結尾拿的最少次數,等價於求總長度-余下數的最大長度

        int sum = 0;
        for (int n:nums) {
            sum += n;
        }
        int target = sum - x;
        // 如果target小於0,就不存在這樣的子數組
        if (target < 0) {
            return -1;
        }

        int left = 0,right = 0;
        int curSum = 0;
        // 最小的操作數
        int minCount = Integer.MAX_VALUE;

        while (right < nums.length) {
            curSum += nums[right];
            while (curSum > target) {
                curSum -= nums[left];
                left++;
            }
            if (curSum == target) {
                minCount = Math.min(minCount,nums.length - (right - left + 1));
            }

            right++;
        }

        return minCount == Integer.MAX_VALUE?-1:minCount;
    }
}

30. 串聯所有單詞的子串

給定一個字符串 s 和一些 長度相同 的單詞 words 。找出 s 中恰好可以由 words 中所有單詞串聯形成的子串的起始位置。

注意子串要與 words 中的單詞完全匹配,中間不能有其他字符 ,但不需要考慮 words 中單詞串聯的順序。

思路:

滑動窗口,這里以單詞長度n為單位進行滑動,類似於找到字符串中所有字母異位詞。

代碼如下:

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        // 滑動窗口,這里以單詞長度n為單位進行滑動,類似於找到字符串中所有字母異位詞
        List<Integer> res = new ArrayList<>();
        int m = words.length,n = words[0].length(),len = s.length();

        // 將字符串s按照n的長度來進行划分多個長度n的單詞組,每一次的滑動窗口的初始最左側的范圍在0-n-1之間
        for (int i = 0;i < n;i++) {
            if (i + m * n > len) {
                break;
            }
            Map<String,Integer> differ = new HashMap<>();
            // 先將s從i開始划分成m個單詞,然后放進differ里
            for (int j = 0;j < m;j++) {
                String word = s.substring(i + j * n,i + (j + 1) * n);
                differ.put(word,differ.getOrDefault(word,0) + 1);
            }
            // 然后去判斷words里的這些單詞是否已經有放進去,放進去了減一,如果最后個數為0就remove掉
            for (String word:words) {
                differ.put(word,differ.getOrDefault(word,0) - 1);
                if (differ.get(word) == 0) {
                    differ.remove(word);
                }
            }

            // 在這里窗口開始進行滑動,從start位置開始滑動,知道窗口的長度加start超過了s的長度為止,滑動以n為單位
            for (int start = i;start < len - m * n + 1;start += n) {
                if (start != i) {
                    // 這里計算新進入窗口的單詞
                    String rightWord = s.substring(start + (m - 1) * n,start + m * n);
                    differ.put(rightWord,differ.getOrDefault(rightWord,0) + 1);
                    if (differ.get(rightWord) == 0) {
                        differ.remove(rightWord);
                    }

                    // 這里計算左側滑出窗口的單詞
                    String leftWord = s.substring(start - n,start);
                    differ.put(leftWord,differ.getOrDefault(leftWord,0) - 1);
                    if (differ.get(leftWord) == 0) {
                        differ.remove(leftWord);
                    }
                }
                // 如果differ全空,則找到了一個滿足條件的子串,加入左側索引
                if (differ.isEmpty()) {
                    res.add(start);
                }
            }
        }

        return res;
    }
}

貪心

670. 最大交換

給定一個非負整數,你至多可以交換一次數字中的任意兩位。返回你能得到的最大值。

思路:

  • 將計算last[d] = i,最后一次出現的數字 d(如果存在)的索引i。
  • 然后,從左到右掃描數字時,如果將來有較大的數字,我們將用最大的數字交換;如果有多個這樣的數字,我們將用最開始遇到的數字交換。

具體看代碼

代碼:

class Solution {

    public int maximumSwap(int num) {
        String str = String.valueOf(num);
        char[] numChars = str.toCharArray();
        int[] lastPlace = new int[10];
        // 記錄num數組形式中每個數出現的最后位置
        for (int i = 0;i < numChars.length;i++) {
            lastPlace[numChars[i] - '0'] = i;
        }
        // 從頭遍歷num數組,然后從大到num[i]大小的值遍歷lastPlace數組,如果比num[i]出現的最大值的位置在i之后,就交換,然后直接返回
        for (int i = 0;i < numChars.length;i++) {
            for (int d = 9;d > (numChars[i] - '0');d--) {
                if (lastPlace[d] > i) {
                    swap(numChars,i,lastPlace[d]);
                    return Integer.parseInt(new String(numChars));
                }
            }
        }
        return num;
    }

    public void swap(char[] numChars,int i,int j) {
        char temp = numChars[i];
        numChars[i] = numChars[j];
        numChars[j] = temp;
    }

    // public int maximumSwap(int num) {
    //     int[] nums = getNums(num);
    //     int beginIndex = 0;
    //     int maxValue = 0;
    //     int maxValueIndex = 0;

    //     // 我的思路:不斷從當前的beginIndex及之后找到最大值,如果最大值不是beginIndex位置上的值,就將該最大值與beginIndex位置的數交換
    //     while (beginIndex < nums.length - 1) {
    //         maxValue = 0;
    //         for (int i = beginIndex;i < nums.length;i++) {
    //             if (nums[i] >= maxValue) {
    //                 maxValueIndex = i;
    //                 maxValue = nums[i];
    //             }
    //         }
    //         if (nums[beginIndex] != maxValue) {
    //             swap(nums,beginIndex,maxValueIndex);
    //             break;
    //         }
    //         beginIndex++;
    //     }

    //     int res = 0;
    //     for (int i = 0;i < nums.length;i++) {
    //         res *= 10;
    //         res += nums[i];
    //     }

    //     return res;
    // }

    public int[] getNums(int num) {
        List<Integer> numList = new LinkedList<>();
        while (num > 0) {
            numList.add(num % 10);
            num = num/10;
        }
        int[] nums = new int[numList.size()];
        for (int i = nums.length - 1;i >= 0;i--) {
            nums[i] = numList.get(numList.size() - 1 - i);
        }
        return nums;
    }

    public void swap(int[] nums,int i,int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

動態規划

410. 分割數組的最大值

給定一個非負整數數組 nums 和一個整數 m ,你需要將這個數組分成 m 個非空的連續子數組。

設計一個算法使得這 m 個子數組各自和的最大值最小。

思路

二分思路:結果必定落在【max(nums), sum(nums)】這個區間內,因為左端點對應每個單獨的元素構成一個子數組,右端點對應所有元素構成一個子數組。然后可以利用二分查找法逐步縮小區間范圍,當區間長度為1時,即找到了最終答案;
題目要求的是連續子數組,所以如果我們假設一個組最多放和為x的數,肯定是要盡可能放滿才能讓組盡可能的小,前面組少放點並不可能對后面有幫助,因此是順序划分即可

動態規划思路:dp[i][j] 表示前i個數字分為j段子數組,最大連續子數組和的最小值
在進行狀態轉移時,我們可以考慮第 j 段的具體范圍,即我們可以枚舉 k,其中前 k 個數被分割為 j−1 段,而第 k+1 到第 i 個數(preSum[i] - preSum[k]:區間[k,i - 1])為第 j 段。此時,這 j 段子數組中和的最大值,就等於 dp[k] [j−1] 與 sub(k+1,i) 中的較大值,其中 sub(i,j) 表示數組nums 中下標落在區間[i,j] 內的數的和。

代碼如下:

class Solution {
    public int splitArray(int[] nums, int m) {
        /**
            二分思路:結果必定落在【max(nums), sum(nums)】這個區間內,因為左端點對應每個單獨的元素構成一個子數組,右端點對應所有元素構成一個子數組。然后可以利用二分查找法逐步縮小區間范圍,當區間長度為1時,即找到了最終答案;
            題目要求的是連續子數組,所以如果我們假設一個組最多放和為x的數,肯定是要盡可能放滿才能讓組盡可能的小,前面組少放點並不可能對后面有幫助,因此是順序划分即可
         */
         /**
            動態規划思路:dp[i][j] 表示前i個數字分為j段子數組,最大連續子數組和的最小值
            在進行狀態轉移時,我們可以考慮第 j 段的具體范圍,即我們可以枚舉 k,其中前 k 個數被分割為 j−1 段,而第 k+1 到第 i 個數(preSum[i] - preSum[k]:區間[k,i - 1])為第 j 段。此時,這 j 段子數組中和的最大值,就等於 dp[k][j−1] 與 sub(k+1,i) 中的較大值,其中 sub(i,j) 表示數組nums 中下標落在區間[i,j] 內的數的和。
          */
          int n = nums.length;
          int[][] dp = new int[n + 1][m + 1];
          for (int i = 0;i <= n;i++) {
              Arrays.fill(dp[i],Integer.MAX_VALUE);
          }

          int[] preSum = new int[n + 1];
          for (int i = 0;i < n;i++) {
              preSum[i + 1] = preSum[i] + nums[i];
          }
          dp[0][0] = 0;
          for (int i = 1;i <= n;i++) {
              for (int j = 1;j <= Math.min(i,m);j++) {
                  for (int k = 0;k < i;k++) {
                      dp[i][j] = Math.min(dp[i][j],Math.max(dp[k][j - 1],preSum[i] - preSum[k]));
                  }
              }
          }

          return dp[n][m];
    }
}

813. 最大平均值和的分組

給定數組 nums 和一個整數 k 。我們將給定的數組 nums 分成 最多 k 個相鄰的非空子數組 。 分數 由每個子數組內的平均值的總和構成。

注意我們必須使用 nums 數組中的每一個數進行分組,並且分數不一定需要是整數。

返回我們所能得到的最大 分數 是多少。答案誤差在 10-6 內被視為是正確的。

思路

類似於410:分割子數組最大和的最小值
動態規划思路:dp[i] [j] 表示前i個數字分為j段子數組所得到的最大分數
在進行狀態轉移時,我們可以考慮第 j 段的具體范圍,即我們可以枚舉 k,其中前 k 個數被分割為 j−1 段,而第 m+1 到第 i 個數(preSum[i] - preSum[m]:區間[m,i - 1])為第 j 段。此時,這 j 段子數組的最大分數,就等於 dp[m] [j−1] 與 sub(k+1,i) 中的分數的和,其中 sub(i,j) 表示數組nums 中下標落在區間[i,j] 內的數的和。

代碼如下:

class Solution {
    public double largestSumOfAverages(int[] nums, int k) {
        /**
            類似於410:分割子數組最大和的最小值
            動態規划思路:dp[i][j] 表示前i個數字分為j段子數組所得到的最大分數
            在進行狀態轉移時,我們可以考慮第 j 段的具體范圍,即我們可以枚舉 k,其中前 k 個數被分割為 j−1 段,而第 m+1 到第 i 個數(preSum[i] - preSum[m]:區間[m,i - 1])為第 j 段。此時,這 j 段子數組的最大分數,就等於 dp[m][j−1] 與 sub(k+1,i) 中的分數的和,其中 sub(i,j) 表示數組nums 中下標落在區間[i,j] 內的數的和。
         */
        int n = nums.length;
        double[][] dp = new double[n + 1][k + 1];

        double[] preSum = new double[n + 1];
        for (int i = 0;i < n;i++) {
            preSum[i + 1] = preSum[i] + nums[i];
            dp[i + 1][1] = preSum[i + 1]/(i + 1);
        }

        
        // dp[0][0] = 0;
        for (int i = 1;i <= n;i++) {
            for (int j = 2;j <= k;j++) {
                for (int m = 0;m < i;m++) {
                    dp[i][j] = Math.max(dp[i][j],dp[m][j - 1] + (preSum[i] - preSum[m])/(i - m));
                }
            }
        }

        return dp[n][k];
    }
}

二分

410. 分割數組的最大值

給定一個非負整數數組 nums 和一個整數 m ,你需要將這個數組分成 m 個非空的連續子數組。

設計一個算法使得這 m 個子數組各自和的最大值最小。

思路

二分思路:結果必定落在【max(nums), sum(nums)】這個區間內,因為左端點對應每個單獨的元素構成一個子數組,右端點對應所有元素構成一個子數組。然后可以利用二分查找法逐步縮小區間范圍,當區間長度為1時,即找到了最終答案;
題目要求的是連續子數組,所以如果我們假設一個組最多放和為x的數,肯定是要盡可能放滿才能讓組盡可能的小,前面組少放點並不可能對后面有幫助,因此是順序划分即可

動態規划思路:dp[i][j] 表示前i個數字分為j段子數組,最大連續子數組和的最小值
在進行狀態轉移時,我們可以考慮第 j 段的具體范圍,即我們可以枚舉 k,其中前 k 個數被分割為 j−1 段,而第 k+1 到第 i 個數(preSum[i] - preSum[k]:區間[k,i - 1])為第 j 段。此時,這 j 段子數組中和的最大值,就等於 dp[k] [j−1] 與 sub(k+1,i) 中的較大值,其中 sub(i,j) 表示數組nums 中下標落在區間[i,j] 內的數的和。

代碼如下:

class Solution {
    public int splitArray(int[] nums, int m) {
        //我們求的是「最大子數組和」的「最小值」,且 split 函數的返回值有單調性,所以從小到大遍歷,第一個滿足條件的值就是「最小值」。
        int lo = getMax(nums);
        // 一般搜索區間是左開右閉的,所以 hi 要額外加一
        int hi = getSum(nums)+1;
        while (lo<hi) {
            int mid = lo + (hi - lo)/2;
            // 根據分割子數組的個數收縮搜索區間,這里的n是隨着mid增加遞減的
            int n = split(nums,mid);
            if(n == m) {
                // 收縮右邊界,達到搜索左邊界的目的
                hi = mid;
            } else if (n < m) {
                // 最大子數組和上限高了,減小一些,在滿足n<m的情況下找到一個更小的
                hi =mid;
            } else if (n > m)  {
                // 最大子數組和上限低了,增加一些
                lo = mid+1;
            }
        }
        return lo;
    }

    /* 輔助函數,若限制最大子數組和為 max,
    計算 nums 至少可以被分割成幾個子數組 */
    public int split(int[] nums,int max) {
        // 至少可以分割的子數組數量
        int count = 1;
        // 記錄每個子數組的元素和
        int sum = 0;
        for(int i=0;i<nums.length;i++) {
            if(sum+nums[i]>max) {
                // 如果當前子數組和大於 max 限制
            // 則這個子數組不能再添加元素了
                count++;
                sum = nums[i];
            } else {
                // 當前子數組和還沒達到 max 限制
            // 還可以添加元素
                sum+=nums[i];
            }
        }
        return count;
    }

    public int getMax(int[] nums) {
        int max = 0;
        for(int n:nums) {
            max = Math.max(max,n);
        }
        return max;
    }

    public int getSum(int[] nums) {
        int sum = 0;
        for(int n:nums) {
            sum+=n;
        }
        return sum;
    }
}

1011. 在 D 天內送達包裹的能力

傳送帶上的包裹必須在 days 天內從一個港口運送到另一個港口。

傳送帶上的第 i 個包裹的重量為 weights[i]。每一天,我們都會按給出重量(weights)的順序往傳送帶上裝載包裹。我們裝載的重量不會超過船的最大運載重量。

返回能在 days 天內將傳送帶上的所有包裹送達的船的最低運載能力。

思路

思路類似於410:分割子數組;

二分思路:結果必定落在【max(nums), sum(nums)】這個區間內,因為左端點對應每個單獨的元素構成一個子數組,右端點對應所有元素構成一個子數組。然后可以利用二分查找法逐步縮小區間范圍,當區間長度為1時,即找到了最終答案; 題目要求的是連續子數組,所以如果我們假設一個組最多放和為x的數,肯定是要盡可能放滿才能讓組盡可能的小,前面組少放點並不可能對后面有幫助,因此是順序划分即可

代碼如下:

class Solution {
    public int shipWithinDays(int[] weights, int days) {
        /**
            思路類似於410:分割子數組;二分思路:結果必定落在【max(nums), sum(nums)】這個區間內,因為左端點對應每個單獨的元素構成一個子數組,右端點對應所有元素構成一個子數組。然后可以利用二分查找法逐步縮小區間范圍,當區間長度為1時,即找到了最終答案;
            題目要求的是連續子數組,所以如果我們假設一個組最多放和為x的數,肯定是要盡可能放滿才能讓組盡可能的小,前面組少放點並不可能對后面有幫助,因此是順序划分即可
         */
        int n = weights.length;
        int low = getMax(weights);
        int high = getSum(weights) + 1;

        while (low < high) {
            int mid = (high + low) / 2;
            int count = split(weights,mid);
            if (count > days) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }

    private int split(int[] weights,int target) {
        int count = 1;
        int sum = 0;
        for (int weight:weights) {
            if (sum + weight > target) {
                sum = weight;
                count++;
            } else {
                sum += weight;
            }
        }
        return count;
    }
    private int getMax(int[] weights) {
        int maxValue = Integer.MIN_VALUE;
        for (int weight:weights) {
            if (maxValue < weight) {
                maxValue = weight;
            }
        }

        return maxValue;
    }

    private int getSum(int[] weights) {
        int sum = 0;
        for (int weight:weights) {
            sum += weight;
        }

        return sum;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM