【滑动窗口专题】第二讲:无重复字符的最长子串
🌿 滑动窗口去重,右扩左缩,轻盈地捕获最长无重复子串!
🌱 文章摘要
从暴力枚举到滑动窗口优化,本篇带你掌握“窗口去重”的核心技巧,快速求解最长无重复子串问题。
通过灵活地右扩覆盖、左缩去重,我们能在 O(n) 时间内高效求解,彻底告别 O(n²) 的低效暴力法。这一题,是掌握字符串滑动窗口的必经之路!
🧭 导读
在上一篇《长度最小的子数组》中,我们初次体验了滑动窗口的节奏:右扩 → 满足条件 → 左缩 → 优化结果。
这一讲,我们将把这套节奏用在字符串问题上,进入滑动窗口的另一个核心方向——窗口去重。👉 如果你还没有阅读过我的《双指针专题》,强烈建议从那里起航
👉 双指针专题_明天会有多晴朗的博客-CSDN博客
从“移动零”到“四数之和”,你会更好地理解为什么滑动窗口是双指针思想的自然延伸。
一、题目描述
无重复字符的最长子串
给定一个字符串
s
,请找出其中不含有重复字符的最长子串的长度。
示例 1:
输入:s = "abcabcbb" 输出:3 解释:最长子串是 "abc",长度为 3。
示例 2:
输入:s = "bbbbb" 输出:1 解释:最长子串是 "b"。
示例 3:
输入:s = "pwwkew" 输出:3 解释:最长子串是 "wke"。注意 "pwke" 是子序列,不是子串。
二、思路引导:窗口去重的伸缩技巧
1. 暴力法(直觉,但低效)
遍历所有起点
i
,从每个起点向后扩张,直到遇到重复字符。每次扩张时使用一个哈希表来检测是否出现重复字符。
不断更新最大长度。
缺点很明显:
对于长度为
n
的字符串,需要 O(n²) 的枚举;每次都要清空哈希结构,性能差;
当 n 达到 5×10⁴ 时,轻松超时。
// 暴力解法 O(n^2)
int lengthOfLongestSubstring(string s) {int ret = 0, n = s.size();for (int i = 0; i < n; i++) {int hash[128] = {0};for (int j = i; j < n; j++) {if (++hash[s[j]] > 1) break;ret = max(ret, j - i + 1);}}return ret;
}
2. 滑动窗口法(高效,优雅)
关键观察:
窗口内不允许出现重复字符;
当右指针右移加入一个字符时,如果该字符已经在窗口中,就必须收缩左指针,直到它不重复为止;
这样,窗口始终保持「无重复」性质。
具体做法:
用
left
、right
表示当前窗口;
right
不断右移,将字符加入窗口;如果发现当前字符出现次数 > 1,则说明重复,开始移动
left
,逐个移除左侧字符,直到重复被消除;在每次合法窗口下,更新最大长度
ans
。
三、窗口变化示意
字符串:abcabcbb
Step 1: [a] → max = 1
Step 2: [ab] → max = 2
Step 3: [abc] → max = 3
Step 4: [abca] → a 重复 → 左移,直到去掉第一个 a → [bca]
Step 5: [bcab] → b 重复 → 左移,直到去掉第一个 b → [cab]
...
关键:当右扩导致重复,就用左缩去除重复,保持窗口合法。
四、算法流程
1. 初始化
准备数组
hash[128]
统计字符出现次数(也可以用 map)。初始化左右指针
left = 0, right = 0
,答案ans = 0
。
2. 右扩
每次把
s[right]
纳入窗口,hash[s[right]]++
。
right++
。
3. 左缩
当
不断移动hash[s[right]] > 1
时,说明当前字符重复:left
,对每个字符hash[s[left]]--
,直到当前重复字符次数降为 1。
4. 更新结果
- 每次窗口合法时更新
ans = max(ans, right - left)
。
遍历结束返回 ans。
五、代码实现(C++)
#include <string>
#include <algorithm>
using namespace std;class Solution {
public:int lengthOfLongestSubstring(string s) {int hash[128] = {0}; // 字符频次表(ASCII)int left = 0, right = 0; // 滑动窗口左右指针int n = s.size();int ans = 0;while (right < n) {hash[s[right]]++; // 右扩:加入一个字符// 出现重复 → 左缩while (hash[s[right]] > 1) {hash[s[left]]--;left++;}// 窗口合法时更新最大长度ans = max(ans, right - left + 1);right++;}return ans;}
};
六、代码剖析与细节问题
1. 为什么用 while 而不是 if
因为可能出现多个相同字符连在一起,比如
"abba"
。如果只判断一次,窗口可能还不合法;而 while 会一直收缩直到合法。
2. hash 数组大小 128 的原因
题目限定输入字符是英文或符号,ASCII 范围足够,用数组代替 map 速度更快,空间 O(1)。
3. 更新 ans 的位置很重要
必须在窗口合法之后更新,即完成收缩后。
4. 暴力 vs 滑动窗口
暴力每次都从头统计 → O(n²);
滑动窗口左右指针各最多走 n 步 → O(n)。
七、复杂度分析
- 时间复杂度:O(n)
虽然有嵌套 while,但
left
和right
各自最多移动 n 次,总共 2n,摊还线性。
- 空间复杂度:O(1)
仅使用固定大小的 hash 数组,不依赖额外数据结构。
八、总结
这道题是滑动窗口中最经典的「窗口去重」问题:
右扩增加长度,左缩去重保持合法;
维护一个哈希频次表是关键;
时间复杂度从 O(n²) → O(n),性能飞跃。
掌握这个模式,你就能自如地应对后续字符串窗口题,例如「最小覆盖子串」「字符串的排列」等进阶题。
🔜 下一讲预告
下一篇我们将进入《最大连续 1 的个数 III》。
当窗口中允许有限次“容错”时,如何用滑动窗口保持最大连续段?这将是对“窗口状态维护”的一次小进阶 👣
📚 系列更新提示
本文是《滑动窗口专题》的第 2 篇。
本专题将带你从最基础的“最短子数组”,一路深入到「最小覆盖子串」「滑动窗口最大值」等经典高频题,系统掌握滑动窗口在数组与字符串问题中的全部核心用法。
📌 系列持续更新中,欢迎 点赞、收藏、关注,不要错过每一次窗口技巧的进阶!🚀