LeetCode刷題指南(Java版)
這位大俠,這是我的公眾號:程式設計師江湖。
分享程式設計師面試與技術的那些事。 乾貨滿滿,關注就送。
參考@CyC2018的leetcode題解。Java工程師LeetCode刷題必備。主要根據LeetCode的tag進行模組劃分,每部分都選取了比較經典的題目,題目以medium和easy為主,少量hard題目。
我在@CyC2018大佬的基礎上又加上了部分題解,並且篩選了比較經典的題目,去除了比較晦澀難懂的題目,以及一些很少考的題目,以便大家積累經驗,在面試筆試中能夠遊刃有餘。
陣列和矩陣
把陣列中的 0 移到末尾
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
字串
兩個字串包含的字元是否完全相同
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; }
計算一組字元集合可以組成的迴文字串的最大長度
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
棧和佇列
雜湊表
雜湊表使用 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
貪心演算法
一般什麼時候需要用到貪心,其實就是在題目推導比較難解,但是直觀思維卻比較簡單。比如經典的排課問題,就是使用貪心,先進行排序,再進行選擇,貪心演算法也時常用來求近似解。
所以一般解法可以考慮為,先排序,再根據條件求結果。證明的過程是非常難的,所以我們一般不會討論證明
貪心思想
貪心思想保證每次操作都是區域性最優的,並且最後得到的結果是全域性最優的。
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時的陣列
解析:本題我剛開始想的辦法是使用dp求出LCS最長公共子序列,判斷長度是否等於t的長度,結果超時了。事實證明我想太多了。 只需要按順序查詢t的字母是否都在s中即可,當然,要注意查詢時候的下標移動,否則也是O(N2)的複雜度 DP解法:超時
- 判斷子序列
給定字串 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.
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;
}
}
題意:只要出現價差為正時就買入,這樣一定是最賺的,注意本題中同一天可以進行賣出後再進行買入。 對於 [a, b, c, d],如果有 a
- 非遞減數列 這題暫時沒有想到比較好的方法
給定一個長度為 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。
雙指標
雙指標 雙指標主要用於遍歷陣列,兩個指標指向不同的元素,從而協同完成任務。 雙指標其實一般不會抽取出來單獨作為一種演算法,因為陣列中經常會用到,而且我們熟悉的二分查詢也使用了雙指標。 二分查詢這題基本操作了。
- 兩數之和 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、1、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];
}
153. 尋找旋轉排序陣列中的最小值
假設按照升序排序的陣列在預先未知的某個點上進行了旋轉。
( 例如,陣列 [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;
}
DFS和BFS,回溯
搜尋
深度優先搜尋和廣度優先搜尋廣泛運用於樹和圖中,但是它們的應用遠遠不止如此。
BFS
廣度優先搜尋的搜尋過程有點像一層一層地進行遍歷,每層遍歷都以上一層遍歷的結果作為起點,遍歷一個距離能訪問到的所有節點。需要注意的是,遍歷過的節點不能再次被遍歷。
第一層:
- 0 -> {6,2,1,5};
第二層:
- 6 -> {4}
- 2 -> {}
- 1 -> {}
- 5 -> {3}
第三層:
- 4 -> {}
- 3 -> {}
可以看到,每一層遍歷的節點都與根節點距離相同。設 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;
}
127. 單詞接龍
給定兩個單詞(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;
}
DFS
廣度優先搜尋一層一層遍歷,每一層得到的所有新節點,要用佇列儲存起來以備下一層遍歷的時候再遍歷。
而深度優先搜尋在得到一個新節點時立馬對新節點進行遍歷:從節點 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;
// }
//