Files
interview/16-LeetCode Hot 100/无重复字符的最长子串.md

247 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 无重复字符的最长子串 (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))