1. LeetCode Hot 100双指针专题解析
双指针作为算法面试中的高频考点,在LeetCode Hot 100题库中占据重要位置。这个专题主要考察对数组、链表等数据结构的灵活操作能力,以及通过指针协同解决问题的思维模式。我在刷完所有双指针相关题目后,发现这类问题虽然表面差异大,但核心解题框架高度一致。
关键认知:双指针不是具体算法,而是一种通过控制两个索引/指针的相对位置关系来优化问题解法的思想
1.1 双指针的三种经典模式
根据指针移动方向和行为特征,可将双指针解法分为三类典型场景:
同向快慢指针(主要用于链表操作)
- 快指针先行探路,慢指针滞后处理
- 典型应用:判断链表环、找链表中点
- 时间复杂度通常为O(n)
相向对撞指针(适用于有序数组)
- 左右指针从两端向中间移动
- 典型应用:两数之和、三数之和
- 时间复杂度通常为O(nlogn)(含排序)
滑动窗口指针(处理子数组/子串问题)
- 维护一个动态变化的窗口区间
- 典型应用:最小覆盖子串、长度最小子数组
- 时间复杂度通常为O(n)
下表对比了三种模式的适用场景和解题特征:
| 模式类型 | 适用数据结构 | 经典例题 | 移动规律 | 时间复杂度 |
|---|---|---|---|---|
| 快慢指针 | 链表 | 环形链表 | 快2慢1 | O(n) |
| 对撞指针 | 有序数组 | 三数之和 | 左右夹逼 | O(nlogn) |
| 滑动窗口 | 数组/字符串 | 最小子串 | 右扩左缩 | O(n) |
1.2 Hot 100高频双指针题目精讲
1.2.1 盛最多水的容器(LeetCode 11)
这是对撞指针的经典应用,题目要求找到两条垂直线,使得它们与x轴构成的容器能容纳最多的水。暴力解法需要O(n²)时间,而双指针可以优化到O(n)。
def maxArea(height): left, right = 0, len(height) - 1 max_water = 0 while left < right: current = min(height[left], height[right]) * (right - left) max_water = max(max_water, current) if height[left] < height[right]: left += 1 else: right -= 1 return max_water移动策略分析:每次移动较矮的指针,因为容器的容量受限于较矮的一边。移动较高的指针不会增加容量,反而可能减少宽度导致容量下降。
1.2.2 三数之和(LeetCode 15)
这道题需要找到所有不重复的三元组,使其和为0。双指针解法需要先排序,然后固定一个数,用对撞指针找另外两个数。
def threeSum(nums): nums.sort() res = [] 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: s = nums[i] + nums[left] + nums[right] if s < 0: left += 1 elif s > 0: right -= 1 else: res.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 res去重技巧:在找到有效三元组后,需要跳过所有相同的元素,这是避免重复结果的关键步骤。
1.2.3 接雨水(LeetCode 42)
这道hard题目可以用双指针在O(n)时间内解决,关键在于理解如何计算每个位置的积水量。
def trap(height): left, right = 0, len(height) - 1 left_max = right_max = 0 res = 0 while left < right: if height[left] < height[right]: if height[left] >= left_max: left_max = height[left] else: res += left_max - height[left] left += 1 else: if height[right] >= right_max: right_max = height[right] else: res += right_max - height[right] right -= 1 return res核心思想:每个位置的积水量由左右两边最高柱子的较小值决定。双指针从两端向中间移动,动态维护左右最大值。
1.3 滑动窗口的典型应用
滑动窗口是双指针的一种特殊形式,特别适合解决子数组/子串相关问题。以下是两个Hot 100中的经典案例:
1.3.1 最小覆盖子串(LeetCode 76)
要求在字符串S中找到包含字符串T所有字符的最短子串。滑动窗口解法的时间复杂度为O(n)。
def minWindow(s, t): from collections import defaultdict need = defaultdict(int) for c in t: need[c] += 1 need_cnt = len(t) left = 0 res = (0, float('inf')) for right, c in enumerate(s): if need[c] > 0: need_cnt -= 1 need[c] -= 1 if need_cnt == 0: while True: c = s[left] if need[c] == 0: break need[c] += 1 left += 1 if right - left < res[1] - res[0]: res = (left, right) need[s[left]] += 1 need_cnt += 1 left += 1 return s[res[0]:res[1]+1] if res[1] < float('inf') else ""窗口收缩条件:当窗口包含T的所有字符时,尝试左移左指针以缩小窗口,直到窗口不再满足条件为止。
1.3.2 长度最小的子数组(LeetCode 209)
找出数组中满足其和≥target的长度最小的连续子数组。
def minSubArrayLen(target, nums): left = total = 0 res = float('inf') for right in range(len(nums)): total += nums[right] while total >= target: res = min(res, right - left + 1) total -= nums[left] left += 1 return res if res != float('inf') else 0优化点:当窗口和≥target时立即记录长度,然后尝试缩小窗口以寻找更优解。
1.4 快慢指针的妙用
快慢指针在链表操作中尤为常见,以下是两个典型问题:
1.4.1 环形链表(LeetCode 141)
判断链表中是否有环,快指针每次走两步,慢指针每次走一步,如果相遇则说明有环。
def hasCycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow == fast: return True return False数学原理:如果有环,快指针最终会追上慢指针,因为它们的相对速度是1个节点/次。
1.4.2 寻找链表的中间节点(LeetCode 876)
使用快慢指针可以在一次遍历中找到链表的中间节点。
def middleNode(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow边界情况:当链表长度为偶数时,返回的是第二个中间节点。
1.5 双指针的进阶应用
1.5.1 合并两个有序数组(LeetCode 88)
题目要求将nums2合并到nums1中,并保持有序。可以从后向前使用双指针避免额外空间。
def merge(nums1, m, nums2, n): p1, p2, p = m-1, n-1, m+n-1 while p1 >= 0 and p2 >= 0: if nums1[p1] > nums2[p2]: nums1[p] = nums1[p1] p1 -= 1 else: nums1[p] = nums2[p2] p2 -= 1 p -= 1 nums1[:p2+1] = nums2[:p2+1]关键点:从后向前填充可以避免覆盖nums1中尚未处理的元素。
1.5.2 无重复字符的最长子串(LeetCode 3)
虽然不是严格的双指针,但滑动窗口思想与此高度相关。
def lengthOfLongestSubstring(s): used = {} max_len = start = 0 for i, c in enumerate(s): if c in used and start <= used[c]: start = used[c] + 1 else: max_len = max(max_len, i - start + 1) used[c] = i return max_len哈希表辅助:记录字符最后出现的位置,当遇到重复字符时快速调整窗口起始位置。
1.6 双指针解题的通用框架
经过对Hot 100双指针题目的系统梳理,我总结出以下解题框架:
确定指针初始位置:
- 同向指针:通常都从起点开始
- 对撞指针:分别置于数组两端
- 滑动窗口:左指针在起点,右指针逐步扩展
明确移动条件:
- 快慢指针:快指针每次走两步,慢指针走一步
- 对撞指针:根据比较结果决定移动哪个指针
- 滑动窗口:右指针扩展直到满足条件,左指针收缩优化
终止条件:
- 同向指针:快指针到达末尾
- 对撞指针:左右指针相遇
- 滑动窗口:右指针遍历完整个数组
结果更新时机:
- 在每次指针移动后
- 或在满足特定条件时
1.7 双指针问题的常见陷阱
边界条件处理不足:
- 空数组或单元素数组
- 全部元素相同的情况
- 指针越界访问
移动逻辑错误:
- 该移动左指针时移动了右指针
- 指针移动步长不正确
- 遗漏了去重步骤
时间复杂度误判:
- 嵌套循环实际是O(n²)误认为O(n)
- 忽略预处理(如排序)的时间成本
空间复杂度优化不足:
- 使用不必要的数据结构
- 可以原地修改却创建了新数组
1.8 双指针的变种与组合应用
在实际面试中,双指针常与其他算法思想结合出现:
双指针+哈希表:
- 用于需要快速查找的场景
- 典型题目:两数之和、无重复字符的最长子串
双指针+排序:
- 先排序再使用双指针
- 典型题目:三数之和、最接近的三数之和
双指针+贪心算法:
- 每次移动都采取局部最优选择
- 典型题目:盛最多水的容器
多指针协同:
- 使用三个或更多指针协同工作
- 典型题目:四数之和、颜色分类
1.9 双指针题目的调试技巧
打印指针轨迹:
print(f"left={left}, right={right}, nums[left]={nums[left]}, nums[right]={nums[right]}")可视化中间状态:
- 对于数组问题,可以画出指针位置和数组元素
- 对于链表问题,可以画出节点和指针指向关系
小规模测试用例:
- 先用手算验证简单案例
- 再逐步增加复杂度
边界测试:
- 空输入
- 单元素输入
- 全部元素相同
- 已排序/逆序输入
1.10 双指针的复杂度分析要点
时间复杂度:
- 纯双指针通常为O(n)
- 如果包含排序,则为O(nlogn)
- 嵌套双指针可能是O(n²)
空间复杂度:
- 原地操作通常为O(1)
- 需要辅助数据结构时可能为O(n)
最坏情况分析:
- 所有元素都需要处理
- 指针需要遍历整个数组多次
平均情况分析:
- 指针移动路径通常短于数组长度
- 提前终止的可能性
1.11 面试中的双指针问题应答策略
问题澄清:
- 确认输入输出要求
- 询问边界条件和特殊案例
思路阐述:
- 先说明暴力解法
- 再提出双指针优化思路
- 解释为什么双指针更高效
代码实现:
- 保持代码整洁
- 添加必要注释
- 处理好边界条件
测试验证:
- 用简单案例手动验证
- 讨论可能的优化空间
1.12 双指针与其他算法的对比
| 算法思想 | 适用场景 | 时间复杂度 | 空间复杂度 | 与双指针的关系 |
|---|---|---|---|---|
| 暴力枚举 | 所有问题 | O(n²)~O(n!) | O(1) | 双指针通常优化暴力法 |
| 哈希表 | 查找问题 | O(n) | O(n) | 可与双指针结合使用 |
| 滑动窗口 | 子数组问题 | O(n) | O(1) | 双指针的特殊形式 |
| 二分查找 | 有序数组 | O(logn) | O(1) | 可视为单指针的移动 |
1.13 双指针的扩展练习建议
为了真正掌握双指针技巧,建议按照以下顺序进行系统练习:
基础阶段:
- 移动零(LeetCode 283)
- 反转字符串(LeetCode 344)
- 两数之和 II(LeetCode 167)
进阶阶段:
- 四数之和(LeetCode 18)
- 最接近的三数之和(LeetCode 16)
- 删除排序数组中的重复项 II(LeetCode 80)
高手阶段:
- 串联所有单词的子串(LeetCode 30)
- 滑动窗口最大值(LeetCode 239)
- 最小窗口子序列(LeetCode 727)
1.14 双指针在真实工程中的应用
虽然双指针主要用于算法题,但其思想在实际工程中也有广泛应用:
数据库合并操作:
- 合并两个有序结果集
- 类似合并两个有序数组
日志分析:
- 查找特定时间窗口的事件
- 类似滑动窗口问题
资源调度:
- 分配有限的资源给多个任务
- 类似对撞指针的分配思想
版本比较:
- 逐段比较两个版本号
- 类似双指针遍历字符串
1.15 个人刷题经验分享
在刷完Hot 100所有双指针题目后,我总结了以下几点心得:
模板化记忆:
- 将双指针解法归纳为几种固定模式
- 遇到新题先尝试套用已知模式
画图辅助:
- 在纸上画出指针移动过程
- 特别适合链表和二维数组问题
刻意练习:
- 集中时间刷同一类型的题目
- 比较不同题目的解法和异同
错题复盘:
- 记录每次错误的根本原因
- 建立自己的常见错误清单
速度训练:
- 限时完成题目
- 逐步减少解题时间
双指针技巧的掌握需要大量练习,但一旦理解其核心思想,很多看似复杂的问题都能迎刃而解。建议从简单的同向指针开始,逐步过渡到更复杂的滑动窗口和对撞指针问题。