Leetcode題解(超讚!!!)

我是技術搬運工,好東西當然要和大家分享啦.原文地址

算法思想

二分查找

二分查找思想簡單,但是在實現時有一些需要注意的細節:

  1. 在計算 mid 時不能使用 mid = (l + h) / 2 這種方式,因爲 l + h 可能會導致加法溢出,應該使用 mid = l + (h - l) / 2。

  2. 對 h 的賦值和循環條件有關,當循環條件爲 l <= h 時,h = mid - 1;當循環條件爲 l < h 時,h = mid。解釋如下:在循環條件爲 l <= h 時,如果 h = mid,會出現循環無法退出的情況,例如 l = 1,h = 1,此時 mid 也等於 1,如果此時繼續執行 h = mid,那麼就會無限循環;在循環條件爲 l < h,如果 h = mid - 1,會錯誤跳過查找的數,例如對於數組 [1,2,3],要查找 1,最開始 l = 0,h = 2,mid = 1,判斷 key < arr[mid] 執行 h = mid - 1 = 0,此時循環退出,直接把查找的數跳過了。

  3. l 的賦值一般都爲 l = mid + 1。

public int search(int key, int[] arr) {
    int l = 0, h = arr.length - 1;
    while (l <= h) {
        int mid = l + (h - l) / 2;
        if (key == arr[mid]) return mid;
        if (key < arr[mid]) h = mid - 1;
        else l = mid + 1;
    }
    return -1;
}

求開方

Leetcode : 69. Sqrt(x) (Easy)

一個數 x 的開方 sqrt 一定在 0 ~ x 之間,並且滿足 sqrt == x / sqrt 。可以利用二分查找在 0 ~ x 之間查找 sqrt。

public int mySqrt(int x) {
    if(x <= 1) return x;
    int l = 1, h = x;
    while(l <= h){
        int mid = l + (h - l) / 2;
        int sqrt = x / mid;
        if(sqrt == mid) return mid;
        else if(sqrt < mid) h = mid - 1;
        else l = mid + 1;
    }
    return h;
}

擺硬幣

Leetcode : 441. Arranging Coins (Easy)

n = 8

The coins can form the following rows:
¤
¤ ¤
¤ ¤ ¤
¤ ¤

Because the 4th row is incomplete, we return 3.

題目描述:第 i 行擺 i 個,統計能夠擺的行數。

返回 h 而不是 l,因爲擺的硬幣最後一行不能算進去。

public int arrangeCoins(int n) {
    int l = 0, h = n;
    while(l <= h){
        int m = l + (h - l) / 2;
        long x = m * (m + 1L) / 2;
        if(x == n) return m;
        else if(x < n) l = m + 1;
        else h = m - 1;
    }
    return h;
}

可以不用二分查找,更直觀的解法如下:

public int arrangeCoins(int n) {
    int level = 1;
    while (n > 0) {
        n -= level;
        level++;
    }
    return n == 0 ? level - 1 : level - 2;
}

有序數組的 Single Element

Leetcode : 540. Single Element in a Sorted Array (Medium)

題目描述:一個有序數組只有一個數不出現兩次,找出這個數。

public int singleNonDuplicate(int[] nums) {
    int l = 0, h = nums.length - 1;
    while(l < h) {
        int m = l + (h - l) / 2;
        if(m % 2 == 1) m--; // 保證 l/h/m 都在偶數位,使得查找區間大小一直都是奇數
        if(nums[m] == nums[m + 1]) l = m + 2;
        else h = m;
    }
    return nums[l];
}

貪心思想

貪心思想保證每次操作都是局部最優的,並且最後得到的結果是全局最優的。

分配餅乾

Leetcode : 455. Assign Cookies (Easy)

題目描述:每個孩子都有一個滿足度,每個餅乾都有一個大小,只有餅乾的大小大於一個孩子的滿足度,該孩子纔會獲得滿足。求解最多可以獲得滿足的孩子數量。

因爲最小的孩子最容易得到滿足,因此先滿足最小孩子。給一個孩子的餅乾應當儘量小又能滿足該孩子,這樣大餅乾就能拿來給滿足度比較大的孩子。

證明:假設在某次選擇中,貪心策略選擇給第 i 個孩子分配第 m 個餅乾,並且第 i 個孩子滿足度最小,第 m 個餅乾爲可以滿足第 i 個孩子的最小餅乾,利用貪心策略最終可以滿足 k 個孩子。假設最優策略在這次選擇中給 i 個孩子分配第 n 個餅乾,並且這個餅乾大於第 m 個餅乾。我們發現使用第 m 個餅乾去替代第 n 個餅乾完全不影響後續的結果,因此不存在比貪心策略更優的策略,即貪心策略就是最優策略。

public int findContentChildren(int[] g, int[] s) {
    Arrays.sort(g);
    Arrays.sort(s);
    int i = 0, j = 0;
    while(i < g.length && j < s.length){
        if(g[i] <= s[j]) i++;
        j++;
    }
    return i;
}

投飛鏢刺破氣球

Leetcode : 452. Minimum Number of Arrows to Burst Balloons (Medium)

Input:
[[10,16], [2,8], [1,6], [7,12]]

Output:
2

題目描述:氣球在一個水平數軸上擺放,可以重疊,飛鏢垂直射向座標軸,使得路徑上的氣球都會刺破。求解最小的投飛鏢次數使所有氣球都被刺破。

從左往右投飛鏢,並且在每次投飛鏢時滿足以下條件:

  1. 左邊已經沒有氣球了;
  2. 本次投飛鏢能夠刺破最多的氣球。
public int findMinArrowShots(int[][] points) {
    if(points.length == 0) return 0;
    Arrays.sort(points,(a,b) -> (a[1] - b[1]));
    int curPos = points[0][1];
    int ret = 1;
    for (int i = 1; i < points.length; i++) {
        if(points[i][0] <= curPos) {
            continue;
        }
        curPos = points[i][1];
        ret++;
    }
    return ret;
 }

股票的最大收益

Leetcode : 122. Best Time to Buy and Sell Stock II (Easy)

題目描述:一次交易包含買入和賣出,多個交易之間不能交叉進行。

對於 [a, b, c, d],如果有 a <= b <= c <= d ,那麼最大收益爲 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此當訪問到一個 prices[i] 且 prices[i] - prices[i-1] > 0,那麼就把 prices[i] - prices[i-1] 添加加到收益中,從而在局部最優的情況下也保證全局最優。

public int maxProfit(int[] prices) {
    int profit = 0;
    for(int i = 1; i < prices.length; i++){
        if(prices[i] > prices[i-1]) profit += (prices[i] - prices[i-1]);
    }
    return profit;
}

種植花朵

Leetcode : 605. Can Place Flowers (Easy)

Input: flowerbed = [1,0,0,0,1], n = 1
Output: True

題目描述:花朵之間至少需要一個單位的間隔。

public boolean canPlaceFlowers(int[] flowerbed, int n) {
    int cnt = 0;
    for(int i = 0; i < flowerbed.length; i++){
        if(flowerbed[i] == 1) continue;
        int pre = i == 0 ? 0 : flowerbed[i - 1];
        int next = i == flowerbed.length - 1 ? 0 : flowerbed[i + 1];
        if(pre == 0 && next == 0) {
            cnt++;
            flowerbed[i] = 1;
        }
    }
    return cnt >= n;
}

修改一個數成爲非遞減數組

Leetcode : 665. Non-decreasing Array (Easy)

題目描述:判斷一個數組能不能只修改一個數就成爲非遞減數組。

在出現 nums[i] < nums[i - 1] 時,需要考慮的是應該修改數組的哪個數,使得本次修改能使 i 之前的數組成爲非遞減數組,並且 不影響後續的操作。優先考慮令 nums[i - 1] = nums[i],因爲如果修改 nums[i] = nums[i - 1] 的話,那麼 nums[i] 這個數會變大,那麼就有可能比 nums[i + 1] 大,從而影響了後續操作。還有一個比較特別的情況就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能令數組成爲非遞減,只能通過修改 nums[i] = nums[i - 1] 纔行。

public boolean checkPossibility(int[] nums) {
    int cnt = 0;
    for(int i = 1; i < nums.length; i++){
        if(nums[i] < nums[i - 1]){
            cnt++;
            if(i - 2 >= 0 && nums[i - 2] > nums[i]) nums[i] = nums[i-1];
            else nums[i - 1] = nums[i];
        }
    }
    return cnt <= 1;
}

判斷是否爲子串

Leetcode : 392. Is Subsequence (Medium)

s = "abc", t = "ahbgdc"
Return true.
public boolean isSubsequence(String s, String t) {
    for (int i = 0, pos = 0; i < s.length(); i++, pos++) {
        pos = t.indexOf(s.charAt(i), pos);
        if(pos == -1) return false;
    }
    return true;
}

分隔字符串使同種字符出現在一起

Leetcode : 763. Partition Labels (Medium)

Input: S = "ababcbacadefegdehijhklij"
Output: [9,7,8]
Explanation:
The partition is "ababcbaca", "defegde", "hijhklij".
This is a partition so that each letter appears in at most one part.
A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts.
public List<Integer> partitionLabels(String S) {
    List<Integer> ret = new ArrayList<>();
    int[] lastIdxs = new int[26];
    for(int i = 0; i < S.length(); i++) lastIdxs[S.charAt(i) - 'a'] = i;
    int startIdx = 0;
    while(startIdx < S.length()) {
        int endIdx = startIdx;
        for(int i = startIdx; i < S.length() && i <= endIdx; i++) {
            int lastIdx = lastIdxs[S.charAt(i) - 'a'];
            if(lastIdx == i) continue;
            if(lastIdx > endIdx) endIdx = lastIdx;
        }
        ret.add(endIdx - startIdx + 1);
        startIdx = endIdx + 1;
    }
    return ret;
}

根據身高和序號重組隊列

Leetcode : 406. Queue Reconstruction by Height(Medium)

Input:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]

Output:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]

題目描述:一個學生用兩個分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 個學生的身高比他高或者和他一樣高。

爲了在每次插入操作時不影響後續的操作,身高較高的學生應該先做插入操作,否則身高較小的學生原先正確插入第 k 個位置可能會變成第 k+1 個位置。

身高降序、k 值升序,然後按排好序的順序插入隊列的第 k 個位置中。

public int[][] reconstructQueue(int[][] people) {
    if(people == null || people.length == 0 || people[0].length == 0) return new int[0][0];

    Arrays.sort(people, new Comparator<int[]>() {
       public int compare(int[] a, int[] b) {
           if(a[0] == b[0]) return a[1] - b[1];
           return b[0] - a[0];
       }
    });
    
    int n = people.length;
    List<int[]> tmp = new ArrayList<>();
    for(int i = 0; i < n; i++) {
        tmp.add(people[i][1], new int[]{people[i][0], people[i][1]});
    }
    
    int[][] ret = new int[n][2];
    for(int i = 0; i < n; i++) {
        ret[i][0] = tmp.get(i)[0];
        ret[i][1] = tmp.get(i)[1];
    }
    return ret;
}

雙指針

雙指針主要用於遍歷數組,兩個指針指向不同的元素,從而協同完成任務。

從一個已經排序的數組中查找出兩個數,使它們的和爲 0

Leetcode :167. Two Sum II - Input array is sorted (Easy)

使用雙指針,一個指針指向元素較小的值,一個指針指向元素較大的值。指向較小元素的指針從頭向尾遍歷,指向較大元素的指針從尾向頭遍歷。

如果兩個指針指向元素的和 sum == target,那麼得到要求的結果;如果 sum > target,移動較大的元素,使 sum 變小一些;如果 sum < target,移動較小的元素,使 sum 變大一些。

public int[] twoSum(int[] numbers, int target) {
    int i = 0, j = numbers.length - 1;
    while (i < j) {
        int sum = numbers[i] + numbers[j];
        if (sum == target) return new int[]{i + 1, j + 1};
        else if (sum < target) i++;
        else j--;
    }
    return null;
}

反轉字符串中的元音字符

Leetcode : 345. Reverse Vowels of a String (Easy)

使用雙指針,指向待反轉的兩個元音字符,一個指針從頭向尾遍歷,一個指針從尾到頭遍歷。

private HashSet<Character> vowels = new HashSet<>(Arrays.asList('a','e','i','o','u','A','E','I','O','U'));

public String reverseVowels(String s) {
    if(s.length() == 0) return s;
    int i = 0, j = s.length() - 1;
    char[] result = new char[s.length()];
    while(i <= j){
        char ci = s.charAt(i);
        char cj = s.charAt(j);
        if(!vowels.contains(ci)){
            result[i] = ci;
            i++;
        } else if(!vowels.contains(cj)){
            result[j] = cj;
            j--;
        } else{
            result[i] = cj;
            result[j] = ci;
            i++;
            j--;
        }
    }
    return new String(result);
}

兩數平方和

Leetcode : 633. Sum of Square Numbers (Easy)

題目描述:判斷一個數是否爲兩個數的平方和,例如 5 = 12 + 22

public boolean judgeSquareSum(int c) {
    int left = 0, right = (int) Math.sqrt(c);
    while(left <= right){
        int powSum = left * left + right * right;
        if(powSum == c) return true;
        else if(powSum > c) right--;
        else left++;
    }
    return false;
}

迴文字符串

Leetcode : 680. Valid Palindrome II (Easy)

題目描述:字符串可以刪除一個字符,判斷是否能構成迴文字符串。

public boolean validPalindrome(String s) {
    int i = 0, j = s.length() -1;
    while(i < j){
        if(s.charAt(i) != s.charAt(j)){
            return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j);
        }
        i++;
        j--;
    }
    return true;
}

private boolean isPalindrome(String s, int l, int r){
    while(l < r){
        if(s.charAt(l) != s.charAt(r))
            return false;
        l++;
        r--;
    }
    return true;
}

歸併兩個有序數組

Leetcode : 88. Merge Sorted Array (Easy)

題目描述:把歸併結果存到第一個數組上

public void merge(int[] nums1, int m, int[] nums2, int n) {
    int i = m - 1, j = n - 1; // 需要從尾開始遍歷,否則在 nums1 上歸併得到的值會覆蓋還未進行歸併比較的值
    int idx = m + n - 1;
    while(i >= 0 || j >= 0){
        if(i < 0) nums1[idx] = nums2[j--];
        else if(j < 0) nums1[idx] = nums1[i--];
        else if(nums1[i] > nums2[j]) nums1[idx] = nums1[i--];
        else nums1[idx] = nums2[j--];
        idx--;
    }
}

判斷鏈表是否存在環

Leetcode : 141. Linked List Cycle (Easy)

使用雙指針,一個指針每次移動一個節點,一個指針每次移動兩個節點,如果存在環,那麼這兩個指針一定會相遇。

public boolean hasCycle(ListNode head) {
    if(head == null) return false;
    ListNode l1 = head, l2 = head.next;
    while(l1 != null && l2 != null){
        if(l1 == l2) return true;
        l1 = l1.next;
        if(l2.next == null) break;
        l2 = l2.next.next;
    }
    return false;
}

最長子序列

Leetcode : 524. Longest Word in Dictionary through Deleting (Medium)

Input:
s = "abpcplea", d = ["ale","apple","monkey","plea"]

Output:
"apple"

題目描述:可以刪除 s 中的一些字符,使得它成爲字符串列表 d 中的一個字符串。要求在 d 中找到滿足條件的最長字符串。

public String findLongestWord(String s, List<String> d) {
    String ret = "";
    for (String str : d) {
        for (int i = 0, j = 0; i < s.length() && j < str.length(); i++) {
            if (s.charAt(i) == str.charAt(j)) j++;
            if (j == str.length()) {
                if (ret.length() < str.length()
                        || (ret.length() == str.length() && ret.compareTo(str) > 0)) {
                    ret = str;
                }
            }
        }
    }
    return ret;
}

排序

快速選擇

一般用於求解 Kth Element 問題,可以在 O(n) 時間複雜度,O(1) 空間複雜度完成求解工作。

與快速排序一樣,快速選擇一般需要先打亂數組,否則最壞情況下時間複雜度爲 O(n2)。

堆排序

