LeetCode 3. 无重复字符的最长子串:滑动窗口最优解演进与解析

在LeetCode的字符串类题目中,「3. 无重复字符的最长子串」是经典的入门级难题,核心考察对「滑动窗口」与「哈希表」结合用法的理解。本题的关键在于如何从暴力解法的O(n²)时间复杂度,优化到O(n)的最优解。本文将通过两段功能一致但风格不同的代码,拆解滑动窗口的优化逻辑,对比代码细节差异,帮你彻底吃透这道题。

一、题目回顾

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

示例:

  • 输入:s = "abcabcbb",输出:3(最长子串为"abc")

  • 输入:s = "bbbbb",输出:1(最长子串为"b")

  • 输入:s = "pwwkew",输出:3(最长子串为"wke"或"kew")

二、核心解法:滑动窗口+哈希表

本题的最优解法是「滑动窗口+哈希表」组合,核心思路是用两个指针(left、right)维护一个无重复字符的窗口,通过哈希表记录字符最新位置,实现窗口的动态调整,最终找到最长窗口长度。

核心逻辑拆解:

  1. left 为窗口左边界,right 为窗口右边界,初始时窗口为空;

  2. right 指针遍历字符串,将当前字符存入哈希表(key为字符,value为最新索引);

  3. 若当前字符已在哈希表中,且其索引在left右侧(说明在当前窗口内重复),则将left跳至重复字符的下一个位置,收缩窗口;

  4. 每次遍历后计算当前窗口长度(right - left + 1),更新最长长度记录res;

  5. 遍历结束后,res即为无重复字符的最长子串长度。

该思路的关键优化的是:无需删除哈希表中的历史数据,仅通过判断字符索引与left的位置关系,就能确定是否在当前窗口内重复,避免了暴力删除带来的O(n)开销,最终实现O(n)时间复杂度(每个字符仅被right遍历一次)。

三、两段代码的对比与解析

以下两段代码均实现了上述核心逻辑,功能完全一致、返回结果无差异,仅在循环结构、初始化方式上存在细节不同,代表了从「过渡版」到「简洁版」的优化过程。

版本1:while循环过渡版(lengthOfLongestSubstring_1)

typescript 复制代码
function lengthOfLongestSubstring_1(s: string): number {
  const sL = s.length;
  const map = new Map<string, number>();
  if (sL === 0) {
    return 0;
  }
  let left = 0;
  let right = 1;
  let res = 1;
  map.set(s[0], 0);
  while (right < sL && left < sL) {
    if (map.has(s[right]) && map.get(s[right])! >= left) {
      left = map.get(s[right])! + 1;
    }
    res = Math.max(res, right - left + 1);
    map.set(s[right], right);
    right++;
  }
  return res;
};

版本2:for循环简洁版(lengthOfLongestSubstring_2)

typescript 复制代码
function lengthOfLongestSubstring_2(s: string): number {
  const map = new Map<string, number>(); // 存储字符 -> 字符最新出现的索引
  let left = 0; // 滑动窗口左边界(左闭)
  let res = 0; // 记录最长无重复子串长度
  const sL = s.length;

  // 右指针right遍历字符串,作为滑动窗口右边界(右闭)
  for (let right = 0; right < sL; right++) {
    const currentChar = s[right];
    // 关键:如果当前字符已存在,且其索引在左边界右侧(说明在当前窗口内重复)
    if (map.has(currentChar) && map.get(currentChar)! >= left) {
      // 直接将左边界跳到重复字符的下一个位置,无需删除map中的旧数据
      left = map.get(currentChar)! + 1;
    }
    // 更新当前字符的最新索引(无论是否重复,都要更新,保证后续判断准确)
    map.set(currentChar, right);
    // 计算当前窗口长度,更新最大值(每次循环都计算,避免else分支的遗漏)
    res = Math.max(res, right - left + 1);
  }

  return res;
}

细节差异对比

