|
| 1 | +# 3. Longest Substring Without Repeating Characters |
| 2 | + |
| 3 | +Given a string `s`, find the length of the longest substring without duplicate characters. |
| 4 | + |
| 5 | +**Constraints:** |
| 6 | + |
| 7 | +- `0 <= s.length <= 5 * 10^4` |
| 8 | +- `s` consists of English letters, digits, symbols and spaces. |
| 9 | + |
| 10 | +## 基礎思路 |
| 11 | + |
| 12 | +本題要求找出字串中「不含重複字元」的最長子字串長度。 |
| 13 | +這是一道經典的滑動視窗問題,我們需要關注以下關鍵觀察: |
| 14 | + |
| 15 | +- **子字串需要連續**:因此我們只能調整左右邊界,而不能跳著選字元。 |
| 16 | +- **避免重複字元**:只要加入某字元後有重複,就必須從左端開始收縮,直到該字元再次變為唯一。 |
| 17 | +- **滑動視窗最佳化策略**:使用一個字元頻率表,搭配兩個指標 `left`、`right`, |
| 18 | + 維持「當前無重複字元的最大區間」。 |
| 19 | +- **鴿籠原理(Pigeonhole Principle)提早結束**:由於題目只限定 ASCII 可列印字元(最多約 95 種), |
| 20 | + 故最長答案不可能超過 95 或字串長度本身,若已達此上限即可提早返回。 |
| 21 | + |
| 22 | +透過滑動視窗結構,每個字元最多被加入與移除視窗一次,因此整體可於線性時間完成。 |
| 23 | + |
| 24 | +## 解題步驟 |
| 25 | + |
| 26 | +### Step 1:初始化與邊界處理 |
| 27 | + |
| 28 | +先取得字串長度,若為空字串則答案為 0。 |
| 29 | +同時計算可列印 ASCII 字元的上限,作為最長可能答案的提早結束條件。 |
| 30 | + |
| 31 | +```typescript |
| 32 | +const stringLength = s.length; |
| 33 | + |
| 34 | +if (stringLength === 0) { |
| 35 | + return 0; |
| 36 | +} |
| 37 | + |
| 38 | +// 可列印 ASCII(字母、數字、符號、空白)約 95 個 |
| 39 | +const maximumDistinctCharacters = 95; |
| 40 | + |
| 41 | +// 鴿籠原理:最長子字串不會超出 distinct 上限或字串長度 |
| 42 | +const maximumPossibleAnswer = |
| 43 | + stringLength < maximumDistinctCharacters |
| 44 | + ? stringLength |
| 45 | + : maximumDistinctCharacters; |
| 46 | +``` |
| 47 | + |
| 48 | +### Step 2:準備滑動視窗使用的字元頻率表與指標 |
| 49 | + |
| 50 | +建立 ASCII 區間的頻率表,用來記錄視窗內每個字元出現次數。 |
| 51 | +同時初始化左指標 `leftIndex` 與目前找到的最大視窗長度 `longestWindowLength`。 |
| 52 | + |
| 53 | +```typescript |
| 54 | +// ASCII 0..127 的頻率表 |
| 55 | +const characterFrequency = new Uint8Array(128); |
| 56 | + |
| 57 | +let leftIndex = 0; |
| 58 | +let longestWindowLength = 0; |
| 59 | +``` |
| 60 | + |
| 61 | +### Step 3:主迴圈 — 以 rightIndex 逐步擴展滑動視窗右端 |
| 62 | + |
| 63 | +用 `rightIndex` 遍歷整個字串,在視窗右側加入新字元; |
| 64 | +過程中依需求調整視窗左端以保持「無重複」。 |
| 65 | + |
| 66 | +```typescript |
| 67 | +for (let rightIndex = 0; rightIndex < stringLength; rightIndex++) { |
| 68 | + const currentCharacterCode = s.charCodeAt(rightIndex); |
| 69 | + |
| 70 | + // 將新字元加入視窗 |
| 71 | + characterFrequency[currentCharacterCode] = |
| 72 | + characterFrequency[currentCharacterCode] + 1; |
| 73 | + |
| 74 | + // ... |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +### Step 4:若右端加入的字元重複,需從左端開始縮小視窗 |
| 79 | + |
| 80 | +當某字元計數超過 1 時,表示視窗中出現重複字元, |
| 81 | +需要從左端開始移除字元並前進 leftIndex,直到該字元恢復唯一。 |
| 82 | + |
| 83 | +```typescript |
| 84 | +for (let rightIndex = 0; rightIndex < stringLength; rightIndex++) { |
| 85 | + // Step 3:擴展視窗右端 |
| 86 | + |
| 87 | + // 若新加入字元造成重複,收縮視窗左端 |
| 88 | + while (characterFrequency[currentCharacterCode] > 1) { |
| 89 | + const leftCharacterCode = s.charCodeAt(leftIndex); |
| 90 | + characterFrequency[leftCharacterCode] = |
| 91 | + characterFrequency[leftCharacterCode] - 1; |
| 92 | + leftIndex = leftIndex + 1; |
| 93 | + } |
| 94 | + |
| 95 | + // ... |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +### Step 5:更新最大視窗長度並應用鴿籠原理提前結束 |
| 100 | + |
| 101 | +當視窗符合「無重複字元」條件時,更新目前最長長度。 |
| 102 | +若達到理論上限 `maximumPossibleAnswer`,則可以立刻返回。 |
| 103 | + |
| 104 | +```typescript |
| 105 | +for (let rightIndex = 0; rightIndex < stringLength; rightIndex++) { |
| 106 | + // Step 3:擴展視窗右端 |
| 107 | + |
| 108 | + // Step 4:必要時從左端縮小視窗 |
| 109 | + |
| 110 | + const currentWindowLength = rightIndex - leftIndex + 1; |
| 111 | + |
| 112 | + if (currentWindowLength > longestWindowLength) { |
| 113 | + longestWindowLength = currentWindowLength; |
| 114 | + |
| 115 | + // 若已達最可能上限,可提前返回 |
| 116 | + if (longestWindowLength === maximumPossibleAnswer) { |
| 117 | + return longestWindowLength; |
| 118 | + } |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +### Step 6:返回最終結果 |
| 124 | + |
| 125 | +主迴圈結束後,`longestWindowLength` 即為最長無重複子字串長度。 |
| 126 | + |
| 127 | +```typescript |
| 128 | +return longestWindowLength; |
| 129 | +``` |
| 130 | + |
| 131 | +## 時間複雜度 |
| 132 | + |
| 133 | +- 每個字元最多被加入視窗一次、移除視窗一次; |
| 134 | +- 視窗收縮與擴張皆為線性總成本。 |
| 135 | +- 總時間複雜度為 $O(n)$。 |
| 136 | + |
| 137 | +> $O(n)$ |
| 138 | +
|
| 139 | +## 空間複雜度 |
| 140 | + |
| 141 | +- 使用一個長度 128 的 `Uint8Array` 作為頻率表; |
| 142 | +- 其餘僅有少量變數。 |
| 143 | +- 總空間複雜度為 $O(1)$。 |
| 144 | + |
| 145 | +> $O(1)$ |
0 commit comments