1. 程式人生 > >LeetCode 438. Find All Anagrams in a String(找到字串中所有字母異位詞)

LeetCode 438. Find All Anagrams in a String(找到字串中所有字母異位詞)

Given a string s and a non-empty string p, find all the start indices of p’s anagrams in s.

Strings consists of lowercase English letters only and the length of both strings s and p will not be larger than 20,100.

The order of output does not matter.

Example 1:

Input:
s: "cbaebabacd" p: "abc"

Output:
[0
, 6] Explanation: The substring with start index = 0 is "cba", which is an anagram of "abc". The substring with start index = 6 is "bac", which is an anagram of "abc".

Example 2:

Input:
s: "abab" p: "ab"

Output:
[0, 1, 2]

Explanation:
The substring with start index = 0 is "ab", which is an anagram of
"ab". The substring with start index = 1 is "ba", which is an anagram of "ab". The substring with start index = 2 is "ab", which is an anagram of "ab".

給定一個字串 s 和一個非空字串 p,找到 s 中所有是 p 的字母異位詞的子串,返回這些子串的起始索引。

字串只包含小寫英文字母,並且字串 s 和 p 的長度都不超過 20100。

說明:
字母異位詞指字母相同,但排列不同的字串。
不考慮答案輸出的順序。

示例 1:

輸入:
s:
"cbaebabacd" p: "abc" 輸出: [0, 6] 解釋: 起始索引等於 0 的子串是 "cba", 它是 "abc" 的字母異位詞。 起始索引等於 6 的子串是 "bac", 它是 "abc" 的字母異位詞。

示例 2:

輸入:
s: "abab" p: "ab"

輸出:
[0, 1, 2]

解釋:
起始索引等於 0 的子串是 "ab", 它是 "ab" 的字母異位詞。
起始索引等於 1 的子串是 "ba", 它是 "ab" 的字母異位詞。
起始索引等於 2 的子串是 "ab", 它是 "ab" 的字母異位詞。

解法一:

題目給了兩個字串,要求我們從 s 中找到所有是 p 的異位詞的子串。所謂異位詞就是字串字元種類均相同但順序可以不同的兩個子串,那麼首先就要先統計字串 p 中的字元出現的次數,然後依次從字串 s 中取出 p 字串長度的子串進行比較,如果 s 子串中的每一個字元都在 p 字串中出現,那麼就記錄當前子串的起始位置,否則 break 當前迴圈。

public List<Integer> findAnagrams(String s, String p) {

    List<Integer> result = new ArrayList<>();
    if (s == null || s.length() == 0 || p == null || p.length() == 0)
        return result;

    int[] hash = new int[256];

    for (char i : p.toCharArray()) {
        hash[i]++;
    }

    int ns = s.length(), np = p.length();
    int i = 0;
    boolean success;

    while (i < ns - np + 1) {
        success = true;
        int[] tmp = hash.clone();
        for (int j = i ; j < i + np ; j ++) {
            if (--tmp[s.charAt(j)] < 0) {
                success = false;
                break;
            }
        }
        if (success) {
            result.add(i);
        }
        ++i;
    }
    return result;
}

解法二:

從解法一可以看出,內層迴圈不斷在對一個 p 長度大小的子串進行遍歷,連起來就是一個滑動視窗。
這個解法是參考LeetCode一大神的分享,程式碼很簡潔,使用到了 hash 陣列和滑動視窗。

public List<Integer> findAnagrams(String s, String p) {

        List<Integer> result = new ArrayList<>();
        if (s == null || s.length() == 0 || p == null || p.length() == 0)
            return result;

        int[] hash = new int[256];

        for (char i : p.toCharArray()) {
            hash[i]++;
        }
        int left = 0, right = 0, count = p.length(); // 差異度

        while (right < s.length()) {

            // 視窗右移;相應的hash值減小;如果這個位置的Hash值是正的,
            // 表示當前字元包含在 p 字串中,所以差異度減一
            if (hash[s.charAt(right)] > 0) {  
                count--;
            }
            hash[s.charAt(right)] --;
            right ++;

            // 差異度為0,加入結果集合中
            if (count == 0)
                result.add(left);

            // 如果當視窗大小一定的時候即視窗大小和需要比較的字串大小一致的時候,
            // 將視窗左邊的指標向右邊移動,移動的同時左邊的字元計數因為在第一個if的地方hash值減小過,
            // 所以需要執行對應恢復操作,即:hash值增加,count計數值增加。
            if (right - left == p.length()) { 

                if (hash[s.charAt(left)] >= 0) {
                    count++;
                }
                hash[s.charAt(left)] ++;
                left ++;
            }
        }
        return result;
    }

首先就是定義的 hash 陣列長度是 256,因為 ascii 碼的長度是 256 位的,所以每一位的索引表示一個字元的計數值。然後 count 是 s 的子串與 p 字串相差的字元個數,即差異度。

迴圈右移的過程中,如果當前字元包含在 p 中,那麼差異度減一,同時維護 hash 陣列對應的值減一。如果差異度為 0 了,也就是子串符合要求了,則將滑動視窗的左邊界加入結果集。當滑動視窗的大小等於 p 的長度時,就需要將滑動視窗的左邊界右移一位,並維護 hash 陣列的值加一,同時判斷左邊界當前的值是否大於等於 0 :因為如果是小於 0,也就是第一個 if 語句中減一之後變為負數的,說明在 p 中不存在,差異度無須恢復;如果存在,那麼把差異度恢復加一。

最後右邊界到達終點,迴圈結束,返回記錄下的起始索引即可。

更加簡單的寫法:

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> list = new ArrayList<>();
    if (s == null || s.length() == 0 || p == null || p.length() == 0) return list;
    int[] hash = new int[256]; //  hash 陣列
    // 儲存字元的計數值
    for (char c : p.toCharArray()) {
        hash[c]++;
    }

    int left = 0, right = 0, count = p.length();
    while (right < s.length()) {
        // 每次右移都將對應的 hash 值減一
        if (hash[s.charAt(right++)]-- >= 1) count--; 
        // 差異度為0,加入結果集合中
        if (count == 0) list.add(left);
        // 如果當視窗大小一定的時候即視窗大小和需要比較的字串大小一致的時候,
        // 將視窗左邊的指標向右邊移動,移動的同時左邊的字元計數因為在第一個if的地方hash值減小過,
        // 所以需要執行對應恢復操作,即:hash值增加,count計數值增加。
        if (right - left == p.length() && hash[s.charAt(left++)]++ >= 0) count++;
    }
    return list;
}

簡單寫法我覺得不太容易理解,整個過程自己除錯了很多遍,希望大家多多指點。:)