【LeetCode01】—— 无重复字符的最长子串:滑动窗口经典题详解

【LeetCode】------ 无重复字符的最长子串:滑动窗口经典题详解

导语

LeetCode 第 3 题"无重复字符的最长子串"(Longest Substring Without Repeating Characters)是滑动窗口算法的入门经典。这道题在面试中出现频率极高,不仅考察你对字符串处理的理解,更是检验你是否真正掌握滑动窗口思想的试金石。

很多初学者拿到这题会想到暴力枚举所有子串,但那样做的时间复杂度是 O(n³),在 LeetCode 上直接超时。用滑动窗口可以做到 O(n),这就是本文要讲的核心。


一、题目描述

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

示例 1:

复制代码
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

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

示例 3:

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

约束条件:

  • 0 <= s.length <= 5 * 10⁴
  • s 由英文字母、数字、符号和空格组成

二、核心概念:什么是滑动窗口

滑动窗口本质上是双指针的一种应用。维护一个窗口 [left, right],窗口内的元素满足某个条件(这里是"无重复字符")。通过移动左右边界来探索所有可能的合法窗口,同时记录最优解。

说白了,就是用两个指针框住一段字符串,右边往右扩直到不满足条件,左边往右缩来恢复条件,过程中不断更新答案。

本章小结

概念 说明
窗口 leftright 两个指针界定的子串
窗口扩张 right 右移,尝试纳入新字符
窗口收缩 left 右移,移除左侧字符以消除重复
合法条件 窗口内无重复字符
最优解 所有合法窗口中的最大长度

三、解法一:暴力枚举(理解用,实际不推荐)

暴力思路:枚举所有子串,检查每个子串是否无重复,取最长的。

python 复制代码
def lengthOfLongestSubstring(s: str) -> int:
    def all_unique(sub: str) -> bool:
        return len(set(sub)) == len(sub)

    max_len = 0
    for i in range(len(s)):
        for j in range(i + 1, len(s) + 1):
            if all_unique(s[i:j]):
                max_len = max(max_len, j - i)
    return max_len

复杂度分析:

  • 时间:O(n³) --- 枚举子串 O(n²),检查是否重复 O(n)
  • 空间:O(min(n, m)),m 为字符集大小

这个解法在 LeetCode 上会 TLE(超时),放在这里只是为了让你理解问题的朴素形式。


四、解法二:滑动窗口 + 哈希集合(标准解法)

核心思路:

  1. leftright 两个指针维护一个窗口
  2. 用一个 Set 记录窗口内已有字符
  3. right 不断右移,如果 s[right] 不在集合中,加入集合,更新最大长度
  4. 如果 s[right] 已在集合中(重复了),就不断右移 left 并从集合中移除 s[left],直到重复消失
  5. 重复 3-4 直到 right 遍历完整个字符串

图解过程

s = "abcabcbb" 为例:

复制代码
步骤   窗口          集合           max_len
1      [a]           {a}            1
2      [ab]          {a,b}          2
3      [abc]         {a,b,c}        3
4      [abca]b       重复! 左移 →
       a[bca]b       {b,c,a}        3
5      ...继续推进...

代码实现(Python)

python 复制代码
def lengthOfLongestSubstring(s: str) -> int:
    char_set = set()
    left = 0
    max_len = 0

    for right in range(len(s)):
        # 如果当前字符已在窗口中,收缩左边界
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1
        # 当前字符加入窗口
        char_set.add(s[right])
        # 更新最大长度
        max_len = max(max_len, right - left + 1)

    return max_len

代码实现(Java)

java 复制代码
public int lengthOfLongestSubstring(String s) {
    Set<Character> charSet = new HashSet<>();
    int left = 0, maxLen = 0;

    for (int right = 0; right < s.length(); right++) {
        while (charSet.contains(s.charAt(right))) {
            charSet.remove(s.charAt(left));
            left++;
        }
        charSet.add(s.charAt(right));
        maxLen = Math.max(maxLen, right - left + 1);
    }

    return maxLen;
}

代码实现(Go)

go 复制代码
func lengthOfLongestSubstring(s string) int {
    charSet := make(map[byte]bool)
    left, maxLen := 0, 0

    for right := 0; right < len(s); right++ {
        for charSet[s[right]] {
            delete(charSet, s[left])
            left++
        }
        charSet[s[right]] = true
        if right-left+1 > maxLen {
            maxLen = right - left + 1
        }
    }

    return maxLen
}

复杂度分析:

  • 时间:O(n) --- 虽然有内层 while 循环,但每个字符最多被 left 扫过一次,总体是 O(2n) = O(n)
  • 空间:O(min(n, m)),m 为字符集大小

五、解法三:滑动窗口 + 哈希表(优化版)

上面的解法中,当发现重复时,left 需要一步步右移。有没有办法直接跳到重复字符的下一个位置?

用哈希表记录每个字符最后出现的位置。当发现 s[right] 重复时,直接把 left 跳到 重复位置 + 1,省去中间的逐步收缩。

代码实现(Python)

