精选专栏链接 🔗
欢迎订阅,点赞+关注,每日精进1%,与百万开发者共攀技术珠峰
更多内容持续更新中~
【LeetCode 热题 100】无重复字符的最长子串
- 📝题目描述
- 💡提示信息
- 关键概念区分
- 解题思路分析
- [方法一:HashSet + 双指针](#方法一:HashSet + 双指针)
- [方法二:HashMap 优化](#方法二:HashMap 优化)
- 总结
📝题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
bash
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
注意 "bca" 和 "cab" 也是正确答案。
示例 2:
bash
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
bash
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
💡提示信息
- 0 <= s.length <= 5 * 10 4 10^4 104;
- s 由英文字母、数字、符号和空格组成;
关键概念区分
在动手写代码前,必须明确两个概念的区别,这也是容易踩坑的地方:
- 子串 :必须是原字符串中
连续的一段。例如 "abc" 是 "abcde" 的子串; - 子序列:不需要连续,只要
保持相对顺序即可。例如 "ace" 是 "abcde" 的子序列;
注意:本题明确要求的是 子串。这意味着我们不能跳着选字符,必须在一段连续的区间内寻找答案。
解题思路分析
为了消除重复计算,我们引入 滑动窗口 的思想。
算法核心思想
维护一个窗口 [left, right],保证窗口内的字符 始终是不重复的。
- 右指针 (
right):主动向右移动,负责"探索"新字符,扩大窗口; - 左指针 (
left):被动向右移动,负责"收缩"窗口。只有当窗口内出现重复字符时,才移动左指针,直到把那个导致重复的旧字符移出窗口;
效果演示
以字符串 s = "abcabcbb" 为例:
text
步骤 1: [a] b c a b c b b -> 窗口 {a}, 长度 1
步骤 2: [a b] c a b c b b -> 窗口 {a,b}, 长度 2
步骤 3: [a b c] a b c b b -> 窗口 {a,b,c}, 长度 3 (目前最大)
步骤 4: a [b c a] b c b b -> 遇到 'a' 重复! 左指针右移,移除旧的 'a'。窗口 {b,c,a}, 长度 3
步骤 5: a b [c a b] c b b -> 遇到 'b' 重复! 左指针右移,移除旧的 'b'。窗口 {c,a,b}, 长度 3
...以此类推
通过这种方式,左右指针各最多遍历一次字符串,时间复杂度仅为 O ( n ) O(n) O(n)。
方法一:HashSet + 双指针
此方法这是最标准的滑动窗口模板。具体来说是使用 HashSet 来存储当前窗口内的字符,利用集合的特性快速判断是否重复。
算法流程如下:
- 使用一个 HashSet 记录当前窗口里有哪些字符;
- 右指针 right 不断向右走,把新字符加进来;
- 若新字符已经在 Set 里了(说明重复了),我们就让 左指针 (left) 一步一步 向右挪,并从 Set 中移除对应的字符,直到把那个导致重复的旧字符"挤"出去为止。
Java 代码实现如下:
java
class Solution {
public int lengthOfLongestSubstring(String s) {
HashSet<Character> window = new HashSet<>(); // HashSet存储当前窗口内的字符
int left = 0;
int maxLen = 0;
for(int right = 0; right < s.length(); right++){
char c = s.charAt(right);
// 如果发生冲突,左指针一步步右移,直到移除重复的那个字符
while(window.contains(c)){
window.remove(s.charAt(left));
left++;
}
// 此时窗口内无重复,加入新字符并更新最大长度
window.add(c);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
提交代码,运行结果如下:

方法二:HashMap 优化
在方法一中,当遇到重复字符时,左指针是一步步挪动的。如果我们用 HashMap 记录每个字符 最后一次出现的索引,左指针就可以直接"跳跃"到重复字符的下一位,减少无效操作。
算法流程如下:
- HashMap 不仅记录字符本身(Key),还记录该字符
最后一次出现的下标(Value); - 右指针 向右遍历时,如果发现当前字符在 Map 中存在;
- 我们不需要一步步挪左指针,而是直接查看该字符上次出现的位置;
- 将 左指针直接"跳跃" 到 上次出现位置 + 1 的地方(当然,要保证左指针不能回退)。这样就瞬间消除了重复。
说明:
HashMap Key 唯一且不可重复。 在滑动窗口向右推进的过程中,同一个字符可能会多次出现。因为 HashMap 的 Key 是唯一的,当我们每次调用 map.put(c, right) 时,如果这个字符 c 之前已经存在,新的索引 right 就会直接覆盖掉旧的索引。
Java代码实现如下:
java
import java.util.HashMap;
import java.util.Map;
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>(); // Key存字符,Value存该字符最后出现的索引
int left = 0;
int maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 如果字符已存在,且其索引在当前窗口内,左指针直接跳跃到旧位置的下一位
if (map.containsKey(c) && map.get(c) >= left) {
left = map.get(c) + 1;
}
// 更新字符的最新索引,并计算当前窗口长度
map.put(c, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
提交代码,运行结果如下:

优化后的效率要比方法一快接近一倍。
总结
这道题是学习 滑动窗口 算法的经典入门题。
- 识别特征:当你看到求"最长/最短子串/子数组"且满足某种条件(如无重复、和为K)时,首先考虑滑动窗口。
- 核心难点 :在于确定何时移动左指针。
- 在本题中,条件是"无重复",所以当
set.contains(nextChar)为真时,必须收缩左边界。
- 在本题中,条件是"无重复",所以当
- 避坑指南 :在使用 HashMap 优化时(方法二),务必注意
left = Math.max(left, ...)这一步。这是因为左指针只能向右走,不能回头(例如处理"abba"这种回文结构时,如果不取 max,left 可能会倒退)。
希望这篇博客能帮你彻底搞懂这道题!