堆排序用於求解 TopK Elements 問題,通過維護一個大小爲 K 的堆,堆中的元素就是 TopK Elements。當然它也可以用於求解 Kth Element 問題,因爲最後出堆的那個元素就是 Kth Element。快速選擇也可以求解 TopK Elements 問題,因爲找到 Kth Element 之後,再遍歷一次數組,所有小於等於 Kth Element 的元素都是 TopK Elements。可以看到,快速選擇和堆排序都可以求解 Kth Element 和 TopK Elements 問題。

Kth Element

Leetocde : 215. Kth Largest Element in an Array (Medium)

排序:時間複雜度 O(nlgn),空間複雜度 O(1) 解法

public int findKthLargest(int[] nums, int k) {
        int N = nums.length;
        Arrays.sort(nums);
        return nums[N - k];
}

堆排序:時間複雜度 O(nlgk),空間複雜度 O(k)

public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    for(int val : nums) {
        pq.offer(val);
        if(pq.size() > k) {
            pq.poll();
        }
    }
    return pq.peek();
}

快速選擇:時間複雜度 O(n),空間複雜度 O(1)

public int findKthLargest(int[] nums, int k) {
        k = nums.length - k;
        int lo = 0;
        int hi = nums.length - 1;
        while (lo < hi) {
            final int j = partition(nums, lo, hi);
            if(j < k) {
                lo = j + 1;
            } else if (j > k) {
                hi = j - 1;
            } else {
                break;
            }
        }
        return nums[k];
    }

    private int partition(int[] a, int lo, int hi) {
        int i = lo;
        int j = hi + 1;
        while(true) {
            while(i < hi && less(a[++i], a[lo]));
            while(j > lo && less(a[lo], a[--j]));
            if(i >= j) {
                break;
            }
            exch(a, i, j);
        }
        exch(a, lo, j);
        return j;
    }

    private void exch(int[] a, int i, int j) {
        final int tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
    }

    private boolean less(int v, int w) {
        return v < w;
    }
}

桶排序

找出出現頻率最多的 k 個數

Leetcode : 347. Top K Frequent Elements (Medium)

public List<Integer> topKFrequent(int[] nums, int k) {
    List<Integer> ret = new ArrayList<>();
    Map<Integer, Integer> map = new HashMap<>();
    for(int num : nums) {
        map.put(num, map.getOrDefault(num, 0) + 1);
    }
    List<Integer>[] bucket = new List[nums.length + 1];
    for(int key : map.keySet()) {
        int frequency = map.get(key);
        if(bucket[frequency] == null) {
            bucket[frequency] = new ArrayList<>();
        }
        bucket[frequency].add(key);
    }
    
    for(int i = bucket.length - 1; i >= 0 && ret.size() < k; i--) {
        if(bucket[i] != null) {
            ret.addAll(bucket[i]);
        }
    }
    return ret;
}

搜索

深度優先搜索和廣度優先搜索廣泛運用於樹和圖中,但是它們的應用遠遠不止如此。

BFS

廣度優先搜索的搜索過程有點像一層一層地進行遍歷:從節點 0 出發,遍歷到 6、2、1 和 5 這四個新節點。

繼續從 6 開始遍歷,得到節點 4 ;從 2 開始遍歷,沒有下一個節點;從 1 開始遍歷,沒有下一個節點;從 5 開始遍歷,得到 3 和 4 節點。這一輪總共得到兩個新節點:4 和 3 。

反覆從新節點出發進行上述的遍歷操作。

可以看到,每一輪遍歷的節點都與根節點路徑長度相同。設 di 表示第 i 個節點與根節點的路徑長度,推導出一個結論:對於先遍歷的節點 i 與後遍歷的節點 j,有 di<=dj。利用這個結論,可以求解最短路徑 最優解 問題:第一次遍歷到目的節點,其所經過的路徑爲最短路徑,如果繼續遍歷,之後再遍歷到目的節點,所經過的路徑就不是最短路徑。

在程序實現 BFS 時需要考慮以下問題:

  • 隊列:用來存儲每一輪遍歷的節點
  • 標記:對於遍歷過得節點,應該將它標記,防止重複遍歷;

計算在網格中從原點到特定點的最短路徑長度

[[1,1,0,1],
[1,0,1,0],
[1,1,1,1],
[1,0,1,1]]
public int minPathLength(int[][] grids, int tr, int tc) {
    int[][] next = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    int m = grids.length, n = grids[0].length;
    Queue<Position> queue = new LinkedList<>();
    queue.add(new Position(0, 0, 1));
    while (!queue.isEmpty()) {
        Position pos = queue.poll();
        for (int i = 0; i < 4; i++) {
            Position nextPos = new Position(pos.r + next[i][0], pos.c + next[i][1], pos.length + 1);
            if (nextPos.r < 0 || nextPos.r >= m || nextPos.c < 0 || nextPos.c >= n) continue;
            if (grids[nextPos.r][nextPos.c] != 1) continue;
            grids[nextPos.r][nextPos.c] = 0;
            if (nextPos.r == tr && nextPos.c == tc) return nextPos.length;
            queue.add(nextPos);
        }
    }
    return -1;
}

private class Position {
    int r, c, length;
    public Position(int r, int c, int length) {
        this.r = r;
        this.c = c;
        this.length = length;
    }
}

DFS

廣度優先搜索一層一層遍歷,每一層遍歷到的所有新節點,要用隊列先存儲起來以備下一層遍歷的時候再遍歷;而深度優先搜索在遍歷到一個新節點時立馬對新節點進行遍歷:從節點 0 出發開始遍歷,得到到新節點 6 時,立馬對新節點 6 進行遍歷,得到新節點 4;如此反覆以這種方式遍歷新節點,直到沒有新節點了,此時返回。返回到根節點 0 的情況是,繼續對根節點 0 進行遍歷,得到新節點 2,然後繼續以上步驟。

從一個節點出發,使用 DFS 對一個圖進行遍歷時,能夠遍歷到的節點都是從初始節點可達的,DFS 常用來求解這種 可達性 問題。

在程序實現 DFS 時需要考慮以下問題:

  • 棧:用棧來保存當前節點信息,當遍歷新節點返回時能夠繼續遍歷當前節點。也可以使用遞歸棧。
  • 標記:和 BFS 一樣同樣需要對已經遍歷過得節點進行標記。

查找最大的連通面積

Leetcode : 695. Max Area of Island (Easy)

[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
public int maxAreaOfIsland(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    int max = 0;
    for(int i = 0; i < m; i++){
        for(int j = 0; j < n; j++){
            if(grid[i][j] == 1) max = Math.max(max, dfs(grid, i, j));
        }
    }
    return max;
}

private int dfs(int[][] grid, int i, int j){
    int m = grid.length, n = grid[0].length;
    if(i < 0 || i >= m || j < 0 || j >= n) return 0;
    if(grid[i][j] == 0) return 0;
    grid[i][j] = 0;
    return dfs(grid, i + 1, j) + dfs(grid, i - 1, j) + dfs(grid, i, j + 1) + dfs(grid, i, j - 1) + 1;
}

圖的連通分量

Leetcode : 547. Friend Circles (Medium)

Input:
[[1,1,0],
 [1,1,0],
 [0,0,1]]
Output: 2
Explanation:The 0th and 1st students are direct friends, so they are in a friend circle.
The 2nd student himself is in a friend circle. So return 2.
public int findCircleNum(int[][] M) {
    int n = M.length;
    int ret = 0;
    boolean[] hasFind = new boolean[n];
    for(int i = 0; i < n; i++) {
        if(!hasFind[i]) {
            dfs(M, i, hasFind);
            ret++;
        }
    }
    return ret;
}

private void dfs(int[][] M, int i, boolean[] hasFind) {
    hasFind[i] = true;
    int n = M.length;
    for(int k = 0; k < n; k++) {
        if(M[i][k] == 1 && !hasFind[k]) {
            dfs(M, k, hasFind);
        }
    }
}

矩陣中的連通區域數量

Leetcode : 200. Number of Islands (Medium)

11110
11010
11000
00000
Answer: 1
private int m, n;
private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) return 0;
    m = grid.length;
    n = grid[0].length;
    int ret = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == '1') {
                dfs(grid, i, j);
                ret++;
            }
        }
    }
    return ret;
}

private void dfs(char[][] grid, int i, int j) {
    if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') return;
    grid[i][j] = '0';
    for (int k = 0; k < direction.length; k++) {
        dfs(grid, i + direction[k][0], j + direction[k][1]);
    }
}

輸出二叉樹中所有從根到葉子的路徑

Leetcode : 257. Binary Tree Paths (Easy)

  1
/  \
2    3
\
  5
["1->2->5", "1->3"]
public List<String> binaryTreePaths(TreeNode root) {
    List<String> ret = new ArrayList();
    if(root == null) return ret;
    dfs(root, "", ret);
    return ret;
}

private void dfs(TreeNode root, String prefix, List<String> ret){
    if(root == null) return;
    if(root.left == null && root.right == null){
        ret.add(prefix + root.val);
        return;
    }
    prefix += (root.val + "->");
    dfs(root.left, prefix, ret);
    dfs(root.right, prefix, ret);
}

填充封閉區域

Leetcode : 130. Surrounded Regions (Medium)

For example,
X X X X
X O O X
X X O X
X O X X

After running your function, the board should be:
X X X X
X X X X
X X X X
X O X X

題目描述:使得被 'X' 的 'O' 轉換爲 'X'。

先填充最外側,剩下的就是裏側了。

private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
private int m, n;

public void solve(char[][] board) {
    if (board == null || board.length == 0) return;
    m = board.length;
    n = board[0].length;
    for (int i = 0; i < m; i++) {
        dfs(board, i, 0);
        dfs(board, i, n - 1);
    }
    for (int i = 0; i < n; i++) {
        dfs(board, 0, i);
        dfs(board, m - 1, i);
    }
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (board[i][j] == 'T') board[i][j] = 'O';
            else if (board[i][j] == 'O') board[i][j] = 'X';
        }
    }
}

private void dfs(char[][] board, int r, int c) {
    if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') return;
    board[r][c] = 'T';
    for (int i = 0; i < direction.length; i++) {
        dfs(board, r + direction[i][0], c + direction[i][1]);
    }
}

從兩個方向都能到達的區域

Leetcode : 417. Pacific Atlantic Water Flow (Medium)

Given the following 5x5 matrix:

  Pacific \~   \~   \~   \~   \~ 
       \~  1   2   2   3  (5) *
       \~  3   2   3  (4) (4) *
       \~  2   4  (5)  3   1  *
       \~ (6) (7)  1   4   5  *
       \~ (5)  1   1   2   4  *
          *   *   *   *   * Atlantic

Return:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix).

題目描述:左邊和上邊是太平洋,右邊和下邊是大西洋,內部的數字代表海拔,海拔高的地方的水能夠流到低的地方,求解水能夠流到太平洋和大西洋的所有位置。

private int m, n;
private int[][] matrix;
private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

public List<int[]> pacificAtlantic(int[][] matrix) {
    List<int[]> ret = new ArrayList<>();
    if (matrix == null || matrix.length == 0) return ret;
    this.m = matrix.length;
    this.n = matrix[0].length;
    this.matrix = matrix;
    boolean[][] canReachP = new boolean[m][n];
    boolean[][] canReachA = new boolean[m][n];
    for (int i = 0; i < m; i++) {
        dfs(i, 0, canReachP);
        dfs(i, n - 1, canReachA);
    }
    for (int i = 0; i < n; i++) {
        dfs(0, i, canReachP);
        dfs(m - 1, i, canReachA);
    }
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (canReachP[i][j] && canReachA[i][j]) {
                ret.add(new int[]{i, j});
            }
        }
    }
    return ret;
}

private void dfs(int r, int c, boolean[][] canReach) {
    if(canReach[r][c]) return;
    canReach[r][c] = true;
    for (int i = 0; i < direction.length; i++) {
        int nextR = direction[i][0] + r;
        int nextC = direction[i][1] + c;
        if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n
                || matrix[r][c] > matrix[nextR][nextC]) continue;
        dfs(nextR, nextC, canReach);
    }
}

N 皇后

Leetcode : 51. N-Queens (Hard)

題目描述:在 n*n 的矩陣中擺放 n 個皇后,並且每個皇后不能在同一行,同一列,同一對角線上,要求解所有的 n 皇后解。

一行一行地擺放,在確定一行中的那個皇后應該擺在哪一列時,需要用三個標記數組來確定某一列是否合法,這三個標記數組分別爲:列標記數組、45 度對角線標記數組和 135 度對角線標記數組。

45 度對角線標記數組的維度爲 2*n - 1,通過下圖可以明確 (r,c) 的位置所在的數組下標爲 r + c。

135 度對角線標記數組的維度也是 2*n - 1,(r,c) 的位置所在的數組下標爲 n - 1 - (r - c)。

private List<List<String>> ret;
private char[][] nQueens;
private boolean[] colUsed;
private boolean[] diagonals45Used;
private boolean[] diagonals135Used;
private int n;

public List<List<String>> solveNQueens(int n) {
    ret = new ArrayList<>();
    nQueens = new char[n][n];
    Arrays.fill(nQueens, '.');
    colUsed = new boolean[n];
    diagonals45Used = new boolean[2 * n - 1];
    diagonals135Used = new boolean[2 * n - 1];
    this.n = n;
    backstracking(0);
    return ret;
}

private void backstracking(int row) {
    if (row == n) {
        List<String> list = new ArrayList<>();
        for (char[] chars : nQueens) {
            list.add(new String(chars));
        }
        ret.add(list);
        return;
    }

    for (int col = 0; col < n; col++) {
        int diagonals45Idx = row + col;
        int diagonals135Idx = n - 1 - (row - col);
        if (colUsed[col] || diagonals45Used[diagonals45Idx] || diagonals135Used[diagonals135Idx]) {
            continue;
        }
        nQueens[row][col] = 'Q';
        colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = true;
        backstracking(row + 1);
        colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = false;
        nQueens[row][col] = '.';
    }
}

Backtracking

回溯是 DFS 的一種,它不是用在遍歷圖的節點上,而是用於求解 排列組合 問題,例如有 { 'a','b','c' } 三個字符,求解所有由這三個字符排列得到的字符串。

在程序實現時,回溯需要注意對元素進行標記的問題。使用遞歸實現的回溯,在訪問一個新元素進入新的遞歸調用,此時需要將新元素標記爲已經訪問,這樣才能在繼續遞歸調用時不用重複訪問該元素;但是在遞歸返回時,需要將該元素標記爲未訪問,因爲只需要保證在一個遞歸鏈中不同時訪問一個元素,而在不同的遞歸鏈是可以訪問已經訪問過但是不在當前遞歸鏈中的元素。

數字鍵盤組合

Leetcode : 17. Letter Combinations of a Phone Number (Medium)

Input:Digit string "23"
Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

public List<String> letterCombinations(String digits) {
    List<String> ret = new ArrayList<>();
    if (digits != null && digits.length() != 0) {
        combination("", digits, 0, ret);
    }
    return ret;
}

private void combination(String prefix, String digits, int offset, List<String> ret) {
    if (offset == digits.length()) {
        ret.add(prefix);
        return;
    }
    String letters = KEYS[digits.charAt(offset) - '0'];
    for (char c : letters.toCharArray()) {
        combination(prefix + c, digits, offset + 1, ret);
    }
}

在矩陣中尋找字符串

Leetcode : 79. Word Search (Medium)

For example,
Given board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]
word = "ABCCED", -> returns true,
word = "SEE", -> returns true,
word = "ABCB", -> returns false.
private static int[][] shift = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static boolean[][] visited;
private int m;
private int n;

public boolean exist(char[][] board, String word) {
    if (word == null || word.length() == 0) return true;
    if (board == null || board.length == 0 || board[0].length == 0) return false;
    m = board.length;
    n = board[0].length;
    visited = new boolean[m][n];
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (dfs(board, word, 0, i, j)) return true;
        }
    }
    return false;
}

private boolean dfs(char[][] board, String word, int start, int r, int c) {
    if (start == word.length()) {
        return true;
    }
    if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(start) ||  visited[r][c] ) {
        return false;
    }
    visited[r][c] = true;
    for (int i = 0; i < shift.length; i++) {
        int nextR = r + shift[i][0];
        int nextC = c + shift[i][1];
        if (dfs(board, word, start + 1, nextR, nextC)) return true;
    }
    visited[r][c] = false;
    return false;
}

IP 地址劃分

Leetcode : 93. Restore IP Addresses(Medium)

Given "25525511135",
return ["255.255.11.135", "255.255.111.35"].
private List<String> ret;

