无重复字符的最长子串 图解题解
不含重复字符的最长一段有多长?一扇伸缩窗帘从头拉到尾就知道了。
就像拉一扇可伸缩的窗帘扫过字符串:右边一格格拉开、把新字符纳进来;一旦窗内出现重复,就从左边往里收,直到重复消失——然后继续往右拉。左端只往右走、永不回退,窗帘一遍扫完,全程只存当前窗口里有哪些字符。
这道题到底在问什么
- 输入
- s="abcabcbbdd"
- 输出
- 3
最优解:为什么这么做
一句话答案:LeetCode 3 无重复字符的最长子串的标准解法是滑动窗口加哈希表:右指针不断读入新字符,哈希表记录每个字符最近出现的下标,一旦新字符在窗口内出现过,左边界直接跳到旧位置的下一格,窗口始终保持无重复。左右指针各走一遍,时间 O(n)、空间 O(min(n, 字符集大小))。
这道题真正在问什么
给一个字符串 s,找出其中最长的「连续且字符互不相同」的子串,返回它的长度。两个关键词都藏着约束:「连续」意味着答案是子串而不是子序列,不能跳着取字符;「互不相同」意味着窗口里任何字符最多出现一次。以题目示例 s = "abcabcbbdd" 来说,串中任何长度为 4 的连续段都必含重复字符,而开头的 "abc" 三个字符互不重复,所以答案是 3。
为什么暴力枚举所有子串会慢
最直觉的做法是枚举每个起点,从它出发一路向右扩,撞到重复字符就停,记录能扩多远。起点有 n 个,每个起点最坏要扫 O(n) 个字符,整体 O(n²)。慢的根源是大量重复劳动:相邻两个起点对应的子串绝大部分字符重叠,上一轮扫描获得的信息却被整个扔掉重来。
关键观察是:如果以 r 结尾的最长无重复子串左边界是 l,那么当右端推进到 r+1 时,新的左边界绝不可能比 l 更靠左——往左只会把刚才那个重复冲突重新引进来。左边界单调右移,这正是滑动窗口能成立的根基。
窗口不变量与哈希表各记什么
算法维护一个窗口 [l, r],不变量是:窗口内字符互不相同,且它是以 r 结尾的最长无重复子串。配一张哈希表 last,记录每个字符最近一次出现的下标。右指针每读入一个字符 c,就查 last:如果 c 出现过且旧位置 last[c] 落在窗口内(即 last[c] >= l),说明窗口里已经有一个 c,此时把 l 直接跳到 last[c] + 1,一步排掉重复;然后更新 last[c] = r,并用窗口长度 r - l + 1 刷新答案。
注意 last[c] >= l 这个判断不能省:字符 c 可能很久以前出现过、但早已被挤出窗口,这种「窗口外的旧影子」不构成重复,左边界不该动。
为什么左指针跳到 last[c] + 1 而不是逐格挪
重复冲突的源头只有一个——窗口里那个旧的 c。要消除冲突,左边界至少要越过它,跳到 last[c] + 1 是恰好够用的最小步幅:再多跳会白白丢掉合法字符,少跳一格则旧 c 还留在窗口里。哈希表存了旧位置,所以不必让 l 一格一格试探,一次跳跃就完成收缩,正确性和效率同时成立。
还要小心一个方向性错误:遇到重复时绝不能把 l 重置回 0 重新数。l 一旦回退,已经排除的重复字符会被再次引入窗口,算法退化回 O(n²),而且逻辑上也不对——以 r 结尾的最长窗口左边界本来就只会右移。
复杂度怎么算,边界在哪里
时间 O(n):右指针扫一遍字符串,左指针只向右跳、全程累计也不超过 n 步,每个字符的哈希查询和写入都是 O(1)。空间 O(min(n, Σ)):哈希表里最多存字符集大小个键,比如纯小写字母时至多 26 个。
边界方面:空串答案是 0,把 ans 初始化为 0 即可自然覆盖;全部字符相同(如 "bbbb")时窗口永远只有一格,答案 1;如果题目变体要求返回子串本身而不只是长度,在刷新最大长度的同时记下当时的 l 和 r,最后切片即可。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这条:右扩进新字符,遇重复就左缩,窗口始终无重复。
- 4上面是字符数组(下标固定)。窗口从最左开始,右指针 r 准备读入第一个字符。
- 5右指针 r 扩到下标 0,读入字符 'a'。窗口内无重复,长度 1,当前最长 1。
- 6右指针 r 扩到下标 1,读入字符 'b'。窗口内无重复,长度 2,当前最长 2。
- 7右指针 r 扩到下标 2,读入字符 'c'。窗口内无重复,长度 3,当前最长 3。
- 8读入的 'a' 和窗口里已有的重复了,左指针 l 右移挤出 'a',窗口收缩保证无重复。
- 9右指针 r 扩到下标 3,读入字符 'a'。窗口内无重复,长度 3,当前最长 3。
- 10读入的 'b' 和窗口里已有的重复了,左指针 l 右移挤出 'b',窗口收缩保证无重复。
- 11右指针 r 扩到下标 4,读入字符 'b'。窗口内无重复,长度 3,当前最长 3。
- 12读入的 'c' 和窗口里已有的重复了,左指针 l 右移挤出 'c',窗口收缩保证无重复。
- 13右指针 r 扩到下标 5,读入字符 'c'。窗口内无重复,长度 3,当前最长 3。
- 14读入的 'b' 和窗口里已有的重复了,左指针 l 右移挤出 'a',窗口收缩保证无重复。
- 15读入的 'b' 和窗口里已有的重复了,左指针 l 右移挤出 'b',窗口收缩保证无重复。
- 16右指针 r 扩到下标 6,读入字符 'b'。窗口内无重复,长度 2,当前最长 3。
- 17读入的 'b' 和窗口里已有的重复了,左指针 l 右移挤出 'c',窗口收缩保证无重复。
- 18读入的 'b' 和窗口里已有的重复了,左指针 l 右移挤出 'b',窗口收缩保证无重复。
- 19右指针 r 扩到下标 7,读入字符 'b'。窗口内无重复,长度 1,当前最长 3。
- 20右指针 r 扩到下标 8,读入字符 'd'。窗口内无重复,长度 2,当前最长 3。
- 21读入的 'd' 和窗口里已有的重复了,左指针 l 右移挤出 'b',窗口收缩保证无重复。
- 22读入的 'd' 和窗口里已有的重复了,左指针 l 右移挤出 'd',窗口收缩保证无重复。
- 23右指针 r 扩到下标 9,读入字符 'd'。窗口内无重复,长度 1,当前最长 3。
- 24整个过程见过的最长无重复窗口是 [0,2],长度 3——这就是答案。
⚠️ 容易写错的地方
✗ 错:每遇重复就把 l 设回 0
✓ 对:l 只能往右、不回退
l 回退会重复计数、退化成 O(n²)
✗ 错:l 跳到 last[c] 本身
✓ 对:应跳到 last[c]+1
要把重复字符本身排除在窗口外
✗ 错:没判 last[c] >= l
✓ 对:重复字符若已在窗口左侧外则忽略
它已不在当前窗口内,不算重复
完整代码(Python / C++ / Java)
Python
def lengthOfLongestSubstring(s):
last = {} # 字符 -> 最近下标
l = ans = 0
for r, c in enumerate(s):
if c in last and last[c] >= l:
l = last[c] + 1 # 左指针跳过重复
last[c] = r
ans = max(ans, r - l + 1)
return ansC++
int lengthOfLongestSubstring(string s){
vector<int> last(128, -1);
int l = 0, ans = 0;
for(int r = 0; r < s.size(); r++){
char c = s[r];
if(last[c] >= l) l = last[c] + 1;
last[c] = r;
ans = max(ans, r - l + 1);
}
return ans;
}Java
int lengthOfLongestSubstring(String s){
int[] last = new int[128];
java.util.Arrays.fill(last, -1);
int l = 0, ans = 0;
for(int r = 0; r < s.length(); r++){
char c = s.charAt(r);
if(last[c] >= l) l = last[c] + 1;
last[c] = r;
ans = Math.max(ans, r - l + 1);
}
return ans;
}复杂度
时间
O(n)
l、r 各自只向右走,每字符最多进出窗口一次
空间
O(min(n,Σ))
哈希里最多存字符集大小个键
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 无重复字符的最长子串 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么时间是 O(n) 而不是 O(n²)?+
l 和 r 都只向右移动、各自最多走 n 步,每个字符最多进窗口一次、出窗口一次,合计 O(n)。
如果要返回最长子串本身而不只是长度?+
记录取得最大长度时的 l、r,最后切片 s[l..r] 即可(本动画结尾高亮的就是那段窗口)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 无重复字符的最长子串 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。