3.无重复字符的最长子串
中等
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
一、滑动窗口思想详解
1. 基本概念
- 窗口:一个连续的子区间,通过左右指针界定范围。
- 滑动:窗口的左右边界可以向某个方向 "滑动",每次滑动一个单位。
- 动态调整:根据当前窗口的状态,动态收缩左边界或扩展右边界。
2. 适用场景
-
问题特征:
- 求满足条件的最长 / 最短子串。
- 子串需满足连续性(元素连续出现)。
- 条件通常涉及元素不重复 、元素和 / 积等。
-
典型问题:
- 无重复字符的最长子串(当前代码)。
- 最小覆盖子串(LeetCode 76)。
- 和为 K 的最长子数组(LeetCode 325)。
二、为什么使用滑动窗口?
1. 暴力解法的瓶颈
以 "无重复字符的最长子串" 为例,暴力解法需枚举所有子串(时间复杂度 O (n²)),并检查每个子串是否无重复(时间复杂度 O (n)),总时间复杂度为 O (n³)。
2. 滑动窗口的优化
-
避免重复检查 :
滑动窗口通过维护一个动态区间,利用历史信息快速判断新窗口是否合法。例如,当右边界扩展时,若发现重复字符,只需收缩左边界直至无重复,无需重新检查整个子串。
-
时间复杂度降为 O (n) :
每个元素最多被左右指针各访问一次,总操作次数为 2n,时间复杂度 O (n)。
三、滑动窗口的核心逻辑
1. 窗口的维护
-
扩展右边界 :
不断将元素加入窗口,更新窗口状态(如字符频率、和等)。
-
收缩左边界 :
当窗口状态不满足条件时(如出现重复字符),收缩左边界并更新状态,直至条件满足。
2. 状态的高效更新
-
数据结构辅助 :
使用哈希表(HashSet/HashMap)记录窗口内元素的状态,支持 O (1) 时间的查询和更新。
-
不变量的维护 :
在窗口滑动过程中,某些属性(如元素和、不重复元素数)需动态维护,确保每次操作后状态的正确性。
java
public int lengthOfLongestSubstring(String s) {
int left = 0, right = 0;
//定义一个哈希集合,因为HashSet不允许存储重复字符,可用来快速判断字符是否重复
HashSet<Character> set = new HashSet<>();
//初始化最长子串长度
int maxLen = 0;
//右指针遍历
while (right < s.length()) {
// 当set里面有字符与右指针所指的s字符串中字符重复时,左指针开始滑动,收缩左边界
while (set.contains(s.charAt(right))) {
set.remove(s.charAt(left));
left++;
}
// 没重复时加入当前右指针所指s字符串的字符,扩展右边界
set.add(s.charAt(right));
right++;
// 比较历史最长子串长度与当前set的大小,更新最大长度
maxLen = Math.max(maxLen, set.size());
}
return maxLen;
}
学习时比较注意的知识点:
1. s.charAt(index)
是什么?
这是 Java 中 String
类的实例方法,用于获取字符串中指定位置的字符。
-
作用 :返回字符串
s
中索引为index
的字符(索引从 0 开始)。 -
示例 :
String s = "abc"; char c = s.charAt(1); // c 的值为 'b'(索引 1 对应第二个字符)
-
在代码中的意义 :
s.charAt(right)
:获取右指针当前指向的字符。s.charAt(left)
:获取左指针当前指向的字符。
2. 为什么要用 HashSet<Character>
?
这里的哈希表(HashSet
)主要用于快速判断字符是否重复。
- 特性 :
- 元素唯一性 :
HashSet
不允许存储重复元素。 - O (1) 查询效率:通过哈希函数直接定位元素,判断存在性的速度极快。
- 元素唯一性 :
- 在代码中的作用 :
- 存储当前窗口的字符 :每次扩展右边界时,将字符加入
set
。 - 检测重复 :通过
set.contains(...)
快速判断新字符是否已存在于窗口中。 - 收缩窗口 :遇到重复字符时,通过
set.remove(...)
移除左指针字符,直到无重复。
- 存储当前窗口的字符 :每次扩展右边界时,将字符加入
对比其他数据结构:
- 若用数组遍历判断重复,时间复杂度为 O (n),会导致整体算法变为 O (n²)。
- 而
HashSet
的 O (1) 查询使算法保持 O (n) 高效。
3. Math.max(maxLen, set.size())
如何工作?
这是 Java 中 Math
类的静态方法,用于比较两个值的大小并返回较大值。
-
语法 :
Math.max(a, b); // 返回 a 和 b 中的较大值(a ≥ b ? a : b)
-
在代码中的意义 :
set.size()
:当前窗口的大小(无重复字符的子串长度)。maxLen
:历史记录的最长子串长度。- 每次扩展窗口后,通过
Math.max
更新maxLen
,确保其始终记录最大值
示例:
java
// 假设当前窗口为 "abc",set.size() = 3,maxLen = 2
maxLen = Math.max(2, 3); // maxLen 更新为 3
总结:三个关键设计的作用
代码元素 | 作用 |
---|---|
s.charAt(index) |
访问字符串中指定位置的字符,用于遍历和窗口边界操作。 |
HashSet<Character> |
快速检测字符重复,确保窗口内无重复字符,是 O (n) 时间复杂度的核心。 |
Math.max(a, b) |
动态更新最长子串长度,保证结果的正确性。 |
补充:为什么用 HashSet
而非 HashMap
?
在这个问题中,HashSet
已足够,因为只需存储字符本身,无需记录其位置。
若改用 HashMap<Character, Integer>
记录字符的最新位置,可进一步优化收缩逻辑(直接将左指针跳转到重复位置的下一位),但会增加代码复杂度。当前实现用 HashSet
更简洁,适合入门理解。