[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"
這道求最長無重複子串的題和之前那道 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; } };
類似題目:
參考資料: