主要參考@CYC2018大佬的LeetCode題解java
把數組中的 0 移到末尾node
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 idx = 0; for (int num : nums) { if (num != 0) { nums[idx++] = num; } } while (idx < nums.length) { nums[idx++] = 0; } }
改變矩陣維度git
566. Reshape the Matrix (Easy)程序員
Input: nums = [[1,2], [3,4]] r = 1, c = 4 Output: [[1,2,3,4]] Explanation: The row-traversing of nums is [1,2,3,4]. The new reshaped matrix is a 1 * 4 matrix, fill it row by row by using the previous list.
public int[][] matrixReshape(int[][] nums, int r, int c) { int m = nums.length, n = nums[0].length; if (m * n != r * c) { return nums; } int[][] reshapedNums = new int[r][c]; int index = 0; for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { reshapedNums[i][j] = nums[index / n][index % n]; index++; } } return reshapedNums; }
找出數組中最長的連續 1github
485. Max Consecutive Ones (Easy)面試
public int findMaxConsecutiveOnes(int[] nums) { int max = 0, cur = 0; for (int x : nums) { cur = x == 0 ? 0 : cur + 1; max = Math.max(max, cur); } return max; }
一個數組元素在 [1, n] 之間,其中一個數被替換爲另外一個數,找出重複的數和丟失的數正則表達式
Input: nums = [1,2,2,4] Output: [2,3]
Input: nums = [1,2,2,4] Output: [2,3]
最直接的方法是先對數組進行排序,這種方法時間複雜度爲 O(NlogN)。本題能夠以 O(N) 的時間複雜度、O(1) 空間複雜度來求解。編程
主要思想是經過交換數組元素,使得數組上的元素在正確的位置上。遍歷數組,若是第 i 位上的元素不是 i + 1,那麼一直交換第 i 位和 nums[i] - 1 位置上的元素。
public int[] findErrorNums(int[] nums) { for (int i = 0; i < nums.length; i++) { while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) { swap(nums, i, nums[i] - 1); } } for (int i = 0; i < nums.length; i++) { if (nums[i] != i + 1) { 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; }
相似題目:
找出數組中重複的數,數組值在 [1, n] 之間
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; }
有序矩陣查找
240. Search a 2D Matrix II (Medium)
[ [ 1, 5, 9], [10, 11, 13], [12, 13, 15] ]
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
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 pq = new PriorityQueue(); 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 { 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; } }
數組相鄰差值的個數
667. Beautiful Arrangement II (Medium)
Input: n = 3, k = 2 Output: [1, 3, 2] Explanation: The [1, 3, 2] has three different positive integers ranging from 1 to 3, and the [2, 1] has exactly 2 distinct integers: 1 and 2.
題目描述:數組元素爲 1~n 的整數,要求構建數組,使得相鄰元素的差值不相同的個數爲 k。
讓前 k+1 個元素構建出 k 個不相同的差值,序列爲:1 k+1 2 k 3 k-1 ... k/2 k/2+1.
public int[] constructArray(int n, int k) { int[] ret = new int[n]; ret[0] = 1; for (int i = 1, interval = k; i <= k; i++, interval--) { ret[i] = i % 2 == 1 ? ret[i - 1] + interval : ret[i - 1] - interval; } for (int i = k + 1; i < n; i++) { ret[i] = i + 1; } return ret; }
數組的度
697. Degree of an Array (Easy)
Input: [1,2,2,3,1,4,2] Output: 6
題目描述:數組的度定義爲元素出現的最高頻率,例如上面的數組度爲 3。要求找到一個最小的子數組,這個子數組的度和原數組同樣。
public int findShortestSubArray(int[] nums) { Map numsCnt = new HashMap<>(); Map numsLastIndex = new HashMap<>(); Map numsFirstIndex = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int num = nums[i]; numsCnt.put(num, numsCnt.getOrDefault(num, 0) + 1); numsLastIndex.put(num, i); if (!numsFirstIndex.containsKey(num)) { numsFirstIndex.put(num, i); } } int maxCnt = 0; for (int num : nums) { maxCnt = Math.max(maxCnt, numsCnt.get(num)); } int ret = nums.length; for (int i = 0; i < nums.length; i++) { int num = nums[i]; int cnt = numsCnt.get(num); if (cnt != maxCnt) continue; ret = Math.min(ret, numsLastIndex.get(num) - numsFirstIndex.get(num) + 1); } return ret; }
對角元素相等的矩陣
1234 5123 9512 In the above grid, the diagonals are "[9]", "[5, 5]", "[1, 1, 1]", "[2, 2, 2]", "[3, 3]", "[4]", and in each diagonal all elements are the same, so the answer is True.
public boolean isToeplitzMatrix(int[][] matrix) { for (int i = 0; i < matrix[0].length; i++) { if (!check(matrix, matrix[0][i], 0, i)) { return false; } } for (int i = 0; i < matrix.length; i++) { if (!check(matrix, matrix[i][0], i, 0)) { return false; } } return true; } private boolean check(int[][] matrix, int expectValue, int row, int col) { if (row >= matrix.length || col >= matrix[0].length) { return true; } if (matrix[row][col] != expectValue) { return false; } return check(matrix, expectValue, row + 1, col + 1); }
嵌套數組
Input: A = [5,4,0,3,1,6,2] Output: 4 Explanation: A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2. One of the longest S[K]: S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0}
題目描述:S[i] 表示一個集合,集合的第一個元素是 A[i],第二個元素是 A[A[i]],如此嵌套下去。求最大的 S[i]。
public int arrayNesting(int[] nums) { int max = 0; for (int i = 0; i < nums.length; i++) { int cnt = 0; for (int j = i; nums[j] != -1; ) { cnt++; int t = nums[j]; nums[j] = -1; // 標記該位置已經被訪問 j = t; } max = Math.max(max, cnt); } return max; }
分隔數組
769. Max Chunks To Make Sorted (Medium)
Input: arr = [1,0,2,3,4] Output: 4 Explanation: We can split into two chunks, such as [1, 0], [2, 3, 4]. However, splitting into [1, 0], [2], [3], [4] is the highest number of chunks possible.
題目描述:分隔數組,使得對每部分排序後數組就爲有序。
public int maxChunksToSorted(int[] arr) { if (arr == null) return 0; int ret = 0; int right = arr[0]; for (int i = 0; i < arr.length; i++) { right = Math.max(right, arr[i]); if (right == i) ret++; } return ret; }
兩個字符串包含的字符是否徹底相同
s = "anagram", t = "nagaram", return true. s = "rat", t = "car", return false.
字符串只包含小寫字符,總共有 26 個小寫字符。能夠用 HashMap 來映射字符與出現次數。由於鍵的範圍很小,所以可使用長度爲 26 的整型數組對字符串出現的字符進行統計,而後比較兩個字符串出現的字符數量是否相同。
public boolean isAnagram(String s, String t) { int[] cnts = new int[26]; for (char c : s.toCharArray()) { cnts[c - 'a']++; } for (char c : t.toCharArray()) { cnts[c - 'a']--; } for (int cnt : cnts) { if (cnt != 0) { return false; } } return true; }
計算一組字符集合能夠組成的迴文字符串的最大長度
409. Longest Palindrome (Easy)
Input : "abccccdd" Output : 7 Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7.
使用長度爲 256 的整型數組來統計每一個字符出現的個數,每一個字符有偶數個能夠用來構成迴文字符串。
由於迴文字符串最中間的那個字符能夠單獨出現,因此若是有單獨的字符就把它放到最中間。
public int longestPalindrome(String s) { int[] cnts = new int[256]; for (char c : s.toCharArray()) { cnts[c]++; } int palindrome = 0; for (int cnt : cnts) { palindrome += (cnt / 2) * 2; } if (palindrome < s.length()) { palindrome++; // 這個條件下 s 中必定有單個未使用的字符存在,能夠把這個字符放到迴文的最中間 } return palindrome; }
字符串同構
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[] preIndexOfS = new int[256]; int[] preIndexOfT = new int[256]; for (int i = 0; i < s.length(); i++) { char sc = s.charAt(i), tc = t.charAt(i); if (preIndexOfS[sc] != preIndexOfT[tc]) { return false; } preIndexOfS[sc] = i + 1; preIndexOfT[tc] = i + 1; } return true; }
迴文子字符串
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++; } }
判斷一個整數是不是迴文數
要求不能使用額外空間,也就不能將整數轉換爲字符串進行判斷。
將整數分紅左右兩部分,右邊那部分須要轉置,而後判斷這兩部分是否相等。
public boolean isPalindrome(int x) { if (x == 0) { return true; } if (x < 0 || x % 10 == 0) { return false; } int right = 0; while (x > right) { right = right * 10 + x % 10; x /= 10; } return x == right || x == right / 10; }
統計二進制字符串中連續 1 和連續 0 數量相同的子字符串個數
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, count = 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) { count++; } } return count; }
字符串循環移位包含
s1 = AABCD, s2 = CDAA Return : true
給定兩個字符串 s1 和 s2,要求斷定 s2 是否可以被 s1 作循環移位獲得的字符串包含。
s1 進行循環移位的結果是 s1s1 的子字符串,所以只要判斷 s2 是不是 s1s1 的子字符串便可。
字符串循環移位
s = "abcd123" k = 3 Return "123abcd"
將字符串向右循環移動 k 位。
將 abcd123 中的 abcd 和 123 單獨逆序,獲得 dcba321,而後對整個字符串進行逆序,獲得 123abcd。
字符串中單詞的翻轉
s = "I am a student" return "student a am I"
將每一個單詞逆序,而後將整個字符串逆序。
哈希表使用 O(N) 空間複雜度存儲數據,從而可以以 O(1) 時間複雜度求解問題。
Java 中的 HashSet 用於存儲一個集合,能夠查找元素是否在集合中。
若是元素有窮,而且範圍不大,那麼能夠用一個布爾數組來存儲一個元素是否存在。例如對於只有小寫字符的元素,就能夠用一個長度爲 26 的布爾數組來存儲一個字符集合,使得空間複雜度下降爲 O(1)。
Java 中的 HashMap 主要用於映射關係,從而把兩個元素聯繫起來。
在對一個內容進行壓縮或者其它轉換時,利用 HashMap 能夠把原始內容和轉換後的內容聯繫起來。例如在一個簡化 url 的系統中Leetcdoe : 535. Encode and Decode TinyURL (Medium),利用 HashMap 就能夠存儲精簡後的 url 到原始 url 的映射,使得不只能夠顯示簡化的 url,也能夠根據簡化的 url 獲得原始 url 從而定位到正確的資源。
HashMap 也能夠用來對元素進行計數統計,此時鍵爲元素,值爲計數。和 HashSet 相似,若是元素有窮而且範圍不大,能夠用整型數組來進行統計。
數組中的兩個數和爲給定值
能夠先對數組進行排序,而後使用雙指針方法或者二分查找方法。這樣作的時間複雜度爲 O(NlogN),空間複雜度爲 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 indexForNum = new HashMap<>(); for (int i = 0; i < nums.length; i++) { if (indexForNum.containsKey(target - nums[i])) { return new int[]{indexForNum.get(target - nums[i]), i}; } else { indexForNum.put(nums[i], i); } } return null; }
判斷數組是否含有重複元素
217. Contains Duplicate (Easy)
public boolean containsDuplicate(int[] nums) { Set set = new HashSet<>(); for (int num : nums) { set.add(num); } return set.size() < nums.length; }
最長和諧序列
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 countForNum = new HashMap<>(); for (int num : nums) { countForNum.put(num, countForNum.getOrDefault(num, 0) + 1); } int longest = 0; for (int num : countForNum.keySet()) { if (countForNum.containsKey(num + 1)) { longest = Math.max(longest, countForNum.get(num + 1) + countForNum.get(num)); } } return longest; }
最長連續序列
128. Longest Consecutive Sequence (Hard)
Given [100, 4, 200, 1, 3, 2], The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4.
要求以 O(N) 的時間複雜度求解。
public int longestConsecutive(int[] nums) { Map countForNum = new HashMap<>(); for (int num : nums) { countForNum.put(num, 1); } for (int num : nums) { forward(countForNum, num); } return maxCount(countForNum); } private int forward(Map countForNum, int num) { if (!countForNum.containsKey(num)) { return 0; } int cnt = countForNum.get(num); if (cnt > 1) { return cnt; } cnt = forward(countForNum, num + 1) + 1; countForNum.put(num, cnt); return cnt; } private int maxCount(Map countForNum) { int max = 0; for (int num : countForNum.keySet()) { max = Math.max(max, countForNum.get(num)); } return max; }
通常何時須要用到貪心,其實就是在題目推導比較難解,可是直觀思惟卻比較簡單。好比經典的排課問題,就是使用貪心,先進行排序,再進行選擇,貪心算法也時經常使用來求近似解。
因此通常解法能夠考慮爲,先排序,再根據條件求結果。證實的過程是很是難的,因此咱們通常不會討論證實
貪心思想 貪心思想保證每次操做都是局部最優的,而且最後獲得的結果是全局最優的。
455.分發餅乾: 假設你是一位很棒的家長,想要給你的孩子們一些小餅乾。可是,每一個孩子最多隻能給一塊餅乾。對每一個孩子 i ,都有一個胃口值 gi ,這是能讓孩子們知足胃口的餅乾的最小尺寸;而且每塊餅乾 j ,都有一個尺寸 sj 。若是 sj >= gi ,咱們能夠將這個餅乾 j 分配給孩子 i ,這個孩子會獲得知足。你的目標是儘量知足越多數量的孩子,並輸出這個最大數值。
注意:
你能夠假設胃口值爲正。 一個小朋友最多隻能擁有一塊餅乾。
示例 1:
輸入: [1,2,3], [1,1]
輸出: 1
解釋: 你有三個孩子和兩塊小餅乾,3個孩子的胃口值分別是:1,2,3。 雖然你有兩塊小餅乾,因爲他們的尺寸都是1,你只能讓胃口值是1的孩子知足。 因此你應該輸出1。 示例 2:
輸入: [1,2], [1,2,3]
輸出: 2
解釋: 你有兩個孩子和三塊小餅乾,2個孩子的胃口值分別是1,2。 你擁有的餅乾數量和尺寸都足以讓全部孩子知足。 因此你應該輸出2.
因爲題目想讓儘可能多的孩子知足胃口值,因此應該先用量小的餅乾知足胃口小的。這樣獲得的結果是最優的。
public int findContentChildren(int[] g, int[] s) {
int count = 0;
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 ++;
count ++;
}else {
j ++;
}
}
return count;
}
複製代碼
- 無重疊區間:給定一個區間的集合,找到須要移除區間的最小數量,使剩餘區間互不重疊。
注意:
能夠認爲區間的終點老是大於它的起點。 區間 [1,2] 和 [2,3] 的邊界相互「接觸」,但沒有相互重疊。 示例 1:
輸入: [ [1,2], [2,3], [3,4], [1,3] ]
輸出: 1
解釋: 移除 [1,3] 後,剩下的區間沒有重疊。 示例 2:
輸入: [ [1,2], [1,2], [1,2] ]
輸出: 2
解釋: 你須要移除兩個 [1,2] 來使剩下的區間沒有重疊。 示例 3:
輸入: [ [1,2], [2,3] ]
輸出: 0
解釋: 你不須要移除任何區間,由於它們已是無重疊的了。
本題相似於課程排課,咱們應該讓課程結束時間最先的先排課,這樣可讓排課最大化,而且須要讓課程結束的時間小於下一節課程開始的時間。而且[1,2][2,3]不算課程重疊。
因此咱們的想法是,根據數組的第二位進行排序,也就是按照課程的結束時間排序,而後依次尋找不重疊的區間,而後用總個數減去不重疊的區間,剩下的就是要刪除的區間。
不過,要注意的是,不重疊的區間並不必定是連續的,若是1和2區間重疊了,還要判斷1和3是否重疊,直到找到不重疊的區間,再從3區間開始找下一個區間。
/**
* Definition for an interval.
* public class Interval {
* int start;
* int end;
* Interval() { start = 0; end = 0; }
* Interval(int s, int e) { start = s; end = e; }
* }
*/
import java.util.*;
class Solution {
public int eraseOverlapIntervals(Interval[] intervals) {
int len = intervals.length;
if (len <= 1)return 0;
Arrays.sort(intervals, (a,b) -> a.end - b.end);
int count = 1;
int end = intervals[0].end;
for (int i = 1;i < intervals.length;i ++) {
if (intervals[i].start < end) {
continue;
}
count ++;
end = intervals[i].end;
}
return len - count;
}
}
複製代碼
本題要注意的點有幾個:
1 須要用一個值標識起始值的end,而後再日後找一個符合條件的end。因爲是順序查找,因此只須要一個變量i。而且使用end標識起始元素。
2 默認的count應該爲1,由於本身自己就是不重疊的。因此找到其餘不重疊的區域,使用n-count纔對。
- 用最少數量的箭引爆氣球 在二維空間中有許多球形的氣球。對於每一個氣球,提供的輸入是水平方向上,氣球直徑的開始和結束座標。因爲它是水平的,因此y座標並不重要,所以只要知道開始和結束的x座標就足夠了。開始座標老是小於結束座標。平面內最多存在104個氣球。
一支弓箭能夠沿着x軸從不一樣點徹底垂直地射出。在座標x處射出一支箭,如有一個氣球的直徑的開始和結束座標爲 xstart,xend, 且知足 xstart ≤ x ≤ xend,則該氣球會被引爆。能夠射出的弓箭的數量沒有限制。 弓箭一旦被射出以後,能夠無限地前進。咱們想找到使得全部氣球所有被引爆,所需的弓箭的最小數量。
Example:
輸入: [[10,16], [2,8], [1,6], [7,12]]
輸出: 2
解釋: 對於該樣例,咱們能夠在x = 6(射爆[2,8],[1,6]兩個氣球)和 x = 11(射爆另外兩個氣球)。
import java.util.*;
class Solution {
public int findMinArrowShots(int[][] points) {
if (points.length <= 1){
return points.length;
}
Arrays.sort(points, (a, b) -> a[1] - b[1]);
int end = points[0][1];
int cnt = 1;
for (int i = 1;i < points.length;i ++) {
if (points[i][0] <= end) {
continue;
}
end = points[i][1];
cnt ++;
}
return cnt;
}
}
複製代碼
和上一題相似,要注意的地方是: 1.本題是求不重疊區域的個數,而上一題是求要刪除重疊區域的個數。 2.本題中[1,2][2,3]也算是重疊區域
- 根據身高重建隊列
這題思路不直觀,跳過
字符串 S 由小寫字母組成。咱們要把這個字符串劃分爲儘量多的片斷,同一個字母只會出如今其中的一個片斷。返回一個表示每一個字符串片斷的長度的列表。
示例 1:
輸入: S = "ababcbacadefegdehijhklij" 輸出: [9,7,8] 解釋: 劃分結果爲 "ababcbaca", "defegde", "hijhklij"。 每一個字母最多出如今一個片斷中。 像 "ababcbacadefegde", "hijhklij" 的劃分是錯誤的,由於劃分的片斷數較少。 注意:
S的長度在[1, 500]之間。 S只包含小寫字母'a'到'z'。
本題的思路是,先把每一個字母的最後一位找出來,存在數組裏,而後從頭開始找到這樣一個字符串,對於字符串中的每一個字母,它出現的最後一個字母已經包含在整個字符串內。
import java.util.*;
class Solution {
public List<Integer> partitionLabels(String S) {
int []arr = new int[26];
List<Integer> list = new ArrayList<>();
for (int i = 0;i < S.length();i ++) {
arr[S.charAt(i) - 'a'] = i;
}
int start = 0;
int end = arr[S.charAt(0) - 'a'];
for (int i = 0;i < S.length();i ++) {
end = Math.max(arr[S.charAt(i) - 'a'], end);
if (i < end) {
continue;
}else {
list.add(end - start + 1);
start = i + 1;
}
}
return list;
}
}
複製代碼
本題要點:
1.要使用一個數組存儲每一個字母的最後出現位置。 經過x - 'a'的方式獲得其下標。
2.因爲須要每一次截取的長度,因此用start和end來表示,能夠用於保存長度。
- 種花問題 假設你有一個很長的花壇,一部分地塊種植了花,另外一部分卻沒有。但是,花卉不能種植在相鄰的地塊上,它們會爭奪水源,二者都會死去。
給定一個花壇(表示爲一個數組包含0和1,其中0表示沒種植花,1表示種植了花),和一個數 n 。可否在不打破種植規則的狀況下種入 n 朵花?能則返回True,不能則返回False。
示例 1:
輸入: flowerbed = [1,0,0,0,1], n = 1 輸出: True 示例 2:
輸入: flowerbed = [1,0,0,0,1], n = 2 輸出: False 注意:
數組內已種好的花不會違反種植規則。 輸入的數組長度範圍爲 [1, 20000]。 n 是非負整數,且不會超過輸入數組的大小。
思路:算出花壇中一共有幾個空位,看看是否大於等於花的數量
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int cnt = 0;
if (flowerbed.length == 1 && flowerbed[0] == 0) {
return n <= 1;
}
if (flowerbed.length >= 2) {
if (flowerbed[0] == 0 && flowerbed[1] == 0) {
flowerbed[0] = 1;
cnt ++;
}
if (flowerbed[flowerbed.length - 1] == 0 && flowerbed[flowerbed.length - 2] == 0) {
flowerbed[flowerbed.length - 1] = 1;
cnt ++;
}
}
for (int i = 1;i < flowerbed.length - 1;) {
if (flowerbed[i - 1] == 0 && flowerbed[i] == 0 && flowerbed[i + 1] == 0 ) {
cnt ++;
flowerbed[i] = 1;
i = i + 2;
}else {
i ++;
}
}
return cnt >= n;
}
}
複製代碼
注意點:
1從頭至尾找到符合0 0 0狀況的個數。
2注意數組兩邊的特殊狀況處理 0 0。當長度大於1時處理便可。
3。處理長度爲1時的數組
- 判斷子序列
給定字符串 s 和 t ,判斷 s 是否爲 t 的子序列。
你能夠認爲 s 和 t 中僅包含英文小寫字母。字符串 t 可能會很長(長度 ~= 500,000),而 s 是個短字符串(長度 <=100)。
字符串的一個子序列是原始字符串刪除一些(也能夠不刪除)字符而不改變剩餘字符相對位置造成的新字符串。(例如,"ace"是"abcde"的一個子序列,而"aec"不是)。
示例 1: s = "abc", t = "ahbgdc"
返回 true.
示例 2: s = "axc", t = "ahbgdc"
返回 false.
解析:本題我剛開始想的辦法是使用dp求出LCS最長公共子序列,判斷長度是否等於t的長度,結果超時了。事實證實我想太多了。
只須要按順序查找t的字母是否都在s中便可,固然,要注意查找時候的下標移動,不然也是O(N2)的複雜度
DP解法:超時
import java.util.*;
class Solution {
public boolean isSubsequence(String s, String t) {
return LCS(s,t);
}
public boolean LCS(String s, String t) {
int [][]dp = new int[s.length() + 1][t.length() + 1];
for (int i = 1;i <= s.length();i ++) {
for (int j = 1;j <= t.length();j ++) {
if (s.charAt(i - 1) == t.charAt(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]);
}
}
}
int len = dp[s.length()][t.length()];
return len == s.length();
}
}
複製代碼
正解: 巧用indexOf方法indexOf(c,index + 1)來找到從index + 1開始的c字母。
import java.util.*;
class Solution {
public boolean isSubsequence(String s, String t) {
int index = -1;
for (int i = 0;i < s.length();i ++) {
index = t.indexOf(s.charAt(i), index + 1);
if (index == -1) {
return false;
}
}
return true;
}
}
複製代碼
- 非遞減數列 這題暫時沒有想到比較好的方法
給定一個長度爲 n 的整數數組,你的任務是判斷在最多改變 1 個元素的狀況下,該數組可否變成一個非遞減數列。
咱們是這樣定義一個非遞減數列的: 對於數組中全部的 i (1 <= i < n),知足 array[i] <= array[i + 1]。
示例 1:
輸入: [4,2,3] 輸出: True 解釋: 你能夠經過把第一個4變成1來使得它成爲一個非遞減數列。 示例 2:
輸入: [4,2,1] 輸出: False 解釋: 你不能在只改變一個元素的狀況下將其變爲非遞減數列。
- 買賣股票的最佳時機 II 題意:
給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。
設計一個算法來計算你所能獲取的最大利潤。你能夠儘量地完成更多的交易(屢次買賣一支股票)。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉以前的股票)。
示例 1:
輸入: [7,1,5,3,6,4] 輸出: 7 解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能得到利潤 = 5-1 = 4 。 隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能得到利潤 = 6-3 = 3 。 示例 2:
輸入: [1,2,3,4,5] 輸出: 4 解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能得到利潤 = 5-1 = 4 。 注意你不能在第 1 天和第 2 天接連購買股票,以後再將它們賣出。 由於這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉以前的股票。 示例 3:
輸入: [7,6,4,3,1] 輸出: 0 解釋: 在這種狀況下, 沒有交易完成, 因此最大利潤爲 0。
題意:只要出現價差爲正時就買入,這樣必定是最賺的,注意本題中同一天能夠進行賣出後再進行買入。
對於 [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] 添加到收益中,從而在局部最優的狀況下也保證全局最優。
class Solution {
public int maxProfit(int[] prices) {
int buy = 0;
int sell = 1;
int cnt = 0;
while(buy < sell && sell < prices.length) {
if(prices[sell] > prices[buy]) {
cnt += prices[sell] - prices[buy];
}
buy = sell;
sell = buy + 1;
}
return cnt;
}
}
複製代碼
雙指針 雙指針主要用於遍歷數組,兩個指針指向不一樣的元素,從而協同完成任務。
雙指針其實通常不會抽取出來單獨做爲一種算法,由於數組中常常會用到,並且咱們熟悉的二分查找也使用了雙指針。
二分查找
- 兩數之和 II - 輸入有序數組
給定一個已按照升序排列 的有序數組,找到兩個數使得它們相加之和等於目標數。
函數應該返回這兩個下標值 index1 和 index2,其中 index1 必須小於 index2。
說明:
返回的下標值(index1 和 index2)不是從零開始的。 你能夠假設每一個輸入只對應惟一的答案,並且你不能夠重複使用相同的元素。 示例:
輸入: numbers = [2, 7, 11, 15], target = 9 輸出: [1,2] 解釋: 2 與 7 之和等於目標數 9 。所以 index1 = 1, index2 = 2 。
這題基本操做了。
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left = 0,right = numbers.length - 1;
int []arr = new int[2];
while (left < right) {
if (numbers[left] + numbers[right] < target) {
left ++;
}else if (numbers[left] + numbers[right] > target) {
right --;
}else {
arr[0] = left + 1;
arr[1] = right + 1;
return arr;
}
}
return arr;
}
}
複製代碼
- 平方數之和
給定一個非負整數 c ,你要判斷是否存在兩個整數 a 和 b,使得 a2 + b2 = c。
示例1:
輸入: 5 輸出: True 解釋: 1 * 1 + 2 * 2 = 5
示例2:
輸入: 3 輸出: False
基操
import java.util.*;
class Solution {
public boolean judgeSquareSum(int c) {
double n = Math.sqrt(c);
for (double i = 0;i <= n;i ++) {
double diff = c - i * i;
int j = (int) Math.sqrt(diff);
if (j * j == diff) {
return true;
}
}
return false;
}
}
複製代碼
示例 1: 給定 s = "hello", 返回 "holle".
示例 2: 給定 s = "leetcode", 返回 "leotcede".
注意: 元音字母不包括 "y".
快排思想進行交換便可
import java.util.*;
class Solution {
public String reverseVowels(String s) {
char[] arr = s.toCharArray();
int left = 0,right = s.length() - 1;
while (left < right){
while (left < right && !isVowels(arr[left])) {
left ++;
}
while (left < right && !isVowels(arr[right])) {
right --;
}
char temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left ++;
right --;
}
return String.valueOf(arr);
}
public boolean isVowels(char c) {
char[]arr = {'a', 'i', 'e', 'u', 'o', 'A', 'I', 'E', 'U', 'O'};
for (int k = 0;k < arr.length;k ++) {
if (c == arr[k]) {
return true;
}
}
return false;
}
}
複製代碼
- 驗證迴文字符串 Ⅱ
給定一個非空字符串 s,最多刪除一個字符。判斷是否能成爲迴文字符串。
示例 1:
輸入: "aba" 輸出: True 示例 2:
輸入: "abca" 輸出: True 解釋: 你能夠刪除c字符。 注意:
字符串只包含從 a-z 的小寫字母。字符串的最大長度是50000。
在驗證迴文的基礎上加上一步,當遇到不符合要求的字符時,再往前走一步便可。固然機會只有一次。
本題可能遇到一個問題,若是直接用while循環寫的話,會遇到兩種狀況,一種是左邊加一,一種是右邊減一。只要一種狀況知足便可。因此咱們要另外寫一個判斷函數,而後用||來表示兩種狀況便可。
class Solution {
public boolean validPalindrome(String s) {
int left = 0,right = s.length() - 1;
while (left < right) {
if (s.charAt(left) == s.charAt(right)) {
left ++;
right --;
}else {
return valid(s, left + 1,right) || valid(s, left, right - 1);
}
}
return true;
}
public boolean valid(String s, int i, int j) {
int left = i,right = j;
while (left < right) {
if (s.charAt(left) == s.charAt(right)) {
left ++;
right --;
}
else return false;
}
return true;
}
}
複製代碼
- 合併兩個有序數組
這題給的用例有毒,不談。
- 環形鏈表
劍指offer 使用雙指針,一個指針每次移動一個節點,一個指針每次移動兩個節點,若是存在環,那麼這兩個指針必定會相遇。
- 經過刪除字母匹配到字典裏最長單詞 給定一個字符串和一個字符串字典,找到字典裏面最長的字符串,該字符串能夠經過刪除給定字符串的某些字符來獲得。若是答案不止一個,返回長度最長且字典順序最小的字符串。若是答案不存在,則返回空字符串。
示例 1:
輸入: s = "abpcplea", d = ["ale","apple","monkey","plea"]
輸出: "apple" 示例 2:
輸入: s = "abpcplea", d = ["a","b","c"]
輸出: "a" 說明:
全部輸入的字符串只包含小寫字母。 字典的大小不會超過 1000。 全部輸入的字符串長度不會超過 1000。
解析:本題的雙指針不是指左右指針了,而是分別掃描兩個字符串所用的指針。
因爲題目要求先按照長度排序再按照字典序排序,因而使用比較器能夠實現該邏輯,而後再一一匹配便可。
import java.util.*;
class Solution {
public String findLongestWord(String s, List<String> d) {
Collections.sort(d, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.length() != o2.length()) {
return o2.length() - o1.length();
} else {
return o1.compareTo(o2);
}
}
});
for (String str : d) {
int i = 0,j = 0;
while (i < s.length() && j < str.length()) {
if (s.charAt(i) == str.charAt(j)) {
i ++;
j ++;
}else {
i ++;
}
}
if (j == str.length()) {
return str;
}
}
return "";
}
}
複製代碼
排序
快速選擇 通常用於求解 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 問題。
排序 :時間複雜度 O(NlogN),空間複雜度 O(1)
public int findKthLargest(int[] nums, int k) {
Arrays.sort(nums);
return nums[nums.length - k];
}
堆排序 :時間複雜度 O(NlogK),空間複雜度 O(K)。
每次插入一個元素,當元素超過k個時,彈出頂部的最小值,當元素push完之後,剩下的元素就是前k大的元素,堆頂元素就是第K大的元素。
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小頂堆
for (int val : nums) {
pq.add(val);
if (pq.size() > k) // 維護堆的大小爲 K
pq.poll();
}
return pq.peek();
}
快速選擇(也能夠認爲是快速排序的partition加上二分的算法)
利用partition函數求出一個數的最終位置,再經過二分來逼近第k個位置,算法結論代表該算法的時間複雜度是O(N)
class Solution {
public int findKthLargest(int[] nums, int k) {
k = nums.length - k;
int l = 0, r = nums.length - 1;
while (l < r) {
int pos = partition(nums, l , r);
if (pos == k) return nums[pos];
else if (pos < k) {
l = pos + 1;
}else {
r = pos - 1;
}
}
return nums[k];
}
public int partition(int[] nums, int left, int right) {
int l = left, r = right;
int temp = nums[l];
while (l < r) {
while (l < r && nums[r] >= temp) {
r --;
}
while (l < r && nums[l] <= temp) {
l ++;
}
if (l < r) {
int tmp = nums[l];
nums[l] = nums[r];
nums[r] = tmp;
}
}
nums[left] = nums[l];
nums[l] = temp;
return l;
}
}
複製代碼
桶排序
- 前K個高頻元素
給定一個非空的整數數組,返回其中出現頻率前 k 高的元素。
例如,
給定數組 [1,1,1,2,2,3] , 和 k = 2,返回 [1,2]。
注意:
你能夠假設給定的 k 老是合理的,1 ≤ k ≤ 數組中不相同的元素的個數。 你的算法的時間複雜度必須優於 O(n log n) , n 是數組的大小。
解析:
設置若干個桶,每一個桶存儲出現頻率相同的數,而且桶的下標表明桶中數出現的頻率,即第 i 個桶中存儲的數出現的頻率爲 i。把數都放到桶以後,從後向前遍歷桶,最早獲得的 k 個數就是出現頻率最多的的 k 個數。
import java.util.*;
class Solution {
public List<Integer> topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int i : nums) {
if (map.containsKey(i)) {
map.put(i, map.get(i) + 1);
}else {
map.put(i, 1);
}
}
ArrayList<Integer>[] timesMap = new ArrayList[nums.length + 1];
for (int key : map.keySet()) {
int times = map.get(key);
if (timesMap[times] == null) {
timesMap[times] = new ArrayList<>();
timesMap[times].add(key);
}
else {
timesMap[times].add(key);
}
}
List<Integer> top = new ArrayList<Integer>();
for (int i = timesMap.length - 1;i > 0 && top.size() < k;i --) {
if (timesMap[i] != null) {
top.addAll(timesMap[i]);
}
}
return top;
}
}
注意:
1本題的難點在於先用hashmap存儲數據獲得每一個數的頻率,再用數組存儲每一個頻率對應哪些數。
2最後再經過頻率數組的最後一位開始往前找,找到k個數爲之,就是出現頻率最高的k個數了。
複製代碼
- 根據字符出現頻率排序
給定一個字符串,請將字符串裏的字符按照出現的頻率降序排列。
輸入: "tree"
輸出: "eert"
解釋: 'e'出現兩次,'r'和't'都只出現一次。 所以'e'必須出如今'r'和't'以前。此外,"eetr"也是一個有效的答案。
我下面這個寫法只考慮了小寫字母的狀況,大寫字母與其餘字符沒有考慮,是錯誤的。正確的作法仍是應該用一個128長度的char數組 。由於char是1一個字節長度,也就是8位,2的8次方是256,考慮正數的話就是128。
上題使用map是由於32位整數太大,數組存不下,而本題char數組只須要長度爲128便可,不用使用map。
錯誤解:
public static String frequencySort(String s) {
int []arr = new int[26];
char []crr = s.toCharArray();
for (char c : crr) {
arr[c - 'a']++;
}
List<Character>[]times = new ArrayList[s.length() + 1];
for (int i = 0;i < arr.length;i ++) {
if (times[arr[i]] == null) {
times[arr[i]] = new ArrayList<>();
times[arr[i]].add((char) ('a' + i));
}else {
times[arr[i]].add((char) ('a' + i));
}
}
StringBuilder sb = new StringBuilder();
for (int i = times.length - 1;i > 0 ;i --) {
if (times[i] != null) {
for (char c : times[i]) {
int time = 0;
while (time < i) {
sb.append(c);
time ++;
}
}
}
}
return sb.toString();
}
複製代碼
正解:
class Solution {
public static String frequencySort(String s) {
int []arr = new int[128];
char []crr = s.toCharArray();
for (char c : crr) {
arr[c]++;
}
List<Character>[]times = new ArrayList[s.length() + 1];
for (int i = 0;i < arr.length;i ++) {
if (times[arr[i]] == null) {
times[arr[i]] = new ArrayList<>();
times[arr[i]].add((char) (i));
}else {
times[arr[i]].add((char) (i));
}
}
StringBuilder sb = new StringBuilder();
for (int i = times.length - 1;i > 0 ;i --) {
if (times[i] != null) {
for (char c : times[i]) {
int time = 0;
while (time < i) {
sb.append(c);
time ++;
}
}
}
}
return sb.toString();
}
}
複製代碼
- 分類顏色
給定一個包含紅色、白色和藍色,一共 n 個元素的數組,原地對它們進行排序,使得相同顏色的元素相鄰,並按照紅色、白色、藍色順序排列。此題中,咱們使用整數 0、 1 和 2 分別表示紅色、白色和藍色。
注意: 不能使用代碼庫中的排序函數來解決這道題。
進階:
一個直觀的解決方案是使用計數排序的兩趟掃描算法。 首先,迭代計算出0、1 和 2 元素的個數,而後按照0、一、2的排序,重寫當前數組。 你能想出一個僅使用常數空間的一趟掃描算法嗎?
解析:本題的思路一個就是題目所說的計數排序,還有一個即是使用交換算法,設置三個下標,zero, one, two,分別表示0的結尾,1的結尾,2的結尾,而且在遍歷過程當中把0換到one前面,把2換到one後面,中間的就是1了。
class Solution {
public void sortColors(int[] nums) {
if (nums.length <= 1)return;
int zero = -1, one = 0,two = nums.length;
while (one < two) {
if (nums[one] == 0) {
swap(nums, ++zero, one++);
}else if (nums[one] == 2) {
swap(nums, --two, one);
}else {
one ++;
}
}
}
public void swap(int []nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
複製代碼
正常實現
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (nums[m] == key) {
return m;
} else if (nums[m] > key) {
h = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
複製代碼
時間複雜度
二分查找也稱爲折半查找,每次都能將查找區間減半,這種折半特性的算法時間複雜度都爲 O(logN)。
m 計算
有兩種計算中值 m 的方式:
m = (l + h) / 2 m = l + (h - l) / 2 l + h 可能出現加法溢出,最好使用第二種方式。
返回值
循環退出時若是仍然沒有查找到 key,那麼表示查找失敗。能夠有兩種返回值:
-1:以一個錯誤碼錶示沒有查找到 key l:將 key 插入到 nums 中的正確位置
變種
題目:在一個有重複元素的數組中查找 key 的最左位置
若是是直接查找那麼複雜度爲O(n)因此能夠採用二分優化
二分查找能夠有不少變種,變種實現要注意邊界值的判斷。 例如在一個有重複元素的數組中查找 key 的最左位置的實現以下:
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= key) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
複製代碼
該實現和正常實現有如下不一樣:
循環條件爲 l < h h 的賦值表達式爲 h = m 最後返回 l 而不是 -1 在 nums[m] >= key 的狀況下,能夠推導出最左 key 位於 [l, m] 區間中,這是一個閉區間。h 的賦值表達式爲 h = m,由於 m 位置也多是解。
在 h 的賦值表達式爲 h = mid 的狀況下,若是循環條件爲 l <= h,那麼會出現循環沒法退出的狀況,所以循環條件只能是 l < h。如下演示了循環條件爲 l <= h 時循環沒法退出的狀況:
nums = {0, 1, 2}, key = 1 l m h 0 1 2 nums[m] >= key 0 0 1 nums[m] < key 1 1 1 nums[m] >= key 1 1 1 nums[m] >= key ... 當循環體退出時,不表示沒有查找到 key,所以最後返回的結果不該該爲 -1。爲了驗證有沒有查找到,須要在調用端判斷一下返回位置上的值和 key 是否相等
- x 的平方根
實現 int sqrt(int x) 函數。
計算並返回 x 的平方根,其中 x 是非負整數。
因爲返回類型是整數,結果只保留整數的部分,小數部分將被捨去。
示例 1:
輸入: 4 輸出: 2 示例 2:
輸入: 8 輸出: 2 說明: 8 的平方根是 2.82842..., 因爲返回類型是整數,小數部分將被捨去。
一個數 x 的開方 sqrt 必定在 0 ~ x 之間,而且知足 sqrt == x / sqrt。能夠利用二分查找在 0 ~ x 之間查找 sqrt。
對於 x = 8,它的開方是 2.82842...,最後應該返回 2 而不是 3。在循環條件爲 l <= h 而且循環退出時,h 老是比 l 小 1,也就是說 h = 2,l = 3,所以最後的返回值應該爲 h 而不是 l。
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 (mid > sqrt) {
h = mid - 1;
} else {
l = mid + 1;
}
}
return h;
}
複製代碼
注意:因爲要取的值是比原值小的整數,因此等sqrt小於mid時,而且此時l > h時說明h此時已是最接近sqrt且比它小的值了。固然若是前面有相等的狀況時已經返回了。 744. 尋找比目標字母大的最小字母
給定一個只包含小寫字母的有序數組letters 和一個目標字母 target,尋找有序數組裏面比目標字母大的最小字母。
數組裏字母的順序是循環的。舉個例子,若是目標字母target = 'z' 而且有序數組爲 letters = ['a', 'b'],則答案返回 'a'。
示例:
輸入: letters = ["c", "f", "j"] target = "a" 輸出: "c"
輸入: letters = ["c", "f", "j"] target = "c" 輸出: "f"
輸入: letters = ["c", "f", "j"] target = "d" 輸出: "f"
輸入: letters = ["c", "f", "j"] target = "g" 輸出: "j"
輸入: letters = ["c", "f", "j"] target = "j" 輸出: "c"
輸入: letters = ["c", "f", "j"] target = "k" 輸出: "c" 注:
letters長度範圍在[2, 10000]區間內。 letters 僅由小寫字母組成,最少包含兩個不一樣的字母。 目標字母target 是一個小寫字母。
解析:使用二分查找逼近,找到字母后右邊那個就是最小的,找不到的話返回結束位置的右邊第一個字母。
注意: 1 與上一題相反,本題的要找的是比指定值大一點的數,因此此時l > r知足時,l就是比指定值大一點的數了。
2 注意可能有連續重複的數字,因此一直往右找到一個數大於指定值
class Solution {
public char nextGreatestLetter(char[] letters, char target) {
if (letters == null || letters.length == 0) return 'a';
int l = 0,r = letters.length - 1;
while (l <= r) {
int m = l + (r - l)/2;
if (letters[m] <= target ) {
l = m + 1;
}else {
r = m - 1;
}
}
if (l <= letters.length - 1) {
return letters[l];
}else {
return letters[0];
}
}
}
複製代碼
給定一個只包含整數的有序數組,每一個元素都會出現兩次,惟有一個數只會出現一次,找出這個數。
示例 1:
輸入: [1,1,2,3,3,4,4,8,8] 輸出: 2 示例 2:
輸入: [3,3,7,7,10,11,11] 輸出: 10 注意: 您的方案應該在 O(log n)時間複雜度和 O(1)空間複雜度中運行。
解析:本題其實能夠用位運算作,可是限制了時間複雜度,因此考慮使用二分,這題我作不出來,能夠參考下面答案
令 index 爲 Single Element 在數組中的位置。若是 m 爲偶數,而且 m + 1 < index,那麼 nums[m] == nums[m + 1];m + 1 >= index,那麼 nums[m] != nums[m + 1]。
從上面的規律能夠知道,若是 nums[m] == nums[m + 1],那麼 index 所在的數組位置爲 [m + 2, h],此時令 l = m + 2;若是 nums[m] != nums[m + 1],那麼 index 所在的數組位置爲 [l, m],此時令 h = m。
由於 h 的賦值表達式爲 h = m,那麼循環條件也就只能使用 l < h 這種形式。
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];
}
複製代碼
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
你能夠假設數組中不存在重複元素。
示例 1:
輸入: [3,4,5,1,2] 輸出: 1 示例 2:
輸入: [4,5,6,7,0,1,2] 輸出: 0
解析:比較經典的題目,正常狀況下是順序的,僅當arr[i] > arr[i + 1]能夠得知arr[i + 1]是最小值。 順序掃描須要O(n),使用二分查找能夠優化到Log2n
旋轉數組的兩個遞增數組由最小值來劃分。 因此對於l, m, r來講,若是arr[m] < arr[h],說明到m到h是有序部分,最小值應該在l到m之間。因此令r = m; 若是arr[h] < arr[m],說明最小值在m到h之間。因此令l = m + 1。 當l > r時,說明nums[m] > nums[h]已經到達終點,此時nums[m + 1 ]就是最小值
public int findMin(int[] nums) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] <= nums[h]) {
h = m;
} else {
l = m + 1;
}
}
return nums[l];
}
複製代碼
- 在排序數組中查找元素的第一個和最後一個位置
給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。
你的算法時間複雜度必須是 O(log n) 級別。
若是數組中不存在目標值,返回 [-1, -1]。
示例 1:
輸入: nums = [5,7,7,8,8,10], target = 8 輸出: [3,4] 示例 2:
輸入: nums = [5,7,7,8,8,10], target = 6 輸出: [-1,-1]
解析:參考別人的答案:
1 首先經過二分查找找到該數出現的最左邊位置(與例題同樣)
2 而後經過二分查找找到比該數大1的數出現的位置,若是不存在,則恰好在所求數右邊一位,再減1便可。
3 邊界條件判斷
public int[] searchRange(int[] nums, int target) {
int first = binarySearch(nums, target);
int last = binarySearch(nums, target + 1) - 1;
if (first == nums.length || nums[first] != target) {
return new int[]{-1, -1};
} else {
return new int[]{first, Math.max(first, last)};
}
}
private int binarySearch(int[] nums, int target) {
int l = 0, h = nums.length; // 注意 h 的初始值
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= target) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
複製代碼
深度優先搜索和廣度優先搜索普遍運用於樹和圖中,可是它們的應用遠遠不止如此。
廣度優先搜索的搜索過程有點像一層一層地進行遍歷,每層遍歷都以上一層遍歷的結果做爲起點,遍歷一個距離能訪問到的全部節點。須要注意的是,遍歷過的節點不能再次被遍歷。
第一層:
第二層:
第三層:
能夠看到,每一層遍歷的節點都與根節點距離相同。設 di 表示第 i 個節點與根節點的距離,推導出一個結論:對於先遍歷的節點 i 與後遍歷的節點 j,有 di<=dj。利用這個結論,能夠求解最短路徑等 最優解 問題:第一次遍歷到目的節點,其所通過的路徑爲最短路徑。應該注意的是,使用 BFS 只能求解無權圖的最短路徑。
在程序實現 BFS 時須要考慮如下問題:
計算在網格中從原點到特定點的最短路徑長度
[[1,1,0,1], [1,0,1,0], [1,1,1,1], [1,0,1,1]]
1 表示能夠通過某個位置,求解從 (0, 0) 位置到 (tr, tc) 位置的最短路徑長度。 2 因爲每一個點須要保存x座標,y座標以及長度,因此必需要用一個類將三個屬性封裝起來。 3 因爲bfs每次只將距離加一,因此當位置抵達終點時,此時的距離就是最短路徑了。
private static class Position {
int r, c, length;
public Position(int r, int c, int length) {
this.r = r;
this.c = c;
this.length = length;
}
}
public static 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;
}
複製代碼
組成整數的最小平方數數量
給定正整數 n,找到若干個徹底平方數(好比 1, 4, 9, 16, ...)使得它們的和等於 n。你須要讓組成和的徹底平方數的個數最少。
示例 1:
輸入: n = 12 輸出: 3 解釋: 12 = 4 + 4 + 4. 示例 2:
輸入: n = 13 輸出: 2 解釋: 13 = 4 + 9.
1 能夠將每一個整數當作圖中的一個節點,若是兩個整數之差爲一個平方數,那麼這兩個整數所在的節點就有一條邊。
2 要求解最小的平方數數量,就是求解從節點 n 到節點 0 的最短路徑。
3 首先生成平方數序列放入數組,而後經過隊列,每次減去一個平方數,把剩下的數加入隊列,也就是經過bfs的方式,當此時的數恰好等於平方數,則知足題意,因爲每次循環level加一,因此最後輸出的level就是須要的平方數個數。
本題也能夠用動態規劃求解,在以後動態規劃部分中會再次出現。
public int numSquares(int n) {
List<Integer> squares = generateSquares(n);
Queue<Integer> queue = new LinkedList<>();
boolean[] marked = new boolean[n + 1];
queue.add(n);
marked[n] = true;
int level = 0;
while (!queue.isEmpty()) {
int size = queue.size();
level++;
while (size-- > 0) {
int cur = queue.poll();
for (int s : squares) {
int next = cur - s;
if (next < 0) {
break;
}
if (next == 0) {
return level;
}
if (marked[next]) {
continue;
}
marked[next] = true;
queue.add(cur - s);
}
}
}
return n;
}
/**
* 生成小於 n 的平方數序列
* @return 1,4,9,...
*/
private List<Integer> generateSquares(int n) {
List<Integer> squares = new ArrayList<>();
int square = 1;
int diff = 3;
while (square <= n) {
squares.add(square);
square += diff;
diff += 2;
}
return squares;
}
複製代碼
給定兩個單詞(beginWord 和 endWord)和一個字典,找到從 beginWord 到 endWord 的最短轉換序列的長度。轉換需遵循以下規則:
每次轉換隻能改變一個字母。 轉換過程當中的中間單詞必須是字典中的單詞。 說明:
若是不存在這樣的轉換序列,返回 0。 全部單詞具備相同的長度。 全部單詞只由小寫字母組成。 字典中不存在重複的單詞。 你能夠假設 beginWord 和 endWord 是非空的,且兩者不相同。 示例 1:
輸入: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
輸出: 5
解釋: 一個最短轉換序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的長度 5。 示例 2:
輸入: beginWord = "hit" endWord = "cog" wordList = ["hot","dot","dog","lot","log"]
輸出: 0
解釋: endWord "cog" 不在字典中,因此沒法進行轉換。
找出一條從 beginWord 到 endWord 的最短路徑,每次移動規定爲改變一個字符,而且改變以後的字符串必須在 wordList 中。
單詞臺階問題,亞馬遜面試時考了。
這個參考別人的答案,我會加上解析。
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
//注意此處把首個單詞放到了list的最後面,因此start纔會是N-1。別搞錯了。
wordList.add(beginWord);
int N = wordList.size();
int start = N - 1;
int end = 0;
while (end < N && !wordList.get(end).equals(endWord)) {
end++;
}
if (end == N) {
return 0;
}
List<Integer>[] graphic = buildGraphic(wordList);
return getShortestPath(graphic, start, end);
}
本方法用於把每一個單詞開頭的完整序列保存起來,以便讓bfs過程當中遍歷到全部狀況。
private List<Integer>[] buildGraphic(List<String> wordList) {
int N = wordList.size();
List<Integer>[] graphic = new List[N];
for (int i = 0; i < N; i++) {
graphic[i] = new ArrayList<>();
for (int j = 0; j < N; j++) {
if (isConnect(wordList.get(i), wordList.get(j))) {
graphic[i].add(j);
}
}
}
return graphic;
}
本方法用於上面這個方法鏈接單詞序列時,須要判斷兩個單詞是否只須要一次改變便可,若是不知足要求,則跳過這個單詞。
private boolean isConnect(String s1, String s2) {
int diffCnt = 0;
for (int i = 0; i < s1.length() && diffCnt <= 1; i++) {
if (s1.charAt(i) != s2.charAt(i)) {
diffCnt++;
}
}
return diffCnt == 1;
}
這一步就是經過BFS進行單詞序列鏈接了。
讓初始所在位置入隊,而後去遍歷它能轉變成的單詞,接着進行bfs的遍歷。
最終當next = end時,說明已經能到達最終位置了。因此此時的路徑時最短的。每次出隊都是一個路徑,因此返回path即爲最短路徑長度。
private int getShortestPath(List<Integer>[] graphic, int start, int end) {
Queue<Integer> queue = new LinkedList<>();
boolean[] marked = new boolean[graphic.length];
queue.add(start);
marked[start] = true;
int path = 1;
while (!queue.isEmpty()) {
int size = queue.size();
path++;
while (size-- > 0) {
int cur = queue.poll();
for (int next : graphic[cur]) {
if (next == end) {
return path;
}
if (marked[next]) {
continue;
}
marked[next] = true;
queue.add(next);
}
}
}
return 0;
}
複製代碼
廣度優先搜索一層一層遍歷,每一層獲得的全部新節點,要用隊列存儲起來以備下一層遍歷的時候再遍歷。
而深度優先搜索在獲得一個新節點時立馬對新節點進行遍歷:從節點 0 出發開始遍歷,獲得到新節點 6 時,立馬對新節點 6 進行遍歷,獲得新節點 4;如此反覆以這種方式遍歷新節點,直到沒有新節點了,此時返回。返回到根節點 0 的狀況是,繼續對根節點 0 進行遍歷,獲得新節點 2,而後繼續以上步驟。
從一個節點出發,使用 DFS 對一個圖進行遍歷時,可以遍歷到的節點都是從初始節點可達的,DFS 經常使用來求解這種 可達性 問題。
在程序實現 DFS 時須要考慮如下問題:
棧:用棧來保存當前節點信息,當遍歷新節點返回時可以繼續遍歷當前節點。可使用遞歸棧。 標記:和 BFS 同樣一樣須要對已經遍歷過的節點進行標記。
給定一個包含了一些 0 和 1的非空二維數組 grid , 一個 島嶼 是由四個方向 (水平或垂直) 的 1 (表明土地) 構成的組合。你能夠假設二維矩陣的四個邊緣都被水包圍着。
找到給定的二維數組中最大的島嶼面積。(若是沒有島嶼,則返回面積爲0。)
示例 1:
[[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]] 對於上面這個給定矩陣應返回 6。注意答案不該該是11,由於島嶼只能包含水平或垂直的四個方向的‘1’。
示例 2:
[[0,0,0,0,0,0,0,0]] 對於上面這個給定的矩陣, 返回 0。
注意: 給定的矩陣grid 的長度和寬度都不超過 50。
//只須要從每一個1出發,而後遍歷相連的全部1,獲得總和,更新最大值便可。
public static int maxAreaOfIsland(int[][] grid) {
int [][]visit = new int[grid.length][grid[0].length];
int max = 0;
for (int i = 0;i < grid.length;i ++) {
for (int j = 0;j < grid[0].length;j ++) {
if (grid[i][j] == 1) {
max = Math.max(max, dfs(grid, i, j, visit, 0));
}
}
}
return max;
}
//經過遞歸進行了各個方向的可達性遍歷,因而能夠遍歷到全部的1,而後更新最大值。
public static int dfs(int [][]grid, int x, int y, int [][]visit, int count) {
if (x < 0 || x > grid.length - 1 || y < 0 || y > grid[0].length - 1) {
return count;
}
if (visit[x][y] == 1 || grid[x][y] == 0) {
return count;
}
visit[x][y] = 1;
count ++;
count += dfs(grid, x + 1, y, visit, 0);
count += dfs(grid, x - 1, y, visit, 0);
count += dfs(grid, x, y + 1, visit, 0);
count += dfs(grid, x, y - 1, visit, 0);
return count;
}
複製代碼
給定一個由 '1'(陸地)和 '0'(水)組成的的二維網格,計算島嶼的數量。一個島被水包圍,而且它是經過水平方向或垂直方向上相鄰的陸地鏈接而成的。你能夠假設網格的四個邊均被水包圍。
示例 1:
輸入: 11110 11010 11000 00000
輸出: 1 示例 2:
輸入: 11000 11000 00100 00011
輸出: 3
public class 圖的連通份量個數 {
static int count = 0;
public int findCircleNum(int[][] M) {
count = 0;
int []visit = new int[M.length];
Arrays.fill(visit, 0);
for (int i = 0;i < M.length;i ++) {
if (visit[i] == 0) {
dfs(M, i, visit);
count ++;
}
}
return count;
}
//每次訪問把能到達的點標記爲1,而且訪問結束時計數加一。最終獲得島嶼個數。
public void dfs (int [][]M, int j, int []visit) {
for (int i = 0;i < M.length;i ++) {
if (M[j][i] == 1 && visit[i] == 0) {
visit[i] = 1;
dfs(M, i, visit);
}
}
}
}
複製代碼
班上有 N 名學生。其中有些人是朋友,有些則不是。他們的友誼具備是傳遞性。若是已知 A 是 B 的朋友,B 是 C 的朋友,那麼咱們能夠認爲 A 也是 C 的朋友。所謂的朋友圈,是指全部朋友的集合。
給定一個 N * N 的矩陣 M,表示班級中學生之間的朋友關係。若是M[i][j] = 1,表示已知第 i 個和 j 個學生互爲朋友關係,不然爲不知道。你必須輸出全部學生中的已知的朋友圈總數。
示例 1:
輸入: [[1,1,0], [1,1,0], [0,0,1]] 輸出: 2 說明:已知學生0和學生1互爲朋友,他們在一個朋友圈。 第2個學生本身在一個朋友圈。因此返回2。 示例 2:
輸入: [[1,1,0], [1,1,1], [0,1,1]] 輸出: 1 說明:已知學生0和學生1互爲朋友,學生1和學生2互爲朋友,因此學生0和學生2也是朋友,因此他們三個在一個朋友圈,返回1。 注意:
N 在[1,200]的範圍內。 對於全部學生,有M[i][i] = 1。 若是有M[i][j] = 1,則有M[j][i] = 1。
這題的答案是這樣的:
private int n;
public int findCircleNum(int[][] M) {
n = M.length;
int circleNum = 0;
boolean[] hasVisited = new boolean[n];
for (int i = 0; i < n; i++) {
if (!hasVisited[i]) {
dfs(M, i, hasVisited);
circleNum++;
}
}
return circleNum;
}
private void dfs(int[][] M, int i, boolean[] hasVisited) {
hasVisited[i] = true;
for (int k = 0; k < n; k++) {
if (M[i][k] == 1 && !hasVisited[k]) {
dfs(M, k, hasVisited);
}
}
}
複製代碼
可是個人作法跟他同樣,卻會遞歸棧溢出,我只是把boolean判斷換成了int判斷,有點奇怪,還望指教。
// private static int n;
// public static int findCircleNum(int[][] M) {
// n = M.length;
// int cnt = 0 ;
// int []visit = new int[n];
// for (int i = 0;i < M.length;i ++) {
// if(visit[i] == 0) {
// dfs(M, visit, i);
// cnt ++;
// }
// }
// return cnt;
// }
// public static void dfs(int[][]M, int[] visit, int i) {
// visit[i] = 1;
// for (int j = 0;j < M.length;j ++) {
// if(visit[j] == 0 && M[i][j] == 1) {
// dfs(M, visit, i);
// }
// }
// }
複製代碼
給定一個二維的矩陣,包含 'X' 和 'O'(字母 O)。
找到全部被 'X' 圍繞的區域,並將這些區域裏全部的 'O' 用 'X' 填充。
示例:
X X X X X O O X X X O X X O X X 運行你的函數後,矩陣變爲:
X X X X X X X X X X X X X O X X 解釋:
被圍繞的區間不會存在於邊界上,換句話說,任何邊界上的 'O' 都不會被填充爲 'X'。 任何不在邊界上,或不與邊界上的 'O' 相連的 'O' 最終都會被填充爲 'X'。若是兩個元素在水平或垂直方向相鄰,則稱它們是「相連」的。
參考大佬答案:頗有意思的解法
1 咱們是要把X包圍的O變成X可是有一個很麻煩的問題就是,如何判斷O被X徹底包住,這是很是難解決的。
2 因而換一種思路,把不被X包住的那些O找出來,剩下的不就是X了嗎。
3 不被X包住的O,首先它們的起點必定是在邊緣處,因此咱們從邊緣處找出一個O,而後從O出發,找到全部相連的O,把它們變成T(爲了避免跟裏面的O混淆)
4 最後遍歷一次棋盤,把T變成O,把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[] d : direction) {
dfs(board, r + d[0], c + d[1]);
}
}
複製代碼
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). 左邊和上邊是太平洋,右邊和下邊是大西洋,內部的數字表明海拔,海拔高的地方的水可以流到低的地方,求解水可以流到太平洋和大西洋的全部位置。
1 若是說上一題已經頗有趣了,這一題能夠說是更奇葩了。 力扣中國甚至沒有翻譯這道題。根據題意,咱們要求的是水能流到太平洋和大西洋的全部點。
2 首先,在大西洋和 太平洋兩邊的水必定能夠分別流入這兩個海洋。 咱們用一個數組canreach[i][j]來表達可以流入到海洋。因此我麼須要兩個數組。
3 而後從海洋邊上的水開始進行dfs,遇到海拔比本身高的水就把它也設置成canreach便可,因而咱們就能夠獲得兩個數組。最後遍歷兩個數組,都知足的點就是結果了。
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;
}
m = matrix.length;
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[] d : direction) {
int nextR = d[0] + r;
int nextC = d[1] + c;
if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n
|| matrix[r][c] > matrix[nextR][nextC]) {
continue;
}
dfs(nextR, nextC, canReach);
}
}
複製代碼
Backtracking(回溯)屬於 DFS。
普通 DFS 主要用在 可達性問題 ,這種問題只須要執行到特色的位置而後返回便可。 而 Backtracking 主要用於求解 排列組合 問題,例若有 { 'a','b','c' } 三個字符,求解全部由這三個字符排列獲得的字符串,這種問題在執行到特定的位置返回以後還會繼續執行求解過程。 由於 Backtracking 不是當即就返回,而要繼續求解,所以在程序實現時,須要注意對元素的標記問題:
在訪問一個新元素進入新的遞歸調用時,須要將新元素標記爲已經訪問,這樣才能在繼續遞歸調用時不用重複訪問該元素; 可是在遞歸返回時,須要將元素標記爲未訪問,由於只須要保證在一個遞歸鏈中不一樣時訪問一個元素,能夠訪問已經訪問過可是不在當前遞歸鏈中的元素。
給定一個僅包含數字 2-9 的字符串,返回全部它能表示的字母組合。
給出數字到字母的映射以下(與電話按鍵相同)。注意 1 不對應任何字母。
示例:
輸入:"23" 輸出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
解析:回溯法通常用於須要保存結果集的dfs,好比迷宮路徑,走到頭若是沒有達到終點則回頭。而通常的dfs走到頭就開始嘗試下一種狀況,由於它沒有保存結果集。
1 本題須要把數字鍵盤和字母作一個映射,使用數組是一個不錯的辦法。而使用hashmap可能會顯得有點臃腫。
2 接着咱們可使用String或者Stringbuilder保存結果,因爲string不會被改變,因此咱們不須要維持其狀態,直接遞歸便可。
而使用stringbuilder則須要在dfs先後維護相應變化。
class Solution {
public List<String> letterCombinations(String digits) {
if (digits.equals("")) {
return new ArrayList();
}
List<String> list = new ArrayList<>();
String []arr = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
dfs(list, arr, new StringBuilder(), 0, digits);
return list;
}
public void dfs(List<String> list, String []arr, StringBuilder sb, int cur, String digits) {
if (cur == digits.length()) {
list.add(sb.toString());
return;
}
int num = Integer.parseInt(digits.substring(cur, cur + 1));
for (int i = 0;i < arr[num].length();i ++) {
sb.append(arr[num].charAt(i));
dfs(list, arr, sb, cur + 1, digits);
sb.deleteCharAt(sb.length() - 1);
}
}
}
複製代碼
給定一個只包含數字的字符串,復原它並返回全部可能的 IP 地址格式。
示例:
輸入: "25525511135" 輸出: ["255.255.11.135", "255.255.111.35"]
解析:本題的解題思路不難,從左到右拼湊ip地址便可
可是須要判斷每一個段的ip是否有效,包括開頭不能是0,長度<=3,大小<=255等。
下面是一個暴力解法,4重循環能夠遍歷全部狀況。
public List<String> restoreIpAddresses(String s) {
int i,j,k,m;
ArrayList <String> list = new ArrayList<>();
int len = s.length();
for (i = 1;i< 4 && i < len - 2;i ++) {
for (j = i + 1;j < i + 4 && j < len - 1;j ++) {
for (m = j + 1;m < j + 4 && m < len;m ++) {
//substring後面的下標是不算在內的。
String s1 = s.substring(0,i);
String s2 = s.substring(i,j);
String s3 = s.substring(j,m);
String s4 = s.substring(m,len);
if(isValid(s1) && isValid(s2) && isValid(s3) && isValid(s4))
{
list.add(s1 + '.' + s2 + '.' + s3 + '.' + s4);
}
}
}
}
return list;
}
public boolean isValid(String s) {
if (s.length() == 0 || s.length() > 3 || s.charAt(0) == '0' && s.length() > 1
|| Integer.parseInt(s) > 255) {
return false;
}else return true;
}
複製代碼
解析: 使用回溯法來作該題。首先從頭開始,k表明ip的段數,s表明總長度,每次使用一個數字則s -= 1,直到s = 0而且k = 4時,符合題意,加入結果集。
固然在for循環中,i 從0到2進行遍歷。而後更新當前結果。
public List<String> restoreIpAddresses(String s) {
List<String> addresses = new ArrayList<>();
StringBuilder tempAddress = new StringBuilder();
doRestore(0, tempAddress, addresses, s);
return addresses;
}
private void doRestore(int k, StringBuilder tempAddress, List<String> addresses, String s) {
if (k == 4 || s.length() == 0) {
if (k == 4 && s.length() == 0) {
addresses.add(tempAddress.toString());
}
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) {
if (tempAddress.length() != 0) {
part = "." + part;
}
tempAddress.append(part);
doRestore(k + 1, tempAddress, addresses, s.substring(i + 1));
tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length());
}
}
}
複製代碼
給定一個二維網格和一個單詞,找出該單詞是否存在於網格中。
單詞必須按照字母順序,經過相鄰的單元格內的字母構成,其中「相鄰」單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母不容許被重複使用。
示例: board = [ ['A','B','C','E'], ['S','F','C','S'], ['A','D','E','E'] ]
給定 word = "ABCCED", 返回 true. 給定 word = "SEE", 返回 true. 給定 word = "ABCB", 返回 false.
本題使用dfs中的回溯法,主要思路也是遞歸。 但比較麻煩的一點是當遍歷到知足條件的狀況時,不該該繼續其餘遞歸分支了。有一個辦法是使用for循環進行方向遍歷,只要有一個分支知足就返回true。
private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
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;
boolean[][] hasVisited = new boolean[m][n];
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (backtracking(0, r, c, hasVisited, board, word)) {
return true;
}
}
}
return false;
}
private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) {
if (curLen == word.length()) {
return true;
}
if (r < 0 || r >= m || c < 0 || c >= n
|| board[r][c] != word.charAt(curLen) || visited[r][c]) {
return false;
}
visited[r][c] = true;
for (int[] d : direction) {
//此處完成了剪枝,若是分支i知足則不會走後續分支。
if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) {
return true;
}
}
visited[r][c] = false;
return false;
}
複製代碼
解析:方法二:
首先使用dfs進行遞歸遍歷,當知足條件時,直接拋出異常退出循環,腦洞比較大,可是確實可行,能夠用於遞歸時的剪枝 。
可是實際上時間複雜度要比第一種方法大得多,這裏只是提供一個終止遞歸的思路。
class Solution {
static class StopMsgException extends RuntimeException {
}
static boolean flag;
public boolean exist(char[][] board, String word) {
if (word.equals("")) {
return true;
}
int [][]visit = new int[board.length][board[0].length];
flag = false;
try {
for (int i = 0;i < board.length;i ++) {
for (int j = 0;j < board[0].length;j ++) {
if (word.charAt(0) == board[i][j]) {
dfs(board, word, visit, i, j);
}
}
}
} catch (StopMsgException e) {
System.out.println(e);
}
return flag;
}
public void dfs(char[][] board, String word, int [][]visit, int i, int j) {
if (word.equals("")) {
flag = true;
throw new StopMsgException();
}
if (i > board.length - 1 || i < 0 || j > board[0].length - 1 || j < 0) {
return;
}
if (visit[i][j] == 1) {
return;
}
if (word.charAt(0) == board[i][j]) {
visit[i][j] = 1;
//沒有進行剪枝,效率比較低,因而在遞歸判斷條件中進行剪枝,避免後續沒必要要的遞歸。
dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i + 1, j);
dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i - 1, j);
dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i, j - 1);
dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i, j + 1);
visit[i][j] = 0;
}
}
}
複製代碼
給定一個沒有重複數字的序列,返回其全部可能的全排列。
示例:
輸入: [1,2,3] 輸出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
解析:經典而不失優雅。dfs中使用for循環遍歷,visit和list記錄狀態進行回溯,知足條件時加入集合並返回。
class Solution {
static List<List<Integer>> Alllist = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
Alllist.clear();
List<Integer> list = new ArrayList<>();
int []visit = new int[nums.length];
dfs(list, nums, visit);
return Alllist;
}
public void dfs(List<Integer> list, int []nums, int []visit) {
if (list.size() == nums.length) {
Alllist.add(new ArrayList(list));
return;
}
for (int i = 0;i < nums.length;i ++) {
if (visit[i] == 0) {
visit[i] = 1;
list.add(nums[i]);
dfs(list, nums, visit);
visit[i] = 0;
list.remove(list.size() - 1);
}
}
}
}
複製代碼
給定一個可包含重複數字的序列,返回全部不重複的全排列。
示例:
輸入: [1,1,2] 輸出: [ [1,1,2], [1,2,1], [2,1,1] ]
解析:本題在上一題的基礎上加上了一個條件,數組中有重複數字,可是結果集返回的是不一樣的排列。
這就要求咱們對相同數字作過濾了。
咱們要明確的是,不重複排列,要求的是相同數字不能在同一次遞歸中出如今同一個位置。
好比 1 2 1 和1 2 1,這裏開頭的兩個1可能分別對應數組下標的0和1,但只能取一個。
因此咱們加了一個條件,當該數沒有被訪問過期,咱們直接過濾掉全部重複的數,只把當前數做爲本次遞歸的首位數。
class Solution {
static List<List<Integer>> Alllist = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Alllist.clear();
List<Integer> list = new ArrayList<>();
int []visit = new int[nums.length];
Arrays.sort(nums);
dfs(list, nums, visit);
return Alllist;
}
public void dfs(List<Integer> list, int []nums, int []visit) {
if (list.size() == nums.length) {
Alllist.add(new ArrayList(list));
return;
}
for (int i = 0;i < nums.length;i ++) {
if(i - 1 >= 0 && nums[i] == nums[i - 1] && visit[i - 1] == 0) {
continue;
}
if (visit[i] == 0) {
visit[i] = 1;
list.add(nums[i]);
dfs(list, nums, visit);
visit[i] = 0;
list.remove(list.size() - 1);
}
}
}
}
複製代碼
給定兩個整數 n 和 k,返回 1 ... n 中全部可能的 k 個數的組合。
示例:
輸入: n = 4, k = 2 輸出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
解析:組合長度固定爲k,不能有重複組合,因此咱們規定順序是從小到大進行組合,這樣的話就不會有重複狀況出現了。
class Solution {
static List<List<Integer>> Alllist = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
Alllist.clear();
List<Integer> list = new ArrayList<>();
dfs(list, n, k, 1);
return Alllist;
}
public void dfs(List<Integer> list, int n, int k, int cur) {
if (list.size() == k) {
Alllist.add(new ArrayList(list));
return;
}
for (int i = cur;i <= n;i ++) {
list.add(i);
dfs(list, n, k, i + 1);
list.remove(list.size() - 1);
}
}
}
複製代碼
給定一個無重複元素的數組 candidates 和一個目標數 target ,找出 candidates 中全部可使數字和爲 target 的組合。
candidates 中的數字能夠無限制重複被選取。
說明:
全部數字(包括 target)都是正整數。 解集不能包含重複的組合。 示例 1:
輸入: candidates = [2,3,6,7], target = 7, 所求解集爲: [ [7], [2,2,3] ] 示例 2:
輸入: candidates = [2,3,5], target = 8, 所求解集爲: [ [2,2,2,2], [2,3,3], [3,5] ]
解析:因爲組合中沒有重複數字,而且每一個數能夠出現屢次,因此咱們依然能夠按順序進行組合,每次i取本身或比本身更大的數組下標便可。
class Solution {
static List<List<Integer>> Alllist = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Alllist.clear();
List<Integer> list = new ArrayList<>();
Arrays.sort(candidates);
dfs(list, candidates, 0, target, 0);
return Alllist;
}
public void dfs(List<Integer> list, int [] candidates, int sum, int target, int cur) {
if (sum == target) {
Alllist.add(new ArrayList(list));
return;
}
for (int i = cur;i < candidates.length;i ++) {
if (sum + candidates[i] <= target) {
list.add(candidates[i]);
dfs(list, candidates, sum + candidates[i], target, i);
list.remove(list.size() - 1);
}
}
}
複製代碼
給定一個數組 candidates 和一個目標數 target ,找出 candidates 中全部可使數字和爲 target 的組合。
candidates 中的每一個數字在每一個組合中只能使用一次。
說明:
全部數字(包括目標數)都是正整數。 解集不能包含重複的組合。 示例 1:
輸入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集爲: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ] 示例 2:
輸入: candidates = [2,5,2,1,2], target = 5, 所求解集爲: [ [1,2,2], [5] ]
解析:本題相似排序的第二題,組合求和,可是組合不能重複,而數組中容許重複數字。
因此咱們依然要過濾掉同一位置上的重複數字。
class Solution {
static List<List<Integer>> Alllist = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Alllist.clear();
List<Integer> list = new ArrayList<>();
int []visit = new int[candidates.length];
Arrays.sort(candidates);
dfs(list, candidates, 0, target, 0, visit);
return Alllist;
}
public void dfs(List<Integer> list, int [] candidates, int sum, int target, int cur, int[] visit) {
if (sum == target) {
Alllist.add(new ArrayList(list));
return;
}
for (int i = cur;i < candidates.length;i ++) {
if (i - 1 >= 0 && candidates[i] == candidates[i - 1] && visit[i - 1] == 0) {
continue;
}
if (sum + candidates[i] <= target) {
visit[i] = 1;
list.add(candidates[i]);
dfs(list, candidates, sum + candidates[i], target, i + 1, visit);
list.remove(list.size() - 1);
visit[i] = 0;
}
}
}
}
複製代碼
找出全部相加之和爲 n 的 k 個數的組合。組合中只容許含有 1 - 9 的正整數,而且每種組合中不存在重複的數字。
說明:
全部數字都是正整數。 解集不能包含重複的組合。 示例 1:
輸入: k = 3, n = 7 輸出: [[1,2,4]] 示例 2:
輸入: k = 3, n = 9 輸出: [[1,2,6], [1,3,5], [2,3,4]]
解析:與前面相似,沒啥難度
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
Alllist.clear();
List<Integer> list = new ArrayList<>();
dfs(list, 0, n, k, 1);
return Alllist;
}
static List<List<Integer>> Alllist = new ArrayList<>();
public void dfs(List<Integer> list, int sum, int n, int k, int cur) {
if (sum == n && list.size() == k) {
Alllist.add(new ArrayList(list));
return;
}
for (int i = cur;i <= 9;i ++) {
if (sum + i <= n && list.size() < k) {
list.add(i);
dfs(list, sum + i, n, k, i + 1);
list.remove(list.size() - 1);
}
}
}
}
複製代碼
給定一組不含重複元素的整數數組 nums,返回該數組全部可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入: nums = [1,2,3] 輸出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
解析:本題要注意的是,數字不重複,因此咱們能夠按順序來查找子集,而且遞歸結束條件是當cur = nums.length。時結束。
class Solution {
List<List<Integer>> Alllist = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
List<Integer> list = new ArrayList<>();
dfs(nums, 0, list);
return Alllist;
}
public void dfs(int []nums, int cur, List<Integer> list) {
if (cur <= nums.length) {
Alllist.add(new ArrayList(list));
} else {
return;
}
for (int i = cur;i < nums.length;i ++) {
list.add(nums[i]);
dfs(nums, i + 1, list);
list.remove(list.size() - 1);
}
}
}
複製代碼
給定一個可能包含重複元素的整數數組 nums,返回該數組全部可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入: [1,2,2] 輸出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
class Solution {
List<List<Integer>> Alllist = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<Integer> list = new ArrayList<>();
Arrays.sort(nums);
int []visit = new int[nums.length];
dfs(nums, 0, visit, list);
return Alllist;
}
public void dfs(int []nums, int cur, int []visit, List<Integer> list) {
if (cur <= nums.length) {
Alllist.add(new ArrayList(list));
} else {
return;
}
for (int i = cur;i < nums.length;i ++) {
if (i > 0 && visit[i - 1] == 0 && nums[i] == nums[i - 1]) {
continue;
}
if (visit[i] == 0) {
visit[i] = 1;
list.add(nums[i]);
dfs(nums, i + 1,visit, list);
list.remove(list.size() - 1);
visit[i] = 0;
}
}
}
}
複製代碼
給定一個字符串 s,將 s 分割成一些子串,使每一個子串都是迴文串。
返回 s 全部可能的分割方案。
示例:
輸入: "aab" 輸出: [ ["aa","b"], ["a","a","b"] ]
解析: 這題其實也相似,只不過比較巧妙一點。
1 要把字符串分割成迴文字符串組合,那麼咱們要存的其實仍是全部的字符串,因此返回條件仍是當cur = s.length()。
2 字符串分割,首先在循環中遍歷知足迴文的第一個子串,在此基礎上找下一個迴文子串,達到了剪枝的目的。
class Solution {
List<List<String>> allList = new ArrayList<>();
public List<List<String>> partition(String s) {
List<String> list = new ArrayList<>();
dfs(s, 0, list);
return allList;
}
public void dfs(String s, int cur, List<String> list) {
if (cur == s.length()) {
allList.add(new ArrayList(list));
return;
}
for (int i = cur;i < s.length();i ++) {
String str = s.substring(cur, i + 1);
if (legal(str)) {
list.add(str);
dfs(s, i + 1, list);
list.remove(list.size() - 1);
}
}
}
public boolean legal(String s) {
int i = 0,j = s.length() - 1;
while (i < j) {
if (s.charAt(i) == s.charAt(j)) {
i ++;
j --;
}else {
return false;
}
}
return true;
}
}
複製代碼
動態規劃 遞歸和動態規劃都是將原問題拆成多個子問題而後求解,他們之間最本質的區別是,動態規劃保存了子問題的解,避免重複計算。
爬樓梯
題目描述:有 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 <= 2) {
return n;
}
int pre2 = 1, pre1 = 2;
for (int i = 2; i < n; i++) {
int cur = pre1 + pre2;
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
複製代碼
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有必定的現金,影響你偷竊的惟一制約因素就是相鄰的房屋裝有相互連通的防盜系統,若是兩間相鄰的房屋在同一夜被小偷闖入,系統會自動報警。
給定一個表明每一個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的狀況下,可以偷竊到的最高金額。
示例 1:
輸入: [1,2,3,1] 輸出: 4 解釋: 偷竊 1 號房屋 (金額 = 1) ,而後偷竊 3 號房屋 (金額 = 3)。 偷竊到的最高金額 = 1 + 3 = 4 。
解析:注意搶劫的時候,對於 [1,2,3,4],要麼偷1,要麼投2。 可是對於4來講,前面的偷法能夠分爲兩種,要麼偷1不偷2和3,要麼偷2不偷3。
舉個例子[100,2,3,4]偷4的時候,咱們應該放棄2和3,直接偷100。同時咱們不用再日後判斷了,由於前面也考慮到了這種狀況。
public static int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1)return nums[0];
if (nums.length == 2)return nums[0] > nums[1] ? nums[0] : nums[1];
int []dp = new int[nums.length + 1];
//dp表明最右只搶到第n家時的總錢數。
dp[1] = nums[0];
dp[2] = nums[0] > nums[1] ? nums[0] : nums[1];
for (int i = 3;i <= nums.length;i ++) {
dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
}
return dp[nums.length];
}
複製代碼
你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有必定的現金。這個地方全部的房屋都圍成一圈,這意味着第一個房屋和最後一個房屋是緊挨着的。同時,相鄰的房屋裝有相互連通的防盜系統,若是兩間相鄰的房屋在同一夜被小偷闖入,系統會自動報警。
給定一個表明每一個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的狀況下,可以偷竊到的最高金額。
示例 1:
輸入: [2,3,2] 輸出: 3 解釋: 你不能先偷竊 1 號房屋(金額 = 2),而後偷竊 3 號房屋(金額 = 2), 由於他們是相鄰的。 示例 2:
輸入: [1,2,3,1] 輸出: 4 解釋: 你能夠先偷竊 1 號房屋(金額 = 1),而後偷竊 3 號房屋(金額 = 3)。 偷竊到的最高金額 = 1 + 3 = 4 。
解析:環形區域主要考慮兩種狀況,第一家要搶的話,最後一家必定不能搶,第一家不搶的話,最後一家能夠搶也能夠不搶。而後根據上一題的作法使用dp便可。
public static int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1)return nums[0];
if (nums.length == 2)return nums[0] > nums[1] ? nums[0] : nums[1];
int []dp = new int[nums.length + 1];
//dp表明最右只搶到第n家時的總錢數。
//若是搶了第一家
dp[1] = nums[0];
dp[2] = nums[0] > nums[1] ? nums[0] : nums[1];
for (int i = 3;i < nums.length;i ++) {
dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
}
int max = dp[nums.length - 1];
//若是不搶第一家
dp[1] = 0;
dp[2] = nums[1];
for (int i = 3;i <= nums.length;i ++) {
dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
}
if (dp[nums.length] > max)max = dp[nums.length];
return max;
}
複製代碼
母牛生產
程序員代碼面試指南-P181
題目描述:假設農場中成熟的母牛每一年都會生 1 頭小母牛,而且永遠不會死。第一年有 1 只小母牛,從第二年開始,母牛開始生小母牛。每隻小母牛 3 年以後成熟又能夠生小母牛。給定整數 N,求 N 年後牛的數量。
第 i 年成熟的牛的數量爲:
信件錯排
題目描述:有 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 種取值,所以共有 (i-1)*dp[i-1] 種錯誤裝信方式。 綜上所述,錯誤裝信數量方式數量爲:
dp[N] 即爲所求。
求矩陣路徑和,走法總數或者最短距離等
給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和爲最小。
說明:每次只能向下或者向右移動一步。
示例:
輸入: [ [1,3,1], [1,5,1], [4,2,1] ] 輸出: 7 解釋: 由於路徑 1→3→1→1→1 的總和最小。
class Solution {
public int minPathSum(int[][] grid) {
int [][]dp = new int[grid.length][grid[0].length];
dp[0][0] = grid[0][0];
for (int i = 1;i < grid.length;i ++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int i = 1;i < grid[0].length;i ++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
for (int i = 1;i < grid.length;i ++) {
for (int j = 1;j < grid[0].length;j ++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[grid.length - 1][grid[0].length - 1];
}
}
複製代碼
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲「Start」 )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲「Finish」)。
問總共有多少條不一樣的路徑?
例如,上圖是一個7 x 3 的網格。有多少可能的路徑?
說明:m 和 n 的值均不超過 100。
示例 1:
輸入: m = 3, n = 2 輸出: 3 解釋: 從左上角開始,總共有 3 條路徑能夠到達右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右 示例 2:
輸入: m = 7, n = 3 輸出: 28
class Solution {
public int uniquePaths(int m, int n) {
int [][]dp = new int[m][n];
for (int i = 0; i < m;i ++) {
dp[i][0] = 1;
}
for (int i = 0; i < n;i ++) {
dp[0][i] = 1;
}
for (int i = 1;i < m;i ++) {
for (int j = 1;j < n;j ++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
複製代碼
給定一個整數數組 nums ,找到一個具備最大和的連續子數組(子數組最少包含一個元素),返回其最大和。
示例:
輸入: [-2,1,-3,4,-1,2,1,-5,4], 輸出: 6 解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。 進階:
若是你已經實現複雜度爲 O(n) 的解法,嘗試使用更爲精妙的分治法求解。
解析:設置數組dp表示結尾爲第i個數的最大子數組和是多少。而後方程是dp[i] = Math.max(dp[i - 1] + nums[i], nums[i])
最後遍歷一下dp數組找到最大子序和便可,
class Solution {
public int maxSubArray(int[] nums) {
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
int []dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1;i < nums.length;i ++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
}
int max = nums[0];
for (int i = 0;i < dp.length;i ++) {
max = Math.max(max, dp[i]);
}
return max;
}
}
複製代碼
給定一個正整數 n,將其拆分爲至少兩個正整數的和,並使這些整數的乘積最大化。 返回你能夠得到的最大乘積。
例如,給定 n = 2,返回1(2 = 1 + 1);給定 n = 10,返回36(10 = 3 + 3 + 4)。
注意:你能夠假設 n 不小於2且不大於58。
參考答案:
解析:對於一個數來講,拆分包括兩種狀況,一種是把i拆成j和i - j,另外一種是i拆成j與i - j這個數所能構成的組合。 因此方程就是dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
因爲dp表明的是數字n可以獲得的最大乘積,爲了知足上面這個式子,dp[2] = dp[1] * 1,因此dp[1] = 1;
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];
}
複製代碼
給定正整數 n,找到若干個徹底平方數(好比 1, 4, 9, 16, ...)使得它們的和等於 n。你須要讓組成和的徹底平方數的個數最少。
示例 1:
輸入: n = 12 輸出: 3 解釋: 12 = 4 + 4 + 4. 示例 2:
輸入: n = 13 輸出: 2 解釋: 13 = 4 + 9.
解析:恕在下直言,有些DP的題目也是神仙題,確實不容易想到。dp表明的和爲n的徹底平方數所需的最少個數。
本題還能夠用bfs來作,詳細可參見上面的bfs部分。
dp[1] = 1。因此對於每一個i,咱們都要遍歷全部小於等於它的平方數,以便找到所需個數最少的個數。
方程寫做:對於每一個數i有一個最小值dp[i] min = Math.min(min, dp[i - square] + 1); 最後dp[n]即爲所求。
很是巧妙,值得學習和記憶。
public int numSquares(int n) {
List<Integer> squareList = generateSquareList(n);
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for (int square : squareList) {
if (square > i) {
break;
}
min = Math.min(min, dp[i - square] + 1);
}
dp[i] = min;
}
return dp[n];
}
private List<Integer> generateSquareList(int n) {
List<Integer> squareList = new ArrayList<>();
int diff = 3;
int square = 1;
while (square <= n) {
squareList.add(square);
square += diff;
diff += 2;
}
return squareList;
}
複製代碼
最長遞增子序列 已知一個序列 {S1, S2,...,Sn} ,取出若干數組成新的序列 {Si1, Si2,..., Sim},其中 i一、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,即:
解析: dp數組表明以第i個元素做爲序列結尾時的最大遞增序列,因爲以前的序列是可選的,因此咱們遍歷以前全部知足狀況的點(也就是知足nums[i] > nums[j]點),找到前面最長的遞增序列便可。
轉移方程是 dp[i] = Math.max(dp[j] + 1, dp[i]);
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0)return 0;
if (nums.length == 1)return 1;
int []dp = new int[nums.length];
dp[0] = 1;
for (int i = 1;i < nums.length;i ++) {
for (int j = 0;j < i;j ++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
}else {
dp[i] = Math.max(dp[i], 1);
}
}
}
int max = 0;
for (int i = 0;i < dp.length;i ++) {
max = Math.max(max, dp[i]);
}
return max;
}
}
複製代碼
最長公共子序列 對於兩個子序列 S1 和 S2,找出它們最長的公共子序列。
定義一個二維數組 dp 用來存儲最長公共子序列的長度,其中 dp[i][j] 表示 S1 的前 i 個字符與 S2 的前 j 個字符最長公共子序列的長度。考慮 S1i 與 S2j 值是否相等,分爲兩種狀況:
當 S1i==S2j 時,那麼就能在 S1 的前 i-1 個字符與 S2 的前 j-1 個字符最長公共子序列的基礎上再加上 S1i 這個值,最長公共子序列長度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。 當 S1i != S2j 時,此時最長公共子序列爲 S1 的前 i-1 個字符和 S2 的前 j 個字符最長公共子序列,與 S1 的前 i 個字符和 S2 的前 j-1 個字符最長公共子序列,它們的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 綜上,最長公共子序列的狀態轉移方程爲:
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]);
}
複製代碼
對於長度爲 N 的序列 S1 和 長度爲 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最長公共子序列長度。
與最長遞增子序列相比,最長公共子序列有如下不一樣點:
針對的是兩個序列,求它們的最長公共子序列。 在最長遞增子序列中,dp[i] 表示以 Si 爲結尾的最長遞增子序列長度,子序列必須包含 Si ;在最長公共子序列中,dp[i][j] 表示 S1 中前 i 個字符與 S2 中前 j 個字符的最長公共子序列長度,不必定包含 S1i 和 S2j 。 在求最終解時,最長公共子序列中 dp[N][M] 就是最終解,而最長遞增子序列中 dp[N] 不是最終解,由於以 SN 爲結尾的最長遞增子序列不必定是整個序列最長遞增子序列,須要遍歷一遍 dp 數組找到最大者。
public int lengthOfLCS(int[] nums1, int[] nums2) {
int n1 = nums1.length, n2 = nums2.length;
int[][] dp = new int[n1 + 1][n2 + 1];
for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[n1][n2];
}
複製代碼
0-1 揹包 有一個容量爲 N 的揹包,要用這個揹包裝下物品的價值最大,這些物品有兩個屬性:體積 w 和價值 v。
定義一個二維數組 dp 存儲最大價值,其中 dp[i][j] 表示前 i 件物品體積不超過 j 的狀況下能達到的最大價值。設第 i 件物品體積爲 w,價值爲 v,根據第 i 件物品是否添加到揹包中,能夠分兩種狀況討論:
第 i 件物品沒添加到揹包,整體積不超過 j 的前 i 件物品的最大價值就是整體積不超過 j 的前 i-1 件物品的最大價值,dp[i][j] = dp[i-1][j]。 第 i 件物品添加到揹包中,dp[i][j] = dp[i-1][j-w] + v。 第 i 件物品可添加也能夠不添加,取決於哪一種狀況下最大價值更大。
綜上,0-1 揹包的狀態轉移方程爲:
public int knapsack(int W, int N, int[] weights, int[] values) {
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
for (int j = 1; j <= W; j++) {
if (j >= w) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
複製代碼
在程序實現時能夠對 0-1 揹包作優化。觀察狀態轉移方程能夠知道,前 i 件物品的狀態僅由前 i-1 件物品的狀態有關,所以能夠將 dp 定義爲一維數組,其中 dp[j] 既能夠表示 dp[i-1][j] 也能夠表示 dp[i][j]。此時,
由於 dp[j-w] 表示 dp[i-1][j-w],所以不能先求 dp[i][j-w],以防止將 dp[i-1][j-w] 覆蓋。也就是說要先計算 dp[i][j] 再計算 dp[i][j-w],在程序實現時須要按倒序來循環求解。
public int knapsack(int W, int N, int[] weights, int[] values) { int[] dp = new int[W + 1]; for (int i = 1; i <= N; i++) { int w = weights[i - 1], v = values[i - 1]; for (int j = W; j >= 1; j--) { if (j >= w) { dp[j] = Math.max(dp[j], dp[j - w] + v); } } } return dp[W]; } 沒法使用貪心算法的解釋
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 變種
徹底揹包:物品數量爲無限個
多重揹包:物品數量有限制
多維費用揹包:物品不只有重量,還有體積,同時考慮這兩種限制
其它:物品之間相互約束或者依賴
劃分數組爲和相等的兩部分
Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11]. 能夠當作一個揹包大小爲 sum/2 的 0-1 揹包問題。
public boolean canPartition(int[] nums) {
int sum = computeArraySum(nums);
if (sum % 2 != 0) {
return false;
}
int W = sum / 2;
boolean[] dp = new boolean[W + 1];
dp[0] = true;
Arrays.sort(nums);
for (int num : nums) { // 0-1 揹包一個物品只能用一次
for (int i = W; i >= num; i--) { // 從後往前,先計算 dp[i] 再計算 dp[i-num]
dp[i] = dp[i] || dp[i - num];
}
}
return dp[W];
}
private int computeArraySum(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
return sum;
}
複製代碼
找零錢的方法數
Example 1: coins = [1, 2, 5], amount = 11 return 3 (11 = 5 + 5 + 1)
Example 2: coins = [2], amount = 3 return -1. 題目描述:給一些面額的硬幣,要求用這些硬幣來組成給定面額的錢數,而且使得硬幣數量最少。硬幣能夠重複使用。
物品:硬幣 物品大小:面額 物品價值:數量 由於硬幣能夠重複使用,所以這是一個徹底揹包問題。
public int coinChange(int[] coins, int amount) {
if (coins == null || coins.length == 0) {
return 0;
}
int[] minimum = new int[amount + 1];
Arrays.fill(minimum, amount + 1);
minimum[0] = 0;
Arrays.sort(coins);
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length && coins[j] <= i; j++) {
minimum[i] = Math.min(minimum[i], minimum[i - coins[j]] + 1);
}
}
return minimum[amount] > amount ? -1 : minimum[amount];
}
複製代碼
只能進行 k 次的股票交易
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if (k >= n / 2) { // 這種狀況下該問題退化爲普通的股票交易問題
int maxProfit = 0;
for (int i = 1; i < n; i++) {
if (prices[i] > prices[i - 1]) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
int[][] maxProfit = new int[k + 1][n];
for (int i = 1; i <= k; i++) {
int localMax = maxProfit[i - 1][0] - prices[0];
for (int j = 1; j < n; j++) {
maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax);
localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]);
}
}
return maxProfit[k][n - 1];
}
複製代碼
只能進行兩次的股票交易
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;
}
複製代碼
須要冷卻期的股票交易
題目描述:交易以後須要有一天的冷卻時間。
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int N = prices.length;
int[] buy = new int[N];
int[] s1 = new int[N];
int[] sell = new int[N];
int[] s2 = new int[N];
s1[0] = buy[0] = -prices[0];
sell[0] = s2[0] = 0;
for (int i = 1; i < N; i++) {
buy[i] = s2[i - 1] - prices[i];
s1[i] = Math.max(buy[i - 1], s1[i - 1]);
sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i];
s2[i] = Math.max(s2[i - 1], sell[i - 1]);
}
return Math.max(sell[N - 1], s2[N - 1]);
}
複製代碼
須要交易費用的股票交易
Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 Output: 8 Explanation: The maximum profit can be achieved by: Buying at prices[0] = 1 Selling at prices[3] = 8 Buying at prices[4] = 4 Selling at prices[5] = 9 The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. 題目描述:每交易一次,都要支付必定的費用。
public int maxProfit(int[] prices, int fee) {
int N = prices.length;
int[] buy = new int[N];
int[] s1 = new int[N];
int[] sell = new int[N];
int[] s2 = new int[N];
s1[0] = buy[0] = -prices[0];
sell[0] = s2[0] = 0;
for (int i = 1; i < N; i++) {
buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i];
s1[i] = Math.max(buy[i - 1], s1[i - 1]);
sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i];
s2[i] = Math.max(s2[i - 1], sell[i - 1]);
}
return Math.max(sell[N - 1], s2[N - 1]);
}
複製代碼
買入和售出股票最大的收益
題目描述:只進行一次交易。
只要記錄前面的最小价格,將這個最小价格做爲買入價格,而後將當前的價格做爲售出價格,查看當前收益是否是最大收益。
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;
}
複製代碼
刪除兩個字符串的字符使它們相等
Input: "sea", "eat" Output: 2 Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea". 能夠轉換爲求兩個字符串的最長公共子序列問題。
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;
}
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return m + n - 2 * dp[m][n];
}
複製代碼
複製粘貼字符
題目描述:最開始只有一個字符 A,問須要多少次操做可以獲得 n 個字符 A,每次操做能夠複製當前全部的字符,或者粘貼。
Input: 3 Output: 3 Explanation: Intitally, we have one character 'A'. In step 1, we use Copy All operation. In step 2, we use Paste operation to get 'AA'. In step 3, we use Paste operation to get 'AAA'.
public int minSteps(int n) {
if (n == 1) return 0;
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0) return i + minSteps(n / i);
}
return n;
}
public int minSteps(int n) {
int[] dp = new int[n + 1];
int h = (int) Math.sqrt(n);
for (int i = 2; i <= n; i++) {
dp[i] = i;
for (int j = 2; j <= h; j++) {
if (i % j == 0) {
dp[i] = dp[j] + dp[i / j];
break;
}
}
}
return dp[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) * ...
給定一個含有數字和運算符的字符串,爲表達式添加括號,改變其運算優先級以求出不一樣的結果。你須要給出全部可能的組合的結果。有效的運算符號包含 +, - 以及 * 。
示例 1:
輸入: "2-1-1" 輸出: [0, 2] 解釋: ((2-1)-1) = 0 (2-(1-1)) = 2 示例 2:
輸入: "23-45" 輸出: [-34, -14, -10, -10, 10] 解釋: (2*(3-(45))) = -34 ((23)-(45)) = -14 ((2(3-4))5) = -10 (2((3-4)5)) = -10 (((23)-4)*5) = 10
Trie,又稱前綴樹或字典樹,用於判斷字符串是否存在或者是否具備某種字符串前綴。
實現一個 Trie
208. Implement Trie (Prefix Tree) (Medium)
class Trie { private class Node { Node[] childs = new Node[26]; boolean isLeaf; } private Node root = new Node(); public Trie() { } public void insert(String word) { insert(word, root); } private void insert(String word, Node node) { if (node == null) return; if (word.length() == 0) { node.isLeaf = true; return; } int index = indexForChar(word.charAt(0)); if (node.childs[index] == null) { node.childs[index] = new Node(); } insert(word.substring(1), node.childs[index]); } public boolean search(String word) { return search(word, root); } private boolean search(String word, Node node) { if (node == null) return false; if (word.length() == 0) return node.isLeaf; int index = indexForChar(word.charAt(0)); return search(word.substring(1), node.childs[index]); } 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 index = indexForChar(prefix.charAt(0)); return startWith(prefix.substring(1), node.childs[index]); } private int indexForChar(char c) { return c - 'a'; } }
實現一個 Trie,用來求前綴和
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 Node { Node[] child = new Node[26]; int value; } private Node root = new Node(); public MapSum() { } public void insert(String key, int val) { insert(key, root, val); } private void insert(String key, Node node, int val) { if (node == null) return; if (key.length() == 0) { node.value = val; return; } int index = indexForChar(key.charAt(0)); if (node.child[index] == null) { node.child[index] = new Node(); } insert(key.substring(1), node.child[index], val); } public int sum(String prefix) { return sum(prefix, root); } private int sum(String prefix, Node node) { if (node == null) return 0; if (prefix.length() != 0) { int index = indexForChar(prefix.charAt(0)); return sum(prefix.substring(1), node.child[index]); } int sum = node.value; for (Node child : node.child) { sum += sum(prefix, child); } return sum; } private int indexForChar(char c) { return c - 'a'; } }
若是能夠用兩種顏色對圖中的節點進行着色,而且保證相鄰的節點顏色不一樣,那麼這個圖就是二分圖。
判斷是否爲二分圖
785. Is Graph Bipartite? (Medium)
Input: [[1,3], [0,2], [1,3], [0,2]] Output: true Explanation: The graph looks like this: 0----1 | | | | 3----2 We can divide the vertices into two groups: {0, 2} and {1, 3}.
Example 2: Input: [[1,2,3], [0,2], [0,1,3], [0,2]] Output: false Explanation: The graph looks like this: 0----1 | \ | | \ | 3----2 We cannot find a way to divide the set of nodes into two independent subsets.
public boolean isBipartite(int[][] graph) { int[] colors = new int[graph.length]; Arrays.fill(colors, -1); for (int i = 0; i < graph.length; i++) { // 處理圖不是連通的狀況 if (colors[i] == -1 && !isBipartite(i, 0, colors, graph)) { return false; } } return true; } private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] graph) { if (colors[curNode] != -1) { return colors[curNode] == curColor; } colors[curNode] = curColor; for (int nextNode : graph[curNode]) { if (!isBipartite(nextNode, 1 - curColor, colors, graph)) { return false; } } return true; }
經常使用於在具備先序關係的任務規劃中。
課程安排的合法性
2, [[1,0]] return true
2, [[1,0],[0,1]] return false
題目描述:一個課程可能會先修課程,判斷給定的先修課程規定是否合法。
本題不須要使用拓撲排序,只須要檢測有向圖是否存在環便可。
public boolean canFinish(int numCourses, int[][] prerequisites) { List[] graphic = new List[numCourses]; for (int i = 0; i < numCourses; i++) { graphic[i] = new ArrayList<>(); } for (int[] pre : prerequisites) { graphic[pre[0]].add(pre[1]); } boolean[] globalMarked = new boolean[numCourses]; boolean[] localMarked = new boolean[numCourses]; for (int i = 0; i < numCourses; i++) { if (hasCycle(globalMarked, localMarked, graphic, i)) { return false; } } return true; } private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List[] graphic, int curNode) { if (localMarked[curNode]) { return true; } if (globalMarked[curNode]) { return false; } globalMarked[curNode] = true; localMarked[curNode] = true; for (int nextNode : graphic[curNode]) { if (hasCycle(globalMarked, localMarked, graphic, nextNode)) { return true; } } localMarked[curNode] = false; return false; }
課程安排的順序
210. Course Schedule II (Medium)
4, [[1,0],[2,0],[3,1],[3,2]] There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2\. Both courses 1 and 2 should be taken after you finished course 0\. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3].
使用 DFS 來實現拓撲排序,使用一個棧存儲後序遍歷結果,這個棧的逆序結果就是拓撲排序結果。
證實:對於任何先序關係:v->w,後序遍歷結果能夠保證 w 先進入棧中,所以棧的逆序結果中 v 會在 w 以前。
public int[] findOrder(int numCourses, int[][] prerequisites) { List[] graphic = new List[numCourses]; for (int i = 0; i < numCourses; i++) { graphic[i] = new ArrayList<>(); } for (int[] pre : prerequisites) { graphic[pre[0]].add(pre[1]); } Stack postOrder = new Stack<>(); boolean[] globalMarked = new boolean[numCourses]; boolean[] localMarked = new boolean[numCourses]; for (int i = 0; i < numCourses; i++) { if (hasCycle(globalMarked, localMarked, graphic, i, postOrder)) { return new int[0]; } } int[] orders = new int[numCourses]; for (int i = numCourses - 1; i >= 0; i--) { orders[i] = postOrder.pop(); } return orders; } private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List[] graphic, int curNode, Stack postOrder) { if (localMarked[curNode]) { return true; } if (globalMarked[curNode]) { return false; } globalMarked[curNode] = true; localMarked[curNode] = true; for (int nextNode : graphic[curNode]) { if (hasCycle(globalMarked, localMarked, graphic, nextNode, postOrder)) { return true; } } localMarked[curNode] = false; postOrder.push(curNode); return false; }
並查集能夠動態地連通兩個點,而且能夠很是快速地判斷兩個點是否連通。
冗餘鏈接
684. Redundant Connection (Medium)
Input: [[1,2], [1,3], [2,3]] Output: [2,3] Explanation: The given undirected graph will be like this: 1 / \ 2 - 3
題目描述:有一系列的邊連成的圖,找出一條邊,移除它以後該圖可以成爲一棵樹。
public int[] findRedundantConnection(int[][] edges) { int N = edges.length; UF uf = new UF(N); for (int[] e : edges) { int u = e[0], v = e[1]; if (uf.connect(u, v)) { return e; } uf.union(u, v); } return new int[]{-1, -1}; } private class UF { private int[] id; UF(int N) { id = new int[N + 1]; for (int i = 0; i < id.length; i++) { id[i] = i; } } void union(int u, int v) { int uID = find(u); int vID = find(v); if (uID == vID) { return; } for (int i = 0; i < id.length; i++) { if (id[i] == uID) { id[i] = vID; } } } int find(int p) { return id[p]; } boolean connect(int u, int v) { return find(u) == find(v); } }
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
複製代碼
位與運算技巧:
移位運算:
n 爲算術右移,至關於除以 2n;
n 爲無符號右移,左邊會補上 0。
2. mask 計算
要獲取 111111111,將 0 取反便可,~0。
要獲得只有第 i 位爲 1 的 mask,將 1 向左移動 i-1 位便可,1<<(i-1) 。例如 1<<4 獲得只有第 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. Java 中的位操做
static int Integer.bitCount(); // 統計 1 的數量 static int Integer.highestOneBit(); // 得到最高位 static String toBinaryString(int i); // 轉換爲二進制表示的字符串
統計兩個數的二進制表示有多少位不一樣
Input: x = 1, y = 4 Output: 2 Explanation: 1 (0 0 0 1) 4 (0 1 0 0) ↑ ↑ The above arrows point to positions where the corresponding bits are different.
對兩個數進行異或操做,位級表示不一樣的那一位爲 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; }
使用 z&(z-1) 去除 z 位級表示最低的那一位。
public int hammingDistance(int x, int y) { int z = x ^ y; int cnt = 0; while (z != 0) { z &= (z - 1); cnt++; } return cnt; }
可使用 Integer.bitcount() 來統計 1 個的個數。
public int hammingDistance(int x, int y) { return Integer.bitCount(x ^ y); }
數組中惟一一個不重複的元素
Input: [4,1,2,1,2] Output: 4
兩個相同的數異或的結果爲 0,對全部數進行異或操做,最後的結果就是單獨出現的那個數。
public int singleNumber(int[] nums) { int ret = 0; for (int n : nums) ret = ret ^ n; return ret; }
找出數組中缺失的那個數
Input: [3,0,1] Output: 2
題目描述:數組元素在 0-n 之間,可是有一個數是缺失的,要求找到這個缺失的數。
public int missingNumber(int[] nums) { int ret = 0; for (int i = 0; i < nums.length; i++) { ret = ret ^ i ^ nums[i]; } return ret ^ nums.length; }
數組中不重複的兩個元素
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; }
翻轉一個數的比特位
public int reverseBits(int n) { int ret = 0; for (int i = 0; i < 32; i++) { ret <<= 1; ret |= (n & 1); n >>>= 1; } return ret; }
若是該函數須要被調用不少次,能夠將 int 拆成 4 個 byte,而後緩存 byte 對應的比特位翻轉,最後再拼接起來。
private static Map cache = new HashMap<>(); public int reverseBits(int n) { int ret = 0; for (int i = 0; i < 4; i++) { ret <<= 8; ret |= reverseByte((byte) (n & 0b11111111)); n >>= 8; } return ret; } private int reverseByte(byte b) { if (cache.containsKey(b)) return cache.get(b); int ret = 0; byte t = b; for (int i = 0; i < 8; i++) { ret <<= 1; ret |= t & 1; t >>= 1; } cache.put(b, ret); return ret; }
不用額外變量交換兩個整數
a = a ^ b; b = a ^ b; a = a ^ b;
判斷一個數是否是 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; }
判斷一個數是否是 4 的 n 次方
這種數在二進制表示中有且只有一個奇數位爲 1,例如 16(10000)。
public boolean isPowerOfFour(int num) { return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0; }
也可使用正則表達式進行匹配。
public boolean isPowerOfFour(int num) { return Integer.toString(num, 4).matches("10*"); }
判斷一個數的位級表示是否不會出現連續的 0 和 1
693. Binary Number with Alternating Bits (Easy)
Input: 10 Output: True Explanation: The binary representation of 10 is: 1010. Input: 11 Output: False Explanation: The binary representation of 11 is: 1011.
對於 1010 這種位級表示的數,把它向右移動 1 位獲得 101,這兩個數每一個位都不一樣,所以異或獲得的結果爲 1111。
public boolean hasAlternatingBits(int n) { int a = (n ^ (n >> 1)); return (a & (a + 1)) == 0; }
求一個數的補碼
Input: 5 Output: 2 Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010\. So you need to output 2.
題目描述:不考慮二進制表示中的首 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); }
實現整數的加法
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); }
字符串數組最大乘積
318. Maximum Product of Word Lengths (Medium)
Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"] Return 16 The two words can be "abcw", "xtfn".
題目描述:字符串數組的字符串只含有小寫字符。求解字符串數組中兩個字符串長度的最大乘積,要求這兩個字符串不能含有相同字符。
本題主要問題是判斷兩個字符串是否含相同字符,因爲字符串只含有小寫字符,總共 26 位,所以能夠用一個 32 位的整數來存儲每一個字符是否出現過。
public int maxProduct(String[] words) { int n = words.length; 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; }
統計從 0 ~ n 每一個數的二進制表示中 1 的個數
對於數字 6(110),它能夠當作是 4(100) 再加一個 2(10),所以 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; }