Skip to content

Commit 7317472

Browse files
committed
Add: Add 2025/11/23
1 parent e8ad325 commit 7317472

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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)$
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
function lengthOfLongestSubstring(s: string): number {
2+
const stringLength = s.length;
3+
4+
if (stringLength === 0) {
5+
return 0;
6+
}
7+
8+
// Printable ASCII (letters, digits, symbols, space) is at most around 95 distinct characters.
9+
const maximumDistinctCharacters = 95;
10+
11+
// By pigeonhole principle, the answer cannot be larger than the number
12+
// of distinct possible characters or the string length itself.
13+
const maximumPossibleAnswer =
14+
stringLength < maximumDistinctCharacters
15+
? stringLength
16+
: maximumDistinctCharacters;
17+
18+
// Frequency table for ASCII codes 0..127
19+
const characterFrequency = new Uint8Array(128);
20+
21+
let leftIndex = 0;
22+
let longestWindowLength = 0;
23+
24+
for (let rightIndex = 0; rightIndex < stringLength; rightIndex++) {
25+
const currentCharacterCode = s.charCodeAt(rightIndex);
26+
27+
// Add current character to the window
28+
characterFrequency[currentCharacterCode] =
29+
characterFrequency[currentCharacterCode] + 1;
30+
31+
// Shrink window from the left until this character becomes unique
32+
while (characterFrequency[currentCharacterCode] > 1) {
33+
const leftCharacterCode = s.charCodeAt(leftIndex);
34+
characterFrequency[leftCharacterCode] =
35+
characterFrequency[leftCharacterCode] - 1;
36+
leftIndex = leftIndex + 1;
37+
}
38+
39+
const currentWindowLength = rightIndex - leftIndex + 1;
40+
41+
if (currentWindowLength > longestWindowLength) {
42+
longestWindowLength = currentWindowLength;
43+
44+
// Pigeonhole early exit: cannot exceed maximumPossibleAnswer
45+
if (longestWindowLength === maximumPossibleAnswer) {
46+
return longestWindowLength;
47+
}
48+
}
49+
}
50+
51+
return longestWindowLength;
52+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function lengthOfLongestSubstring(s: string): number {
2+
3+
}

0 commit comments

Comments
 (0)