public List<String> restoreIpAddresses(String s) {
    ret = new ArrayList<>();
    doRestore(0, "", s);
    return ret;
}

private void doRestore(int k, String path, String s) {
    if (k == 4 || s.length() == 0) {
        if (k == 4 && s.length() == 0) {
            ret.add(path);
        }
        return;
    }
    for (int i = 0; i < s.length() && i <= 2; i++) {
        if (i != 0 && s.charAt(0) == '0') break;
        String part = s.substring(0, i + 1);
        if (Integer.valueOf(part) <= 255) {
            doRestore(k + 1, path.length() != 0 ? path + "." + part : part, s.substring(i + 1));
        }
    }
}

排列

Leetcode : 46. Permutations (Medium)

[1,2,3] have the following permutations:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]
public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> ret = new ArrayList<>();
    List<Integer> permuteList = new ArrayList<>();
    boolean[] visited = new boolean[nums.length];
    backtracking(permuteList, visited, nums, ret);
    return ret;
}

private void backtracking(List<Integer> permuteList, boolean[] visited, int[] nums, List<List<Integer>> ret){
    if(permuteList.size() == nums.length){
        ret.add(new ArrayList(permuteList));
        return;
    }

    for(int i = 0; i < visited.length; i++){
        if(visited[i]) continue;
        visited[i] = true;
        permuteList.add(nums[i]);
        backtracking(permuteList, visited, nums, ret);
        permuteList.remove(permuteList.size() - 1);
        visited[i] = false;
    }
}

含有相同元素求排列

Leetcode : 47. Permutations II (Medium)

[1,1,2] have the following unique permutations:
[[1,1,2], [1,2,1], [2,1,1]]

題目描述:數組元素可能含有相同的元素,進行排列時就有可能出先重複的排列,要求重複的排列只返回一個。

在實現上,和 Permutations 不同的是要先排序,然後在添加一個元素時,判斷這個元素是否等於前一個元素,如果等於,並且前一個元素還未訪問,那麼就跳過這個元素。

public List<List<Integer>> permuteUnique(int[] nums) {
    List<List<Integer>> ret = new ArrayList<>();
    List<Integer> permuteList = new ArrayList<>();
    Arrays.sort(nums);
    boolean[] visited = new boolean[nums.length];
    backtracking(permuteList, visited, nums, ret);
    return ret;
}

private void backtracking(List<Integer> permuteList, boolean[] visited, int[] nums, List<List<Integer>> ret) {
    if (permuteList.size() == nums.length) {
        ret.add(new ArrayList(permuteList));
        return;
    }

    for (int i = 0; i < visited.length; i++) {
        if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue;
        if (visited[i]) continue;
        visited[i] = true;
        permuteList.add(nums[i]);
        backtracking(permuteList, visited, nums, ret);
        permuteList.remove(permuteList.size() - 1);
        visited[i] = false;
    }
}

組合

Leetcode : 77. Combinations (Medium)

If n = 4 and k = 2, a solution is:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> ret = new ArrayList<>();
    List<Integer> combineList = new ArrayList<>();
    backtracking(1, n, k, combineList, ret);
    return ret;
}

private void backtracking(int start, int n, int k, List<Integer> combineList, List<List<Integer>> ret){
    if(k == 0){
        ret.add(new ArrayList(combineList)); // 這裏要重新構造一個 List
        return;
    }
    
    for(int i = start; i <= n - k + 1; i++){ // 剪枝

        combineList.add(i);                        // 把 i 標記爲已訪問
        backtracking(i + 1, n, k - 1, combineList, ret);
        combineList.remove(combineList.size() - 1); // 把 i 標記爲未訪問
    }
}

組合求和

Leetcode : 39. Combination Sum (Medium)

given candidate set [2, 3, 6, 7] and target 7,
A solution set is:
[[7],[2, 2, 3]]
 private List<List<Integer>> ret;

 public List<List<Integer>> combinationSum(int[] candidates, int target) {
     ret = new ArrayList<>();
     doCombination(candidates, target, 0, new ArrayList<>());
     return ret;
 }

 private void doCombination(int[] candidates, int target, int start, List<Integer> list) {
     if (target == 0) {
         ret.add(new ArrayList<>(list));
         return;
     }
     for (int i = start; i < candidates.length; i++) {
         if (candidates[i] <= target) {
             list.add(candidates[i]);
             doCombination(candidates, target - candidates[i], i, list);
             list.remove(list.size() - 1);
         }
     }
 }

含有相同元素的求組合求和

Leetcode : 40. Combination Sum II (Medium)

For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, 
A solution set is: 
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
private List<List<Integer>> ret;

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    ret = new ArrayList<>();
    Arrays.sort(candidates);
    doCombination(candidates, target, 0, new ArrayList<>(), new boolean[candidates.length]);
    return ret;
}

private void doCombination(int[] candidates, int target, int start, List<Integer> list, boolean[] visited) {
    if (target == 0) {
        ret.add(new ArrayList<>(list));
        return;
    }
    for (int i = start; i < candidates.length; i++) {
        if (i != 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) continue;
        if (candidates[i] <= target) {
            list.add(candidates[i]);
            visited[i] = true;
            doCombination(candidates, target - candidates[i], i + 1, list, visited);
            visited[i] = false;
            list.remove(list.size() - 1);
        }
    }
}

子集

Leetcode : 78. Subsets (Medium)

題目描述:找出集合的所有子集,子集不能重複,[1, 2] 和 [2, 1] 這種子集算重複

private List<List<Integer>> ret;
private List<Integer> subsetList;

public List<List<Integer>> subsets(int[] nums) {
    ret = new ArrayList<>();
    subsetList = new ArrayList<>();
    for (int i = 0; i <= nums.length; i++) {
        backtracking(0, i, nums);
    }
    return ret;
}

private void backtracking(int startIdx, int size, int[] nums) {
    if (subsetList.size() == size) {
        ret.add(new ArrayList(subsetList));
        return;
    }

    for (int i = startIdx; i < nums.length; i++) {
        subsetList.add(nums[i]);
        backtracking(i + 1, size, nums); // startIdx 設爲下一個元素,使 subset 中的元素都遞增排序
        subsetList.remove(subsetList.size() - 1);
    }
}

含有相同元素求子集

Leetcode : 90. Subsets II (Medium)

For example,
If nums = [1,2,2], a solution is:

[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]
private List<List<Integer>> ret;
private List<Integer> subsetList;
private boolean[] visited;

public List<List<Integer>> subsetsWithDup(int[] nums) {
    ret = new ArrayList<>();
    subsetList = new ArrayList<>();
    visited = new boolean[nums.length];
    Arrays.sort(nums);
    for (int i = 0; i <= nums.length; i++) {
        backtracking(0, i, nums);
    }
    return ret;
}

private void backtracking(int startIdx, int size, int[] nums) {
    if (subsetList.size() == size) {
        ret.add(new ArrayList(subsetList));
        return;
    }

    for (int i = startIdx; i < nums.length; i++) {
        if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue;
        subsetList.add(nums[i]);
        visited[i] = true;
        backtracking(i + 1, size, nums);
        visited[i] = false;
        subsetList.remove(subsetList.size() - 1);
    }
}

分割字符串使得每部分都是迴文數

Leetcode : 131. Palindrome Partitioning (Medium)

private List<List<String>> ret;

public List<List<String>> partition(String s) {
    ret = new ArrayList<>();
    doPartion(new ArrayList<>(), s);
    return ret;
}

private void doPartion(List<String> list, String s) {
    if (s.length() == 0) {
        ret.add(new ArrayList<>(list));
        return;
    }
    for (int i = 0; i < s.length(); i++) {
        if (isPalindrome(s, 0, i)) {
            list.add(s.substring(0, i + 1));
            doPartion(list, s.substring(i + 1));
            list.remove(list.size() - 1);
        }
    }
}

private boolean isPalindrome(String s, int begin, int end) {
    while (begin < end) {
        if (s.charAt(begin++) != s.charAt(end--)) return false;
    }
    return true;
}

數獨

Leetcode : 37. Sudoku Solver (Hard)

private boolean[][] rowsUsed = new boolean[9][10];
private boolean[][] colsUsed = new boolean[9][10];
private boolean[][] cubesUsed = new boolean[9][10];
private char[][] board;

public void solveSudoku(char[][] board) {
    this.board = board;
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            if (board[i][j] == '.') continue;
            int num = board[i][j] - '0';
            rowsUsed[i][num] = true;
            colsUsed[j][num] = true;
            cubesUsed[cubeNum(i, j)][num] = true;
        }
    }
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            backtracking(i, j);
        }
    }
}

private boolean backtracking(int row, int col) {
    while (row < 9 && board[row][col] != '.') {
        row = col == 8 ? row + 1 : row;
        col = col == 8 ? 0 : col + 1;
    }
    if (row == 9) {
        return true;
    }
    for (int num = 1; num <= 9; num++) {
        if (rowsUsed[row][num] || colsUsed[col][num] || cubesUsed[cubeNum(row, col)][num]) continue;
        rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = true;
        board[row][col] = (char) (num + '0');
        if (backtracking(row, col)) return true;
        board[row][col] = '.';
        rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = false;
    }
    return false;
}

private int cubeNum(int i, int j) {
    int r = i / 3;
    int c = j / 3;
    return r * 3 + c;
}

分治

給表達式加括號

Leetcode : 241. Different Ways to Add Parentheses (Medium)

Input: "2-1-1".

((2-1)-1) = 0
(2-(1-1)) = 2

Output : [0, 2]
public List<Integer> diffWaysToCompute(String input) {
    int n = input.length();
    List<Integer> ret = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        char c = input.charAt(i);
        if (c == '+' || c == '-' || c == '*') {
            List<Integer> left = diffWaysToCompute(input.substring(0, i));
            List<Integer> right = diffWaysToCompute(input.substring(i + 1));
            for (int l : left) {
                for (int r : right) {
                    switch (c) {
                        case '+': ret.add(l + r); break;
                        case '-': ret.add(l - r); break;
                        case '*': ret.add(l * r); break;
                    }
                }
            }
        }
    }
    if (ret.size() == 0) ret.add(Integer.valueOf(input));
    return ret;
}

動態規劃

遞歸和動態規劃都是將原問題拆成多個子問題然後求解,他們之間最本質的區別是,動態規劃保存了子問題的解。

分割整數

分割整數的最大乘積

Leetcode : 343. Integer Break (Medim)

題目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).

public int integerBreak(int n) {
    int[] dp = new int[n + 1];
    dp[1] = 1;
    for(int i = 2; i <= n; i++) {
        for(int j = 1; j <= i - 1; j++) {
            dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
        }
    }
    return dp[n];
}

按平方數來分割整數

Leetcode : 279. Perfect Squares(Medium)

題目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.

public int numSquares(int n) {
    List<Integer> squares = new ArrayList<>(); // 存儲小於 n 的平方數
    int diff = 3;
    while(square <= n) {
        squares.add(square);
        square += diff;
        diff += 2;
    }
    int[] dp = new int[n + 1];
    for(int i = 1; i <= n; i++) {
        int max = Integer.MAX_VALUE;
        for(int s : squares) {
            if(s > i) break;
            max = Math.min(max, dp[i - s] + 1);
        }
        dp[i] = max;
    }
    return dp[n];
}

分割整數構成字母字符串

Leetcode : 91. Decode Ways (Medium)

題目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).

public int numDecodings(String s) {
    if(s == null || s.length() == 0) return 0;
    int n = s.length();
    int[] dp = new int[n + 1];
    dp[0] = 1;
    dp[1] = s.charAt(0) == '0' ? 0 : 1;
    for(int i = 2; i <= n; i++) {
        int one = Integer.valueOf(s.substring(i - 1, i));
        if(one != 0) dp[i] += dp[i - 1];
        if(s.charAt(i - 2) == '0') continue;
        int two = Integer.valueOf(s.substring(i - 2, i));
        if(two <= 26) dp[i] += dp[i - 2];
    }
    return dp[n];
}

矩陣路徑

矩陣的總路徑數

Leetcode : 62. Unique Paths (Medium)

題目描述:統計從矩陣左上角到右下角的路徑總數,每次只能向左和向下移動。

public int uniquePaths(int m, int n) {
    int[] dp = new int[n];
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if(i == 0) dp[j] = 1;
            else if(j != 0) dp[j] = dp[j] + dp[j - 1];
        }
    }
    return dp[n - 1];
}

矩陣的最小路徑和

Leetcode : 64. Minimum Path Sum (Medium)

題目描述:求從矩陣的左上角到右下角的最小路徑和,每次只能向左和向下移動。

public int minPathSum(int[][] grid) {
    if(grid.length == 0 || grid[0].length == 0) return 0;
    int m = grid.length, n = grid[0].length;
    int[] dp = new int[n];
    for(int i = 0; i < m; i++) {
        for(int j = 0; j < n; j++) {
            if(j == 0) dp[0] = dp[0] + grid[i][0];
            else if(i == 0) dp[j] = dp[j - 1] + grid[0][j];
            else dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[n - 1];
}

斐波那契數列

爬樓梯

Leetcode : 70. Climbing Stairs (Easy)

題目描述:有 N 階樓梯,每次可以上一階或者兩階,求有多少種上樓梯的方法。

定義一個數組 dp 存儲上樓梯的方法數(爲了方便討論,數組下標從 1 開始),dp[i] 表示走到第 i 個樓梯的方法數目。第 i 個樓梯可以從第 i-1 和 i-2 個樓梯再走一步到達,走到第 i 個樓梯的方法數爲走到第 i-1 和第 i-2 個樓梯的方法數之和。

dp[N] 即爲所求。

考慮到 dp[i] 只與 dp[i - 1] 和 dp[i - 2] 有關,因此可以只用兩個變量來存儲 dp[i - 1] 和 dp[i - 2] 即可,使得原來的 O(n) 空間複雜度優化爲 O(1) 複雜度。

public int climbStairs(int n) {
    if(n == 1) return 1;
    if(n == 2) return 2;
    // 前一個樓梯、後一個樓梯
    int pre1 = 2, pre2 = 1;
    for(int i = 2; i < n; i++){
        int cur = pre1 + pre2;
        pre2 = pre1;
        pre1 = cur;
    }
    return pre1;
}

母牛生產

程序員代碼面試指南-P181

題目描述:假設農場中成熟的母牛每年都會生 1 頭小母牛,並且永遠不會死。第一年有 1 只小母牛,從第二年開始,母牛開始生小母牛。每隻小母牛 3 年之後成熟又可以生小母牛。給定整數 N,求 N 年後牛的數量。

第 i 年成熟的牛的數量爲:

強盜搶劫

Leetcode : 198. House Robber (Easy)

題目描述:搶劫一排住戶,但是不能搶鄰近的住戶,求最大搶劫量。

定義 dp 數組用來存儲最大的搶劫量,其中 dp[i] 表示搶到第 i 個住戶時的最大搶劫量。由於不能搶劫鄰近住戶,因此如果搶劫了第 i 個住戶那麼只能搶劫 i - 2 和 i - 3 的住戶,所以

O(n) 空間複雜度實現方法:

public int rob(int[] nums) {
    int n = nums.length;
    if(n == 0) return 0;
    if(n == 1) return nums[0];
    if(n == 2) return Math.max(nums[0], nums[1]);
    int[] dp = new int[n];
    dp[0] = nums[0];
    dp[1] = nums[1];
    dp[2] = nums[0] + nums[2];
    for(int i = 3; i < n; i++){
        dp[i] = Math.max(dp[i -2], dp[i - 3]) + nums[i];
    }
    return Math.max(dp[n - 1], dp[n - 2]);
}

O(1) 空間複雜度實現方法:

public int rob(int[] nums) {
    int n = nums.length;
    if(n == 0) return 0;
    if(n == 1) return nums[0];
    if(n == 2) return Math.max(nums[0], nums[1]);
    int pre3 = nums[0], pre2 = nums[1], pre1 = nums[2] + nums[0];
    for(int i = 3; i < n; i++){
        int cur = Math.max(pre2, pre3) + nums[i];
        pre3 = pre2;
        pre2 = pre1;
        pre1 = cur;
    }
    return Math.max(pre1, pre2);
}

強盜在環形街區搶劫

Leetcode : 213. House Robber II (Medium)

public int rob(int[] nums) {
    if(nums == null || nums.length == 0) return 0;
    int n = nums.length;
    if(n == 1) return nums[0];
    return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
}

private int rob(int[] nums, int s, int e) {
    int n = nums.length;
    if(e - s == 0) return nums[s];
    if(e - s == 1) return Math.max(nums[s], nums[s + 1]);
    int[] dp = new int[n];
    dp[s] = nums[s];
    dp[s + 1] = nums[s + 1];
    dp[s + 2] = nums[s] + nums[s + 2];
    for (int i = s + 3; i <= e; i++) {
        dp[i] = Math.max(dp[i - 2], dp[i - 3]) + nums[i];
    }
    return Math.max(dp[e], dp[e - 1]);
}

信件錯排

題目描述:有 N 個 信 和 信封,它們被打亂,求錯誤裝信的方式數量。

定義一個數組 dp 存儲錯誤方式數量,dp[i] 表示前 i 個信和信封的錯誤方式數量。假設第 i 個信裝到第 j 個信封裏面,而第 j 個信裝到第 k 個信封裏面。根據 i 和 k 是否相等,有兩種情況:

① i==k,交換 i 和 k 的信後,它們的信和信封在正確的位置,但是其餘 i-2 封信有 dp[i-2] 種錯誤裝信的方式。由於 j 有 i-1 種取值,因此共有 (i-1)*dp[i-2] 種錯誤裝信方式。

② i != k,交換 i 和 j 的信後,第 i 個信和信封在正確的位置,其餘 i-1 封信有 dp[i-1] 種錯誤裝信方式。由於 j 有 i-1 種取值,因此共有 (n-1)*dp[i-1] 種錯誤裝信方式。

綜上所述,錯誤裝信數量方式數量爲:

dp[N] 即爲所求。

和上樓梯問題一樣,dp[i] 只與 dp[i-1] 和 dp[i-2] 有關,因此也可以只用兩個變量來存儲 dp[i-1] 和 dp[i-2]。

最長遞增子序列

已知一個序列 {S1, S2,...,Sn} ,取出若干數組成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持遞增,即新序列中各個數仍然保持原數列中的先後順序,稱新序列爲原序列的一個子序列

如果在子序列中,當下標 ix > iy 時,Six > Siy,稱子序列爲原序列的一個遞增子序列

定義一個數組 dp 存儲最長遞增子序列的長度,dp[n] 表示以 Sn 結尾的序列的最長遞增子序列長度。對於一個遞增子序列 {Si1, Si2,...,Sim},如果 im < n 並且 Sim < Sn ,此時 {Si1, Si2,..., Sim, Sn} 爲一個遞增子序列,遞增子序列的長度增加 1。滿足上述條件的遞增子序列中,長度最長的那個遞增子序列就是要找的,在長度最長的遞增子序列上加上 Sn 就構成了以 Sn 爲結尾的最長遞增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。

因爲在求 dp[n] 時可能無法找到一個滿足條件的遞增子序列,此時 {Sn} 就構成了遞增子序列,因此需要對前面的求解方程做修改,令 dp[n] 最小爲 1,即:

對於一個長度爲 N 的序列,最長子序列並不一定會以 SN 爲結尾,因此 dp[N] 不是序列的最長遞增子序列的長度,需要遍歷 dp 數組找出最大值纔是所要的結果,即 max{ dp[i] | 1 <= i <= N} 即爲所求。

最長遞增子序列

Leetcode : 300. Longest Increasing Subsequence (Medium)

public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    for(int i = 0; i < n; i++){
        int max = 1;
        for(int j = 0; j < i; j++){
            if(nums[i] > nums[j]) max = Math.max(max, dp[j] + 1);
        }
        dp[i] = max;
    }
    int ret = 0;
    for(int i = 0; i < n; i++){
        ret = Math.max(ret, dp[i]);
    }
    return ret;
}

以上解法的時間複雜度爲 O(n2) ,可以使用二分查找使得時間複雜度降低爲 O(nlogn)。定義一個 tails 數組,其中 tails[i] 存儲長度爲 i + 1 的最長遞增子序列的最後一個元素,例如對於數組 [4,5,6,3],有

len = 1  :      [4], [5], [6], [3]  => tails[0] = 3
len = 2  :      [4, 5], [5, 6]      => tails[1] = 5
len = 3  :      [4, 5, 6]            => tails[2] = 6

對於一個元素 x,如果它大於 tails 數組所有的值,那麼把它添加到 tails 後面;如果 tails[i-1] < x <= tails[i],那麼更新 tails[i] = x 。

可以看出 tails 數組保持有序,因此在查找 Si 位於 tails 數組的位置時就可以使用二分查找。

public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] tails = new int[n];
    int size = 0;
    for(int i = 0; i < n; i++){
        int idx = binarySearch(tails, 0, size, nums[i]);
        tails[idx] = nums[i];
        if(idx == size) size++;
    }
    return size;
}

private int binarySearch(int[] nums, int sIdx, int eIdx, int key){
    while(sIdx < eIdx){
        int mIdx = sIdx + (eIdx - sIdx) / 2;
        if(nums[mIdx] == key) return mIdx;
        else if(nums[mIdx] > key) eIdx = mIdx;
        else sIdx = mIdx + 1;
    }
    return sIdx;
}

最長擺動子序列

Leetcode : 376. Wiggle Subsequence (Medium)

要求:使用 O(n) 時間複雜度求解。

使用兩個狀態 up 和 down。

public int wiggleMaxLength(int[] nums) {
    int len = nums.length;
    if (len == 0) return 0;
    int up = 1, down = 1;
    for (int i = 1; i < len; i++) {
        if (nums[i] > nums[i - 1]) up = down + 1;
        else if (nums[i] < nums[i - 1]) down = up + 1;
    }
    return Math.max(up, down);
}

最長公共子系列

對於兩個子序列 S1 和 S2,找出它們最長的公共子序列。

定義一個二維數組 dp 用來存儲最長公共子序列的長度,其中 dp[i][j] 表示 S1 的前 i 個字符與 S2 的前 j 個字符最長公共子序列的長度。考慮 S1i 與 S2j 值是否相等,分爲兩種情況:

① 當 S1i==S2j 時,那麼就能在 S1 的前 i-1 個字符與 S2 的前 j-1 個字符最長公共子序列的基礎上再加上 S1i 這個值,最長公共子序列長度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。

② 當 S1i != S2j 時,此時最長公共子序列爲 S1 的前 i-1 個字符和 S2 的前 j 個字符最長公共子序列,與 S1 的前 i 個字符和 S2 的前 j-1 個字符最長公共子序列,它們的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。

綜上,最長公共子系列的狀態轉移方程爲:

![](http://latex.codecogs.com/gif.latex?\\\\dp[i][j]=\left{\begin{array}{rcl}dp[i-1][j-1]&&{S1_i==S2_j}\max(dp[i-1][j],dp[i][j-1])&&{S1_i<>S2_j}\end{array}\right.)

對於長度爲 N 的序列 S1 和 長度爲 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最長公共子序列長度。

與最長遞增子序列相比,最長公共子序列有以下不同點:

① 針對的是兩個序列,求它們的最長公共子序列。② 在最長遞增子序列中,dp[i] 表示以 Si 爲結尾的最長遞增子序列長度,子序列必須包含 Si ;在最長公共子序列中,dp[i][j] 表示 S1 中前 i 個字符與 S2 中前 j 個字符的最長公共子序列長度,不一定包含 S1i 和 S2j 。③ 由於 2 ,在求最終解時,最長公共子序列中 dp[N][M] 就是最終解,而最長遞增子序列中 dp[N] 不是最終解,因爲以 SN 爲結尾的最長遞增子序列不一定是整個序列最長遞增子序列,需要遍歷一遍 dp 數組找到最大者。

public int lengthOfLCS(int[] nums1, int[] nums2) {
    int n1 = nums1.length, n2 = nums2.length;
    int[][] dp = new int[n1 + 1][n2 + 1];
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            if (nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
            else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        }
    }
    return dp[n1][n2];
}

0-1 揹包

有一個容量爲 N 的揹包,要用這個揹包裝下物品的價值最大,這些物品有兩個屬性:體積 w 和價值 v。

定義一個二維數組 dp 存儲最大價值,其中 dp[i][j] 表示體積不超過 j 的情況下,前 i 件物品能達到的最大價值。設第 i 件物品體積爲 w,價值爲 v,根據第 i 件物品是否添加到揹包中,可以分兩種情況討論:

① 第 i 件物品沒添加到揹包,總體積不超過 j 的前 i 件物品的最大價值就是總體積不超過 j 的前 i-1 件物品的最大價值,dp[i][j] = dp[i-1][j]。② 第 i 件物品添加到揹包中,dp[i][j] = dp[i-1][j-w] + v。

第 i 件物品可添加也可以不添加,取決於哪種情況下最大價值更大。

綜上,0-1 揹包的狀態轉移方程爲:

public int knapsack(int W, int N, int[] weights, int[] values) {
    int[][] dp = new int[N][W];
    for (int i = W - 1; i >= 0; i--) {
        dp[0][i] = i > weights[0] ? values[0] : 0;
    }
    for (int i = 1; i < N; i++) {
        for (int j = W - 1; j >= weights[i]; j--) {
            dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
        }
        for (int j = weights[i - 1] - 1; j >= 0; j--) {
            dp[i][j] = dp[i - 1][j];
        }
    }
    return dp[N - 1][W - 1];
}

空間優化

在程序實現時可以對 0-1 揹包做優化。觀察狀態轉移方程可以知道,前 i 件物品的狀態僅由前 i-1 件物品的狀態有關,因此可以將 dp 定義爲一維數組,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此時,

因爲 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w] 防止將 dp[i-1][j-w] 覆蓋。也就是說要先計算 dp[i][j] 再計算 dp[i][j-w],在程序實現時需要按倒序來循環求解。

無法使用貪心算法的解釋

0-1 揹包問題無法使用貪心算法來求解,也就是說不能按照先添加性價比最高的物品來達到最優,這是因爲這種方式可能造成揹包空間的浪費,從而無法達到最優。考慮下面的物品和一個容量爲 5 的揹包,如果先添加物品 0 再添加物品 1,那麼只能存放的價值爲 16,浪費了大小爲 2 的空間。最優的方式是存放物品 1 和物品 2,價值爲 22.

idwvv/w
0166
12105
23124

變種

完全揹包:物品可以無限個,可以轉換爲 0-1 揹包,令每種物品的體積和價值變爲 1/2/4... 倍數,把它們都當成一個新物品,然後一種物品只能添加一次。

多重揹包:物品數量有限制,同樣可以轉換爲 0-1 揹包。

多維費用揹包:物品不僅有重量,還有體積,同時考慮這兩種限制。

其它:物品之間相互約束或者依賴。

劃分數組爲和相等的兩部分

Leetcode : 416. Partition Equal Subset Sum (Medium)

可以看成一個揹包大小爲 sum/2 的 0-1 揹包問題,但是也有不同的地方,這裏沒有價值屬性,並且揹包必須被填滿。

以下實現使用了空間優化。

public boolean canPartition(int[] nums) {
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    if (sum % 2 != 0) {
        return false;
    }
    int W = sum / 2;
    boolean[] dp = new boolean[W + 1];
    int n = nums.length;
    for(int i = 0; i <= W; i++) {
        if(nums[0] == i) dp[i] = true;
    }
    for(int i = 1; i < n; i++) {
        for(int j = W; j >= nums[i]; j--) {
            dp[j] = dp[j] || dp[j - nums[i]];
        }
    }

    return dp[W];
}

字符串按單詞列表分割

Leetcode : 139. Word Break (Medium)

s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".
public boolean wordBreak(String s, List<String> wordDict) {
    int n = s.length();
    boolean[] dp = new boolean[n + 1];
    dp[0] = true;
    for (int i = 1; i <= n; i++) {
        for (String word : wordDict) {
            if (word.length() <= i
                    && word.equals(s.substring(i - word.length(), i))) {
                dp[i] = dp[i] || dp[i - word.length()];
            }
        }
    }
    return dp[n];
}

改變一組數的正負號使得它們的和爲一給定數

Leetcode : 494. Target Sum (Medium)

Input: nums is [1, 1, 1, 1, 1], S is 3. 
Output: 5
Explanation: 

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

There are 5 ways to assign symbols to make the sum of nums be target 3.

該問題可以轉換爲 subset sum 問題,從而使用 0-1 揹包的方法來求解。可以將這組數看成兩部分,P 和 N,其中 P 使用正號,N 使用負號,有以下推導:

                  sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
                       2 * sum(P) = target + sum(nums)

因此只要找到一個子集,令它們都取正號,並且和等於 (target + sum(nums))/2,就證明存在解。

public int findTargetSumWays(int[] nums, int S) {
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    if (sum < S || (sum + S) % 2 == 1) {
        return 0;
    }
    return subsetSum(nums, (sum + S) >>> 1);
}

private int subsetSum(int[] nums, int targetSum) {
    Arrays.sort(nums);
    int[] dp = new int[targetSum + 1];
    dp[0] = 1;
    for (int i = 0; i < nums.length; i++) {
        int num = nums[i];
        for (int j = targetSum; j >= num; j--) {
            dp[j] = dp[j] + dp[j - num];
        }
    }
    return dp[targetSum];
}

01字符構成最多的字符串

Leetcode : 474. Ones and Zeroes (Medium)

Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4

Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are “10,”0001”,”1”,”0”

這是一個多維費用的 0-1 揹包問題,有兩個揹包大小,0 的數量和 1 的數量。

public int findMaxForm(String[] strs, int m, int n) {
    if (strs == null || strs.length == 0) return 0;
    int l = strs.length;
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 0; i < l; i++) {
        String s = strs[i];
        int ones = 0, zeros = 0;
        for (char c : s.toCharArray()) {
            if (c == '0') zeros++;
            else if (c == '1') ones++;
        }
        for (int j = m; j >= zeros; j--) {
            for (int k = n; k >= ones; k--) {
                if (zeros <= j && ones <= k) {
                    dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1);
                }
            }
        }
    }
    return dp[m][n];
}

找零錢

Leetcode : 322. Coin Change (Medium)

題目描述:給一些面額的硬幣,要求用這些硬幣來組成給定面額的錢數,並且使得硬幣數量最少。硬幣可以重複使用。

這是一個完全揹包問題,完全揹包問題和 0-1揹包問題在實現上唯一的不同是,第二層循環是從 0 開始的,而不是從尾部開始。

public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];
    Arrays.fill(dp, amount + 1);
    dp[0] = 0;
    for (int i = 1; i <= amount; i++) {
        for (int j = 0; j < coins.length; j++) {
            if (coins[j] <= i) {
                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
            }
        }
    }
    return dp[amount] > amount ? -1 : dp[amount];
}

組合總和

Leetcode : 377. Combination Sum IV (Medium)

nums = [1, 2, 3]
target = 4

The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

Note that different sequences are counted as different combinations.

Therefore the output is 7.
public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target + 1];
    dp[0] = 1;
    for (int i = 1; i <= target; i++) {
        for (int j = 0; j < nums.length; j++) {
            if(nums[j] <= i) {
                dp[i] += dp[i - nums[j]];
            }
        }
    }
    return dp[target];
}

只能進行兩次的股票交易

Leetcode : 123. Best Time to Buy and Sell Stock III (Hard)

public int maxProfit(int[] prices) {
    int firstBuy = Integer.MIN_VALUE, firstSell = 0;
    int secondBuy = Integer.MIN_VALUE, secondSell = 0;
    for (int curPrice : prices) {
        if (firstBuy < -curPrice) firstBuy = -curPrice;
        if (firstSell < firstBuy + curPrice) firstSell = firstBuy + curPrice;
        if (secondBuy < firstSell - curPrice) secondBuy = firstSell - curPrice;
        if (secondSell < secondBuy + curPrice) secondSell = secondBuy + curPrice;
    }
    return secondSell;
}

只能進行 k 次的股票交易

Leetcode : 188. Best Time to Buy and Sell Stock IV (Hard)

dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj]))
public int maxProfit(int k, int[] prices) {
    int n = prices.length;
    if (k >= n/2) {
        int maxPro = 0;
        for (int i = 1; i < n; i++) {
            if (prices[i] > prices[i-1])
                maxPro += prices[i] - prices[i-1];
        }
        return maxPro;
    }
    int[][] dp = new int[k + 1][n];
    for (int i = 1; i <= k; i++) {
        int localMax = dp[i - 1][0] - prices[0];
        for (int j = 1; j < n; j++) {
            dp[i][j] = Math.max(dp[i][j - 1], prices[j] + localMax);
            localMax = Math.max(localMax, dp[i - 1][j] - prices[j]);
        }
    }
    return dp[k][n - 1];
}

