-
-
數據結構相關
算法思想
二分查找
public int search(int key, int[] array) {
int l = 0, h = array.length - 1;
while (l <= h) {
int mid = l + (h - l) / 2;
if (key == array[mid]) return mid;
if (key < array[mid]) h = mid - 1;
else l = mid + 1;
}
return -1;
}
實現時需要注意以下細節:
-
在計算 mid 時不能使用 mid = (l + h) / 2 這種方式,因為 l + h 可能會導致加法溢出,應該使用 mid = l + (h - l) / 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,此時循環退出,直接把查找的數跳過了。
-
l 的賦值一般都為 l = mid + 1。
求開方
Input: 4
Output: 2
Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.
一個數 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)
Input: [1,1,2,3,3,4,4,8,8]
Output: 2
題目描述:一個有序數組只有一個數不出現兩次,找出這個數。
組序號 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
元素 | 1, 1 | 2,3 | 3,4 | 4,8 | 8 |
取中間組坐標:mid = ( left + right ) / 2 若 mid組中兩元素相同 則唯一出現一次的元素必在 mid+1 組 到 right 組中 (因為mid組前面有偶數個元素 且又只有唯一一個單一元素) 若 mid組中兩元素不相同,則唯一出現一次的元素必在left組到mid組中
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)
Input: [1,2], [1,2,3] Output: 2 Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. You have 3 cookies and their sizes are big enough to gratify all of the children, You need to output 2.
題目描述:每個孩子都有一個滿足度,每個餅干都有一個大小,只有餅干的大小大於一個孩子的滿足度,該孩子才會獲得滿足。求解最多可以獲得滿足的孩子數量。
因為最小的孩子最容易得到滿足,因此先滿足最小孩子。給一個孩子的餅干應當盡量小又能滿足該孩子,這樣大餅干就能拿來給滿足度比較大的孩子。
證明:假設在某次選擇中,貪心策略選擇給第 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
題目描述:氣球在一個水平數軸上擺放,可以重疊,飛鏢垂直射向坐標軸,使得路徑上的氣球都會刺破。求解最小的投飛鏢次數使所有氣球都被刺破。
從左往右投飛鏢,並且在每次投飛鏢時滿足以下條件:
-
左邊已經沒有氣球了;
-
本次投飛鏢能夠刺破最多的氣球。
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; }
public int findMinArrowShots(int[][] points) { if(points == null || points.length < 1) return 0; //根據x軸來進行排序 Arrays.sort(points, (a, b)->(a[0]-b[0])); //射出去的箭,至少為會命中1個氣球 int result = 1; //重合點,假設(0,1為重合點) int end = points[0][1]; //遍歷氣球有多少行 for(int i = 1; i < points.length; i ++) { //結束的位置大於重合點,表示不能為最大射出時間 if(points[i][0] > end) { result ++; end = points[i][1];//將結束的位置賦給飛鏢 } else { end = Math.min(end, points[i][1]);//將位置最小的值給飛鏢 } } return result; }
股票的最大收益
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)
Input: [4,2,3] Output: True Explanation: You could modify the first 4 to 1 to get a non-decreasing array.
題目描述:判斷一個數組能不能只修改一個數就成為非遞減數組。
在出現 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)
Input: numbers={2, 7, 11, 15}, target=9 Output: index1=1, index2=2
使用雙指針,一個指針指向元素較小的值,一個指針指向元素較大的值。指向較小元素的指針從頭向尾遍歷,指向較大元素的指針從尾向頭遍歷。
如果兩個指針指向元素的和 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)
Given s = "leetcode", return "leotcede".
使用雙指針,指向待反轉的兩個元音字符,一個指針從頭向尾遍歷,一個指針從尾到頭遍歷。
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)
Input: 5 Output: True Explanation: 1 * 1 + 2 * 2 = 5
題目描述:判斷一個數是否為兩個數的平方和,例如 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)
Input: "abca" Output: True Explanation: You could delete the character 'c'.
題目描述:字符串可以刪除一個字符,判斷是否能構成回文字符串。
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; } -------------------------------------- if(head ==null) return false; ListNode fast = head; ListNode slow =head; while(fast !=null && slow !=null ){ if(fast.next == null) break; fast = fast.next.next; slow=slow.next; if(fast ==slow){ return true; } } 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)
Given [1,1,1,2,2,3] and k = 2, return [1,2]. public List<Integer> topKFrequent(int[] nums, int k) { List<Integer> ret = new ArrayList<>(); //key為數字 value數字出現的次數 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)。
public List<List<String>> solveNQueens(int n) { List<List<String>> res = new ArrayList<List<String>>(); int[] queenList = new int[n]; //第i個位置存放的數表示row行時,Q的列,表示i行是否為Q placeQueen(queenList, 0, n, res);//在第0行放Q return res; } private void placeQueen(int[] queenList, int row, int n, List<List<String>> res) { //如果已經填滿,就生成結果 if (row == n) { ArrayList<String> list = new ArrayList<String>(); for (int i = 0; i < n; i++) { String str = ""; for (int col = 0; col < n; col++){ if(queenList[i] == col) { str += "Q"; } else { str += "."; } } list.add(str); } res.add(list); } for (int col = 0; col < n; col++) {//循環每一列 if (isValid(queenList, row, col)) { //如果在該列放入Q不沖突的話 queenList[row] = col;//放皇后 placeQueen(queenList, row + 1, n, res); //進行進行下一行校驗 } } } private boolean isValid(int[] queenList, int row, int col) { for (int i = 0; i < row; i++) { int pos = queenList[i]; if (pos == col) { //和新加入的Q處於同一列 return false; } if (pos + row - i == col) { //在新加入的Q的右對角線上 return false; } if (pos - row + i == col) { //在新加入的Q的左對角線上 return false; } } return true; }
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; }
class Solution { public: void solveSudoku(vector<vector<char>>& board) { solve(board); } private: bool solve(vector<vector<char>>& board){ for(int i = 0; i < board.size(); ++i) for(int j = 0; j < board[0].size(); ++j){ if(board[i][j] == '.'){ for(int ch = '1'; ch <= '9'; ++ch) if(isValid(board, i, j, ch)){ board[i][j] = ch; if(solve(board)) return true; else board[i][j] = '.'; } return false; } } return true; } bool isValid(vector<vector<char>>& board, int row, int col, char ch) { for(int i = 0; i < board.size(); ++i){ int x = row / 3 * 3 + i / 3, y = col / 3 * 3 + i % 3; if(board[i][col] == ch || board[row][i] == ch || board[x][y] == ch) return false; } return true; } };
分治
給表達式加括號
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.
//從1開始,value為nums[i]最多的個數,比如:4=1+1+1+1,最多4個 int[] nums = new int[n+1]; for(int i=1; i<=n; i++) nums[i] = i; //從2開始DP for(int i=2; i<=n; i++) { for(int j=1; j*j<=i; j++) { //nums[i]= i上的值和1+(i-j*j)的值 的最小值 nums[i] = Math.min(nums[i], nums[i-j*j]+1); } } //最終返回nums[n] return nums[n]; ------------------------------------------------------------ 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; }
母牛生產
題目描述:假設農場中成熟的母牛每年都會生 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[i]=(i-1) X dp[i-2]+(i-1) X 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 用來存儲最長公共子序列的長度,其中 dpi 表示 S1 的前 i 個字符與 S2 的前 j 個字符最長公共子序列的長度。考慮 S1i 與 S2j 值是否相等,分為兩種情況:
① 當 S1i==S2j 時,那么就能在 S1 的前 i-1 個字符與 S2 的前 j-1 個字符最長公共子序列的基礎上再加上 S1i 這個值,最長公共子序列長度加 1 ,即 dpi = dpi-1 + 1。
② 當 S1i != S2j 時,此時最長公共子序列為 S1 的前 i-1 個字符和 S2 的前 j 個字符最長公共子序列,與 S1 的前 i 個字符和 S2 的前 j-1 個字符最長公共子序列,它們的最大者,即 dpi = max{ dpi-1, dpi }。
綜上,最長公共子系列的狀態轉移方程為:
對於長度為 N 的序列 S1 和 長度為 M 的序列 S2,dpN 就是序列 S1 和序列 S2 的最長公共子序列長度。
與最長遞增子序列相比,最長公共子序列有以下不同點:
① 針對的是兩個序列,求它們的最長公共子序列。 ② 在最長遞增子序列中,dp[i] 表示以 Si 為結尾的最長遞增子序列長度,子序列必須包含 Si ;在最長公共子序列中,dpi 表示 S1 中前 i 個字符與 S2 中前 j 個字符的最長公共子序列長度,不一定包含 S1i 和 S2j 。 ③ 由於 2 ,在求最終解時,最長公共子序列中 dpN 就是最終解,而最長遞增子序列中 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 存儲最大價值,其中 dpi 表示體積不超過 j 的情況下,前 i 件物品能達到的最大價值。設第 i 件物品體積為 w,價值為 v,根據第 i 件物品是否添加到背包中,可以分兩種情況討論:
① 第 i 件物品沒添加到背包,總體積不超過 j 的前 i 件物品的最大價值就是總體積不超過 j 的前 i-1 件物品的最大價值,dpi = dpi-1。 ② 第 i 件物品添加到背包中,dpi = dpi-1 + v。
第 i 件物品可添加也可以不添加,取決於哪種情況下最大價值更大。
有N件物品和一個容量為V的背包。第i件物品的價格(即體積,下同)是w[i],價值是c[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。 這是最基礎的背包問題,總的來說就是:選還是不選,這是個問題<( ̄ˇ ̄)/ 相當於用f[i][j]表示前i個背包裝入容量為v的背包中所可以獲得的最大價值。 對於一個物品,只有兩種情況 情況一: 第i件不放進去,這時所得價值為:f[i-1][v] 情況二: 第i件放進去,這時所得價值為:f[i-1][v-c[i]]+w[i] 狀態轉移方程為:f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+c[i]) if(可以放進去) f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+c[i]) else(不可以放進去) f[i][v]=f[i-1][v];
綜上,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] 既可以表示 dpi-1 也可以表示 dpi。此時,
因為 dp[j-w] 表示 dpi-1,因此不能先求 dpi 防止將 dpi-1 覆蓋。也就是說要先計算 dpi 再計算 dpi,在程序實現時需要按倒序來循環求解。
無法使用貪心算法的解釋
0-1 背包問題無法使用貪心算法來求解,也就是說不能按照先添加性價比最高的物品來達到最優,這是因為這種方式可能造成背包空間的浪費,從而無法達到最優。考慮下面的物品和一個容量為 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的價值為 16,浪費了大小為 2 的空間。最優的方式是存放物品 1 和物品 2,價值為 22.
id | w | v | v/w |
---|---|---|---|
0 | 1 | 6 | 6 |
1 | 2 | 10 | 5 |
2 | 3 | 12 | 4 |
變種
完全背包:物品可以無限個,可以轉換為 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 len = s.length(); //len+1 //dp[i]表示前i個字符能不能被dict完美划分 boolean[] dp = new boolean[len + 1]; dp[0] = true; for (int i = 1; i <= len; i++) for (int j = 0; j < i; j++) { // 注意substring是前閉后開 String tmp = s.substring(j, i); //能否組合出f[i]表示的子串,k表示組合中前半段的 if (dp[j] && wordDict.contains(tmp)) { dp[i] = true; break; } } return dp[len]; }
改變一組數的正負號使得它們的和為一給定數
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; }
空間復雜度可以優化成 O(1) 空間復雜度
public int maxSubArray(int[] nums) { int max = nums[0]; int oldsum = nums[0]; for (int i = 1; i < nums.length; i++) { oldsum = (oldsum > 0 ? oldsum: 0) + nums[i]; max = Math.max(max, oldsum); } 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[] minStep = new int[n+1]; for (int i = 2; i <= n; i++) { minStep[i] = i; } for(int i=4;i<=n;i++) { for(int j=2;j<i;j++) { if(i % j==0) minStep[i] =Math.min(minStep[i], minStep[j]+i/j); } } return minStep[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 進制
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)
a = "11" b = "1" Return "100". 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)
Input: [1,2,3] Output: 2 Explanation: Only two moves are needed (remember each move increments or decrements one element): [1,2,3] => [2,2,3] => [2,2,2]
每次可以對一個數組元素加一或者減一,求最小的改變次數。
這是個典型的相遇問題,移動距離最小的方式是所有元素都移動到中位數。理由如下:
設 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)
Input: 16 Returns: True
平方序列:1,4,9,16,25. 間隔:3,5,7,9
間隔為等差數列,使用這個特性可以得到從 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)
Input: [1,2,3,4] Output: 24 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)
For example, given [1,2,3,4], return [24,12,8,6].
題目描述:給定一個數組,創建一個新數組,新數組的每個元素為原始數組中除了該位置上的元素之外所有元素的乘積。
題目要求:時間復雜度為 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; }
數據結構相關
棧和隊列
用棧實現隊列
Leetcode : 232. Implement Queue using Stacks (Easy)
一個棧實現:
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)
在遍歷數組時用 Stack 把數組中的數存起來,如果當前遍歷的數比棧頂元素來的大,說明棧頂元素的下一個比它大的數就是當前元素。
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] 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 類似,如果元素有窮並且范圍不大,可以用整型數組來進行統計。
數組中的兩個數和為給定值
可以先對數組進行排序,然后使用雙指針方法或者二分查找方法。這樣做的時間復雜度為 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; }
最長和諧序列
Leetcode : 594. Longest Harmonious Subsequence (Easy)
Input: [1,3,2,2,5,2,3,7] Output: 5 Explanation: The longest harmonious subsequence is [3,2,2,2,3].
和諧序列中最大數和最小數只差正好為 1。
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)
s = "anagram", t = "nagaram", return true. s = "rat", t = "car", return false.
字符串只包含小寫字符,總共有 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)
Given "egg", "add", return true. Given "foo", "bar", return false. Given "paper", "title", return true.
記錄一個字符上次出現的位置,如果兩個字符串中某個字符上次出現的位置一樣,那么就屬於同構。
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 (Easy)
Input : "abccccdd" Output : 7 Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7.
使用長度為 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)
Input: "aaa" Output: 6 Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
解決方案是從字符串的某一位開始,嘗試着去擴展子字符串。
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 數量相同的子字符串個數
Leetcode : 696. Count Binary Substrings (Easy)
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". 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; }
字符串循環移位包含
s1 = AABCD, s2 = CDAA Return : true
給定兩個字符串 s1 和 s2 ,要求判定 s2 是否能夠被 s1 做循環移位得到的字符串包含。
s1 進行循環移位的結果是 s1s1 的子字符串,因此只要判斷 s2 是否是 s1s1 的子字符串即可。
字符串循環移位
將字符串向右循環移動 k 位。
例如 abcd123 向右移動 3 位 得到 123abcd
將 abcd123 中的 abcd 和 123 單獨逆序,得到 dcba321,然后對整個字符串進行逆序,得到 123abcd。
字符串中單詞的翻轉
例如將 "I am a student" 翻轉成 "student a am I"
將每個單詞逆序,然后將整個字符串逆序。
數組與矩陣
把數組中的 0 移到末尾
Leetcode : 283. Move Zeroes (Easy)
For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0]. 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 分布
一個數組元素在 [1, n] 之間,其中一個數被替換為另一個數,找出丟失的數和重復的數
Leetcode : 645. Set Mismatch (Easy)
Input: nums = [1,2,2,4] Output: [2,3]
最直接的方法是先對數組進行排序,這種方法時間復雜度為 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 條件。
類似題目:
-
Leetcode :448. Find All Numbers Disappeared in an Array (Easy),尋找所有丟失的元素
-
Leetcode : 442. Find All Duplicates in an Array (Medium),尋找所有重復的元素。
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 (Easy)
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 (Easy)
頭插法能夠按逆序構建鏈表。
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 (Easy)
鏈表和樹一樣,可以用遞歸方式來定義:鏈表是空節點,或者有一個值和一個指向下一個鏈表的指針。因此很多鏈表問題可以用遞歸來處理。
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; }
鏈表元素按奇偶聚集
Leetcode : 328. Odd Even Linked List (Medium)
Example: Given 1->2->3->4->5->NULL, return 1->3->5->2->4->NULL. 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)
Input: Tree 1 Tree 2 1 2 / \ / \ 3 2 1 3 / \ \ 5 4 7 Output: Merged tree: 3 / \ 4 5 / \ \ 5 4 7 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)
Given the below binary tree and sum = 22, 5 / \ 4 8 / / \ 11 13 4 / \ \ 7 2 1 return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22.
路徑和定義為從 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 = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 10 / \ 5 -3 / \ \ 3 2 11 / \ \ 3 -2 1 Return 3. The paths that sum to 8 are: 1. 5 -> 3 2. 5 -> 2 -> 1 3. -3 -> 11
路徑不一定以 root 開頭並以 leaf 結尾,但是必須連續
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)
1 / \ 2 2 / \ / \ 3 4 4 3 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); }
public boolean isSymmetric(TreeNode root) { if(root == null) return true; return isCommon(root.left,root.right); } private boolean isCommon(TreeNode left, TreeNode right) { if(left ==null && right ==null) return true; if(left !=null && right !=null) return left.val == right.val && isCommon(left.left, right.right) && isCommon(left.right, right.left); return false; }
平衡樹
Leetcode : 110. Balanced Binary Tree (Easy)
3 / \ 9 20 / \ 15 7
平衡樹左右子樹高度差都小於等於 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)
樹的根節點到葉子節點的最小路徑長度
if(root == null) { return 0; } if(root.left == null && root.right == null) { return 1; } int l,r; if(root.left != null) { l = minDepth(root.left); } else { l = Integer.MAX_VALUE; } if(root.right != null) { r = minDepth(root.right); } else { r = Integer.MAX_VALUE; } return l>=r?r+1:l+1;
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)
3 / \ 9 20 / \ 15 7 There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24. 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)
Input: 3 / \ 0 4 \ 2 / 1 L = 1 R = 3 Output: 3 / 2 / 1
二叉查找樹(BST):根節點大於等於左子樹所有節點,小於等於右子樹所有節點。
只保留值在 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)
Given tree s: 3 / \ 4 5 / \ 1 2 Given tree t: 4 / \ 1 2 Return true, because t has the same structure and node values with a subtree of s. 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)
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; }
兩節點的最長路徑
Input: 1 / \ 2 3 / \ 4 5 Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3].
Leetcode : 543. Diameter of Binary Tree (Easy)
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)
_______6______ / \ ___2__ ___8__ / \ / \ 0 _4 7 9 / \ 3 5 For example, the lowest common ancestor (LCA) of nodes 2 and 8 is 6. Another example is LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition. 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)
_______3______ / \ ___5__ ___1__ / \ / \ 6 _2 0 8 / \ 7 4 For example, the lowest common ancestor (LCA) of nodes 5 and 1 is 3. Another example is LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition. 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)
3 / \ 2 3 \ \ 3 1 Maximum amount of money the thief can rob = 3 + 3 + 1 = 7. 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)
Input: 1 / \ 2 3 / / \ 4 5 6 / 7 Output: 7 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; }
BST
主要利用 BST 中序遍歷有序的特點。
在 BST 中尋找兩個節點,使它們的和為一個給定值
653. Two Sum IV - Input is a BST (Easy)
Input: 5 / \ 3 6 / \ \ 2 4 7 Target = 9 Output: True
使用中序遍歷得到有序數組之后,再利用雙指針對數組進行查找。
應該注意到,這一題不能用分別在左右子樹兩部分來處理這種思想,因為兩個待求的節點可能分別在左右子樹中。
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)
Input: 1 \ 3 / 2 Output: 1
利用 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)
Input: The root of a Binary Search Tree like this: 5 / \ 2 13 Output: The root of a Greater Tree like this: 18 / \ 20 13
先遍歷右子樹。
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 中出現次數最多的節點
Leetcode : 501. Find Mode in Binary Search Tree (Easy)
1 \ 2 / 2 return [2]. 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)
Input: insert("apple", 3), Output: Null Input: sum("ap"), Output: 3 Input: insert("app", 2), Output: Null Input: sum("ap"), Output: 5 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 的位級表示中最低的那一位。例如對於二進制表示 10110 100 ,減去 1 得到 10110011,這兩個數相與得到 10110000。
n-n&(~n+1) 概運算是去除 n 的位級表示中最高的那一位。
n&(-n) 該運算得到 n 的位級表示中最低的那一位。-n 得到 n 的反碼加 1,對於二進制表示 10110 100 ,-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; }
不用額外變量交換兩個整數
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)
題目描述:字符串數組的字符串只含有小寫字符。求解字符串數組中兩個字符串長度的最大乘積,要求這兩個字符串不能含有相同字符。
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;
}