大家好,我是你们的算法小伙伴。今天我们来练习一道字符串与滑动窗口的经典经典题 ------LeetCode 3. 无重复字符的最长子串。这道题是面试中考察滑动窗口(双指针)和哈希表的标杆题目,核心在于如何用线性时间复杂度动态维护一个 "无重复字符的窗口"。
题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
注意:子串是连续的字符序列,不同于子序列(不要求连续)。
示例 1:
输入:s = "abcabcbb"
输出:3
解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是 "b"。
示例 3:
输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是 "wke",注意 "pwke" 是子序列而不是子串。
提示:
0 <= s.length <= 5 * 10⁴s由英文字母、数字、符号和空格组成
解题思路
核心矛盾
如何在遍历字符串的过程中,快速判断当前字符是否重复,并快速调整窗口左边界?
方法一:滑动窗口 + 哈希表(最优解,O (n))
核心思想 :用左右指针维护一个动态窗口 [left, right],保证窗口内字符唯一。
- 右指针:不断向右扩展,将新字符加入窗口。
- 哈希表(数组):记录每个字符最后一次出现的索引。
- 左指针调整:当右指针遇到重复字符时,将左指针直接跳转到 "重复字符的下一位"(需取当前左指针与历史位置的较大值,防止回退)。
- 更新长度:每次遍历都计算当前窗口长度,更新最大值。
为什么用数组? ASCII 码表共有 128 个字符,因此可以用一个长度为 128 的 int 数组代替 HashMap,将时间复杂度优化到极致,且空间复杂度仅为 O(1)。
代码实现
方法一:滑动窗口(最优解)
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int maxLen = 0;
// 定义数组记录字符最后出现的位置,初始为-1
int[] lastPos = new int[128];
Arrays.fill(lastPos, -1);
int left = 0; // 窗口左边界
// 右指针遍历字符串
for (int right = 0; right < n; right++) {
char c = s.charAt(right);
// 关键步骤:如果当前字符出现过,且其位置在窗口内,更新左边界
// Math.max是为了防止left回退(例如"abba"这种情况)
left = Math.max(left, lastPos[c] + 1);
// 更新当前字符的最新位置
lastPos[c] = right;
// 计算当前窗口长度并更新最大值
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
代码详解
1. 初始化
lastPos[128]:存储每个 ASCII 字符最后一次出现的索引,初始化为-1(表示未出现)。left = 0:窗口起始位置。maxLen = 0:记录最长子串长度。
2. 遍历逻辑(右指针扩展)
char c = s.charAt(right):获取当前字符。left = Math.max(left, lastPos[c] + 1):核心去重逻辑 。- 如果
c是第一次出现,则lastPos[c] = -1,left保持不变。 - 如果
c重复出现,则将左边界移动到该字符上一次出现位置的下一位,确保窗口内无重复。 - 使用
Math.max是为了处理如"abba"这种情况:当处理到第二个'a'时,lastPos['a']是 0,left会变成 1;但当处理第二个'b'时,lastPos['b']是 2,如果不加max,left会回退到 3,这是错误的,因此必须保证left只增不减。
- 如果
lastPos[c] = right:更新该字符的最新位置。maxLen = ...:计算窗口长度right - left + 1并取最大值。
示例 1 模拟:s = "abcabcbb"
表格
| right | char | lastPos (初 - 1) | left (更新) | 窗口 [left, right] | maxLen |
|---|---|---|---|---|---|
| 0 | a | lastPos[97]=0 | 0 | [0,0] | 1 |
| 1 | b | lastPos[98]=1 | 0 | [0,1] | 2 |
| 2 | c | lastPos[99]=2 | 0 | [0,2] | 3 |
| 3 | a | lastPos[97]=3 | max(0,0+1)=1 | [1,3] | 3 |
| 4 | b | lastPos[98]=4 | max(1,1+1)=2 | [2,4] | 3 |
| 5 | c | lastPos[99]=5 | max(2,2+1)=3 | [3,5] | 3 |
| 6 | b | lastPos[98]=6 | max(3,3+1)=4 | [4,6] | 3 |
| 7 | b | lastPos[98]=7 | max(4,4+1)=5 | [5,7] | 3 |
| 最终结果为 3。 |
复杂度分析
表格
| 解法 | 时间复杂度 | 空间复杂度 | 优点 |
|---|---|---|---|
| 滑动窗口 + 数组 | O(n) | O(1) | 每个字符仅遍历一次,常数级空间,效率最高 |
| 暴力枚举 | O(n3) | O(1) | 思路直观,但绝对超时,不推荐 |
| 滑动窗口 + HashMap | O(n) | O(k) (k 为字符集大小) | 通用性强,但对于本题数组更快 |
总结
- 核心考点 :本题是滑动窗口的经典应用,考察对 "双指针动态调整" 和 "重复字符快速定位" 的理解。
- 优化技巧 :使用
int[128]数组代替HashMap,是处理 ASCII 字符集问题的标准技巧,能显著提升运行效率。 - 易错点 :
- left 的更新必须用
Math.max:这是最容易出错的地方,必须保证左指针只向右移动,不向左回退。 - 窗口长度计算 :
right - left + 1,不要忘记加 1。 - 初始值设定:数组初始化为 -1 而非 0,因为索引 0 也是有效位置。
- left 的更新必须用
今天的每日算法练习就到这里,我们明天再见!👋