數組區間

數組區間和

Leetcode : 303. Range Sum Query - Immutable (Easy)

求區間 i ~ j 的和,可以轉換爲 sum[j] - sum[i-1],其中 sum[i] 爲 0 ~ j 的和。

class NumArray {
    
    int[] nums;
    
    public NumArray(int[] nums) {
        for(int i = 1; i < nums.length; i++)
            nums[i] += nums[i - 1];
        this.nums = nums;
    }
    
    public int sumRange(int i, int j) {
        return i == 0 ? nums[j] : nums[j] - nums[i - 1];
    }
}

子數組最大的和

Leetcode : 53. Maximum Subarray (Easy)

令 sum[i] 爲以 num[i] 爲結尾的子數組最大的和,可以由 sum[i-1] 得到 sum[i] 的值,如果 sum[i-1] 小於 0,那麼以 num[i] 爲結尾的子數組不能包含前面的內容,因爲加上前面的部分,那麼和一定會比 num[i] 還小。

public int maxSubArray(int[] nums) {
    int n = nums.length;
    int[] sum = new int[n];
    sum[0] = nums[0];
    int max = sum[0];
    for(int i = 1; i < n; i++){
        sum[i] = (sum[i-1] > 0 ? sum[i-1] : 0) + nums[i];
        max = Math.max(max, sum[i]);
    }
    return max;
}

數組中等差遞增子區間的個數

Leetcode : 413. Arithmetic Slices (Medium)

A = [1, 2, 3, 4]

return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself.

對於 (1,2,3,4),它有三種組成遞增子區間的方式,而對於 (1,2,3,4,5),它組成遞增子區間的方式除了 (1,2,3,4) 的三種外還多了一種,即 (1,2,3,4,5),因此 dp[i] = dp[i - 1] + 1。

public int numberOfArithmeticSlices(int[] A) {
    int n = A.length;
    int[] dp = new int[n];
    for(int i = 2; i < n; i++) {
        if(A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
            dp[i] = dp[i - 1] + 1;
        }
    }
    int ret = 0;
    for(int cnt : dp) {
        ret += cnt;
    }
    return ret;
}

字符串編輯

刪除兩個字符串的字符使它們相等

Leetcode : 583. Delete Operation for Two Strings (Medium)

可以轉換爲求兩個字符串的最長公共子序列問題。

public int minDistance(String word1, String word2) {
    int m = word1.length(), n = word2.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= n; j++) {
            if (i == 0 || j == 0) continue;
            dp[i][j] = word1.charAt(i - 1) == word2.charAt(j - 1) ? dp[i - 1][j - 1] + 1
                    : Math.max(dp[i][j - 1], dp[i - 1][j]);
        }
    }
    return m + n - 2 * dp[m][n];
}

修改一個字符串稱爲另一個字符串 // TODO

Leetcode : 72. Edit Distance (Hard)

其它問題

需要冷卻期的股票交易

Leetcode : 309. Best Time to Buy and Sell Stock with Cooldown(Medium)

題目描述:交易之後需要有一天的冷卻時間。

s0[i] = max(s0[i - 1], s2[i - 1]); // Stay at s0, or rest from s2
s1[i] = max(s1[i - 1], s0[i - 1] - prices[i]); // Stay at s1, or buy from s0
s2[i] = s1[i - 1] + prices[i]; // Only one way from s1
public int maxProfit(int[] prices) {
    if (prices == null || prices.length == 0) return 0;
    int n = prices.length;
    int[] s0 = new int[n];
    int[] s1 = new int[n];
    int[] s2 = new int[n];
    s0[0] = 0;
    s1[0] = -prices[0];
    s2[0] = Integer.MIN_VALUE;
    for (int i = 1; i < n; i++) {
        s0[i] = Math.max(s0[i - 1], s2[i - 1]);
        s1[i] = Math.max(s1[i - 1], s0[i - 1] - prices[i]);
        s2[i] = Math.max(s2[i - 1], s1[i - 1] + prices[i]);
    }
    return Math.max(s0[n - 1], s2[n - 1]);
}

統計從 0 ~ n 每個數的二進制表示中 1 的個數

Leetcode : 338. Counting Bits (Medium)

對於數字 6(110),它可以看成是數字 2(10) 前面加上一個 1 ,因此 dp[i] = dp[i&(i-1)] + 1;

    public int[] countBits(int num) {
        int[] ret = new int[num + 1];
        for(int i = 1; i <= num; i++){
            ret[i] = ret[i&(i-1)] + 1;
        }
        return ret;
    }

一組整數對能夠構成的最長鏈

Leetcode : 646. Maximum Length of Pair Chain (Medium)

對於 (a, b) 和 (c, d) ,如果 b < c,則它們可以構成一條鏈。

public int findLongestChain(int[][] pairs) {
    if(pairs == null || pairs.length == 0) {
        return 0;
    }
    Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
    int n = pairs.length;
    int[] dp = new int[n];
    Arrays.fill(dp, 1);
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < i; j++) {
            if(pairs[i][0] > pairs[j][1]){
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    
    int ret = 0;
    for(int num : dp) {
        ret = Math.max(ret, num);
    }
    return ret;
}

買入和售出股票最大的收益

Leetcode : 121. Best Time to Buy and Sell Stock (Easy)

只進行一次交易。

只要記錄前面的最小价格,將這個最小价格作爲買入價格,然後將當前的價格作爲售出價格,查看這個價格是否是當前的最大價格。

public int maxProfit(int[] prices) {
    int n = prices.length;
    if(n == 0) return 0;
    int soFarMin = prices[0];
    int max = 0;
    for(int i = 1; i < n; i++){
        if(soFarMin > prices[i]) soFarMin = prices[i];
        else max = Math.max(max, prices[i] - soFarMin);
    }
    return max;
}

複製粘貼字符

Leetcode : 650. 2 Keys Keyboard (Medium)

public int minSteps(int n) {
    int[] dp = new int[n + 1];
    for (int i = 2; i <= n; i++) {
        dp[i] = i;
        for (int j = i - 1; j >= 0; j--) {
            if (i % j == 0) {
                dp[i] = dp[j] + dp[i / j];
                break;
            }
        }
    }
    return dp[n];
}
public int minSteps(int n) {
    if (n == 1) return 0;
    for (int i = 2; i <= Math.sqrt(n); i++) {
        if (n % i == 0) return i + minSteps(n / i);
    }
    return n;
}

數學

素數

素數分解

每一個數都可以分解成素數的乘積,例如 84 = 22 * 31 * 50 * 71 * 110 * 130 * 170 * …

整除

令 x = 2m0 * 3m1 * 5m2 * 7m3 * 11m4 * …令 y = 2n0 * 3n1 * 5n2 * 7n3 * 11n4 * …

如果 x 整除 y(y mod x == 0),則對於所有 i,mi <= ni。

x 和 y 的 最大公約數 爲:gcd(x,y) = 2min(m0,n0) * 3min(m1,n1) * 5min(m2,n2) * ...

x 和 y 的 最小公倍數 爲:lcm(x,y) = 2max(m0,n0) * 3max(m1,n1) * 5max(m2,n2) * ...

生成素數序列

Leetcode : 204. Count Primes (Easy)

埃拉託斯特尼篩法在每次找到一個素數時,將能被素數整除的數排除掉。

public int countPrimes(int n) {
    boolean[] notPrimes = new boolean[n + 1];
    int cnt = 0;
    for(int i = 2; i < n; i++){
        if(notPrimes[i]) continue;
        cnt++;
        // 從 i * i 開始,因爲如果 k < i,那麼 k * i 在之前就已經被去除過了
        for(long j = (long) i * i; j < n; j += i){
            notPrimes[(int) j] = true;
        }
    }
    return cnt;
}

最大公約數

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

最大公倍數爲兩數的乘積除以最大公約數。

int lcm(int a, int b){
    return a * b / gcd(a, b);
}

對於最大公約數問題,因爲需要計算 a % b ,而這個操作是比較耗時的,可以使用 編程之美:2.7 的方法,利用減法和移位操作來替換它。

對於 a 和 b 的最大公約數 f(a, b),有:

1. 如果 a 和 b 均爲偶數,f(a, b) = 2*f(a/2, b/2);2. 如果 a 是偶數 b 是奇數,f(a, b) = f(a/2, b);3. 如果 b 是偶數 a 是奇數,f(a, b) = f(a, b/2);4. 如果 a 和 b 均爲奇數,f(a, b) = f(a, a-b);

乘 2 和除 2 都可以轉換爲移位操作。

進制轉換

Java 中 static String toString(int num, int radix) 可以將一個整數裝換爲 redix 進製表示的字符串。

7 進制

Leetcode : 504. Base 7 (Easy)

public String convertToBase7(int num) {
    if (num < 0) {
        return '-' + convertToBase7(-num);
    }
    if (num < 7) {
        return num + "";
    }
    return convertToBase7(num / 7) + num % 7;
}

16 進制

Leetcode : 405. Convert a Number to Hexadecimal (Easy)

public String toHex(int num) {
    char[] map = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
    if(num == 0) return "0";
    String ret = "";
    while(num != 0){
        ret = map[(num & 0b1111)] + ret;
        num >>>= 4;
    }
    return ret;
}

階乘

統計階乘尾部有多少個 0

Leetcode : 172. Factorial Trailing Zeroes (Easy)

尾部的 0 由 2 * 5 得來,2 的數量明顯多於 5 的數量,因此只要統計有多少個 5 即可。

對於一個數 N,它所包含 5 的個數爲:N/5 + N/52 + N/53 + ...,其中 N/5 表示不大於 N 的數中 5 的倍數貢獻一個 5,N/52 表示不大於 N 的數中 52 的倍數再貢獻一個 5 ...。

public int trailingZeroes(int n) {
    return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
}

如果統計的是 N! 的二進制表示中最低位 1 的位置,只要統計有多少個 2 即可,該題目出自 編程之美:2.2 。和求解有多少個 5 一樣,2 的個數爲 N/2 + N/22 + N/23 + ...

字符串加法減法

二進制加法

Leetcode : 67. Add Binary (Easy)

public String addBinary(String a, String b) {
    int i = a.length() - 1, j = b.length() - 1, carry = 0;
    String str = "";
    while(i >= 0 || j >= 0){
        if(i >= 0 && a.charAt(i--) == '1') carry++;
        if(j >= 0 && b.charAt(j--) == '1') carry++;
        str = (carry % 2) + str;
        carry /= 2;
    }
    if(carry == 1) str = "1" + str;
    return str;
}

字符串加法

Leetcode : 415. Add Strings (Easy)

題目描述:字符串的值爲非負整數

public String addStrings(String num1, String num2) {
    StringBuilder sb = new StringBuilder();
    int carry = 0;
    for(int i = num1.length() - 1, j = num2.length() - 1; i >= 0 || j >= 0 || carry == 1; i--, j--){
        int x = i < 0 ? 0 : num1.charAt(i) - '0';
        int y = j < 0 ? 0 : num2.charAt(j) - '0';
        sb.append((x + y + carry) % 10);
        carry = (x + y + carry) / 10;
    }
    return sb.reverse().toString();
}

相遇問題

改變數組元素使所有的數組元素都相等

Leetcode : 462. Minimum Moves to Equal Array Elements II (Medium)

題目描述:每次可以對一個數組元素加一或者減一,求最小的改變次數。

這是個典型的相遇問題,移動距離最小的方式是所有元素都移動到中位數。理由如下:

設 m 爲中位數。a 和 b 是 m 兩邊的兩個元素,且 b > a。要使 a 和 b 相等,它們總共移動的次數爲 b - a,這個值等於 (b - m) + (m - a),也就是把這兩個數移動到中位數的移動次數。

設數組長度爲 N,則可以找到 N/2 對 a 和 b 的組合,使它們都移動到 m 的位置。

解法 1

先排序,時間複雜度:O(NlgN)

public int minMoves2(int[] nums) {
    Arrays.sort(nums);
    int ret = 0;
    int l = 0, h = nums.length - 1;
    while(l <= h) {
        ret += nums[h] - nums[l];
        l++;
        h--;
    }
    return ret;
}

解法 2

使用快速排序找到中位數,時間複雜度 O(N)

public int minMoves2(int[] nums) {
    int ret = 0;
    int n = nums.length;
    int median = quickSelect(nums, 0, n - 1, n / 2 + 1);
    for(int num : nums) ret += Math.abs(num - median);
    return ret;
}

private int quickSelect(int[] nums, int start, int end, int k) {
    int l = start, r = end, privot = nums[(l + r) / 2];
    while(l <= r) {
        while(nums[l] < privot) l++;
        while(nums[r] > privot) r--;
        if(l >= r) break;
        swap(nums, l, r);
        l++; r--;
    }
    int left = l - start + 1;
    if(left > k) return quickSelect(nums, start, l - 1, k);
    if(left == k && l == r) return nums[l];
    int right = r - start + 1;
    return quickSelect(nums, r + 1, end, k - right);
}

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

多數投票問題

數組中出現次數多於 n / 2 的元素

Leetcode : 169. Majority Element (Easy)

先對數組排序,最中間那個數出現次數一定多於 n / 2

public int majorityElement(int[] nums) {
    Arrays.sort(nums);
    return nums[nums.length / 2];
}

可以利用 Boyer-Moore Majority Vote Algorithm 來解決這個問題,使得時間複雜度爲 O(n)。可以這麼理解該算法:使用 cnt 來統計一個元素出現的次數,當遍歷到的元素和統計元素不想等時,令 cnt--。如果前面查找了 i 個元素,且 cnt == 0 ,說明前 i 個元素沒有 majority,或者有 majority,但是出現的次數少於 i / 2 ,因爲如果多於 i / 2 的話 cnt 就一定不會爲 0 。此時剩下的 n - i 個元素中,majority 的數目多於 (n - i) / 2,因此繼續查找就能找出 majority。

public int majorityElement(int[] nums) {
    int cnt = 0, majority = 0;
    for(int i = 0; i < nums.length; i++){
        if(cnt == 0) {
            majority = nums[i];
            cnt++;
        }
        else if(majority == nums[i]) cnt++;
        else cnt--;
    }
    return majority;
}

其它

平方數

Leetcode : 367. Valid Perfect Square (Easy)

平方序列:1,4,9,16,..間隔:3,5,7,...

間隔爲等差數列,使用這個特性可以得到從 1 開始的平方序列。

public boolean isPerfectSquare(int num) {
    int subNum = 1;
    while (num > 0) {
        num -= subNum;
        subNum += 2;
    }
    return num == 0;
}

3 的 n 次方

Leetcode : 326. Power of Three (Easy)

public boolean isPowerOfThree(int n) {
    return n > 0 && (1162261467 % n == 0);
}

找出數組中的乘積最大的三個數

Leetcode : 628. Maximum Product of Three Numbers (Easy)

public int maximumProduct(int[] nums) {
    int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE, min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
    for (int n : nums) {
        if (n > max1) {
            max3 = max2;
            max2 = max1;
            max1 = n;
        } else if (n > max2) {
            max3 = max2;
            max2 = n;
        } else if (n > max3) {
            max3 = n;
        }

        if (n < min1) {
            min2 = min1;
            min1 = n;
        } else if (n < min2) {
            min2 = n;
        }
    }
    return Math.max(max1*max2*max3, max1*min1*min2);
}

乘積數組

Leetcode : 238. Product of Array Except Self (Medium)

題目描述:給定一個數組,創建一個新數組,新數組的每個元素爲原始數組中除了該位置上的元素之外所有元素的乘積。

題目要求:時間複雜度爲 O(n),並且不能使用除法。

public int[] productExceptSelf(int[] nums) {
    int n = nums.length;
    int[] ret = new int[n];
    ret[0] = 1;
    for(int i = 1; i < n; i++) {
        ret[i] = ret[i - 1] * nums[i - 1];
    }
    int right = 1;
    for(int i = n - 1; i >= 0; i--) {
        ret[i] *= right;
        right *= nums[i];
    }
    return ret;
}

數據結構相關

棧和隊列

用棧實現隊列

一個棧實現:

class  MyQueue {
    private Stack<Integer> st = new Stack();

    public void push(int x) {
        Stack<Integer> temp = new Stack();
        while(!st.isEmpty()){
            temp.push(st.pop());
        }
        st.push(x);
        while(!temp.isEmpty()){
            st.push(temp.pop());
        }
    }

    public int pop() {
        return st.pop();
    }

    public int peek() {
        return st.peek();
    }

    public boolean empty() {
        return st.isEmpty();
    }
}

兩個棧實現:

class  MyQueue {
    private Stack<Integer> in = new Stack();
    private Stack<Integer> out = new Stack();
  
    public void push(int x) {
        in.push(x);
    }

    public int pop() {
        in2out();
        return out.pop();
    }

    public int peek() {
        in2out();
        return out.peek();
    }
    
    private void in2out(){
        if(out.isEmpty()){
            while(!in.isEmpty()){
                out.push(in.pop());
            }
        }
    }

    public boolean empty() {
        return in.isEmpty() && out.isEmpty();
    }
}

用隊列實現棧

Leetcode : 225. Implement Stack using Queues (Easy)

class MyStack {
    
    private Queue<Integer> queue;

    public MyStack() {
        queue = new LinkedList<>();
    }
    
    public void push(int x) {
        queue.add(x);
        for(int i = 1; i < queue.size(); i++){ // 翻轉
            queue.add(queue.remove());
        }
    }
    
    public int pop() {
        return queue.remove();
    }
    
    public int top() {
        return queue.peek();
    }
    
    public boolean empty() {
        return queue.isEmpty();
    }
}

最小值棧

Leetcode : 155. Min Stack (Easy)

用兩個棧實現,一個存儲數據,一個存儲最小值。

class MinStack {
    
    private Stack<Integer> dataStack;
    private Stack<Integer> minStack;
    private int min;

    public MinStack() {
        dataStack = new Stack<>();
        minStack = new Stack<>();
        min = Integer.MAX_VALUE;
    }
    
    public void push(int x) {
        dataStack.add(x);
        if(x < min) {
            min = x;
        }
        minStack.add(min);
    }
    
    public void pop() {
        dataStack.pop();
        minStack.pop();
        if(!minStack.isEmpty()) {
            min = minStack.peek();
        } else{
            min = Integer.MAX_VALUE;
        }
    }
    
    public int top() {
        return dataStack.peek();
    }
    
    public int getMin() {
        return min;
    }
}

對於實現最小值隊列問題,可以先將隊列使用棧來實現,然後就將問題轉換爲最小值棧,這個問題出現在 編程之美:3.7。

用棧實現括號匹配

Leetcode : 20. Valid Parentheses (Easy)

"()[]{}"

Output : true
public boolean isValid(String s) {
    Stack<Character> stack = new Stack<>();
    for(int i = 0; i < s.length(); i++){
        char c = s.charAt(i);
        if(c == '(' || c == '{' || c == '[') stack.push(c);
        else{
            if(stack.isEmpty()) return false;
            char cStack = stack.pop();
            if(c == ')' && cStack != '(' ||
              c == ']' && cStack != '[' ||
              c == '}' && cStack != '{' ) {
                return false;
            }
        }
    }
    return stack.isEmpty();
}

數組中比當前元素大的下一個數組元素的距離

Input: [73, 74, 75, 71, 69, 72, 76, 73]
Output: [1, 1, 4, 2, 1, 1, 0, 0]

Leetcode : 739. Daily Temperatures (Medium)

使用棧來存儲還未計算的元素。可以保證從棧頂向下元素遞增,否則上面有一個比下面某個元素大的元素進入棧中,下面那個元素已經找到比它大的元素,因此會出棧。

public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] ret = new int[n];
    Stack<Integer> stack = new Stack<>();
    for(int i = 0; i < n; i++) {
        while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
            int idx = stack.pop();
            ret[idx] = i - idx;
        }
        stack.add(i);
    }
    return ret;
}

