---
tags: leetcode, medium, sliding window
---
# LeetCode 3. Longest Substring Without Repeating Characters
<font color="#ef6c00">Medium</font>
Given a string s, find the length of the longest substring without repeating characters.
Example 1:
```
Input: s = "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3.
```
Example 2:
```
Input: s = "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.
```
Example 3:
```
Input: s = "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.
Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.
```
Example 4:
```
Input: s = ""
Output: 0
```
Constraints:
0 <= s.length <= 5 * 104
s consists of English letters, digits, symbols and spaces.
#### 解決方法 1
暴力解法,我們依序把每個字元當作起點,從它開始往後遍歷每個字元直到出現重複的字元為止,記錄下最長的長度。為了檢查是否有重複的字元出現,我們使用 `Set` 儲存每個遍歷到字元。
###### Java Code
```java=
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s.length() == 1) {
// 處理特殊狀況
return 1;
}
Set<Character> substring = new HashSet<>();
int longest = 0;
// 因為我們會往後遍歷所以終點為長度 - 2
for (int i = 0; i < s.length() - 1; i++) {
substring.add(s.charAt(i));
for (int j = i + 1; j < s.length(); j++) {
if (!substring.add(s.charAt(j))) {
break;
}
}
// substring 的長度即為本次遍歷在遇到重複字元前的長度
longest = Math.max(longest, substring.size());
// 清除內容以記錄下次遍歷的字元
substring.clear();
}
return longest;
}
}
```
時間複雜度:因為我們會從每個字元為起點遍歷一次,所以長度為 $N$,則我們進行 $(N - 1) + (N - 2)...+ 1 = N(N + 1) / 2 = (N^2 + N) \times 1/2$,所以時間複雜度為 $O(N^2)$
#### 解決方法 2 (LeetCode Solution)
使用 **Sliding Window Algorithm**。Sliding Window Algorithm 是建立一區間稱為 **windows**,比方區間起點是 index `i`,而終點是 index `j`,我們用 `[i, j)` 表示這個區間以 `i` 為起點,而 `j` 會待續移動。
同時我們用 1 個 `Set` 記錄下目前 `[i, j)` 中的字元,我們每次檢查 `j` 指向的字元是否存在於 `Set` 之中,如果沒有的話,我們就把該字元加進 `Set` 中,接著 `j` 再往右延展一位。等到 `j` 指向的字元存在 `Set` 中時表示有重複的字元,這時 `[i, j)` 的區間長度即為可能的最長不重複字元子字串的長度,我們再和目前記錄的最長長度 `maxLogest` 比較,如果新的長度比較長就取代掉 `maxLongest`。
因為我們只知道 `[i, j)` 中有重複的字元,但不曉得是哪一個,同時我們知道如果重複字元的 index 為 `k` 則 `[k, j)` 必小於 `[i, j)` 所以我們將 window 的起點右移 1 位再檢查一次是否有重複字元,直到沒有重複字元的 `[i, j)` window 才有可能有比原本 window 更長的子字串出現。
具體的步驟為把 windows 縮小變成 `[i + 1, j)`,同時把原本 `i` 指向的字元從 `Set` 中移除,然後再檢查 `j` 是否包含在新的 window 字串內,如果同樣包含就再縮小 windows `[i + 2, j)`,直到在 windows 字串內沒有重複的字元後再繼續之前的步驟。
###### Java Code
```java=
class Solution {
public int lengthOfLongestSubstring(String s) {
// window 起點
int i = 0;
// window 終點
int j = 0;
// 記錄最長不重複字元子字串長度
int maxLongest = 0;
// 記錄 window 內出現的字元用來比對是否有重複字元
Set<Character> substring = new HashSet<>();
while (j < s.length()) {
// Java 的 Set.add 如果回傳 true 表示沒有重複元素
if (substring.add(s.charAt(j))) {
// 因為是是 0 based, 所以計算長度要 + 1
maxLongest = Math.max(maxLongest, j - i + 1);
j++;
} else {
// 縮小 window 長度, 同時移除原本 i 指向的字元
substring.remove(s.charAt(i++)));
}
}
return maxLongest;
}
}
```
- 時間複雜度:因為每個元素都會被 `i`、`j` 存取到,所以如果有 $N$ 個元素則為 $O(2N)$ 也就是 $O(N)$。
- 空間複雜度:我們需要 `Set` 記錄 window 內的字元,所以 `Set` 的長度取決於 window 的長度。如果字串完全沒有重複字元,則 window 長度即為輸入字串的長度 $N$,所以空間複雜度為 $O(N)$,如果有重複字元出現則 window 的長度即為不重複字元子字串長度 $M$,所以空間複雜度為 $O(min(M, N))$
#### 解決方法 3 (LeetCode Solution)
如解決方法 2 提到的,當出現重複字元時,因為我們不知道重複字元的 index 為何,所以我們必須慢慢縮減 window 的長度來找到新的起始位置,所以每個字元可能都需要存取 2 次,如果我們記錄下 window 中每個字元的 index,就可以直接從重複字元的 index 開始,這樣每個字元的存取數就變成 1 次。
具體的方法是改用 `Map` 記錄每個字元和它所在的 index,如果出現重複的字元就可以直接取得新的起始位置。
###### Java Code (根據概念自寫)
```java=
class Solution {
public int lengthOfLongestSubstring(String s) {
// 記錄 window 起始 index
int i = 0;
// 記錄 window 終點 index
int j = 0;
// 記錄目前最長不重複字元子字串
int maxLongest = 0;
// 記錄 window 中每個字元和它所在的 index
Map<Character, Integer> substring = new HashMap<>();
while (j < s.length()) {
if (!substring.containsKey(s.charAt(j))) {
maxLongest = Math.max(maxLongest, j - i + 1);
substring.put(s.charAt(j), j);
j++
} else {
// 取出重複字元的 index
int duplicateIndex = substring.remove(s.charAt(j));
// 因為我們只會移除掉上次重複字元的記錄
// 所以 Map 中重複字元的 index 可能是在 [i, j) 之前
// 如果是這樣我們就不移動 i, 表示其實本次 [i, j) 的 window 內沒有重複字元
// 但如果重複字元 index >= i 表示落在 [i, j] 中
// 因為我們新的 window 已不包含重複的字元
// 我們就以重複字元 index + 1為新的起點 i
int i = Math.max(substring.remove(s.charAt(j), i);
}
}
return maxLongest;
}
}
```
###### Java Code 2 (LeetCode Solution)
```java=
class Solution {
public int lengthOfLongestSubstring(String s) {
int i = 0;
int j = 0;
int maxLongest = 0;
Map<Character, Integer> substring = new HashMap<>();
while (j < s.length()) {
if (substring.containsKey(s.charAt(j))) {
i = Math.max(substring.get(s.charAt(j)), i);
}
maxLongest = Math.max(maxLongest, j - i + 1);
// 這裡我們放入的時候 + 1 是為了之後發現重複字元取出時
// Math.max(s.charAt(j), i) 這裡就可以直接處理 3 種情況
// 1. 假設 s.charAt(j) < i, 表示重複字元不在 [i, j] 中, 不改變 window
// 2. 假設 s.charAt(j) >= i, 我們應該用重複字元 index + 1 為新的 window 的起點
substring.put(s.charAt(j), ++j);
}
return maxLongest;
}
}
```
- 如果字元有確定範圍,比方一定為 A-Z,則我們可以用陣列儲存每個字元的 index 位置,而儲存時放的位置可以用字元的 ASCII code
- 時間複雜度:因為我們不用一個個慢慢移動 window 的起始位置,所以每個元素只會被存取到 1 次,因此為 $O(N)$
- 空間複雜度:和解決方法 2 相同