vault backup: 2026-03-05 12:23:56
This commit is contained in:
246
16-LeetCode Hot 100/无重复字符的最长子串.md
Normal file
246
16-LeetCode Hot 100/无重复字符的最长子串.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 无重复字符的最长子串 (Longest Substring Without Repeating Characters)
|
||||
|
||||
LeetCode 3. Medium
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个字符串 `s` ,请你找出其中不含有重复字符的 **最长子串** 的长度。
|
||||
|
||||
**示例 1**:
|
||||
```
|
||||
输入: s = "abcabcbb"
|
||||
输出: 3
|
||||
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
|
||||
```
|
||||
|
||||
**示例 2**:
|
||||
```
|
||||
输入: s = "bbbbb"
|
||||
输出: 1
|
||||
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
|
||||
```
|
||||
|
||||
**示例 3**:
|
||||
```
|
||||
输入: s = "pwwkew"
|
||||
输出: 3
|
||||
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
|
||||
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 核心思想
|
||||
使用**滑动窗口**(Sliding Window)+ **哈希表**记录字符位置。
|
||||
|
||||
### 算法流程
|
||||
1. 维护一个窗口 [left, right]
|
||||
2. 使用哈希表记录每个字符最后一次出现的位置
|
||||
3. 遍历字符串:
|
||||
- 如果当前字符在窗口内出现,移动 left 到重复字符的下一位
|
||||
- 更新哈希表和最大长度
|
||||
|
||||
### 复杂度分析
|
||||
- **时间复杂度**:O(n),n 为字符串长度
|
||||
- **空间复杂度**:O(min(m, n)),m 为字符集大小
|
||||
|
||||
---
|
||||
|
||||
## Go 解法
|
||||
|
||||
```go
|
||||
func lengthOfLongestSubstring(s string) int {
|
||||
// 记录字符最后出现的位置
|
||||
charIndex := make(map[rune]int)
|
||||
maxLength := 0
|
||||
left := 0
|
||||
|
||||
for right, char := range s {
|
||||
// 如果字符已存在且在窗口内,移动左边界
|
||||
if idx, ok := charIndex[char]; ok && idx >= left {
|
||||
left = idx + 1
|
||||
}
|
||||
|
||||
// 更新字符位置
|
||||
charIndex[char] = right
|
||||
|
||||
// 更新最大长度
|
||||
if right - left + 1 > maxLength {
|
||||
maxLength = right - left + 1
|
||||
}
|
||||
}
|
||||
|
||||
return maxLength
|
||||
}
|
||||
```
|
||||
|
||||
### Go 代码要点
|
||||
1. 使用 `range` 遍历字符串,自动处理 Unicode
|
||||
2. `map[rune]int` 记录字符索引
|
||||
3. 条件判断:`idx >= left` 确保在窗口内
|
||||
|
||||
---
|
||||
|
||||
## Java 解法
|
||||
|
||||
```java
|
||||
class Solution {
|
||||
public int lengthOfLongestSubstring(String s) {
|
||||
// 记录字符最后出现的位置
|
||||
Map<Character, Integer> charIndex = new HashMap<>();
|
||||
int maxLength = 0;
|
||||
int left = 0;
|
||||
|
||||
for (int right = 0; right < s.length(); right++) {
|
||||
char char = s.charAt(right);
|
||||
|
||||
// 如果字符已存在且在窗口内,移动左边界
|
||||
if (charIndex.containsKey(char) && charIndex.get(char) >= left) {
|
||||
left = charIndex.get(char) + 1;
|
||||
}
|
||||
|
||||
// 更新字符位置
|
||||
charIndex.put(char, right);
|
||||
|
||||
// 更新最大长度
|
||||
maxLength = Math.max(maxLength, right - left + 1);
|
||||
}
|
||||
|
||||
return maxLength;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Java 代码要点
|
||||
1. `HashMap` 记录字符索引
|
||||
2. `charAt()` 遍历字符串
|
||||
3. `Math.max()` 更新最大值
|
||||
|
||||
---
|
||||
|
||||
## 图解过程
|
||||
|
||||
```
|
||||
字符串: "abcabcbb"
|
||||
|
||||
步骤1: [a]bcabcbb
|
||||
left=0, right=0, maxLength=1
|
||||
|
||||
步骤2: [a,b]cabcbb
|
||||
left=0, right=1, maxLength=2
|
||||
|
||||
步骤3: [a,b,c]abcbb
|
||||
left=0, right=2, maxLength=3
|
||||
|
||||
步骤4: a[b,c,a]bcbb (发现重复,left移动)
|
||||
left=1, right=3, maxLength=3
|
||||
|
||||
步骤5: ab[c,a,b]cbb (发现重复,left移动)
|
||||
left=2, right=4, maxLength=3
|
||||
|
||||
步骤6: abc[a,b,c]bb (发现重复,left移动)
|
||||
left=3, right=5, maxLength=3
|
||||
|
||||
步骤7: abca[b,c,b]b (发现重复,left移动)
|
||||
left=4, right=6, maxLength=3
|
||||
|
||||
步骤8: abcab[c,b,b] (发现重复,left移动)
|
||||
left=5, right=7, maxLength=3
|
||||
|
||||
结果: maxLength = 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 进阶问题
|
||||
|
||||
### Q1: 如何返回最长子串本身?
|
||||
```go
|
||||
func longestSubstring(s string) string {
|
||||
charIndex := make(map[rune]int)
|
||||
maxLength := 0
|
||||
left := 0
|
||||
start := 0 // 记录起始位置
|
||||
|
||||
for right, char := range s {
|
||||
if idx, ok := charIndex[char]; ok && idx >= left {
|
||||
left = idx + 1
|
||||
}
|
||||
charIndex[char] = right
|
||||
|
||||
if right - left + 1 > maxLength {
|
||||
maxLength = right - left + 1
|
||||
start = left
|
||||
}
|
||||
}
|
||||
|
||||
return s[start : start+maxLength]
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如果字符集有限(如只有小写字母),如何优化?
|
||||
**优化**:使用数组代替哈希表
|
||||
|
||||
```go
|
||||
func lengthOfLongestSubstring(s string) int {
|
||||
charIndex := [128]int{} // ASCII 字符集
|
||||
for i := range charIndex {
|
||||
charIndex[i] = -1
|
||||
}
|
||||
|
||||
maxLength := 0
|
||||
left := 0
|
||||
|
||||
for right := 0; right < len(s); right++ {
|
||||
char := s[right]
|
||||
if charIndex[char] >= left {
|
||||
left = charIndex[char] + 1
|
||||
}
|
||||
charIndex[char] = right
|
||||
maxLength = max(maxLength, right-left+1)
|
||||
}
|
||||
|
||||
return maxLength
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## P7 加分项
|
||||
|
||||
### 深度理解
|
||||
- **滑动窗口**:维护动态窗口,左边界根据重复字符调整
|
||||
- **哈希表优化**:数组 vs HashMap,时间/空间权衡
|
||||
- **边界处理**:重复字符在窗口外的情况
|
||||
|
||||
### 实战扩展
|
||||
- **流式数据**:处理超大字符串或流式输入
|
||||
- **多线程**:分段计算后合并
|
||||
- **业务场景**:日志去重、用户行为分析
|
||||
|
||||
### 变形题目
|
||||
1. [159. 至多包含两个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/)
|
||||
2. [340. 至多包含 K 个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-k-distinct-characters/)
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
这道题的核心是:
|
||||
1. **滑动窗口**:动态调整窗口边界
|
||||
2. **哈希表**:记录字符位置,快速判断重复
|
||||
3. **双指针**:left 和 right 指针协同移动
|
||||
|
||||
**易错点**:
|
||||
- 忘记判断重复字符是否在窗口内(`idx >= left`)
|
||||
- 更新 left 的时机
|
||||
- 数组越界(使用数组代替哈希表时)
|
||||
|
||||
**最优解法**:滑动窗口 + 哈希表,时间 O(n),空间 O(min(m, n))
|
||||
Reference in New Issue
Block a user