双指针专题(五):灵活的起跳——「无重复字符的最长子串」

场景想象: 你在排队处理一串项链上的珠子(字符串),你的任务是截取一段颜色都不重复的珠子。

  • 右指针 (right):负责探索,一颗颗把珠子纳入口袋。

  • 左指针 (left):负责"止损"。

    • 一旦右指针发现:"哎呀,这颗红珠子('a')我口袋里已经有一颗了!"

    • 这时候,左指针不需要一步步往右挪,它可以直接跳 到口袋里那颗旧红珠子的后面一位。因为旧红珠子前面的所有区间,只要包含旧红珠子,就一定会有重复,所以统统不要了!

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

https://leetcode.cn/problems/longest-substring-without-repeating-characters/

题目分析:

  • 输入 :字符串 s

  • 目标 :找到其中不含有重复字符的 最长子串 的长度。

  • 输出:长度数值。

例子: s = "abcabcbb"

  1. [abc]: 无重复,长3。

  2. 遇到第二个 a:重复了!

    • 左指针如果不跳,还在第一个 a 那里,那就是 abca(重复)。

    • 左指针必须跳过第一个 a,来到 b 的位置。当前窗口变 [bca]

  3. 遇到第二个 b

    • 左指针跳过旧的 b,来到 c 的位置。当前窗口变 [cab]
  4. ...

核心思维:Map 记录索引 + 懒加载跳跃

我们需要一个 哈希表 (Map) 来记录:"某个字符上一次出现在什么位置(下标)"

关键逻辑:right 遇到一个字符 c,且 c 在 Map 里存在时:

  • 说明遇到重复了。

  • 我们需要把 left 更新到 Map.get(c) + 1 的位置(也就是重复字符的下一位)。

  • 但是! 有个惊天大坑

    • 比如 abba

    • 遇到第二个 b 时,left 跳到了 2 (第二个 b 的位置)。

    • 接着遇到第二个 a。Map 里记着第一个 a0

    • 如果你直接 left = map.get('a') + 1left 会跳回 1

    • 这就倒车了! left 只能往右走,不能往回退。

    • 所以正确的逻辑是:left = Math.max(left, map.get(c) + 1)

代码实现 (JavaScript)

JavaScript

复制代码
/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    // 记录字符上一次出现的位置
    // key: 字符, value: 索引 index
    let map = new Map();
    let left = 0;
    let maxLen = 0;

    for (let right = 0; right < s.length; right++) {
        const c = s[right];

        // 1. 如果 map 里有这个字符,说明可能是重复项
        if (map.has(c)) {
            // 2. 更新左边界
            // 核心:只能向右跳,不能倒车!
            // 比如 "abba",遇到第二个 a 时,map.get('a') 是 0,但 left 已经在 2 了
            // 所以必须取 max
            left = Math.max(left, map.get(c) + 1);
        }

        // 3. 更新/记录当前字符的最新位置
        map.set(c, right);

        // 4. 更新最大长度
        // 当前窗口长度 = right - left + 1
        maxLen = Math.max(maxLen, right - left + 1);
    }

    return maxLen;
};

深度模拟:"abba"

  1. right=0 ('a'): map无。map={a:0}max=1

  2. right=1 ('b'): map无。map={a:0, b:1}max=2

  3. right=2 ('b'): 重复!

    • map 有 'b' (index 1)。

    • left = max(0, 1 + 1) = 2。左指针跳到 index 2。

    • 此时窗口是 b (index 2)。

    • 更新 map={a:0, b:2}

    • len = 2 - 2 + 1 = 1max 还是 2。

  4. right=3 ('a'): 重复!

    • map 有 'a' (index 0)。

    • left = max(2, 0 + 1)

    • 注意!如果不取 max,left 会退回到 1。但我们取 max,所以 left 保持在 2。

    • 逻辑含义:虽然 a 重复了,但那个旧的 a 早就被之前的 b 重复事件给排除在窗口左边了,所以不用管它。

    • 更新 map={a:3, b:2}

    • len = 3 - 2 + 1 = 2

总结

这道题的精髓在于:滑动窗口 + 哈希索引优化 。 普通滑动窗口是 left++ 一步步挪,而这道题利用 Map 实现了 left精准空降

记住那个坑:left = Math.max(left, map.get(c) + 1)。这是防止"时光倒流"的护身符。


下一题预告:水果成篮

做完了"无重复的最长子串",下一题 LC 904. 水果成篮 (Medium) 就是它的孪生兄弟。

  • 题目:你有两个篮子,每个篮子只能装一种类型的水果。

  • 目标:找到最长的连续子序列,使得其中至多 包含两种不同的元素。

    • 比如 [1, 2, 1, 2, 3] -> 最长是 [1, 2, 1, 2],长度 4。一旦遇到 3,就变成三种了,必须扔掉前面的。
  • 这道题其实就是:"至多包含 K 个不同字符的最长子串"(这里 K=2)。

准备好去果园摘水果了吗?逻辑和这道题非常像,但 Map 的用法略有不同哦!

相关推荐
永远都不秃头的程序员(互关)9 小时前
【决策树深度探索(一)】从零搭建:机器学习的“智慧之树”——决策树分类算法!
算法·决策树·机器学习
苦藤新鸡10 小时前
27.合并有序链表,串葫芦
前端·javascript·链表
_OP_CHEN10 小时前
【前端开发之HTML】(四)HTML 标签进阶:表格、表单、布局全掌握,从新手到实战高手!
前端·javascript·css·html·html5·网页开发·html标签
程序员-King.10 小时前
day161—动态规划—最长递增子序列(LeetCode-300)
算法·leetcode·深度优先·动态规划·递归
西柚小萌新10 小时前
【计算机视觉CV:目标检测】--3.算法原理(SPPNet、Fast R-CNN、Faster R-CNN)
算法·目标检测·计算机视觉
高频交易dragon10 小时前
Hawkes LOB Market从论文到生产
人工智能·算法·金融
谢尔登10 小时前
Vue3底层原理——keep-alive
javascript·vue.js·ecmascript
Deca~10 小时前
VueVirtualLazyTree-支持懒加载的虚拟树
前端·javascript·vue.js
2501_9445264210 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 主题切换实现
android·开发语言·javascript·python·flutter·游戏·django
_OP_CHEN10 小时前
【算法基础篇】(五十)扩展中国剩余定理(EXCRT)深度精讲:突破模数互质限制
c++·算法·蓝桥杯·数论·同余方程·扩展欧几里得算法·acm/icpc