langShiftlangShift

滑动窗口(题单顺序)

热题 100 - 滑动窗口分组(按题单顺序):3 / 438

本页包含题单「滑动窗口」分组的全部题目,顺序与 list.json 保持一致。


3. 无重复字符的最长子串

力扣原题:3. 无重复字符的最长子串
Medium
哈希表
字符串
滑动窗口

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

 

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

 

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成
最优解(滑动窗口 + 记录字符最近出现位置)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 朴素想法:枚举所有子串,检查是否有重复字符,取最长。子串数量是 (O(n^2)),每次检查还要扫一遍,整体会更慢。
  • 瓶颈在哪里?:我们在重复做“从某个位置开始,往右扩展直到出现重复字符”的事情;很多扩展过程是重叠的。
  • 关键观察(窗口的本质):如果我们维护一个区间 [left, right],保证这段子串里没有重复字符,那么右指针每往右走一步,我们只需要处理“新增的那一个字符”会不会造成冲突。
  • 冲突怎么处理?为什么 left 不是一步步挪?
    • s[right] = ch 在窗口里已经出现过时,窗口不再合法。
    • 你真正需要的是:把 left 直接跳过上一次 ch 出现的位置(否则窗口里仍然会有两个 ch)。
    • 所以我们记录 ch 最近一次出现的下标 lastPos[ch],并让 left = max(left, lastPos[ch] + 1)
    • 这里用 max 是因为:left 可能已经在更右边(比如处理过别的重复),不能往回退。
  • 为什么整体是 (O(n))right 从左到右只走一遍;left 也只会向右移动且最多移动 n 次。Map 查询均摊 (O(1))。
  • 用例子走一遍(抓住“跳跃”)"abba"
    • right=0 'a':窗口 "a",ans=1
    • right=1 'b':窗口 "ab",ans=2
    • right=2 'b':重复,left 跳到上次 b 的位置+1 → left=2,窗口变 "b"
    • right=3 'a':上次 a 在 0,不在窗口内(left=2),窗口 "ba",ans 仍 2
  • 易错点left 只能前进不能后退,所以要写成 left = Math.max(left, lastPos.get(ch) + 1)
  • 复杂度:时间 (O(n)),空间 (O(\Sigma))(字符集大小)。
类似题目(滑动窗口通用模板:右扩张 + 左收缩)
let left = 0
for (let right = 0; right < n; right++) {
add(arr[right]) // 把右端加入窗口
while (!windowIsValid()) {
remove(arr[left]) // 让窗口重新合法
left++
}
// 此时窗口合法,可更新答案
ans = update(ans, left, right)
}

438. 找到字符串中所有字母异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

 

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

 示例 2:

输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

 

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • s 和 p 仅包含小写字母
最优解(定长滑动窗口 + 计数差为 0)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 题意翻译:在 s 里找所有起点 i,使得长度为 p.length 的子串 s[i..i+m-1]p 是异位词(字母次数完全一样)。
  • 朴素做法:对每个起点都数一遍 26 个字母次数,再比较一次 → 每个窗口要 (O(26 + m)),窗口有 (O(n)) 个,整体也会偏慢。
  • 关键观察:窗口长度是固定的 (m)。固定长度意味着:窗口每右移一步,只会“进来一个字符、出去一个字符”,其余字符不变。
  • 怎么判断两个字符串是不是异位词?:看 26 个字母的计数是否完全相同。
  • 把“相同”改写成“差为 0”
    • 我们维护 diff[26] = count(window) - count(p)
    • 如果 diff 全部为 0,就说明窗口与 p 完全一致(异位词)。
  • 如何 (O(1)) 判断“全部为 0”?
    • 直接每次扫 26 个也可以(仍是 (O(26n)),对 26 这种常数通常够快)。
    • 这里更“套路化”:维护一个 nonZero 计数,表示 diff[i] != 0 的位置有多少个。nonZero==0 就代表全为 0。
    • 每次窗口移动只会影响两个字符的下标,所以 nonZero 也能 (O(1)) 更新。
  • 易错点:维护 nonZero 时要同时处理“从 0 变非 0”和“从非 0 变 0”两种变化,否则会错判窗口是否匹配。
  • 复杂度:时间 (O(n)),空间 (O(1))(固定 26 个计数)。
  • 小例子走一遍s="abab", p="ab", m=2
    • 初始窗口 "ab":diff 全 0 → 记录 0
    • 窗口右移:出 'a' 进 'a',diff 仍全 0 → 记录 1
    • 再右移同理 → 记录 2
类似题目(定长窗口:进一出一维护计数/差分)
// 固定窗口长度 m
initWindow(0, m - 1)
for (let i = m; i < n; i++) {
add(arr[i]) // 进
remove(arr[i - m]) // 出
if (isGood()) ans.push(i - m + 1)
}