python 复制代码
def lengthOfLongestSubstring(s: str) -> int:
    char_index = {}  # 字符 -> 最后出现的索引
    left = 0
    max_len = 0

    for right in range(len(s)):
        char = s[right]
        # 如果字符重复且在当前窗口内,直接跳到重复位置+1
        if char in char_index and char_index[char] >= left:
            left = char_index[char] + 1
        char_index[char] = right
        max_len = max(max_len, right - left + 1)

    return max_len

关键点:char_index[char] >= left

这个条件很重要。如果记录的重复位置在 left 左边,说明那个重复字符已经不在当前窗口里了,不需要跳。

代码实现(Java)

java 复制代码
public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> charIndex = new HashMap<>();
    int left = 0, maxLen = 0;

    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (charIndex.containsKey(c) && charIndex.get(c) >= left) {
            left = charIndex.get(c) + 1;
        }
        charIndex.put(c, right);
        maxLen = Math.max(maxLen, right - left + 1);
    }

    return maxLen;
}

复杂度分析:

  • 时间:O(n) --- 只需一次遍历
  • 空间:O(min(n, m))

六、三种解法对比

解法 时间复杂度 空间复杂度 特点
暴力枚举 O(n³) O(min(n,m)) 理解用,会超时
滑动窗口+集合 O(n) O(min(n,m)) 标准解法,容易理解
滑动窗口+哈希表 O(n) O(min(n,m)) 优化版,减少不必要的收缩

实际面试中,写解法二就够了。解法三是锦上添花,能体现你对滑动窗口的深入理解。


七、常见问题与避坑

1. 为什么 left 可以直接跳而不是逐步收缩?

因为在哈希表优化版中,我们记录了每个字符的最后位置。当发现重复时,我们确定重复字符在 char_index[char] 这个位置,直接跳到它的下一位就行。中间那些字符肯定不重复(否则之前就会被处���掉),所以可以安全跳过。

2. 为什么判断条件是 char_index[char] >= left

举个例子:s = "abba"

  • 处理到第二个 b 时,left = 0char_index['b'] = 1,需要跳到 left = 2
  • 处理到最后一个 a 时,char_index['a'] = 0,但 left = 20 < 2,说明第一个 a 已经不在窗口内了,不应该跳

不加这个条件,结果会错。

3. 空字符串怎么处理?

题目约束 0 <= s.length,所以空串是合法输入。上述代码对空串自然返回 0,不需要特殊处理。

4. 全是相同字符怎么办?

比如 s = "bbbb",窗口会一直保持长度 1,leftright 同步右移,最终返回 1。没问题。

5. 字符集只有 ASCII 吗?

题目说"英文字母、数字、符号和空格",但实际代码对 Unicode 也适用,因为哈希表/集合可以存任意字符。


八、滑动窗口的通用模式

这道题的解法可以抽象为滑动窗口的通用模板:

python 复制代码
def sliding_window(s):
    window = {}  # 或 set,视题目而定
    left = 0
    result = 0

    for right in range(len(s)):
        # 1. 扩大窗口
        c = s[right]
        window[c] = window.get(c, 0) + 1

        # 2. 收缩窗口(当窗口不满足条件时)
        while 窗口需要收缩:
            d = s[left]
            window[d] -= 1
            left += 1

        # 3. 更新结果
        result = max(result, right - left + 1)

    return result

掌握这个模板后,很多子串/子数组问题都能套用,比如:

  • LeetCode 76:最小覆盖子串
  • LeetCode 438:找到字符串中所有字母异位词
  • LeetCode 209:长度最小的子数组

九、总结

核心要点:

  1. 滑动窗口是解决"最长/最短子串/子数组"问题的利器,时间复杂度 O(n)
  2. 用集合记录窗口内容判断重复,用哈希表可以进一步优化跳跃
  3. left 跳跃时要检查 char_index[char] >= left,避免跳出窗口边界
  4. 滑动窗口的套路是固定的:扩大 → 收缩 → 更新结果

验证清单:

  • 能手写滑动窗口 + 集合的标准解法
  • 理解哈希表优化版中 >= left 条件的作用
  • 能用通用模板解决类似的子串问题
  • 能分析时间复杂度并解释为什么是 O(n)

参考资源

相关推荐
wabs6661 小时前
关于动态规划【力扣96.不同的二叉搜索树的递推公式怎么理解?】
算法·动态规划
何以解忧,唯有..1 小时前
Python 中的继承机制:从基础到高级用法详解
java·开发语言·python
QiLinkOS1 小时前
极客与商业思维的融合实践(1)
c语言·数据库·c++·人工智能·算法·开源协议
fu的博客1 小时前
【数据结构16】图:基于邻接矩阵、邻接表实现DFS/BFS
数据结构·算法
阿正的梦工坊1 小时前
【Rust】17-Send、Sync 与并发安全抽象
算法·安全·rust
plainGeekDev1 小时前
算法刷题笔记:一维DP没那么难,状态想清楚就赢了一半
java·算法·面试
try2find1 小时前
agent环境安装spacy
python·智能体
菩提树下的凡夫1 小时前
新版OpenCV5.0在ONNX模型的推理应用
opencv·算法
ellenwan20262 小时前
期货程序化开平标志错了总拒单:天勤 last_msg 排查思路
python