每日一题:LeetCode 3:无重复字符的最长子串——滑动窗口思路详解

这道题是滑动窗口里非常经典的一道题。

题目要求:

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

例如:

复制代码
s = "abcabcbb"

最长的不重复子串可以是:

复制代码
"abc"
"bca"
"cab"

所以答案是:

复制代码
3

这道题为什么适合滑动窗口?

因为题目要求的是:

一段连续区间里的最长结果。

这种题很适合用两个指针维护一个窗口:

复制代码
left
↓
a b c a b c b b
    ↑
   right

left 表示窗口左边界,right 表示窗口右边界。

窗口就是:

复制代码
s[left ... right]

核心思想:

right 不断向右扩大窗口,发现重复字符时,就移动 left 缩小窗口。


什么是滑动窗口?

滑动窗口本质上就是:

用两个指针维护一个连续区间。

例如:

复制代码
a b c a b c b b
↑   ↑
L   R

当前窗口:

复制代码
abc

没有重复字符,所以窗口合法。

于是 right 继续向右:

复制代码
a b c a
↑     ↑
L     R

窗口变成:

复制代码
abca

出现了两个 a,不合法。

这时候就移动左指针:

复制代码
a b c a
  ↑   ↑
  L   R

窗口变成:

复制代码
bca

重新满足"没有重复字符"。

所以滑动窗口的核心就一句话:

右指针负责扩张,左指针负责缩小。



JavaScript 代码

javascript 复制代码
var lengthOfLongestSubstring = function(s) {
  let left = 0
  let maxLength = 0

  const set = new Set()

  for (let right = 0; right < s.length; right++) {

    while (set.has(s[right])) {
      set.delete(s[left])
      left++
    }

    set.add(s[right])

    maxLength = Math.max(
      maxLength,
      right - left + 1
    )
  }

  return maxLength
}

这段代码就是标准的滑动窗口写法。


五、代码逐步讲解

先初始化:

复制代码
let left = 0
let maxLength = 0
const set = new Set()

含义:

复制代码
left
→ 窗口左边界

maxLength
→ 当前找到的最大长度

set
→ 当前窗口里的字符

六、right 为什么一直往右走?

复制代码
for (let right = 0; right < s.length; right++)

right 表示:

当前准备加入窗口的字符位置。

例如:

复制代码
abcabcbb
↑
R

abcabcbb
 ↑
 R

abcabcbb
  ↑
  R

每一轮:

复制代码
right 往右走一步

就是不断扩大窗口。


七、为什么这里要用 while?

核心代码:

复制代码
while (set.has(s[right])) {
  set.delete(s[left])
  left++
}

意思是:

如果当前字符已经存在于窗口中,就不断移动 left,直到重复字符消失。

例如:

复制代码
abc

当前:

复制代码
set = {a, b, c}

现在 right 又遇到:

复制代码
a

窗口变成:

复制代码
abca

发现:

复制代码
set.has("a")
// true

说明重复。

于是:

复制代码
set.delete(s[left])
left++

删掉左边的 a。

窗口变成:

复制代码
bca

现在不重复了。


八、为什么不是 if,而是 while?

因为删除一个字符以后,不一定马上满足条件。

滑动窗口常见模板就是:

复制代码
while (窗口不合法) {
  缩小窗口
}

意思是:

只要窗口还不满足要求,就一直缩。

这也是滑动窗口题里非常常见的写法。


九、什么时候把新字符加入窗口?

当重复已经被处理完之后:

复制代码
set.add(s[right])

这时候可以保证:

当前窗口内没有重复字符。

于是就可以计算当前窗口长度。


十、为什么窗口长度是 right - left + 1?

例如:

复制代码
a b c
0 1 2

如果:

复制代码
left = 0
right = 2

窗口长度应该是:

复制代码
3

计算:

复制代码
right - left + 1

就是:

复制代码
2 - 0 + 1
// 3

所以:

复制代码
maxLength = Math.max(
  maxLength,
  right - left + 1
)

表示:

当前窗口长度和历史最大长度比较,保存更大的那个。


十一、用 abcabcbb 走一遍

字符串:

复制代码
a b c a b c b b

开始:

复制代码
left = 0
set = {}
maxLength = 0

right = 0

字符:

复制代码
a

加入:

复制代码
a

长度:

复制代码
1

right = 1

字符:

复制代码
b

窗口:

复制代码
ab

长度:

复制代码
2

right = 2

字符:

复制代码
c

窗口:

复制代码
abc

长度:

复制代码
3

所以:

复制代码
maxLength = 3

right = 3

字符又是:

