引言:算法选择的十字路口
在算法面试中,双指针和滑动窗口如同两把瑞士军刀,能高效解决80%以上的数组和字符串问题。本文将深入解析这两种技术的核心差异,结合力扣高频题目,提供可直接复用的代码。
一、算法核心思想解析
1. 双指针技术
双指针算法是通过维护两个指针变量(通常为数组/链表索引)协同遍历数据结构的高效策略,主要用于优化暴力解法的时空复杂度。
三种经典模式:
- 对撞指针:首尾指针向中间收敛(如三数之和)
- 快慢指针:同向移动但速度不同(如环形链表检测)
- 滑动窗口:动态维护子区间(特殊双指针实现)
2. 滑动窗口技术
作为双指针的特殊形式,滑动窗口具有以下特点:
- 两个指针同向移动且维护一个连续区间
- 通过窗口扩张/收缩动态调整解空间
3. 算法特性对比
|---------------|-------------|-----------|
| 特性 | 双指针算法 | 滑动窗口算法 |
| 指针移动方向 | 可对撞/同向 | 严格同向移动 |
| 数据要求 | 常需排序 | 原始数据即可 |
| 时间复杂度 | O(n)~O(n²) | O(n) |
| 典型问题 | 有序数组组合问题 | 子串/子数组最优解 |
| 状态维护 | 通常不需要 | 需要哈希表记录状态 |
二、高频面试题分类精讲
1. 双指针经典题型**:**三数之和(LeetCode 15)
解题思路**:**
1. 暴力解法(不推荐)
最直观的方法是三重循环遍历所有可能的三元组,时间复杂度为O(n³),效率太低。
2. 排序 + 双指针法(推荐)
******步骤如下:**
- 排序数组:首先将数组排序,这样可以利用有序性来优化搜索
- 固定一个数:遍历数组,将当前元素作为第一个数
- 双指针搜索:在当前元素后面的部分使用双指针(左指针从当前元素后一位开始,右指针从数组末尾开始)寻找另外两个数
- 去重处理:跳过重复的元素以避免重复解
时间复杂度:O(n²)(排序O(nlogn) + 双指针遍历O(n²))
java
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
int length = nums.length;
if (nums == null || length < 3) {
return result;
}
Arrays.sort(nums); // 关键步骤:排序
for (int i =0; i < length; i++) {
// 需要和上一次枚举的数不相同
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i+1;
int right = length - 1;
int target = -nums[i]; // nums[left] + nums[right] = target
while(left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 跳过重复的left和right
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return result;
}
2. 滑动窗口经典题型**:**最小覆盖子串(LeetCode 76)
算法核心思想
滑动窗口(Sliding Window)算法是解决最小覆盖子串问题的最优方法:
- 双指针定义窗口:使用left和right两个指针表示当前考察的子串窗口
- 哈希表记录需求:使用两个哈希表分别存储:
- need:目标字符串T中各字符的需求量
- window:当前窗口中满足需求的字符数量
3. 动态扩展收缩窗口 :
- 先扩展right指针寻找可行解
- 再收缩left指针优化解
4. 有效字符计数 :通过valid变量统计窗口中已满足需求的字符种类数
5. 实时更新结果 :每当找到更小的满足条件的子串时更新结果
复杂度分析
- 时间复杂度:O(n),其中n是字符串S的长度
- 空间复杂度:O(m),其中m是字符集大小(通常为常数级)
Java实现代码
java
public String minWindow(String s, String t) {
// 边界条件检查
if (s == null || t == null || s.length() < t.length()) {
return "";
}
// 初始化需求哈希表,记录t中每个字符出现的次数
Map<Character, Integer> need = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
// 初始化窗口哈希表,记录当前窗口中满足需求的字符数量
Map<Character, Integer> window = new HashMap<>();
// 记录窗口中满足need条件的字符个数
int valid = 0;
// 记录最小覆盖子串的起始位置和长度
int start = 0, minLen = Integer.MAX_VALUE;
// 滑动窗口左右指针
int left = 0, right = 0;
while (right < s.length()) {
// 获取右指针当前字符
char c = s.charAt(right);
// 右指针向右移动
right++;
// 如果当前字符在需求表中,则更新窗口计数
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
// 如果窗口中该字符数量等于需求数量,则valid加1
if (window.get(c).equals(need.get(c))) {
valid++;
}
}
// 当窗口满足所有字符需求时,尝试收缩左边界
while (valid == need.size()) {
// 更新最小覆盖子串信息
if (right - left < minLen) {
start = left;
minLen = right - left;
}
// 获取左指针当前字符
char d = s.charAt(left);
// 左指针向右移动
left++;
// 如果移出的字符在需求表中,则更新窗口计数
if (need.containsKey(d)) {
// 如果窗口中该字符数量刚好等于需求数量,则valid减1
if (window.get(d).equals(need.get(d))) {
valid--;
}
window.put(d, window.get(d) - 1);
}
}
}
// 返回最小覆盖子串,如果没有找到则返回空字符串
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
三、应用场景选择指南
1. 优先选择双指针的情况
- 有序数组的组合问题:如两数之和、三数之和
- 链表操作:如环形链表检测、链表中点定位
- 对称性问题:如回文串验证、反转字符串
2. 优先选择滑动窗口的情况
- 连续子序列问题:如最小覆盖子串、最长无重复子串
- 区间统计问题:如和大于等于目标值的最短子数组
- 固定窗口大小问题:如大小为k的子数组的最大平均值
四**、力扣经典案例**
4.1 、基础双指针问题
4 . 1 . 1. 对撞指针经典题
- 167. 两数之和 II - 输入有序数组
- 解法特点:有序数组相向指针遍历
- 时间复杂度:O(n)
- 344. 反转字符串
- 解法特点:原地操作字符数组
- 空间复杂度:O(1)
4.2、滑动窗口常规问题
4.2.1. 可变窗口基础题
- 209. 长度最小的子数组
- 解法特点:动态调整窗口大小
- 关键点:维护窗口和与目标值的比较
4.2.2. 哈希表辅助窗口题
- 3. 无重复字符的最长子串
- 解法特点:哈希表记录字符最后出现位置
- 时间复杂度:O(n)
4.3、字符串匹配问题
4.3.1. 困难级窗口问题
- 76. 最小覆盖子串
- 解法特点:精确的窗口收缩条件
- 关键点:有效字符计数机制
4.3.2. 固定窗口典型题
- 438. 找到字符串中所有字母异位词
- 解法特点:固定长度窗口滑动
- 优化点:数组替代哈希表统计字符
4.4、链表双指针问题
4.4.1. 快慢指针经典题
- 141. 环形链表
- 解法特点:Floyd判圈算法
- 空间复杂度:O(1)
4**.4.**2. 前后指针应用
- 19. 删除链表的倒数第 N 个结点
- 解法特点:前后指针保持固定间距
- 关键点:虚拟头节点处理边界
4.5、多维滑动窗口问题
4.5.1. 单调队列配合题
- 239. 滑动窗口最大值
- 解法特点:单调递减队列维护窗口极值
- 时间复杂度:O(n)
4.5.2. 双堆进阶问题
- 480. 滑动窗口中位数
- 解法特点:大小顶堆平衡维护中位数
- 关键点:延迟删除策略
结语:算法精进的阶梯
掌握双指针和滑动窗口不仅是面试的敲门砖,更是提升算法思维的关键。建议读者按照解题模板→理解原理→独立实现→优化变体的路径系统学习,逐步构建自己的算法兵器库。