Hi 各位小伙伴👋,今天咱们来搞定 LeetCode 第 3 题 ——无重复字符的最长子串,这道题是面试高频的「滑动窗口」入门题,我会用最通俗的话,结合我的代码一步步讲透,看完你也能轻松写出!


先看懂题目:到底要找啥?

题目给我们一个字符串 s,要找出最长的、里面没有重复字符的连续子串,返回它的长度。

举个例子:

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

注意:子串必须是连续的,像 "pwke" 这种跳着选的是「子序列」,不算数!


我的思路:用「滑动窗口」找最长无重复串

我最开始想:暴力枚举所有子串,挨个检查有没有重复字符,这肯定能做,但时间复杂度是 O (n²),字符串长一点就超时了。

后来想到用滑动窗口(双指针)+ 哈希表的思路:

  • 用两个指针 l(左)和 r(右),框出一个「窗口」,窗口里的字符都是无重复的
  • 用哈希表记录窗口里每个字符的出现次数,方便快速查重
  • 右指针 r 不断往右扩,把新字符加进窗口;如果发现重复字符,就把左指针 l 往右挪,直到窗口里没有重复字符
  • 每次调整完窗口,都更新一下「最长长度」

代码逐行拆解

完整代码

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 滑动窗口 + 哈希表查重
        // 哈希表:key=字符,value=当前窗口内该字符出现的次数
        HashMap<Character, Integer> map = new HashMap<>();
        // 左右指针:l=窗口左边界,r=窗口右边界
        int l = 0, r = 0;
        // 记录最长无重复子串的长度
        int maxLong = 0;
        
        // 右指针一直往右走,直到遍历完整个字符串
        while (r < s.length()) {
            // 1. 如果当前字符(s.charAt(r))在窗口里已经出现过(次数≥1)
            //    就把左指针往右挪,直到窗口里没有重复字符
            while (map.getOrDefault(s.charAt(r), 0) >= 1) {
                // 把左指针指向的字符从窗口里“移除”:次数减1
                map.put(s.charAt(l), map.getOrDefault(s.charAt(l), 0) - 1);
                // 左指针右移
                l++;
            }
            // 2. 现在窗口里没有重复字符了,把当前字符加进窗口:次数+1
            map.put(s.charAt(r), map.getOrDefault(s.charAt(r), 0) + 1);
            // 3. 右指针继续往右扩
            r++;
            // 4. 更新最长长度:当前窗口长度 = r - l
            maxLong = Math.max(maxLong, r - l);
        }
        // 遍历结束,返回最长长度
        return maxLong;
    }
}

关键步骤拆解(跟着我走一遍就懂)

我们拿示例 s = "abcabcbb" 来演示:

1. 初始化
  • l=0, r=0, maxLong=0,哈希表为空
  • 右指针 r 从 0 开始,逐个字符往后走
2. 右指针扩窗口(无重复时)
  • r=0(字符 a):哈希表没 a,直接加进去 → map={a:1}r=1maxLong=1
  • r=1(字符 b):哈希表没 b,加进去 → map={a:1,b:1}r=2maxLong=2
  • r=2(字符 c):哈希表没 c,加进去 → map={a:1,b:1,c:1}r=3maxLong=3
3. 遇到重复字符,左指针缩窗口
  • r=3(字符 a):哈希表 a 次数是 1(≥1),进入内层循环:
    • l=0 指向的 a 次数减 1 → map={a:0,b:1,c:1}l=1
    • 现在 a 次数是 0,退出内层循环
  • a 加进窗口 → map={a:1,b:1,c:1}r=4maxLong 还是 3(窗口长度还是 3)
4. 继续遍历,直到结束

后面 r 继续走到 4b)、5c)…… 都会触发类似的「缩窗口」操作,但最长长度始终保持 3,最终返回 3。


核心细节讲透

1. 为什么用 getOrDefault

map.getOrDefault(s.charAt(r), 0) 是为了避免 null 异常:

  • 如果字符不在哈希表里,默认返回 0,代表没出现过
  • 如果在表里,就返回它的出现次数,方便判断是否重复

2. 为什么窗口长度是 r-l

  • 右指针 r已经加进窗口的下一个位置,所以当前窗口里的字符是 [l, r-1]
  • 长度 = (r-1) - l + 1 = r - l,直接算 r-l 就对了

3. 内层循环的作用:「去重」

当右指针遇到重复字符时,我们必须把左指针往右挪,直到重复字符被移出窗口,这样才能保证窗口里永远是「无重复字符」的子串。


复杂度分析

  • 时间复杂度:O (n)。每个字符最多被左指针和右指针各遍历一次,所以整体是线性时间。
  • 空间复杂度:O (min (m, n))。哈希表最多存 m 个不同字符(比如字符集是 26 个字母),n 是字符串长度。

我踩过的坑(新手避坑指南)

  1. 忘记更新 maxLong:只记得扩窗口,忘了每次扩完都要算一下当前窗口长度,导致最后返回 0。
  2. 内层循环条件写错:写成 map.containsKey(s.charAt(r)),会导致重复字符已经被移出窗口后还在循环,要判断「出现次数≥1」。
  3. 窗口长度算错:写成 r-l+1,会多算一个字符,因为 r 已经指向下一个位置了。
  4. 字符串为空:如果 s 是空串,直接返回 0,代码里 while(r<s.length()) 会直接不执行,返回 maxLong=0,刚好处理这种情况。

小结:滑动窗口解题三步口诀

  1. 右指针扩窗口:把新字符加进哈希表,记录出现次数
  2. 遇重复就缩窗口:左指针往右挪,直到窗口里无重复
  3. 更新最长长度:每次调整完窗口,都算一下当前长度,更新最大值
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