1. 程式人生 > >LeetCode刷題指南(Java版)

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. 無重疊區間:給定一個區間的集合,找到需要移除區間的最小數量,使剩餘區間互不重疊。

注意:

可以認為區間的終點總是大於它的起點。
區間 [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才對。

  1. 用最少數量的箭引爆氣球
    在二維空間中有許多球形的氣球。對於每個氣球,提供的輸入是水平方向上,氣球直徑的開始和結束座標。由於它是水平的,所以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]也算是重疊區域

  1. 根據身高重建佇列

這題思路不直觀,跳過

  1. 劃分字母區間

    字串 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來表示,可以用於儲存長度。
  1. 種花問題
    假設你有一個很長的花壇,一部分地塊種植了花,另一部分卻沒有。可是,花卉不能種植在相鄰的地塊上,它們會爭奪水源,兩者都會死去。

給定一個花壇(表示為一個數組包含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時的陣列
  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;
    }
}
  1. 非遞減數列 這題暫時沒有想到比較好的方法

給定一個長度為 n 的整數陣列,你的任務是判斷在最多改變 1 個元素的情況下,該陣列能否變成一個非遞減數列。

我們是這樣定義一個非遞減數列的: 對於陣列中所有的 i (1 <= i < n),滿足 array[i] <= array[i + 1]。

示例 1:

輸入: [4,2,3]
輸出: True
解釋: 你可以通過把第一個4變成1來使得它成為一個非遞減數列。
示例 2:

輸入: [4,2,1]
輸出: False
解釋: 你不能在只改變一個元素的情況下將其變為非遞減數列。

  1. 買賣股票的最佳時機 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

雙指標

雙指標 雙指標主要用於遍歷陣列,兩個指標指向不同的元素,從而協同完成任務。 雙指標其實一般不會抽取出來單獨作為一種演算法,因為陣列中經常會用到,而且我們熟悉的二分查詢也使用了雙指標。 二分查詢
  1. 兩數之和 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;
    }
}
  1. 平方數之和

給定一個非負整數 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. 反轉字串中的母音字母
    編寫一個函式,以字串作為輸入,反轉該字串中的母音字母。

示例 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;
    }
}
  1. 驗證迴文字串 Ⅱ

給定一個非空字串 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;
    }
}
  1. 合併兩個有序陣列

這題給的用例有毒,不談。

  1. 環形連結串列

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

  1. 通過刪除字母匹配到字典裡最長單詞
    給定一個字串和一個字串字典,找到字典裡面最長的字串,該字串可以通過刪除給定字串的某些字元來得到。如果答案不止一個,返回長度最長且字典順序最小的字串。如果答案不存在,則返回空字串。

示例 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;
}
}

桶排序

  1. 前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個數了。
  1. 根據字元出現頻率排序

給定一個字串,請將字串裡的字元按照出現的頻率降序排列。

輸入:
“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();
    }
}
  1. 分類顏色
    給定一個包含紅色、白色和藍色,一共 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 是否相等

  1. 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,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];
}
  1. 在排序陣列中查詢元素的第一個和最後一個位置

給定一個按照升序排列的整數陣列 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;
    }
  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 一樣同樣需要對已經遍歷過的節點進行標記。

  1. 島嶼的最大面積

給定一個包含了一些 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. 島嶼的個數

給定一個由 ‘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);
            }
        }
    }


}
  1. 朋友圈

班上有 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;
//    }
//