# 无重复字符的最长子串 (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 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 } ``` --- --- ## 图解过程 ``` 字符串: "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))