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/