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

Grandyang發表於2015-05-06

 

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

Example 1:

Input: "abcabcbb"
Output: 3 
Explanation: The answer is "abc", with the length of 3. 

Example 2:

Input: "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.

Example 3:

Input: "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3. 
             Note that the answer must be a substring, "pwke" is a subsequence and not a substring.

 

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

這裡我們可以建立一個HashMap,建立每個字元和其最後出現位置之間的對映,然後我們需要定義兩個變數res和left,其中res用來記錄最長無重複子串的長度,left指向該無重複子串左邊的起始位置的前一個,由於是前一個,所以初始化就是-1,然後我們遍歷整個字串,對於每一個遍歷到的字元,如果該字元已經在HashMap中存在了,並且如果其對映值大於left的話,那麼更新left為當前對映值。然後對映值更新為當前座標i,這樣保證了left始終為當前邊界的前一個位置,然後計算視窗長度的時候,直接用i-left即可,用來更新結果res。

這裡解釋下程式中那個if條件語句中的兩個條件m.count(s[i]) && m[s[i]] > left,因為一旦當前字元s[i]在HashMap已經存在對映,說明當前的字元已經出現過了,而若m[s[i]] > left 成立,說明之前出現過的字元在我們的視窗內,那麼如果要加上當前這個重複的字元,就要移除之前的那個,所以我們讓left賦值為m[s[i]],由於left是視窗左邊界的前一個位置(這也是left初始化為-1的原因,因為視窗左邊界是從0開始遍歷的),所以相當於已經移除出滑動視窗了。舉一個最簡單的例子"aa",當i=0時,我們建立了a->0的對映,並且此時結果res更新為1,那麼當i=1的時候,我們發現a在HashMap中,並且對映值0大於left的-1,所以此時left更新為0,對映對更新為a->1,那麼此時i-left還為1,不用更新結果res,那麼最終結果res還為1,正確,程式碼如下:

 

C++ 解法一: 

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

 

下面這種寫法是上面解法的精簡模式,這裡我們可以建立一個256位大小的整型陣列來代替HashMap,這樣做的原因是ASCII表共能表示256個字元,但是由於鍵盤只能表示128個字元,所以用128也行,然後我們全部初始化為-1,這樣的好處是我們就不用像之前的HashMap一樣要查詢當前字元是否存在對映對了,對於每一個遍歷到的字元,我們直接用其在陣列中的值來更新left,因為預設是-1,而left初始化也是-1,所以並不會產生錯誤,這樣就省了if判斷的步驟,其餘思路都一樣:

 

C++ 解法二:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        vector<int> m(128, -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;
    }
}

 

類似題目:

Longest Substring with At Most Two Distinct Characters

 

參考資料:

https://leetcode.com/problems/longest-substring-without-repeating-characters/solution/

https://leetcode.com/problems/longest-substring-without-repeating-characters/discuss/1737/C++-code-in-9-lines.

https://leetcode.com/problems/longest-substring-without-repeating-characters/discuss/1812/Share-my-Java-solution-using-HashSet

https://leetcode.com/problems/longest-substring-without-repeating-characters/discuss/1729/11-line-simple-Java-solution-O(n)-with-explanation

 

LeetCode All in One 題目講解彙總(持續更新中...)

相關文章