1. 程式人生 > >[LeetCode] Longest Substring Without Repeating Characters 最長無重複字元的子串

[LeetCode] Longest Substring Without Repeating Characters 最長無重複字元的子串

Given a string, find the length of the longest substring without repeating characters.

Examples:

Given "abcabcbb", the answer is "abc", which the length is 3.

Given "bbbbb", the answer is "b", with the length of 1.

Given "pwwkew", the answer is "wke", with the length of 3. Note that the answer must be a substring, "pwke"

 is a subsequenceand not a substring.

這道求最長無重複子串的題和之前那道 Isomorphic Strings 很類似,屬於LeetCode的早期經典題目,博主認為是可以跟Two Sum媲美的一道題。給了我們一個字串,讓我們求最長的無重複字元的子串,注意這裡是子串,不是子序列,所以必須是連續的。我們先不考慮程式碼怎麼實現,如果給一個例子中的例子"abcabcbb",讓你手動找無重複字元的子串,該怎麼找。博主會一個字元一個字元的遍歷,比如a,b,c,然後又出現了一個a,那麼此時就應該去掉第一次出現的a,然後繼續往後,又出現了一個b,則應該去掉一次出現的b,以此類推,最終發現最長的長度為3。所以說,我們需要記錄之前出現過的字元,記錄的方式有很多,最常見的是統計字元出現的個數,但是這道題字元出現的位置很重要,所以我們可以使用HashMap來建立字元和其出現位置之間的對映。進一步考慮,由於字元會重複出現,到底是儲存所有出現的位置呢,還是隻記錄一個位置?我們之前手動推導的方法實際上是維護了一個滑動視窗,視窗內的都是沒有重複的字元,我們需要儘可能的擴大視窗的大小。由於視窗在不停向右滑動,所以我們只關心每個字元最後出現的位置,並建立對映。視窗的右邊界就是當前遍歷到的字元的位置,為了求出視窗的大小,我們需要一個變數left來指向滑動視窗的左邊界,這樣,如果當前遍歷到的字元從未出現過,那麼直接擴大右邊界,如果之前出現過,那麼就分兩種情況,在或不在滑動視窗內,如果不在滑動視窗內,那麼就沒事,當前字元可以加進來,如果在的話,就需要先在滑動視窗內去掉這個已經出現過的字元了,去掉的方法並不需要將左邊界left一位一位向右遍歷查詢,由於我們的HashMap已經儲存了該重複字元最後出現的位置,所以直接移動left指標就可以了。我們維護一個結果res,每次用出現過的視窗大小來更新結果res,就可以得到最終結果啦。

這裡我們可以建立一個256位大小的整型陣列來代替HashMap,這樣做的原因是ASCII表共能表示256個字元,所以可以記錄所有字元,然後我們需要定義兩個變數res和left,其中res用來記錄最長無重複子串的長度,left指向該無重複子串左邊的起始位置,然後我們遍歷整個字串,對於每一個遍歷到的字元,如果雜湊表中該字串對應的值為0,說明沒有遇到過該字元,則此時計算最長無重複子串,i - left +1,其中i是最長無重複子串最右邊的位置,left是最左邊的位置,還有一種情況也需要計算最長無重複子串,就是當雜湊表中的值小於left,這是由於此時出現過重複的字元,left的位置更新了,如果又遇到了新的字元,就要重新計算最長無重複子串。最後每次都要在雜湊表中將當前字元對應的值賦值為i+1。程式碼如下:

C++ 解法一: 

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int m[256] = {0}, res = 0, left = 0;
        for (int i = 0; i < s.size(); ++i) {
            if (m[s[i]] == 0 || m[s[i]] < left) {
                res = max(res, i - left + 1);
            } else {
                left = m[s[i]];
            }
            m[s[i]] = i + 1;
        }
        return res;
    }
};

這裡解釋下程式中那個if條件語句中為啥要有個m[s[i]] < left,我們用一個例子來說明,當輸入字串為"abbca"的時候,當i=4時,也就是即將要開始遍歷最後一個字母a時,此時雜湊表表中a對應1,b對應3,c對應4,left為2,即當前最長的子字串的左邊界為第二個b的位置,而第一個a已經不在當前最長的字串的範圍內了,那麼對於i=4這個新進來的a,應該要加入結果中,而此時未被更新的雜湊表中a為1,不是0,如果不判斷它和left的關係的話,就無法更新結果,那麼答案就會少一位,所以需要加m[s[i]] < left。

下面這種寫法是上面解法的精簡模式,思路都一樣:

C++ 解法二:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        vector<int> m(256, -1);
        int res = 0, left = -1;
        for (int i = 0; i < s.size(); ++i) {
            left = max(left, m[s[i]]);
            m[s[i]] = i;
            res = max(res, i - left);
        }
        return res;
    }
};

Java 解法二:

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int[] m = new int[256];
        Arrays.fill(m, -1);
        int res = 0, left = -1;
        for (int i = 0; i < s.length(); ++i) {
            left = Math.max(left, m[s.charAt(i)]);
            m[s.charAt(i)] = i;
            res = Math.max(res, i - left);
        }
        return res;
    }
}

下面這種解法使用了set,核心演算法和上面的很類似,把出現過的字元都放入set中,遇到set中沒有的字元就加入set中並更新結果res,如果遇到重複的,則從左邊開始刪字元,直到刪到重複的字元停止:

C++ 解法三:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int res = 0, left = 0, i = 0, n = s.size();
        unordered_set<char> t;
        while (i < n) {
            if (!t.count(s[i])) {
                t.insert(s[i++]);
                res = max(res, (int)t.size());
            }  else {
                t.erase(s[left++]);
            }
        }
        return res;
    }
};

Java 解法三:

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int res = 0, left = 0, right = 0;
        HashSet<Character> t = new HashSet<Character>();
        while (right < s.length()) {
            if (!t.contains(s.charAt(right))) {
                t.add(s.charAt(right++));
                res = Math.max(res, t.size());
            } else {
                t.remove(s.charAt(left++));
            }
        }
        return res;
    }
}

下面這種解法思路上跟解法一和解法二沒有區別,是不過使用了HashMap這個資料結構來建立字元和其最後出現位置之間的對映,其他操作均和解法二相同。

C++ 解法四:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int res = 0, left = 0, i = 0, n = s.size();
        unordered_map<char, int> m;
        for (int i = 0; i < n; ++i) {
            left = max(left, m[s[i]]);
            m[s[i]] = i + 1;
            res = max(res, i - left + 1);
        }
        return res;
    }
};

類似題目:

參考資料: