刷算法题的人,多少都有过这种体验:看了题解,发现解法简单得不可思议,两个指针动来动去就搞定了;可轮到自己写,边界条件一多,不是越界就是死循环,最后怀疑人生。双指针这个技巧,放在整个算法体系里算是入门级,但它牵扯到的核心思维——如何利用数据本身的结构去压缩无效计算——却贯穿了二分、滑动窗口、链表操作、甚至一些复杂的图算法。这篇文章不会罗列所有双指针题,而是想聊清楚三件事:它为什么能省时间、写的时候到底在写什么、以及我踩过的那些坑。
1. 双指针不是什么高端技巧,而是一种省略遍历的思维
很多教程会把双指针总结成“一个左指针一个右指针,根据条件移动”,这种说法不能说错,但有点像说“煎蛋就是鸡蛋放锅里加热”,等于没说。双指针背后真正值钱的东西,是它帮你跳过了一大批永远不可能成为答案的状态。
1.1 暴力解法到底浪费在哪
拿最经典的两数之和来说。给定一个有序数组,找出两个数使它们的和等于目标值。暴力做法是双重循环:
python复制def two_sum_brute_force(nums, target):
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []
这段代码的时间复杂度是 O(n²)。i 和 j 的组合一共有 n(n-1)/2 种,每一组都要计算一次和。问题是这 n² 个状态中有大量是完全不需要看的。比如当 nums[i] + nums[j] 已经大于 target,由于数组有序,任何比 nums[j] 更大的数都不再可能和 nums[i] 组成答案,那后面的所有 j 就都被剪掉了。
双指针的做法是这样:
python复制def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
cur = nums[left] + nums[right]
if cur == target:
return [left, right]
elif cur < target:
left += 1
else:
right -= 1
return []
这里的逻辑一句话就能说清:和太小就往右移左指针,和太大就往左移右指针。每一次移动,都意味着放弃了若干不可能产生答案的组合。
1.2 指针移动的本质是排除状态,而不是“找答案”
我第一次真正理解双指针,不是靠刷题,而是靠想清楚一个问题:为什么两个指针这样移动不会漏解?
以两数之和为例。假设当前 left 指向位置 l,right 指向位置 r。如果 nums[l] + nums[r] < target,说明 nums[l] 加上数组里最大的数都比目标小,那 nums[l] 跟任何数相加都不可能等于 target。于是 nums[l] 这个元素就可以整个排除,left 必须右移。反之,如果 nums[l] + nums[r] > target,说明 nums[r] 跟数组里最小的数相加都超过目标,nums[r] 被整个排除,right 必须左移。
这才是双指针的精髓:每次移动都是在做一次“批量排除”。暴力枚举是一个一个试,双指针是一次扔掉一整批不可能的组合。整个过程下来,每个元素最多被访问两次(一次被 left 访问,一次被 right 访问),所以复杂度是 O(n)。
这个思维和二分查找、和滑动窗口的骨架是相通的。理解了这一点,你再看其它双指针题,就不会觉得它们是独立的小技巧,而是一种“利用有序性或单调性做剪枝”的通用思想。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
