跳至主要內容

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

郑天祺大约 6 分钟刷题LeetCode算法滑动窗口字符串

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

难度:中等 | 标签:字符串、滑动窗口、哈希表


📌 题目描述

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

示例

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

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

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
         注意,"pwke" 是一个子序列,不是子串。

💡 多种解法对比

解法时间复杂度空间复杂度核心思想推荐度
暴力枚举O(n³)O(min(m, n))枚举所有子串,检查是否含重复
滑动窗口(HashSet)O(2n) ≈ O(n)O(min(m, n))双指针 + Set 去重⭐⭐⭐
滑动窗口优化(HashMap)O(n)O(min(m, n))双指针 + Map 记录最后位置⭐⭐⭐⭐⭐

解法一:暴力枚举(Brute Force)

思路

枚举所有可能的子串,对每个子串检查是否有重复字符。

子串定义:s[i...j] 表示从索引 i 到 j 的连续子串

步骤:
1. 外层循环 i:子串起始位置
2. 中层循环 j:子串结束位置(j ≥ i)
3. 内层检查:s[i...j] 是否有重复字符
4. 记录最大长度

代码

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        int maxLen = 0;

        for (int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                if (checkUnique(s, i, j)) {
                    maxLen = Math.max(maxLen, j - i + 1);
                }
            }
        }
        return maxLen;
    }

    // 检查 s[start...end] 是否有重复字符
    private boolean checkUnique(String s, int start, int end) {
        Set<Character> set = new HashSet<>();
        for (int i = start; i <= end; i++) {
            if (set.contains(s.charAt(i))) {
                return false;
            }
            set.add(s.charAt(i));
        }
        return true;
    }
}

复杂度

  • 时间:O(n³) — 枚举 O(n²) 子串 × 检查 O(n)
  • 空间:O(min(m, n)) — Set 大小受字符集限制

❌ LeetCode 会超时,仅作理解用。


解法二:滑动窗口(HashSet)

核心洞察

暴力解法有大量重复检查。观察发现:

  • 如果 s[i...j] 无重复,只需检查 s[j+1] 是否在其中
  • 如果 s[j+1] 重复了,左边界 i 需要右移,直到重复字符被排除

这就是滑动窗口:维护一个无重复的窗口 [left, right],右边界不断扩展,遇到重复就收缩左边界。

Mermaid 图解:滑动窗口过程

代码

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Set<Character> set = new HashSet<>();
        int left = 0, right = 0;
        int maxLen = 0;

        while (right < s.length()) {
            char c = s.charAt(right);

            // 如果字符已在窗口中,收缩左边界直到可以加入
            while (set.contains(c)) {
                set.remove(s.charAt(left));
                left++;
            }

            // 加入当前字符
            set.add(c);
            maxLen = Math.max(maxLen, right - left + 1);
            right++;
        }

        return maxLen;
    }
}

复杂度

  • 时间:O(2n) ≈ O(n) — 每个字符最多被访问两次(加入和移除各一次)
  • 空间:O(min(m, n)) — Set 大小

解法三:滑动窗口优化(HashMap)✅ 最优解

核心优化

解法二中,当发现重复字符时,左边界是逐个右移直到重复字符被排除。能否直接跳到重复字符的下一个位置

HashMap<字符, 最后出现位置> 记录每个字符的最新索引,一旦发现重复,直接把 left 跳到 max(left, map.get(c) + 1)

Mermaid 图解:优化后的跳跃式收缩

关键细节:为什么用 max(left, ...)

代码

class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 记录字符最后出现的位置
        Map<Character, Integer> map = new HashMap<>();
        int maxLen = 0;
        int left = 0; // 窗口左边界

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

            // 如果字符已存在且在窗口内,收缩左边界
            if (map.containsKey(c)) {
                left = Math.max(left, map.get(c) + 1);
            }

            // 更新字符位置和最大长度
            map.put(c, right);
            maxLen = Math.max(maxLen, right - left + 1);
        }

        return maxLen;
    }
}

代码要点

要点说明
map.put(c, right)每次都要更新,确保记录最新位置
left = max(left, map.get(c) + 1)防止左边界回退(如 "abba" 案例)
窗口大小 = right - left + 1包含两端,注意 +1

📊 完整过程图解:以 "abcabcbb" 为例


🎯 总结

要点说明
滑动窗口本质维护一个动态区间,右边界扩张探索,左边界收缩保证性质
HashMap 优化记录字符最后位置,实现 O(1) 跳跃式收缩
max(left, ...)关键细节,防止左边界回退
适用场景所有"最长/最短子串/子数组满足某条件"类问题

类似题目推荐

  • LeetCode 76: 最小覆盖子串
  • LeetCode 209: 长度最小的子数组
  • LeetCode 438: 找到字符串中所有字母异位词
  • LeetCode 567: 字符串的排列

🔗 复杂度对比总结

┌─────────────┬──────────┬──────────┬────────────┐
│    解法     │  时间    │  空间    │   特点     │
├─────────────┼──────────┼──────────┼────────────┤
│ 暴力枚举    │  O(n³)   │ O(min(m,n)) │ 超时      │
│ Set滑动窗口 │  O(2n)   │ O(min(m,n)) │ 逐个收缩  │
│ Map滑动窗口 │  O(n)    │ O(min(m,n)) │ ✅最优解  │
└─────────────┴──────────┴──────────┴────────────┘

💡 m = 字符集大小(ASCII 128/扩展 ASCII 256/Unicode 更多),n = 字符串长度

上次编辑于:
贡献者: zhengtianqi