數組中下一個比當前數大的數

Leetcode : 496. Next Greater Element I (Easy)

Input: nums1 = [4,1,2], nums2 = [1,3,4,2].
Output: [-1,3,-1]

在遍歷數組時用 Stack 把數組中的數存起來,如果當前遍歷的數比棧頂元素來的大,說明棧頂元素的下一個比它大的數就是當前元素。

public int[] nextGreaterElement(int[] nums1, int[] nums2) {
    Map<Integer, Integer> map = new HashMap<>();
    Stack<Integer> stack = new Stack<>();
    for(int num : nums2){
        while(!stack.isEmpty() && num > stack.peek()){
            map.put(stack.pop(), num);
        }
        stack.add(num);
    }
    int[] ret = new int[nums1.length];
    for(int i = 0; i < nums1.length; i++){
        if(map.containsKey(nums1[i])) ret[i] = map.get(nums1[i]);
        else ret[i] = -1;
    }
    return ret;
}

循環數組中下一個比當前元素大的數

Leetcode : 503. Next Greater Element II (Medium)

public int[] nextGreaterElements(int[] nums) {
    int n = nums.length, next[] = new int[n];
    Arrays.fill(next, -1);
    Stack<Integer> stack = new Stack<>();
    for (int i = 0; i < n * 2; i++) {
        int num = nums[i % n];
        while (!stack.isEmpty() && nums[stack.peek()] < num)
            next[stack.pop()] = num;
        if (i < n) stack.push(i);
    }
    return next;
}

哈希表

利用 Hash Table 可以快速查找一個元素是否存在等問題,但是需要一定的空間來存儲。在優先考慮時間複雜度的情況下,可以利用 Hash Table 這種空間換取時間的做法。

Java 中的 HashSet 用於存儲一個集合,並以 O(1) 的時間複雜度查找元素是否在集合中。

如果元素有窮,並且範圍不大,那麼可以用一個布爾數組來存儲一個元素是否存在,例如對於只有小寫字符的元素,就可以用一個長度爲 26 的布爾數組來存儲一個字符集合,使得空間複雜度降低爲 O(1)。

Java 中的 HashMap 主要用於映射關係,從而把兩個元素聯繫起來。

在對一個內容進行壓縮或者其它轉換時,利用 HashMap 可以把原始內容和轉換後的內容聯繫起來。例如在一個簡化 url 的系統中(Leetcdoe : 535. Encode and Decode TinyURL (Medium)),利用 HashMap 就可以存儲精簡後的 url 到原始 url 的映射,使得不僅可以顯示簡化的 url,也可以根據簡化的 url 得到原始 url 從而定位到正確的資源。

HashMap 也可以用來對元素進行計數統計,此時鍵爲元素,值爲計數。和 HashSet 類似,如果元素有窮並且範圍不大,可以用整型數組來進行統計。

數組中的兩個數和爲給定值

Leetcode : 1. Two Sum (Easy)

可以先對數組進行排序,然後使用雙指針方法或者二分查找方法。這樣做的時間複雜度爲 O(nlgn),空間複雜度爲 O(1)。

用 HashMap 存儲數組元素和索引的映射,在訪問到 nums[i] 時,判斷 HashMap 中是否存在 target - nums[i] ,如果存在說明 target - nums[i] 所在的索引和 i 就是要找的兩個數。該方法的時間複雜度爲 O(n),空間複雜度爲 O(n),使用空間來換取時間。

public int[] twoSum(int[] nums, int target) {
    HashMap<Integer, Integer> map = new HashMap<>();
    for(int i = 0; i < nums.length; i++){
        if(map.containsKey(target - nums[i])) return new int[]{map.get(target - nums[i]), i};
        else map.put(nums[i], i);
    }
    return null;
}

最長和諧序列

和諧序列中最大數和最小數只差正好爲 1

Leetcode : 594. Longest Harmonious Subsequence (Easy)

public int findLHS(int[] nums) {
    Map<Long, Integer> map = new HashMap<>();
    for (long num : nums) {
        map.put(num, map.getOrDefault(num, 0) + 1);
    }
    int result = 0;
    for (long key : map.keySet()) {
        if (map.containsKey(key + 1)) {
            result = Math.max(result, map.get(key + 1) + map.get(key));
        }
    }
    return result;
}

字符串

兩個字符串的包含的字符是否完全相同

Leetcode : 242. Valid Anagram (Easy)

字符串只包含小寫字符,總共有 26 個小寫字符。可以用 Hash Table 來映射字符與出現次數,因爲鍵值範圍很小,因此可以用數組來進行映射。

使用長度爲 26 的整型數組對字符串出現的字符進行統計,比較兩個字符串出現的字符數量是否相同。

public boolean isAnagram(String s, String t) {
    int[] cnts = new int[26];
    for(int i  = 0; i < s.length(); i++) cnts[s.charAt(i) - 'a'] ++;
    for(int i  = 0; i < t.length(); i++) cnts[t.charAt(i) - 'a'] --;
    for(int i  = 0; i < 26; i++) if(cnts[i] != 0) return false;
    return true;
}

字符串同構

Leetcode : 205. Isomorphic Strings (Easy)

例如 "egg" 和 "add" 就屬於同構字符串。

記錄一個字符上次出現的位置,如果兩個字符串中某個字符上次出現的位置一樣,那麼就屬於同構。

public boolean isIsomorphic(String s, String t) {
    int[] m1 = new int[256];
    int[] m2 = new int[256];
    for(int i = 0; i < s.length(); i++){
        if(m1[s.charAt(i)] != m2[t.charAt(i)]) {
            return false;
        }
        m1[s.charAt(i)] = i + 1;
        m2[t.charAt(i)] = i + 1;
    }
    return true;
}

計算一組字符集合可以組成的迴文字符串的最大長度

Leetcode : 409. Longest Palindrome

使用長度爲 128 的整型數組來統計每個字符出現的個數,每個字符有偶數個可以用來構成迴文字符串。因爲迴文字符串最中間的那個字符可以單獨出現,所以如果有單獨的字符就把它放到最中間。

public int longestPalindrome(String s) {
    int[] cnts = new int[128]; // ascii 碼總共 128 個
    for(char c : s.toCharArray()) cnts[c]++;
    int ret = 0;
    for(int cnt : cnts)  ret += (cnt / 2) * 2;
    if(ret < s.length()) ret ++; // 這個條件下 s 中一定有單個未使用的字符存在,可以把這個字符放到迴文的最中間
    return ret;
}

判斷一個整數是否是迴文數

Leetcode : 9. Palindrome Number (Easy)

要求不能使用額外空間,也就不能將整數轉換爲字符串進行判斷。

將整數分成左右兩部分,右邊那部分需要轉置,然後判斷這兩部分是否相等。

public boolean isPalindrome(int x) {
    if(x == 0) return true;
    if(x < 0) return false;
    if(x % 10 == 0) return false;
    int right = 0;
    while(x > right){
        right = right * 10 + x % 10;
        x /= 10;
    }
    return x == right || x == right / 10;
}

迴文子字符串

Leetcode : 647. Palindromic Substrings (Medium)

解決方案是從字符串的某一位開始,嘗試着去擴展子字符串。

private int cnt = 0;
public int countSubstrings(String s) {
    for(int i = 0; i < s.length(); i++) {
        extendSubstrings(s, i, i);    // 奇數長度
        extendSubstrings(s, i, i + 1); // 偶數長度
    }
    return cnt;
}

private void extendSubstrings(String s, int start, int end) {
    while(start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
        start--;
        end++;
        cnt++;
    }
}

統計二進制字符串中連續 1 和 連續 0 數量相同的子字符串個數

Input: "00110011"
Output: 6
Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01".

Leetcode : 696. Count Binary Substrings (Easy)

public int countBinarySubstrings(String s) {
    int preLen = 0, curLen = 1, ret = 0;
    for(int i = 1; i < s.length(); i++){
        if(s.charAt(i) == s.charAt(i-1)) curLen++;
        else{
            preLen = curLen;
            curLen = 1;
        }

        if(preLen >= curLen) ret++;
    }
    return ret;
}

字符串循環移位包含

編程之美:3.1

給定兩個字符串 s1 和 s2 ,要求判定 s2 是否能夠被 s1 做循環移位得到的字符串包含。

s1 = AABCD, s2 = CDAA
Return : true

s1 進行循環移位的結果是 s1s1 的子字符串,因此只要判斷 s2 是否是 s1s1 的子字符串即可。

字符串循環移位

編程之美:2.17

將字符串向右循環移動 k 位。

例如 abcd123 向右移動 3 位 得到 123abcd

將 abcd123 中的 abcd 和 123 單獨逆序,得到 dcba321,然後對整個字符串進行逆序,得到123abcd。

字符串中單詞的翻轉

程序員代碼面試指南

例如將 "I am a student" 翻轉成 "student a am I"

將每個單詞逆序,然後將整個字符串逆序。

數組與矩陣

把數組中的 0 移到末尾

Leetcode : 283. Move Zeroes (Easy)

    public void moveZeroes(int[] nums) {
        int n = nums.length;
        int idx = 0;
        for(int i = 0; i < n; i++){
            if(nums[i] != 0) nums[idx++] = nums[i];
        }
        while(idx < n){
            nums[idx++] = 0;
        }
    }

一個數組元素在 [1, n] 之間,其中一個數被替換爲另一個數,找出丟失的數和重複的數

Leetcode : 645. Set Mismatch (Easy)

最直接的方法是先對數組進行排序,這種方法時間複雜度爲 O(nlogn),本題可以以 O(n) 的時間複雜度、O(1) 空間複雜度來求解。

主要思想是讓通過交換數組元素,使得數組上的元素在正確的位置上。

遍歷數組,如果第 i 位上的元素不是 i + 1 ,那麼就交換第 i 位 和 nums[i] - 1 位上的元素,使得 num[i] - 1 的元素爲 nums[i] ,也就是該位的元素是正確的。交換操作需要循環進行,因爲一次交換沒辦法使得第 i 位上的元素是正確的。但是要交換的兩個元素可能就是重複元素,那麼循環就可能永遠進行下去,終止循環的方法是加上 nums[i] != nums[nums[i] - 1 條件。

類似題目:

public int[] findErrorNums(int[] nums) {
    for(int i = 0; i < nums.length; i++){
        while(nums[i] != i + 1 && nums[i] != nums[nums[i] - 1]) swap(nums, i, nums[i] - 1);
    }
    
    for(int i = 0; i < nums.length; i++){
        if(i + 1 != nums[i]) return new int[]{nums[i], i + 1};
    }
    
    return null;
}

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

找出數組中重複的數,數組值在 [0, n-1] 之間

Leetcode : 287. Find the Duplicate Number (Medium)

二分查找解法:

public int findDuplicate(int[] nums) {
     int l = 1, h = nums.length - 1;
     while (l <= h) {
         int mid = l + (h - l) / 2;
         int cnt = 0;
         for (int i = 0; i < nums.length; i++) {
             if (nums[i] <= mid) cnt++;
         }
         if (cnt > mid) h = mid - 1;
         else l = mid + 1;
     }
     return l;
}

雙指針解法,類似於有環鏈表中找出環的入口:

public int findDuplicate(int[] nums) {
      int slow = nums[0], fast = nums[nums[0]];
      while (slow != fast) {
          slow = nums[slow];
          fast = nums[nums[fast]];
      }

      fast = 0;
      while (slow != fast) {
          slow = nums[slow];
          fast = nums[fast];
      }
      return slow;
}

有序矩陣

有序矩陣指的是行和列分別有序的矩陣。

一般可以利用有序性使用二分查找方法。

[
   [ 1,  5,  9],
   [10, 11, 13],
   [12, 13, 15]
]

有序矩陣查找

Leetocde : 240. Search a 2D Matrix II (Medium)

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

有序矩陣的 Kth Element

Leetcode : 378. Kth Smallest Element in a Sorted Matrix ((Medium))

matrix = [
  [ 1,  5,  9],
  [10, 11, 13],
  [12, 13, 15]
],
k = 8,

return 13.

解題參考:Share my thoughts and Clean Java Code

二分查找解法:

public int kthSmallest(int[][] matrix, int k) {
    int m = matrix.length, n = matrix[0].length;
    int lo = matrix[0][0], hi = matrix[m - 1][n - 1];
    while(lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        int cnt = 0;
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n && matrix[i][j] <= mid; j++) {
                cnt++;
            }
        }
        if(cnt < k) lo = mid + 1;
        else hi = mid - 1;
    }
    return lo;
}

