1. 滑动窗口与双指针算法概述
在算法面试和实际工程开发中,滑动窗口(Sliding Window)和双指针(Two Pointers)是解决数组/字符串类问题的两大核心技巧。这两种技术看似简单,但能高效解决一系列经典问题,尤其适合处理连续子数组或子字符串相关的题目。
我第一次接触滑动窗口是在处理一个日志分析需求时,需要在百万级时间序列数据中快速找出异常时间段。传统暴力解法需要O(n²)时间复杂度,而滑动窗口将其优化到O(n),性能提升立竿见影。双指针则是我在开发字符串匹配功能时发现的利器,它能让原本复杂的边界判断变得直观清晰。
这两种技术有以下核心特征:
- 滑动窗口:通过维护一个动态变化的窗口(通常用左右指针表示),在遍历过程中根据条件调整窗口大小,避免重复计算
- 双指针:使用两个指针以不同策略(同向、相向、快慢等)遍历数据结构,降低时间复杂度
2. 滑动窗口算法深度解析
2.1 基本实现模板
滑动窗口的经典实现通常包含以下要素:
python复制def sliding_window(s: str) -> int:
left = 0
result = 0
window = {} # 用于记录窗口内元素状态
for right in range(len(s)):
# 右指针移动,扩展窗口
window[s[right]] = window.get(s[right], 0) + 1
# 当不满足条件时,收缩左边界
while window needs shrink:
window[s[left]] -= 1
if window[s[left]] == 0:
del window[s[left]]
left += 1
# 更新结果
result = max(result, right - left + 1)
return result
关键点:窗口的扩展总是由右指针驱动,而收缩则由条件触发。这种不对称性是滑动窗口高效的核心。
2.2 典型问题场景
2.2.1 固定长度窗口问题
例题:给定整数数组,找出长度为k的子数组的最大平均值
python复制def findMaxAverage(nums: List[int], k: int) -> float:
window_sum = sum(nums[:k])
max_sum = window_sum
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i-k] # 滑动窗口的精髓
max_sum = max(max_sum, window_sum)
return max_sum / k
技巧:固定长度窗口的滑动可以通过"加新减旧"的方式实现O(1)复杂度的窗口更新
2.2.2 可变长度窗口问题
例题:无重复字符的最长子串(LeetCode 3)
python复制def lengthOfLongestSubstring(s: str) -> int:
char_index = {}
left = 0
max_len = 0
for right, char in enumerate(s):
if char in char_index and char_index[char] >= left:
left = char_index[char] + 1 # 跳跃式移动左指针
char_index[char] = right
max_len = max(max_len, right - left + 1)
return max_len
注意事项:当发现重复字符时,左指针可以直接跳到重复字符的下一位,这是优化效率的关键
2.3 滑动窗口的四种常见类型
- 固定长度窗口:窗口大小不变,如求每k个数的平均值
- 可变长度求最大值:窗口可扩大/缩小,求满足条件的最大窗口
- 可变长度求最小值:窗口可扩大/缩小,求满足条件的最小窗口
- 计数型窗口:用哈希表维护窗口元素统计,如字母异位词问题
3. 双指针技术详解
3.1 双指针的三种基本模式
3.1.1 同向指针
两个指针从同一侧开始移动,速度可能不同(快慢指针)
例题:移除有序数组中的重复项(LeetCode 26)
python复制def removeDuplicates(nums: List[int]) -> int:
if not nums:
return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
经验:慢指针指向处理好的序列尾部,快指针探索新元素。这种模式在链表去重等问题中同样适用。
3.1.2 相向指针
两个指针分别从首尾向中间移动
例题:两数之和II(LeetCode 167)
python复制def twoSum(numbers: List[int], target: int) -> List[int]:
left, right = 0, len(numbers)-1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left+1, right+1]
elif current_sum < target:
left += 1
else:
right -= 1
return [-1, -1]
技巧:有序数组的相向指针往往能将O(n²)暴力解优化到O(n)
3.1.3 分离指针
两个指针分别遍历不同数组/字符串
例题:判断子序列(LeetCode 392)
python复制def isSubsequence(s: str, t: str) -> bool:
i = j = 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)
3.2 双指针的进阶应用
3.2.1 滑动窗口中的双指针
滑动窗口本质上是双指针的特殊应用,如最小覆盖子串(LeetCode 76)
python复制def minWindow(s: str, t: str) -> str:
from collections import defaultdict
target = defaultdict(int)
for char in t:
target[char] += 1
left = 0
min_len = float('inf')
result = ""
required = len(target)
formed = 0
window = defaultdict(int)
for right in range(len(s)):
char = s[right]
window[char] += 1
if char in target and window[char] == target[char]:
formed += 1
while formed == required and left <= right:
if right - left + 1 < min_len:
min_len = right - left + 1
result = s[left:right+1]
left_char = s[left]
window[left_char] -= 1
if left_char in target and window[left_char] < target[left_char]:
formed -= 1
left += 1
return result
注意事项:这里使用了两个哈希表分别记录目标字符计数和窗口内字符计数,通过formed变量避免每次都比较整个哈希表
3.2.2 多指针问题
某些问题可能需要三个甚至更多指针,如三数之和(LeetCode 15)
python复制def threeSum(nums: List[int]) -> List[List[int]]:
nums.sort()
result = []
for i in range(len(nums)-2):
if i > 0 and nums[i] == nums[i-1]:
continue
left, right = i+1, len(nums)-1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
left += 1
elif total > 0:
right -= 1
else:
result.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left+1]:
left += 1
while left < right and nums[right] == nums[right-1]:
right -= 1
left += 1
right -= 1
return result
4. 经典题目分类解析
4.1 滑动窗口典型题目
4.1.1 最长子串类问题
- 无重复字符的最长子串(LeetCode 3)
- 至多包含两个不同字符的最长子串(LeetCode 159)
- 至多包含K个不同字符的最长子串(LeetCode 340)
解题要点:
- 使用哈希表记录窗口内字符的最后出现位置
- 当哈希表大小超过限制时,快速定位最左边的删除位置
- 更新结果时机:每次右指针移动后
4.1.2 子数组和问题
- 和大于等于target的最短子数组(LeetCode 209)
- 乘积小于K的子数组个数(LeetCode 713)
特殊处理:
对于乘积问题,当窗口乘积≥K时需要连续收缩左边界,直到乘积<K。此时新增的有效子数组数量为right-left+1
4.1.3 频率统计问题
- 找到字符串中所有字母异位词(LeetCode 438)
- K距离间隔重复字符(LeetCode 358)
优化技巧:
使用固定长度的数组代替哈希表来统计字符频率,可以显著提升性能(特别是当字符集有限时)
4.2 双指针典型题目
4.2.1 链表问题
- 判断链表是否有环(LeetCode 141)
- 寻找链表的中间节点(LeetCode 876)
- 相交链表(LeetCode 160)
快慢指针模式:
python复制def hasCycle(head: ListNode) -> bool:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
4.2.2 数组去重与移动
- 移动零(LeetCode 283)
- 按奇偶排序数组(LeetCode 905)
同向指针模板:
python复制def moveZeroes(nums: List[int]) -> None:
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
4.2.3 相向指针问题
- 盛最多水的容器(LeetCode 11)
- 回文子串(LeetCode 647)
容器盛水问题要点:
python复制def maxArea(height: List[int]) -> int:
left, right = 0, len(height)-1
max_area = 0
while left < right:
max_area = max(max_area, min(height[left], height[right])*(right-left))
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
5. 实战技巧与常见错误
5.1 滑动窗口的调试技巧
-
可视化窗口变化:在纸上画出指针移动过程,特别是处理复杂条件时
code复制示例:s = "ADOBECODEBANC", t = "ABC" 窗口变化:[A D O B E C] → [D O B E C O D E B] → [C O D E B A] → [B A N C] -
打印关键变量:在循环中打印左右指针和窗口状态
python复制print(f"left={left}, right={right}, window={window}") -
边界条件测试:
- 空输入
- 无解情况
- 目标字符串比源字符串长
5.2 双指针的易错点
-
指针移动条件错误:
- 在相向指针问题中,移动哪个指针的判断条件容易混淆
- 解决方案:明确移动较小指针的原则(如两数之和问题)
-
越界访问:
python复制# 错误示例:快指针未检查next while fast and slow: fast = fast.next.next # 可能访问None的next -
更新结果时机不当:
- 结果应该在每次满足条件时更新,而不仅是在循环结束时
5.3 性能优化策略
-
哈希表优化:
- 对于有限字符集(如小写字母),用数组代替哈希表
python复制count = [0] * 26 count[ord(char) - ord('a')] += 1 -
提前终止:
- 当当前窗口大小已经小于已找到的最大值时,可以提前终止
-
跳跃式移动:
- 当发现重复字符时,左指针可以直接跳到重复字符的下一位
python复制left = max(left, char_index[char] + 1)
6. 综合应用与题目扩展
6.1 滑动窗口与动态规划结合
例题:最长湍流子数组(LeetCode 978)
python复制def maxTurbulenceSize(arr: List[int]) -> int:
if len(arr) < 2:
return len(arr)
left = 0
max_len = 1
prev_compare = 0
for right in range(1, len(arr)):
current_compare = arr[right] - arr[right-1]
if current_compare == 0:
left = right
elif (prev_compare * current_compare) >= 0:
left = right - 1
max_len = max(max_len, right - left + 1)
prev_compare = current_compare
return max_len
6.2 多指针协同问题
例题:四数之和(LeetCode 18)
python复制def fourSum(nums: List[int], target: int) -> List[List[int]]:
nums.sort()
result = []
n = len(nums)
for i in range(n-3):
if i > 0 and nums[i] == nums[i-1]:
continue
for j in range(i+1, n-2):
if j > i+1 and nums[j] == nums[j-1]:
continue
left, right = j+1, n-1
while left < right:
total = nums[i] + nums[j] + nums[left] + nums[right]
if total < target:
left += 1
elif total > target:
right -= 1
else:
result.append([nums[i], nums[j], nums[left], nums[right]])
while left < right and nums[left] == nums[left+1]:
left += 1
while left < right and nums[right] == nums[right-1]:
right -= 1
left += 1
right -= 1
return result
6.3 特殊滑动窗口模式
例题:替换后的最长重复字符(LeetCode 424)
python复制def characterReplacement(s: str, k: int) -> int:
count = {}
max_count = 0
left = 0
max_length = 0
for right in range(len(s)):
count[s[right]] = count.get(s[right], 0) + 1
max_count = max(max_count, count[s[right]])
if (right - left + 1 - max_count) > k:
count[s[left]] -= 1
left += 1
max_length = max(max_length, right - left + 1)
return max_length
关键点:窗口长度减去窗口内最高频字符的数量就是需要替换的次数,当这个次数>k时需要收缩窗口