📌 前言:为什么它是面试之王?
在 LeetCode 的“Hot 100”榜单中,第 3 题《无重复字符的最长子串》 绝对是面试界的“钉子户”。
这道题看似简单,实则考察了:
双指针思想(滑动窗口的本质)
哈希表的高效查询
边界条件的精准控制
很多同学卡在“为什么左指针要那么跳?”、“Map 里到底存什么?”。今天,我将用最通俗的语言,带你彻底拆解这个经典难题。
🟢 题目速览
LeetCode 3. 无重复字符的最长子串
给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。🧠 思路进化:从 O(n³) 到 O(n)
方法一:暴力枚举(理解题意用)
核心思想:
枚举所有可能的子串(起点 i,终点 j),然后检查这个子串里是否有重复字符。
缺点:
枚举子串 O(n²),检查重复 O(n),总复杂度高达O(n³)。
绝对会在大数据面前超时。
结论: 仅用于理解题意,实战不可用。
方法二:滑动窗口 + 哈希表(标准答案)
这是本题的正解。
1. 什么是“滑动窗口”?
想象你的手指在字符串上方画了一个框(窗口),这个框有左右两个边界:
右边界 (right):负责探索新字符,不断向右扩大窗口。
左边界 (left):当窗口内出现重复字符时,负责向右收缩窗口,直到把重复的那个字符“挤”出去。
2. 哈希表的作用
我们维护一个Map<Character, Integer>,用来记录:
Key:窗口内的字符
Value:该字符最近一次出现的位置
3. 核心逻辑拆解
我们以s = "abba"为例,看看窗口是如何变化的:
步骤 | right | 字符 | Map 状态 | left | 操作逻辑 |
|---|---|---|---|---|---|
1 | 0 | a |
| 0 | 无重复,更新最大长度 |
2 | 1 | b |
| 0 | 无重复,更新最大长度 |
3 | 2 | b |
| 2 | 发现重复! |
4 | 3 | a |
| 3 | 发现重复! |
注意第 4 步:这就是为什么代码中要用left = Math.max(left, ...),防止左指针倒退!
4. 代码实现(Java 最优解)
class Solution { public int lengthOfLongestSubstring(String s) { // 边界处理 if (s == null || s.length() == 0) { return 0; } // key: 字符, value: 字符最近一次出现的索引 Map<Character, Integer> map = new HashMap<>(); int left = 0; // 窗口左边界 int maxLen = 0; // 最长子串长度 for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); // 如果字符已出现,并且在当前窗口内 if (map.containsKey(c) && map.get(c) >= left) { // 收缩左边界到重复字符的下一个位置 left = map.get(c) + 1; } // 更新字符的最新位置(无论是否重复,都要更新) map.put(c, right); // 更新最大长度 maxLen = Math.max(maxLen, right - left + 1); } return maxLen; } }🚀 进阶优化:数组代替 HashMap(面试加分项)
如果题目中明确说明字符串只包含ASCII 字符(或者字符集范围已知),我们可以用int[]数组来代替HashMap,进一步提高效率。
原理:
字符本质上就是数字(ASCII 码),我们可以直接用字符作为数组下标。
class Solution { public int lengthOfLongestSubstring(String s) { int[] index = new int[128]; // ASCII 字符集 int left = 0; int maxLen = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); // 直接取上次出现的位置,与 left 比较 left = Math.max(left, index[c]); // 更新字符最近出现的位置 (+1 是为了方便计算) index[c] = right + 1; maxLen = Math.max(maxLen, right - left + 1); } return maxLen; } }📊 复杂度分析
解法 | 时间复杂度 | 空间复杂度 | 评价 |
|---|---|---|---|
暴力法 | O(n³) | O(1) | ❌ 不可用 |
滑动窗口 (HashMap) | O(n) | O(min(m, n)) | ✅ 标准解 |
滑动窗口 (数组) | O(n) | O(1) | ✅✅ 面试最优解 |
(注:m 为字符集大小)
💡 总结与套路
滑动窗口的通用模板:
初始化左指针 left = 0 初始化数据结构(Map/Set/数组) for 右指针 right 从 0 到 n-1: 处理 right 指向的元素 while 窗口不满足条件(如出现重复): 移除 left 指向的元素 left++ 更新结果一句话心得:
滑动窗口的本质是双指针的快慢游戏。
右指针负责“探路”,左指针负责“止损”。
哈希表则是你的“侦察兵”,负责告诉你哪里出现了重复。
掌握了这道题,你就掌握了解决“子串 + 最值” 类问题的万能钥匙。