堆解法:

public int kthSmallest(int[][] matrix, int k) {
    int m = matrix.length, n = matrix[0].length;
    PriorityQueue<Tuple> pq = new PriorityQueue<Tuple>();
    for(int j = 0; j < n; j++) pq.offer(new Tuple(0, j, matrix[0][j]));
    for(int i = 0; i < k - 1; i++) { // 小根堆,去掉 k - 1 個堆頂元素,此時堆頂元素就是第 k 的數
        Tuple t = pq.poll();
        if(t.x == m - 1) continue;
        pq.offer(new Tuple(t.x + 1, t.y, matrix[t.x + 1][t.y]));
    }
    return pq.poll().val;
}

class Tuple implements Comparable<Tuple> {
    int x, y, val;
    public Tuple(int x, int y, int val) {
        this.x = x; this.y = y; this.val = val;
    }

    @Override
    public int compareTo(Tuple that) {
        return this.val - that.val;
    }
}

鏈表

判斷兩個鏈表的交點

Leetcode : 160. Intersection of Two Linked Lists

A:          a1 → a2
                  ↘
                    c1 → c2 → c3
                  ↗
B:    b1 → b2 → b3

要求:時間複雜度爲 O(n) 空間複雜度爲 O(1)

設 A 的長度爲 a + c,B 的長度爲 b + c,其中 c 爲尾部公共部分長度,可知 a + c + b = b + c + a。

當訪問 A 鏈表的指針訪問到鏈表尾部時,令它從鏈表 B 的頭部開始訪問鏈表 B;同樣地,當訪問 B 鏈表的指針訪問到鏈表尾部時,令它從鏈表 A 的頭部開始訪問鏈表 A。這樣就能控制訪問 A 和 B 兩個鏈表的指針能同時訪問到交點。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if(headA == null || headB == null) return null;
    ListNode l1 = headA, l2 = headB;
    while(l1 != l2){
        l1 = (l1 == null) ? headB : l1.next;
        l2 = (l2 == null) ? headA : l2.next;
    }
    return l1;
}

如果只是判斷是否存在交點,那麼就是另一個問題,即 編程之美:3.6 的問題。有兩種解法:把第一個鏈表的結尾連接到第二個鏈表的開頭,看第二個鏈表是否存在環;或者直接比較第一個鏈表最後一個節點和第二個鏈表最後一個節點是否相同。

鏈表反轉

Leetcode : 206. Reverse Linked List

頭插法能夠按逆序構建鏈表。

public ListNode reverseList(ListNode head) {
    ListNode newHead = null; // 設爲 null ,作爲新鏈表的結尾
    while(head != null){
        ListNode nextNode = head.next;
        head.next = newHead;
        newHead = head;
        head = nextNode;
    }
    return newHead;
}

歸併兩個有序的鏈表

Leetcode : 21. Merge Two Sorted Lists

鏈表和樹一樣,可以用遞歸方式來定義:鏈表是空節點,或者有一個值和一個指向下一個鏈表的指針,因此很多鏈表問題可以用遞歸來處理。

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if(l1 == null) return l2;
    if(l2 == null) return l1;
    ListNode newHead = null;
    if(l1.val < l2.val){
        newHead = l1;
        newHead.next = mergeTwoLists(l1.next, l2);
    } else{
        newHead = l2;
        newHead.next = mergeTwoLists(l1, l2.next);
    }
    return newHead;
}

從有序鏈表中刪除重複節點

Leetcode : 83. Remove Duplicates from Sorted List (Easy)

public ListNode deleteDuplicates(ListNode head) {
    if(head == null || head.next == null) return head;
    head.next = deleteDuplicates(head.next);
    return head.next != null && head.val == head.next.val ? head.next : head;
}

迴文鏈表

Leetcode : 234. Palindrome Linked List (Easy)

切成兩半,把後半段反轉,然後比較兩半是否相等。

public boolean isPalindrome(ListNode head) {
    if(head == null || head.next == null) return true;
    ListNode slow = head, fast = head.next;
    while(fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
    }

    if(fast != null){  // 偶數節點,讓 slow 指向下一個節點
        slow = slow.next;
    }

    cut(head, slow); // 切成兩個鏈表
    ListNode l1 = head, l2 = slow;
    l2 = reverse(l2);
    return isEqual(l1, l2);
}

private void cut(ListNode head, ListNode cutNode){
    while( head.next != cutNode ) head = head.next;
    head.next = null;
}

private ListNode reverse(ListNode head){
    ListNode newHead = null;
    while(head != null){
        ListNode nextNode = head.next;
        head.next = newHead;
        newHead = head;
        head = nextNode;
    }
    return newHead;
}

private boolean isEqual(ListNode l1, ListNode l2){
    while(l1 != null && l2 != null){
        if(l1.val != l2.val) return false;
        l1 = l1.next;
        l2 = l2.next;
    }
    return true;
}

從鏈表中刪除節點

編程之美:3.4

B.val = C.val;
B.next = C.next;

鏈表元素按奇偶聚集

Leetcode : 328. Odd Even Linked List (Medium)

public ListNode oddEvenList(ListNode head) {
    if (head == null) {
        return head;
    }
    ListNode odd = head, even = head.next, evenHead = even;
    while (even != null && even.next != null) {
        odd.next = odd.next.next;
        odd = odd.next;
        even.next = even.next.next;
        even = even.next;
    }
    odd.next = evenHead;
    return head;
}

遞歸

一棵樹要麼是空樹,要麼有兩個指針,每個指針指向一棵樹。樹是一種遞歸結構,很多樹的問題可以使用遞歸來處理。

樹的高度

Leetcode : 104. Maximum Depth of Binary Tree (Easy)

public int maxDepth(TreeNode root) {
    if(root == null) return 0;
    return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}

翻轉樹

Leetcode : 226. Invert Binary Tree (Easy)

public TreeNode invertTree(TreeNode root) {
    if(root == null) return null;
    TreeNode left = root.left; // 後面的操作會改變 left 指針,因此先保存下來
    root.left = invertTree(root.right);
    root.right = invertTree(left);
    return root;
}

歸併兩棵樹

Leetcode : 617. Merge Two Binary Trees (Easy)

public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
    if(t1 == null && t2 == null) return null;
    if(t1 == null) return t2;
    if(t2 == null) return t1;
    TreeNode root = new TreeNode(t1.val + t2.val);
    root.left = mergeTrees(t1.left, t2.left);
    root.right = mergeTrees(t1.right, t2.right);
    return root;
}

判斷路徑和是否等於一個數

Leetcdoe : 112. Path Sum (Easy)

題目描述:路徑和定義爲從 root 到 leaf 的所有節點的和

public boolean hasPathSum(TreeNode root, int sum) {
    if(root == null) return false;
    if(root.left == null && root.right == null && root.val == sum) return true;
    return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}

統計路徑和等於一個數的路徑數量

Leetcode : 437. Path Sum III (Easy)

題目描述:路徑不一定以 root 開頭並以 leaf 結尾,但是必須連續

pathSumStartWithRoot() 方法統計以某個節點開頭的路徑個數。

public int pathSum(TreeNode root, int sum) {
    if(root == null) return 0;
    int ret = pathSumStartWithRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
    return ret;
}

private int pathSumStartWithRoot(TreeNode root, int sum){
    if(root == null) return 0;
    int ret = 0;
    if(root.val == sum) ret++;
    ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val);
    return ret;
}

樹的對稱

Leetcode : 101. Symmetric Tree (Easy)

public boolean isSymmetric(TreeNode root) {
    if(root == null) return true;
    return isSymmetric(root.left, root.right);
}

private boolean isSymmetric(TreeNode t1, TreeNode t2){
    if(t1 == null && t2 == null) return true;
    if(t1 == null || t2 == null) return false;
    if(t1.val != t2.val) return false;
    return isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left);
}

平衡樹

Leetcode : 110. Balanced Binary Tree (Easy)

題目描述:左右子樹高度差是否都小於等於 1

private boolean result = true;

public boolean isBalanced(TreeNode root) {
    maxDepth(root);
    return result;
}

public int maxDepth(TreeNode root) {
    if (root == null) return 0;
    int l = maxDepth(root.left);
    int r = maxDepth(root.right);
    if (Math.abs(l - r) > 1) result = false;
    return 1 + Math.max(l, r);
}

最小路徑

Leetcode : 111. Minimum Depth of Binary Tree (Easy)

題目描述:樹的根節點到葉子節點的最小長度

public int minDepth(TreeNode root) {
    if(root == null) return 0;
    int left = minDepth(root.left);
    int right = minDepth(root.right);
    if(left == 0 || right == 0) return left + right + 1;
    return Math.min(left, right) + 1;
}

統計左葉子節點的和

Leetcode : 404. Sum of Left Leaves (Easy)

public int sumOfLeftLeaves(TreeNode root) {
    if(root == null) return 0;
    if(isLeaf(root.left)) return root.left.val +  sumOfLeftLeaves(root.right);
    return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
}

private boolean isLeaf(TreeNode node){
    if(node == null) return false;
    return node.left == null && node.right == null;
}

修剪一棵樹

Leetcode : 669. Trim a Binary Search Tree (Easy)

題目描述:只保留值在 L ~ R 之間的節點

public TreeNode trimBST(TreeNode root, int L, int R) {
    if(root == null) return null;
    if(root.val > R) return trimBST(root.left, L, R);
    if(root.val < L) return trimBST(root.right, L, R);
    root.left = trimBST(root.left, L, R);
    root.right = trimBST(root.right, L, R);
    return root;
}

子樹

Leetcode : 572. Subtree of Another Tree (Easy)

public boolean isSubtree(TreeNode s, TreeNode t) {
    if(s == null && t == null) return true;
    if(s == null || t == null) return false;
    if(s.val == t.val && isSame(s, t)) return true;
    return isSubtree(s.left, t) || isSubtree(s.right, t);
}

private boolean isSame(TreeNode s, TreeNode t){
    if(s == null && t == null) return true;
    if(s == null || t == null) return false;
    if(s.val != t.val) return false;
    return isSame(s.left, t.left) && isSame(s.right, t.right);
}

從有序數組中構造二叉查找樹

Leetcode : 108. Convert Sorted Array to Binary Search Tree (Easy)

二叉查找樹(BST):根節點大於等於左子樹所有節點,小於等於右子樹所有節點。

public TreeNode sortedArrayToBST(int[] nums) {
    return toBST(nums, 0, nums.length - 1);
}

private TreeNode toBST(int[] nums, int sIdx, int eIdx){
    if(sIdx > eIdx) return null;
    int mIdx = (sIdx + eIdx) / 2;
    TreeNode root = new TreeNode(nums[mIdx]);
    root.left =  toBST(nums, sIdx, mIdx - 1);
    root.right = toBST(nums, mIdx + 1, eIdx);
    return root;
}

兩節點的最長路徑

          1
        / \
        2  3
      / \
      4  5

Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3].
private int max = 0;

public int diameterOfBinaryTree(TreeNode root) {
    depth(root);
    return max;
}

private int depth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int leftDepth = depth(root.left);
    int rightDepth = depth(root.right);
    max = Math.max(max, leftDepth + rightDepth);
    return Math.max(leftDepth, rightDepth) + 1;
}

找出二叉樹中第二小的節點

Leetcode : 671. Second Minimum Node In a Binary Tree (Easy)

Input:
    2
  / \
  2  5
    / \
    5  7

Output: 5

一個節點要麼具有 0 個或 2 個子節點,如果有子節點,那麼根節點是最小的節點。

public int findSecondMinimumValue(TreeNode root) {
    if(root == null) return -1;
    if(root.left == null && root.right == null) return -1;
    int leftVal = root.left.val;
    int rightVal = root.right.val;
    if(leftVal == root.val) leftVal = findSecondMinimumValue(root.left);
    if(rightVal == root.val) rightVal = findSecondMinimumValue(root.right);
    if(leftVal != -1 && rightVal != -1) return Math.min(leftVal, rightVal);
    if(leftVal != -1) return leftVal;
    return rightVal;
}

尋找兩個節點的最近公共祖先

Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree (Easy)

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if(root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
    if(root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
    return root;
}

最近公共祖先

Leetcode : 236. Lowest Common Ancestor of a Binary Tree (Medium)

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    return left == null ? right : right == null ? left : root;
}

最大相同節點值的路徑長度

Leetcode : 687. Longest Univalue Path (Easy)

              1
            / \
            4  5
          / \  \
          4  4  5

Output : 2
private int path = 0;
public int longestUnivaluePath(TreeNode root) {
    dfs(root);
    return path;
}

private int dfs(TreeNode root){
    if(root == null) return 0;
    int left = dfs(root.left);
    int right = dfs(root.right);
    int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0;
    int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0;
    path = Math.max(path, leftPath + rightPath);
    return Math.max(leftPath, rightPath);
}

間隔遍歷

Leetcode : 337. House Robber III (Medium)

public int rob(TreeNode root) {
    if (root == null) return 0;
    int val1 = root.val;
    if (root.left != null) {
        val1 += rob(root.left.left) + rob(root.left.right);
    }
    if (root.right != null) {
        val1 += rob(root.right.left) + rob(root.right.right);
    }
    int val2 = rob(root.left) + rob(root.right);
    return Math.max(val1, val2);
}

層次遍歷

使用 BFS,不需要使用兩個隊列來分別存儲當前層的節點和下一層的節點, 因爲在開始遍歷一層的節點時,當前隊列中的節點數就是當前層的節點數,只要控制遍歷這麼多節點數,就能保證這次遍歷的都是當前層的節點。

計算一棵樹每層節點的平均數

637. Average of Levels in Binary Tree (Easy)

public List<Double> averageOfLevels(TreeNode root) {
    List<Double> ret = new ArrayList<>();
    if(root == null) return ret;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while(!queue.isEmpty()){
        int cnt = queue.size();
        double sum = 0;
        for(int i = 0; i < cnt; i++){
            TreeNode node = queue.poll();
            sum += node.val;
            if(node.left != null) queue.add(node.left);
            if(node.right != null) queue.add(node.right);
        }
        ret.add(sum / cnt);
    }
    return ret;
}

得到左下角的節點

Leetcode : 513. Find Bottom Left Tree Value (Easy)

public int findBottomLeftValue(TreeNode root) {
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while(!queue.isEmpty()){
        root = queue.poll();
        if(root.right != null) queue.add(root.right);
        if(root.left != null) queue.add(root.left);
    }
    return root.val;
}

前中後序遍歷

   1
  / \
  2  3
 / \  \
4  5  6

層次遍歷順序:[1 2 3 4 5 6]前序遍歷順序:[1 2 4 5 3 6]中序遍歷順序:[4 2 5 1 3 6]後序遍歷順序:[4 5 2 6 3 1]

層次遍歷使用 BFS 實現,利用的就是 BFS 一層一層遍歷的特性;而前序、中序、後序遍歷利用了 DFS 實現。

前序、中序、後序遍只是在對節點訪問的順序有一點不同,其它都相同。

① 前序

void dfs(TreeNode root){
    visit(root);
    dfs(root.left);
    dfs(root.right);
}

② 中序

void dfs(TreeNode root){
    dfs(root.left);
    visit(root);
    dfs(root.right);
}

③ 後序

void dfs(TreeNode root){
    dfs(root.left);
    dfs(root.right);
    visit(root);
}

非遞歸實現二叉樹的前序遍歷

Leetcode : 144. Binary Tree Preorder Traversal (Medium)

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    if (root == null) return ret;
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        ret.add(node.val);
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left); // 先添加右子樹再添加左子樹,這樣是爲了讓左子樹在棧頂
    }
    return ret;
}

非遞歸實現二叉樹的後續遍歷

Leetcode : ### 145. Binary Tree Postorder Traversal (Medium)

前序遍歷爲 root -> left -> right,後序遍歷爲 left -> right -> root,可以修改前序遍歷成爲 root -> right -> left,那麼這個順序就和後序遍歷正好相反。

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    if (root == null) return ret;
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        ret.add(node.val);
        if (node.left != null) stack.push(node.left);
        if (node.right != null) stack.push(node.right);
    }
    Collections.reverse(ret);
    return ret;
}

非遞歸實現二叉樹的中序遍歷

