力扣hot100之3:无重复字符的最长子串
力扣hot100之3:无重复字符的最长子串

LeetCode 3:无重复字符的最长子串(Longest Substring Without Repeating Characters)题解

Longest Substring Without Repeating Characters 是一道非常经典的字符串题。

题目要求我们在一个字符串中,找到不含重复字符的最长子串,并返回这个子串的长度。

这道题通常有两种思路:

  • 暴力枚举

  • 滑动窗口 + HashMap 优化

下面按照这两种方式来分析。


题目示例

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

再看一个例子:

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

一、暴力枚举

最直接的思路是:

  • 枚举每一个位置作为结尾

  • 然后向前寻找,直到出现重复字符为止

  • 在这个过程中更新答案

这就是一种典型的暴力做法。


暴力代码(带注释)

class Solution {

    public int lengthOfLongestSubstring(String s) {
        // ans 用来记录当前找到的最长无重复子串长度
        int ans = 0;

        // 枚举每一个位置 i,把它看作子串的结尾位置
        for (int i = 0; i < s.length(); i++) {

            // 用布尔数组记录当前枚举过程中字符是否出现过
            // 这里默认字符集较小,可以直接用数组做标记
            boolean[] tar = new boolean[300];

            // 从位置 i 开始,向左枚举
            for (int j = i; j >= 0; j--) {

                // 如果当前字符已经出现过,说明出现重复,直接结束本轮
                if (tar[s.charAt(j)]) {
                    break;
                }

                // 标记当前字符已经出现
                tar[s.charAt(j)] = true;

                // 更新最长长度
                ans = Math.max(ans, i - j + 1);
            }
        }

        return ans;
    }
}

暴力思路分析

假设字符串是:

"abcabcbb"

当我们枚举到某个位置 i 时:

  • i 开始向前找

  • 只要没有重复字符,就继续扩大这个子串

  • 一旦发现重复字符,就停止

比如以字符 c 结尾时,我们往前可以得到 "abc" 再继续往前时如果出现重复,就不能再扩展了。


暴力解法复杂度分析

时间复杂度

O(n^2)

原因是:

  • 外层循环枚举每个位置

  • 内层循环最坏情况下会一直往前扫描

所以整体是平方级别复杂度。

空间复杂度

O(1)

这里使用的是固定大小的布尔数组,空间不随输入规模增长,因此可以看作常数空间。

说明:如果严格从字符集通用性出发,这种写法依赖字符范围。 在 LeetCode 这道题的常见数据范围下,这样写是可以工作的。


二、滑动窗口 + HashMap 优化

暴力解法的问题在于:

很多字符被重复扫描了。

能不能做到:

  • 遇到重复字符时,不再回头重新检查

  • 而是直接把左边界移动到合适位置

这就是滑动窗口的核心思想。

我们可以使用一个 HashMap<Character, Integer> 来记录字符最近一次出现的位置,从而快速更新窗口左边界。


优化代码(带注释)

import java.util.HashMap;

class Solution {

    public int lengthOfLongestSubstring(String s) {
        // map 记录字符最近一次出现的位置
        // 这里保存的是“下标 + 1”,这样写可以方便处理左边界更新
        HashMap<Character, Integer> map = new HashMap<>();

        // ans 记录当前最长无重复子串长度
        int ans = 0;

        // i 表示窗口右边界,j 表示窗口左边界
        for (int i = 0, j = 0; i < s.length(); i++) {

            // 如果当前字符之前出现过
            if (map.containsKey(s.charAt(i))) {
                // 更新左边界
                // 取最大值是为了防止左边界回退
                j = Math.max(j, map.get(s.charAt(i)));
            }

            // 以当前 i 作为右边界,更新答案
            ans = Math.max(ans, i - j + 1);

            // 记录当前字符最新出现的位置
            // 注意这里存的是 i + 1
            map.put(s.charAt(i), i + 1);
        }

        return ans;
    }
}

为什么 map.put(s.charAt(i), i + 1) 要存 i + 1

这是这道题里一个非常容易让人迷惑的点。

我们先看这句:

j = Math.max(j, map.get(s.charAt(i)));

这里的 j 表示的是当前窗口的左边界。

如果某个字符之前出现过,那么左边界应该直接跳到:

这个字符上一次出现位置的下一个位置

所以在 map 里直接存 i + 1,就可以在更新左边界时少写一步运算,代码更简洁。


优化思路演示

以字符串:

"abcabcbb"

为例。

初始状态

  • i = 0

  • j = 0

  • 窗口为空

当扫描到 'a'

  • 'a' 没出现过

  • 当前窗口是 "a"

  • 长度为 1

记录:

map['a'] = 1

当扫描到 'b'

  • 'b' 没出现过

  • 当前窗口是 "ab"

  • 长度为 2

记录:

map['b'] = 2

当扫描到 'c'

  • 'c' 没出现过

  • 当前窗口是 "abc"

  • 长度为 3

记录:

map['c'] = 3

当再次扫描到 'a'

  • 'a' 之前出现过

  • 它上一次出现后一个位置是 1

  • 所以左边界更新为 j = 1

此时窗口变成:

"bca"

仍然保持无重复。

这就是滑动窗口“向前滑动”的本质。


为什么更新左边界时要写 Math.max(j, map.get(...))

因为左边界只能向右移动,不能回退。

举个例子:

如果当前窗口左边界已经在比较靠右的位置,而某个字符虽然之前出现过,但它出现的位置在当前窗口左边界之前,那么这个重复其实对当前窗口没有影响。

所以必须写成:

j = Math.max(j, map.get(s.charAt(i)));

这样才能保证窗口始终合法。


优化解法复杂度分析

时间复杂度

O(n)

因为每个字符最多被访问有限次,左右指针整体只会向前移动。

空间复杂度

O(k)

其中 k 是字符集里实际出现在窗口中的不同字符数量。

如果按最坏情况来写,也可以记作:

O(min(n, 字符集大小))

在日常题解中,通常也会简化写成 O(n)


这道题的核心思想

这题最重要的并不是背代码,而是理解这两个点:

1. 窗口中不能出现重复字符

所以一旦有重复,就要移动左边界。

2. 左边界不回退

即使某个字符之前出现过,也要判断它的上次出现位置是否真的落在当前窗口内。

这就是为什么要使用 Math.max(...)


两种解法对比

暴力枚举

优点:

  • 思路直观

  • 容易想到

  • 适合刚开始理解题意时使用

缺点:

  • 存在大量重复扫描

  • 时间复杂度较高

滑动窗口 + HashMap

优点:

  • 只需要线性扫描

  • 性能更好

  • 是这道题的标准解法

缺点:

  • 初学时不太容易想到

  • 对窗口边界的更新要求更细致


总结

无重复字符的最长子串 是滑动窗口的代表题之一。

它非常适合用来理解:

  • 什么是滑动窗口

  • 什么是窗口左边界和右边界

  • 为什么要记录字符最近出现的位置

  • 为什么左边界不能回退

如果你把这道题真正理解透了,后面很多字符串窗口题都会轻松很多。


适合直接发布的结尾

刷这道题时,建议按下面的顺序来练:

  1. 先自己写出暴力解法

  2. 明确暴力做法哪里有重复计算

  3. 再理解滑动窗口为什么能避免重复扫描

  4. 最后彻底吃透 HashMap + 双指针 的写法

很多字符串题,本质上都是在维护一个“合法窗口”。

而这道题,正是理解这个思想的最好起点之一。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