对比维度 lengthOfLongestSubstring_1 lengthOfLongestSubstring_2
循环结构 while循环,手动初始化right=1并递增right++ for循环,自动管理right(从0开始到sL-1)
初始化逻辑 提前处理s[0],map预存s[0]的索引,res初始为1 无预处理,所有字符统一在循环内处理,res初始为0
空字符串处理 单独判断sL===0,返回0 无需单独处理,for循环不执行,res直接返回0
代码简洁度 稍繁琐,存在冗余初始化和条件 更简洁,逻辑统一,可读性更强

关键共性说明

两段代码的核心逻辑完全一致,均规避了原始解法中「暴力删除哈希表数据」的问题,通过「索引判断+left跳转」实现O(1)窗口调整。同时,两者都放弃了「仅在无重复时更新res」的错误逻辑,改为每次遍历后都计算窗口长度,避免遗漏场景(如调整right后窗口长度成为新最大值)。

四、代码优化建议与注意事项

1. 优先选择版本2的原因

版本2的for循环更贴合「右指针完整遍历字符串」的逻辑直觉,无需手动管理right的递增,减少了冗余代码和潜在bug(如while循环中冗余的left < sL条件,实际可删除)。同时,统一的初始化逻辑让代码更易读,也更符合行业内对该题的标准解法写法。

2. 易踩坑点提醒

  • 哈希表必须存储「字符最新索引」:无论字符是否重复,每次都要更新map中的值,否则会因旧索引导致left跳转错误。

  • left跳转需加判断条件:必须确保map中重复字符的索引≥left,否则会误跳(如历史字符不在当前窗口内,无需调整left)。

  • res更新时机:需在每次调整完left、更新完map后执行,确保覆盖所有窗口状态。

3. 进一步优化方向

若想进一步提升性能,可将Map替换为数组(因字符串由ASCII字符组成,可使用长度为128的数组存储索引,访问速度比Map更快),优化后的代码如下:

typescript 复制代码
function lengthOfLongestSubstring(s: string): number {
  const arr = new Array(128).fill(-1); // 存储字符ASCII码对应的最新索引
  let left = 0, res = 0;
  for (let right = 0; right < s.length; right++) {
    const charCode = s.charCodeAt(right);
    // 若字符已存在且在当前窗口内,调整left
    if (arr[charCode] >= left) {
      left = arr[charCode] + 1;
    }
    arr[charCode] = right;
    res = Math.max(res, right - left + 1);
  }
  return res;
}

五、总结

LeetCode 3题的核心是「滑动窗口+哈希表」的优化思路,两段代码的差异仅为细节风格,无本质功能区别。版本2作为简洁版,更适合实际开发和面试答题场景。掌握本题的关键在于理解「无需删除历史数据,仅通过索引判断调整窗口」的优化逻辑,这一思路也可迁移到其他滑动窗口类题目(如最长子串、子数组相关问题)。

建议在练习时,先理解版本1的过渡逻辑,再优化到版本2的简洁写法,最后尝试数组替代Map的性能优化,逐步吃透滑动窗口的核心用法。

相关推荐
沉默-_-2 小时前
备战蓝桥杯--栈
数据结构·算法·力扣·
weixin199701080162 小时前
B2Bitem_get - 获取商标详情接口对接全攻略:从入门到精通
java·大数据·算法
奔跑的web.2 小时前
前端使用7种设计模式的核心原则
前端·javascript·设计模式·typescript·vue
棱镜Coding2 小时前
LeetCode-Hot100 31.K个一组反转链表
算法·leetcode·链表
蜕变菜鸟2 小时前
折叠页面 css
前端
2401_832131952 小时前
模板编译期机器学习
开发语言·c++·算法
嵌入小生0072 小时前
Data Structure Learning: Starting with C Language Singly Linked List
c语言·开发语言·数据结构·算法·嵌入式软件
菩提小狗2 小时前
小迪安全2022-2023|第35天:WEB攻防-通用漏洞&XSS跨站&反射&存储&DOM&盲打&劫持|web安全|渗透测试|
前端·安全·xss
ValhallaCoder2 小时前
hot100-子串
数据结构·python·算法