复制代码
a

当前:

复制代码
abca

重复。

移动 left:

复制代码
abc a
 ↑  ↑
 L  R

窗口重新变成:

复制代码
bca

长度还是:

复制代码
3

后面继续重复这个过程。

最终答案:

复制代码
3

十二、这道题的核心思路

这道题可以压缩成四步:

复制代码
1. right 向右扩大窗口

2. 如果出现重复字符
   → left 向右缩小窗口

3. 保证窗口始终没有重复字符

4. 不断记录最大窗口长度

这就是滑动窗口。


十三、滑动窗口通用模板

很多字符串和数组题都可以套这个思路:

复制代码
let left = 0

for (let right = 0; right < nums.length; right++) {

  // 1. 把 right 对应元素加入窗口

  while (窗口不满足条件) {
    // 2. 移除 left 对应元素
    left++
  }

  // 3. 此时窗口合法
  // 4. 更新答案
}

核心规律:

复制代码
right
→ 扩大窗口

left
→ 缩小窗口

十四、为什么时间复杂度是 O(n)?

虽然代码里有:

复制代码
for

里面还有:

复制代码
while

但并不是 O(n²)。

因为:

复制代码
right 只向右移动
left  也只向右移动

每个字符最多:

复制代码
进入窗口一次
离开窗口一次

所以总操作次数大约是:

复制代码
2n

时间复杂度:

复制代码
O(n)

空间复杂度:

复制代码
O(n)

因为最坏情况下,Set 可能保存整个字符串的字符。


十五、Set 版和 Map 版有什么区别?

这道题常见两种写法。

Set

记录:

复制代码
当前窗口里有哪些字符

遇到重复字符时:

复制代码
left 一个一个向右移动

优点:

思路简单,非常适合学习滑动窗口。


Map

记录:

复制代码
字符最后一次出现的位置

遇到重复字符时,可以直接:

复制代码
跳过重复位置

例如:

复制代码
var lengthOfLongestSubstring = function(s) {
  const map = new Map()

  let left = 0
  let maxLength = 0

  for (let right = 0; right < s.length; right++) {

    if (
      map.has(s[right]) &&
      map.get(s[right]) >= left
    ) {
      left = map.get(s[right]) + 1
    }

    map.set(s[right], right)

    maxLength = Math.max(
      maxLength,
      right - left + 1
    )
  }

  return maxLength
}

不过第一次学习建议先掌握 Set 版。


十六、滑动窗口适合解决什么题?

滑动窗口特别适合:

连续子数组 / 连续子串 + 最长、最短、满足某种条件

例如:

  • 无重复字符的最长子串
  • 至多包含 K 个不同字符的最长子串
  • 最小覆盖子串
  • 长度最小的子数组
  • 固定长度窗口求最大值/平均值

它们的核心套路基本都是:

复制代码
扩大窗口
↓
判断条件
↓
不满足就缩小窗口
↓
满足后更新答案

总结

这道题的核心不是 Set,而是:

滑动窗口 + 双指针。

最核心的代码只有:

复制代码
for (let right = 0; right < s.length; right++) {

  while (set.has(s[right])) {
    set.delete(s[left])
    left++
  }

  set.add(s[right])

  maxLength = Math.max(
    maxLength,
    right - left + 1
  )
}

把它翻译成人话就是:

复制代码
right 不断往右扩

↓

如果出现重复

↓

left 不断往右缩

↓

直到窗口重新合法

↓

记录当前窗口长度

↓

继续向右

一句话记住滑动窗口:

右指针负责扩,左指针负责缩,始终维护一个满足条件的连续区间。

相关推荐
CarIise1 小时前
Java实训阶段查漏补缺复习笔记2
java·linux·算法
无忧.芙桃2 小时前
C++ 并查集详解:原理、实现与应用
c语言·c++·算法·leetcode
蛋蛋的就会顺顺的2 小时前
hot100——二叉树
java·数据结构·算法·leetcode·力扣
Code_Solitude2 小时前
c语言:二分法的完整补充
c语言·开发语言·算法
一切皆是因缘际会2 小时前
掌控信息论:同源星际通信基础理论
人工智能·算法·ai
动词ing5 小时前
【C语言题目练习】算法 整数反转
c语言·开发语言·算法
Nil2085 小时前
leetcode 55跳跃游戏
算法·leetcode·游戏
longlongzihan5 小时前
LeetCode 128. 最长连续序列 —— 从排序到哈希集合
数据结构·c++·算法·leetcode·排序算法·哈希表
mmmmath_35 小时前
二叉树の一些知识
数据结构·算法