Leetcode : 94. Binary Tree Inorder Traversal (Medium)

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(cur != null || !stack.isEmpty()) {
        while(cur != null) { // 模擬遞歸棧的不斷深入
            stack.add(cur);
            cur = cur.left;
        }
        TreeNode node = stack.pop();
        ret.add(node.val);
        cur = node.right;
    }
    return ret;
}

使用中序遍歷和前序遍歷序列重建二叉樹 //TODO

BST

主要利用 BST 中序遍歷有序的特點。

在 BST 中尋找兩個節點,使它們的和爲一個給定值。

653. Two Sum IV - Input is a BST

使用中序遍歷得到有序數組之後,再利用雙指針對數組進行查找。

應該注意到,這一題不能用分別在左右子樹兩部分來處理這種思想,因爲兩個待求的節點可能分別在左右子樹中。

public boolean findTarget(TreeNode root, int k) {
    List<Integer> nums = new ArrayList<>();
    inOrder(root, nums);
    int i = 0, j = nums.size() - 1;
    while(i < j){
        int sum = nums.get(i) + nums.get(j);
        if(sum == k) return true;
        if(sum < k) i++;
        else j--;
    }
    return false;
}

private void inOrder(TreeNode root, List<Integer> nums){
    if(root == null) return;
    inOrder(root.left, nums);
    nums.add(root.val);
    inOrder(root.right, nums);
}

在 BST 中查找最小的兩個節點之差的絕對值

Leetcode : 530. Minimum Absolute Difference in BST (Easy)

利用 BST 的中序遍歷爲有序的性質,計算中序遍歷中臨近的兩個節點之差的絕對值,取最小值。

private int minDiff = Integer.MAX_VALUE;
private int preVal = -1;

public int getMinimumDifference(TreeNode root) {
    inorder(root);
    return minDiff;
}

private void inorder(TreeNode node){
    if(node == null) return;
    inorder(node.left);
    if(preVal != -1) minDiff = Math.min(minDiff, Math.abs(node.val - preVal));
    preVal = node.val;
    inorder(node.right);
}

把 BST 每個節點的值都加上比它大的節點的值

Leetcode : Convert BST to Greater Tree (Easy)

先遍歷右子樹。

private int sum = 0;

public TreeNode convertBST(TreeNode root) {
    traver(root);
    return root;
}

private void traver(TreeNode root) {
    if (root == null) {
        return;
    }
    if (root.right != null) {
        traver(root.right);
    }
    sum += root.val;
    root.val = sum;
    if (root.left != null) {
        traver(root.left);
    }
}

尋找 BST 中出現次數最多的節點

private int cnt = 1;
private int maxCnt = 1;
private TreeNode preNode = null;
private List<Integer> list;

public int[] findMode(TreeNode root) {
    list = new ArrayList<>();
    inorder(root);
    int[] ret = new int[list.size()];
    int idx = 0;
    for(int num : list){
        ret[idx++] = num;
    }
    return ret;
}

private void inorder(TreeNode node){
    if(node == null) return;
    inorder(node.left);
    if(preNode != null){
        if(preNode.val == node.val) cnt++;
        else cnt = 1;
    }
    if(cnt > maxCnt){
        maxCnt = cnt;
        list.clear();
        list.add(node.val);
    } else if(cnt == maxCnt){
        list.add(node.val);
    }
    preNode = node;
    inorder(node.right);
}

尋找 BST 的第 k 個元素

Leetcode : 230. Kth Smallest Element in a BST (Medium)

遞歸解法:

public int kthSmallest(TreeNode root, int k) {
    int leftCnt = count(root.left);
    if(leftCnt == k - 1) return root.val;
    if(leftCnt > k - 1) return kthSmallest(root.left, k);
    return kthSmallest(root.right, k - leftCnt - 1);
}

private int count(TreeNode node) {
    if(node == null) return 0;
    return 1 + count(node.left) + count(node.right);
}

中序遍歷解法:

private int cnt = 0;
private int val;

public int kthSmallest(TreeNode root, int k) {
    inorder(root, k);
    return val;
}

private void inorder(TreeNode node, int k) {
    if(node == null) return;
    inorder(node.left, k);
    cnt++;
    if(cnt == k) {
        val = node.val;
        return;
    }
    inorder(node.right, k);
}

Trie

Trie,又稱前綴樹或字典樹,用於判斷字符串是否存在或者是否具有某種字符串前綴。

實現一個 Trie

Leetcode : 208. Implement Trie (Prefix Tree) (Medium)

class Trie {
    
    private class Node{
        Node[] childs = new Node[26];
        boolean isLeaf;
    }
    
    private Node root = new Node();
    
    /** Initialize your data structure here. */
    public Trie() {
    }
    
    /** Inserts a word into the trie. */
    public void insert(String word) {
        int idx = word.charAt(0) - 'a';
        insert(word, root);
    }
    
    private void insert(String word, Node node){
        int idx = word.charAt(0) - 'a';
        if(node.childs[idx] == null){
            node.childs[idx] = new Node();
        }
        if(word.length() == 1) node.childs[idx].isLeaf = true;
        else insert(word.substring(1), node.childs[idx]);
    }
    
    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        return search(word, root); 
    }
    
    private boolean search(String word, Node node){
        if(node == null) return false;
        int idx = word.charAt(0) - 'a';
        if(node.childs[idx] == null) return false;
        if(word.length() == 1) return node.childs[idx].isLeaf;
        return search(word.substring(1), node.childs[idx]);
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        return startWith(prefix, root);
    }
    
    private boolean startWith(String prefix, Node node){
        if(node == null) return false;
        if(prefix.length() == 0) return true;
        int idx = prefix.charAt(0) - 'a';
        return startWith(prefix.substring(1), node.childs[idx]);
    }
}

實現一個 Trie,用來求前綴和

Leetcode : 677. Map Sum Pairs (Medium)

class MapSum {
    private class Trie {
        int val;
        Map<Character, Trie> childs;
        boolean isWord;
        
        Trie() {
            childs = new HashMap<>();
        }
    }
    
    private Trie root;

    public MapSum() {
        root = new Trie();
    }
    
    public void insert(String key, int val) {
        Trie cur = root;
        for(char c : key.toCharArray()) {
            if(!cur.childs.containsKey(c)) {
                Trie next = new Trie();
                cur.childs.put(c, next);
            }
            cur = cur.childs.get(c);
        }
        cur.val = val;
        cur.isWord = true;
    }
    
    public int sum(String prefix) {
        Trie cur = root;
        for(char c : prefix.toCharArray()) {
            if(!cur.childs.containsKey(c)) return 0;
            cur = cur.childs.get(c);
        }
        return dfs(cur);
    }
    
    private int dfs(Trie cur) {
        int sum = 0;
        if(cur.isWord) {
            sum += cur.val;
        }
        for(Trie next : cur.childs.values()) {
            sum += dfs(next);
        }
        return sum;
    }
}

位運算

1. 基本原理

0s 表示 一串 0 ,1s 表示一串 1。

x ^ 0s = x      x & 0s = 0      x | 0s = x
x ^ 1s = \~x     x & 1s = x      x | 1s = 1s
x ^ x = 0       x & x = x       x | x = x

① 利用 x ^ 1s = ~x 的特點,可以將位級表示翻轉;利用 x ^ x = 0 的特點,可以將三個數中重複的兩個數去除,只留下另一個數;② 利用 x & 0s = 0 和 x & 1s = x 的特點,可以實現掩碼操作。一個數 num 與 mask :00111100 進行位與操作,只保留 num 中與 mask 的 1 部分相對應的位;③ 利用 x | 0s = x 和 x | 1s = 1s 的特點,可以實現設置操作。一個數 num 與 mask:00111100 進行位或操作,將 num 中與 mask 的 1 部分相對應的位都設置爲 1 。

>> n 爲算術右移,相當於除以 2n;>>> n 爲無符號右移,左邊會補上 0。<< n 爲算術左移,相當於乘以 2n

n&(n-1) 該位運算是去除 n 的位級表示中最低的那一位。例如對於二進制表示 10110100,減去 1 得到 10110011,這兩個數相與得到 10110000

n-n&(~n+1) 概運算是去除 n 的位級表示中最高的那一位。

n&(-n) 該運算得到 n 的位級表示中最低的那一位。-n 得到 n 的反碼加 1,對於二進制表示 10110100,-n 得到 01001100,相與得到 00000100

2. mask 計算

要獲取 111111111,將 0 取反即可,~0。

要得到只有第 i 位爲 1 的 mask,將 1 向左移動 i 位即可,1<<i 。例如 1<<5 得到只有第 5 位爲 1 的 mask :00010000。

要得到 1 到 i 位爲 1 的 mask,1<<(i+1)-1 即可,例如將 1<<(4+1)-1 = 00010000-1 = 00001111。

要得到 1 到 i 位爲 0 的 mask,只需將 1 到 i 位爲 1 的 mask 取反,即 ~(1<<(i+1)-1)。

3. 位操作舉例

① 獲取第 i 位

num & 00010000 != 0

(num & (1 << i)) != 0;

② 將第 i 位設置爲 1

num | 00010000

num | (1 << i);

③ 將第 i 位清除爲 0

num & 11101111

num & (\~(1 << i))

④ 將最高位到第 i 位清除爲 0

num & 00001111

num & ((1 << i) - 1);

⑤ 將第 0 位到第 i 位清除爲 0

num & 11110000

num & (\~((1 << (i+1)) - 1));

⑥ 將第 i 位設置爲 0 或者 1

先將第 i 位清零,然後將 v 左移 i 位,執行“位或”運算。

(num & (1 << i)) | (v << i);

4. Java 中的位操作

static int Integer.bitCount()            // 統計 1 的數量
static int Integer.highestOneBit()       // 獲得最高位
static String toBinaryString(int i)      // 轉換位二進制表示的字符串

統計兩個數的二進制表示有多少位不同

Leetcode : 461. Hamming Distance (Easy)

對兩個數進行異或操作,不同的那一位結果爲 1 ,統計有多少個 1 即可。

public int hammingDistance(int x, int y) {
    int z = x ^ y;
    int cnt = 0;
    while(z != 0){
        if((z & 1) == 1) cnt++;
        z = z >> 1;
    }
    return cnt;
}

可以使用 Integer.bitcount() 來統計 1 個的個數。

public int hammingDistance(int x, int y) {
    return Integer.bitCount(x ^ y);
}

翻轉一個數的比特位

Leetcode : 190. Reverse Bits (Easy)

public int reverseBits(int n) {
    int ret = 0;
    for(int i = 0; i < 32; i++){
        ret <<= 1;
        ret |= (n & 1);
        n >>>= 1;
    }
    return ret;
}

不用額外變量交換兩個整數

程序員代碼面試指南 :P317

a = a ^ b;
b = a ^ b;
a = a ^ b;

將 c = a ^ b,那麼 b ^ c = b ^ b ^ a = a,a ^ c = a ^ a ^ b = b。

判斷一個數是不是 4 的 n 次方

Leetcode : 342. Power of Four (Easy)

該數二進制表示有且只有一個奇數位爲 1 ,其餘的都爲 0 ,例如 16 : 10000。可以每次把 1 向左移動 2 位,就能構造出這種數字,然後比較構造出來的數與要判斷的數是否相同。

public boolean isPowerOfFour(int num) {
    int i = 1;
    while(i > 0){
        if(i == num) return true;
        i = i << 2;
    }
    return false;
}

也可以用 Java 的 Integer.toString() 方法將該數轉換爲 4 進制形式的字符串,然後判斷字符串是否以 1 開頭。

public boolean isPowerOfFour(int num) {
    return Integer.toString(num, 4).matches("10*");
}

判斷一個數是不是 2 的 n 次方

Leetcode : 231. Power of Two (Easy)

同樣可以用 Power of Four 的方法,但是 2 的 n 次方更特殊,它的二進制表示只有一個 1 存在。

public boolean isPowerOfTwo(int n) {
    return n > 0 && Integer.bitCount(n) == 1;
}

利用 1000 & 0111 == 0 這種性質,得到以下解法:

public boolean isPowerOfTwo(int n) {
    return n > 0 && (n & (n - 1)) == 0;
}

數組中唯一一個不重複的元素

Leetcode : 136. Single Number (Easy)

兩個相同的數異或的結果爲 0,對所有數進行異或操作,最後的結果就是單獨出現的那個數。

類似的有:Leetcode : 389. Find the Difference (Easy),兩個字符串僅有一個字符不相同,使用異或操作可以以 O(1) 的空間複雜度來求解,而不需要使用 HashSet。

public int singleNumber(int[] nums) {
    int ret = 0;
    for(int n : nums) ret = ret ^ n;
    return ret;
}

數組中不重複的兩個元素

Leetcode : 260. Single Number III (Medium)

兩個不相等的元素在位級表示上必定會有一位存在不同。

將數組的所有元素異或得到的結果爲不存在重複的兩個元素異或的結果。

diff &= -diff 得到出 diff 最右側不爲 0 的位,也就是不存在重複的兩個元素在位級表示上最右側不同的那一位,利用這一位就可以將兩個元素區分開來。

public int[] singleNumber(int[] nums) {
    int diff = 0;
    for(int num : nums) diff ^= num;
    // 得到最右一位
    diff &= -diff;
    int[] ret = new int[2];
    for(int num : nums) {
        if((num & diff) == 0) ret[0] ^= num;
        else ret[1] ^= num;
    }
    return ret;
}

判斷一個數的位級表示是否不會出現連續的 0 和 1

Leetcode : 693. Binary Number with Alternating Bits (Easy)

對於 10101 這種位級表示的數,把它向右移動 1 位得到 1010 ,這兩個數每個位都不同,因此異或得到的結果爲 11111。

public boolean hasAlternatingBits(int n) {
    int a = (n ^ (n >> 1));
    return (a & (a + 1)) == 0;
}

求一個數的補碼

Leetcode : 476. Number Complement (Easy)

不考慮二進制表示中的首 0 部分

對於 00000101,要求補碼可以將它與 00000111 進行異或操作。那麼問題就轉換爲求掩碼 00000111。

public int findComplement(int num) {
    if(num == 0) return 1;
    int mask = 1 << 30;
    while((num & mask) == 0) mask >>= 1;
    mask = (mask << 1) - 1;
    return num ^ mask;
}

可以利用 Java 的 Integer.highestOneBit() 方法來獲得含有首 1 的數。

public int findComplement(int num) {
    if(num == 0) return 1;
    int mask = Integer.highestOneBit(num);
    mask = (mask << 1) - 1;
    return num ^ mask;
}

對於 10000000 這樣的數要擴展成 11111111,可以利用以下方法:

mask |= mask >> 1    11000000
mask |= mask >> 2    11110000
mask |= mask >> 4    11111111
public int findComplement(int num) {
    int mask = num;
    mask |= mask >> 1;
    mask |= mask >> 2;
    mask |= mask >> 4;
    mask |= mask >> 8;
    mask |= mask >> 16;
    return (mask ^ num);
}

實現整數的加法

Leetcode : 371. Sum of Two Integers (Easy)

a ^ b 表示沒有考慮進位的情況下兩數的和,(a & b) << 1 就是進位。遞歸會終止的原因是 (a & b) << 1 最右邊會多一個 0,那麼繼續遞歸,進位最右邊的 0 會慢慢增多,最後進位會變爲 0,遞歸終止。

public int getSum(int a, int b) {
    return b == 0 ? a : getSum((a ^ b), (a & b) << 1);
}

字符串數組最大乘積

Leetcode : 318. Maximum Product of Word Lengths (Medium)

題目描述:字符串數組的字符串只含有小寫字符。求解字符串數組中兩個字符串長度的最大乘積,要求這兩個字符串不能含有相同字符。

解題思路:本題主要問題是判斷兩個字符串是否含相同字符,由於字符串只含有小寫字符,總共 26 位,因此可以用一個 32 位的整數來存儲每個字符是否出現過。

public int maxProduct(String[] words) {
    int n = words.length;
    if (n == 0) return 0;
    int[] val = new int[n];
    for (int i = 0; i < n; i++) {
        for (char c : words[i].toCharArray()) {
            val[i] |= 1 << (c - 'a');
        }
    }
    int ret = 0;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            if ((val[i] & val[j]) == 0) {
                ret = Math.max(ret, words[i].length() * words[j].length());
            }
        }
    }
    return ret;
}

參考資料

  • Leetcode
  • 數據結構與算法分析
  • 算法
  • 劍指 Offer
  • 編程之美
  • 程序員代碼面試指南
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章