LeetCode 3.无重复最长子串|滑动窗口(双指针)超详解析

0 阅读5分钟

image.png

在算法刷题中,最长无重复字符子串是滑动窗口的经典入门题型,也是面试高频考点。很多新手容易陷入暴力枚举的超时陷阱,而滑动窗口(双指针)算法可以将这道题的时间复杂度优化到极致。今天结合完整可运行的 JS 代码,从零拆解解题思路、核心逻辑、代码细节和易错点,帮你彻底吃透这道题。

一、题目描述

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

注意:子串是字符串中连续的字符序列,区别于不连续的子序列,这是解题的关键前提。

示例:

  • 输入: s = "abcabcbb",输出: 3,解释:最长无重复子串为 "abc"
  • 输入: s = "bbbbb",输出: 1,解释:最长无重复子串为 "b"
  • 输入: s = "pwwkew",输出:3,解释:最长无重复子串为 "wke"

二、解题思路:滑动窗口(双指针 + 哈希集合)

1. 暴力解法的弊端

最容易想到的思路是枚举所有子串,逐一判断是否有重复字符,记录最长长度。但这种方法的时间复杂度为 O(n²),遇到长字符串会直接超时,效率极低,无法通过题目测试用例。

2. 滑动窗口核心思想

滑动窗口是处理连续子数组/子串问题的最优解法之一,核心是用两个指针维护一个动态的窗口区间:

  • 快指针(fast) :不断向右扩张窗口,遍历字符串每个字符,负责探索新字符
  • 慢指针(slow) :当窗口内出现重复字符时,向右收缩窗口,剔除重复字符,保证窗口内始终无重复
  • Set 集合:存储当前窗口内的字符,利用 Set 元素唯一的特性,快速判断字符是否重复

全程保持窗口 [slow, fast] 内的字符无任何重复,每次扩张窗口后更新最长子串长度,遍历完成后即可得到答案。

三、完整可运行代码

以下是优化后的原生 JS 解题代码,也是本文解析的核心代码,可直接在 LeetCode 提交通过:

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    // 定义快慢指针、最长子串长度结果
    let fast = 0, slow = 0, ans = 0;
    // Set存储当前窗口内的字符,自动去重、快速查重
    let window = new Set();

    // 快指针遍历整个字符串,扩张窗口
    for(fast = 0; fast < s.length; fast++) {
        // 窗口内存在当前字符,持续收缩窗口,直到无重复
        while(window.has(s[fast])) {
            window.delete(s[slow]);
            slow++;
        }
        // 将当前字符加入窗口,维护无重复窗口
        window.add(s[fast]);
        // 更新最长子串长度
        ans = Math.max(ans, fast - slow + 1);
    }

    return ans;
};

四、逐行代码详细解析

1. 变量初始化

let fast = 0, slow = 0, ans = 0;
let window = new Set();
  • fast:快指针,窗口右边界,逐个遍历字符串字符
  • slow:慢指针,窗口左边界,控制窗口收缩范围
  • ans:记录全局最长无重复子串的长度,初始为0
  • window:Set 集合,存储当前窗口 [slow, fast] 内的所有字符,用于高效查重

2. 窗口扩张:快指针遍历

for(fast = 0; fast < s.length; fast++)

快指针从字符串首位遍历到末尾,每一次循环都将窗口向右扩张一位,逐个纳入新字符。

3. 窗口收缩:去除重复字符

while(window.has(s[fast])) {
    window.delete(s[slow]);
    slow++;
}

这是整道题的核心逻辑

当即将加入窗口的 s[fast] 已经存在于当前窗口中,说明窗口出现重复字符,不满足题意。此时需要持续收缩左边界:删除慢指针指向的字符,慢指针右移,直到窗口中彻底不存在重复字符为止,保证窗口始终是合法的无重复区间。

区别于 if 判断,while 循环可以一次性清除所有前置重复字符,适配所有重复场景。

4. 更新窗口与结果

window.add(s[fast]);
ans = Math.max(ans, fast - slow + 1);
  • 窗口合法后,将当前遍历的字符加入窗口
  • fast - slow + 1:计算当前窗口的字符长度(左右指针闭区间长度计算方式)
  • 每次遍历都对比当前窗口长度和历史最长长度,保留最大值

五、算法复杂度分析

时间复杂度:O(n)

字符串中每个字符只会被快慢指针各访问一次,不存在重复遍历,整体为线性时间复杂度,n 为字符串长度。

空间复杂度:O(1)

字符的取值范围是有限的(英文字母、数字、符号),Set 集合最多存储固定数量的字符,属于常数级空间,和字符串长度无关。

六、核心易错点总结

  1. 必须用 while 而非 if:如果只用 if 判断,只能收缩一次窗口,无法彻底清除前置重复字符,会导致窗口仍存在重复,结果错误。
  2. 窗口长度计算:左右指针为闭区间,必须 fast - slow + 1,少加1会导致长度统计偏小。
  3. 先收缩、后加字符:必须先清理重复字符,再将新字符加入窗口,顺序颠倒会导致重复字符被存入窗口。

七、解题总结

最长无重复子串是滑动窗口的标杆题型,核心解题模板可以总结为:右指针扩张探索,左指针收缩合规,容器存储窗口状态,实时更新最优解

掌握这道题的逻辑后,可以快速迁移到最长子数组、最小覆盖子串、无重复子数组等一系列滑动窗口题型,是算法进阶的必备基础。