题目描述

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

image-20260928181226016

题意分析

在字符串中找出最长的一段连续字符,要求这段范围内每个字符最多出现一次,返回它的长度,不需要返回子串本身。子串必须连续,不能跳过中间字符来消除重复。

重复与否只针对选中的这段子串,不要求字符在整个字符串中只出现一次。空格、数字和符号也参与判断;空串的答案为 0,非空串至少可以选择一个字符。

解法:滑动窗口 + 最近出现位置

核心思路

[!blue]

用左右边界表示当前考察的连续区间,也就是滑动窗口。右边界 right 每次向右加入一个字符,左边界 left 只在必须排除重复时右移。这样可以沿用上一轮已经检查过的结果,不必从每个起点重新扫描。

加入当前字符 ch 之前,窗口内已经没有重复,因此这一步只可能由 ch 引入重复。设它上一次出现在位置 p:如果 p 还在窗口内,要同时保留当前位置的 ch,左边界就必须越过 p。移到 p + 1 恰好排除旧字符,其余字符仍然互不重复,再多移一步只会白白缩短窗口。

如果旧位置已经在窗口左侧,它就不参与当前子串,左边界应保持不动。因此更新规则是 left = max(left, p + 1)。这里取最大值很重要:曾经为了排除其他重复字符移走的部分,不能因为读到较早的旧位置又重新放回窗口。

代码用 last[ch] 保存最近出现位置的下标加一,这样数组默认的 0 就能表示尚未出现,还能直接作为左边界的候选值。每轮先用旧记录更新 left,用 right - left + 1 更新答案,最后再写入 last[ch] = right + 1,避免把本次出现误当成旧记录。

调整后的窗口是以当前 right 结尾的最长合法子串:更靠左的起点会保留已经确认的重复,更靠右的起点只会更短。每个子串都有一个右端点,枚举全部右端点并保留最大的窗口长度,就能覆盖全局最优答案。

解题步骤

  • 初始化数组 last、左边界 left = 0 和答案 ans = 0,从左到右枚举 right。
  • 读取当前字符的旧记录,令 left = max(left, last[ch]),使窗口恢复无重复。
  • 用 right - left + 1 更新 ans,保留遍历过程中出现过的最大长度。
  • 最后记录 last[ch] = right + 1,供后续字符使用;遍历结束返回 ans。

代码实现

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int[] last = new int[128];
        int left = 0;
        int ans = 0;

        for (int right = 0; right < s.length(); right++) {
            char ch = s.charAt(right);

            left = Math.max(left, last[ch]);
            ans = Math.max(ans, right - left + 1);
            last[ch] = right + 1;
        }

        return ans;
    }
}
func lengthOfLongestSubstring(s string) int {
    var last [128]int
    left, ans := 0, 0

    for right := 0; right < len(s); right++ {
        if last[s[right]] > left {
            left = last[s[right]]
        }
        if length := right - left + 1; length > ans {
            ans = length
        }
        last[s[right]] = right + 1
    }
    return ans
}

复杂度分析

  • 时间复杂度:$O(n)$,右指针遍历一次,每次只做常数次操作;左指针直接跳转,不需要回头扫描。
  • 空间复杂度:$O(1)$,只使用固定大小的数组和几个变量。

关键点总结

[!green]

  • 下标加一:旧字符在下标 i 时,下一次遇到它可直接把左边界移到 i + 1,同时利用数组默认的 0 表示未出现。
  • 只处理当前字符的重复:加入字符前窗口已经无重复,因此只需越过当前字符的旧位置,不必重新检查整个窗口。
  • 窗口与答案分开维护:窗口是闭区间,长度为 right - left + 1;ans 记录全局最大值,而不是最后一个窗口的长度。

易错点总结

[!yellow]

  • 左边界不能回退:旧记录可能位于窗口外,直接赋值会把已排除的重复区间放回来,必须与当前 left 取最大值。
  • 先用旧位置,再写新位置:如果先记录 right + 1 再更新 left,就会丢失旧位置,并把当前字符也排除在窗口外。
  • 闭区间长度要加一:只有一个字符时,left = right,窗口长度应为 1,不能写成 right - left。
  • 不能只返回最后的窗口长度:最长子串可能在更早的位置结束,需要用 ans 保存扫描过程中的最大长度。

相似题目

题目 难度 关联与区别
159. 至多包含两个不同字符的最长子串 中等 同样维护可扩张再收缩的字符窗口,原题允许两种字符重复,本题每种字符最多出现一次。
424. 替换后的最长重复字符 中等 同样追踪窗口内字符频次,原题允许替换预算,本题遇重复就必须调整左边界。
340. 至多包含 K 个不同字符的最长子串 中等 用滑动窗口维护字符频次和有效左边界;本题每个字符最多保留一次,该题允许最多 k 种字符。
904. 水果成篮 中等 用滑动窗口维护字符频次和有效左边界;本题每个字符最多保留一次,该题把两种水果限制转为两类元素窗口。
1100. 长度为 K 的无重复字符子串 中等 用滑动窗口维护字符频次和有效左边界;本题每个字符最多保留一次,该题固定窗口长度后统计无重复窗口。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/11034463
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!