力扣hot100:无重复字符的最长子串,找到字符串中所有字母异位词(滑动窗口算法讲解)(3,438)
问题描述
️ 核心思路:滑动窗口
我们使用 双指针 维护一个动态窗口 [left, right]
:
- 右指针
right
向前探索,将新字符纳入窗口 - 左指针
left
在发现重复时收缩窗口 - 哈希集合
set
实时记录窗口内的字符,实现 O(1) 重复检测
整个过程如同一个伸缩的窗口在字符串上滑动,始终保持窗口内无重复字符:
示例:s = "abcabcbb"
Step1: [a]bcabcbb → set=[a] len=1
Step2: [ab]cabcbb → set=[a,b] len=2
Step3: [abc]abcbb → set=[a,b,c] len=3
Step4: [abca]bcbb → 重复! 收缩左边界 → a[bca]bcbb → set=[b,c,a] len=3
...
代码实现(逐行解析)
class Solution {public int lengthOfLongestSubstring(String s) {Set<Character> set = new HashSet<>(); // 存储窗口内的字符int result = 0; // 记录最长长度// 双指针初始化:left固定左边界,right探索右边界for (int left = 0, right = 0; right < s.length(); right++) {// 关键步骤:发现重复字符时,持续收缩左边界while (set.contains(s.charAt(right))) {set.remove(s.charAt(left)); // 移除左边界字符left++; // 左指针右移}set.add(s.charAt(right)); // 将新字符加入窗口result = Math.max(result, right - left + 1); // 更新最大值}return result;}
}
关键操作解析:
set.contains(s.charAt(right))
O(1) 复杂度检测新字符是否重复set.remove(s.charAt(left++))
循环收缩左边界直到消除重复right - left + 1
实时计算当前无重复窗口长度
️ 复杂度分析
- 时间复杂度 O(n) 虽然嵌套循环,但每个字符最多被
left
和right
各访问一次(均摊 O(1)) - 空间复杂度 O(∣Σ∣) 字符集大小决定哈希表空间(ASCII 字符集为 O(128))
总结与思考
- 为什么用
while
而不是if
? 当新字符与窗口中多个字符冲突时(如 "abba"),必须持续收缩直到完全消除重复。 - 滑动窗口的本质 通过动态调整左右边界,将暴力解法的 O(n²) 优化至 O(n),是处理连续区间问题的利器。
- 拓展变种 若题目改为允许重复一次(如 LeetCode 424),只需将集合替换为哈希映射记录频次,稍作调整即可解决。
=========================================================================
题目描述
解题思路
字母异位词的核心特征是字符种类和数量完全相同,只是顺序不同。因此,我们可以通过比较字符频率来判断是否为异位词。具体步骤如下:
- 初始化频率数组:创建长度为 26 的数组
pCount
,统计字符串p
中每个字母的出现次数。 - 滑动窗口遍历:在字符串
s
上维护一个固定长度为p.length()
的窗口:- 每次将右边界字符加入窗口(对应计数加 1)。
- 当窗口大小超过
p.length()
时,移除左边界字符(对应计数减 1)。 - 比较当前窗口的字符频率与
pCount
,若相等则记录起始索引。
- 返回结果:收集所有满足条件的起始索引。
算法特点
- 时间复杂度:O(n),其中 n 是字符串
s
的长度。每个字符进入和离开窗口各一次,频率比较耗时 O(26) ≈ O(1)。 - 空间复杂度:O(1),仅使用固定大小的频率数组(长度 26)。
代码实现
class Solution {public List<Integer> findAnagrams(String s, String p) {List<Character> s_list = s.chars().mapToObj(c->(char)c).collect(Collectors.toList());List<Character> p_list = p.chars().mapToObj(c->(char)c).collect(Collectors.toList());List<Integer> result=new ArrayList<>();int length=s_list.size();// 改进的滑动窗口实现int pLen = p_list.size();if (s_list.size() < pLen) return result;// 创建字符频率数组int[] pCount = new int[26];int[] windowCount = new int[26];// 统计p中字符频率for (char c : p_list) {pCount[c - 'a']++;}// 滑动窗口for (int i = 0; i < s_list.size(); i++) {// 添加右边字符到窗口windowCount[s_list.get(i) - 'a']++;// 如果窗口大小超过p的长度,移除左边字符if (i >= pLen) {windowCount[s_list.get(i - pLen) - 'a']--;}// 比较窗口和p的字符频率if (Arrays.equals(pCount, windowCount)) {result.add(i - pLen + 1);}}return result;}
}
关键点解析
- 频率数组替代排序:避免对每个子串排序(O(p log p)),直接比较字符频率(O(26))。
- 滑动窗口边界处理:
- 当
i >= pLen
时,窗口长度超过p
,需移除左边界的字符。 - 仅当
i >= pLen - 1
时,窗口长度达到p
的长度,才进行频率比较。
- 当
- 索引计算:起始索引为
i - pLen + 1
(当前右边界索引减去窗口长度加 1)。
示例分析
以 s = "cbaebabacd"
, p = "abc"
为例:
- 初始化
pCount = [1,1,1,0,...]
(a/b/c 各出现 1 次)。 - 窗口滑动过程:
- 窗口
"cba"
→ 频率[1,1,1,...]
→ 匹配 → 记录索引 0。 - 窗口
"bae"
→ 频率[1,1,0,...]
→ 不匹配。 - 窗口
"bab"
→ 频率[1,2,0,...]
→ 不匹配。 - ...
- 窗口
"bac"
→ 频率[1,1,1,...]
→ 匹配 → 记录索引 6。
- 窗口
最终返回结果 [0, 6]
。
总结
通过滑动窗口结合字符频率统计,我们高效解决了字母异位词的搜索问题。该方法避免了冗余计算,显著提升了性能,是处理同类字符串匹配问题的经典思路。