448_双指针面试题¶
一句话说明¶
双指针是用两个指针协同扫描数组或字符串,避免嵌套循环,把 O(n²) 降到 O(n),核心是"两端向中间收"或"同向追逐"。
核心知识点¶
白话理解¶
双指针就像两个人从数组两端向中间走(对撞指针),或者一快一慢在同向跑(快慢指针)。用于有序数组找配对、原地去重、判断回文等场景,不需要额外空间。
两种双指针类型¶
| 类型 | 方向 | 典型场景 |
|---|---|---|
| 对撞指针 | ←→ 向中间 | 有序数组两数之和、回文判断 |
| 同向指针(快慢) | → 同向 | 原地去重、移除元素 |
经典题目与解法¶
题目1:有序数组的两数之和(LeetCode 167)¶
题意:有序数组中找两个数使和等于 target,返回下标(1-indexed)。
def two_sum_sorted(numbers, target):
"""
对撞指针:左指针指最小值,右指针指最大值
和小了→左指针右移,和大了→右指针左移
"""
left, right = 0, len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left + 1, right + 1] # 题目要求1-indexed
elif current_sum < target:
left += 1 # 和太小,左指针右移增大和
else:
right -= 1 # 和太大,右指针左移减小和
return [-1, -1] # 无解(题目保证有解)
# 测试
print(two_sum_sorted([2, 7, 11, 15], 9)) # [1, 2]
print(two_sum_sorted([2, 3, 4], 6)) # [1, 3]
print(two_sum_sorted([-1, 0], -1)) # [1, 2]
时间复杂度:O(n)
空间复杂度:O(1)
题目2:三数之和(LeetCode 15,双指针经典)¶
题意:找出数组中所有和为 0 的三元组,不重复。
def three_sum(nums):
"""
思路:排序 + 固定一个数 + 对剩余用对撞双指针
关键:跳过重复元素避免重复三元组
"""
nums.sort() # 先排序,O(n log n)
result = []
for i in range(len(nums) - 2):
if nums[i] > 0: # 最小的数都>0,不可能凑成0
break
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:
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
elif total < 0:
left += 1 # 和太小,左指针右移
else:
right -= 1 # 和太大,右指针左移
return result
# 测试
print(three_sum([-1, 0, 1, 2, -1, -4])) # [[-1,-1,2], [-1,0,1]]
print(three_sum([0, 1, 1])) # []
print(three_sum([0, 0, 0])) # [[0,0,0]]
时间复杂度:O(n²)(排序 O(n log n),两层循环 O(n²))
空间复杂度:O(log n) 排序栈空间
题目3:移除元素(LeetCode 27,快慢指针)¶
题意:原地移除数组中所有等于 val 的元素,返回新长度。
def remove_element(nums, val):
"""
快慢指针:slow 指向下一个可写入位置,fast 扫描所有元素
不等于val的元素写到slow位置
"""
slow = 0 # 慢指针:结果数组的末尾
for fast in range(len(nums)): # 快指针:遍历所有元素
if nums[fast] != val: # 不是要删除的值
nums[slow] = nums[fast] # 复制到slow位置
slow += 1 # slow前进
return slow # slow就是新数组的长度
# 测试
nums = [3, 2, 2, 3]
k = remove_element(nums, 3)
print(k, nums[:k]) # 2 [2, 2]
nums = [0, 1, 2, 2, 3, 0, 4, 2]
k = remove_element(nums, 2)
print(k, nums[:k]) # 5 [0, 1, 3, 0, 4]
时间复杂度:O(n)
空间复杂度:O(1)
题目4:接雨水(LeetCode 42,双指针经典难题)¶
题意:给出柱子高度数组,计算能接多少雨水。
def trap(height):
"""
对撞指针 + 动态维护左右最大高度
核心思路:每个位置能接的雨水 = min(左边最高, 右边最高) - 当前高度
用左右双指针,谁矮谁先处理(矮的那侧的max已经确定)
"""
left, right = 0, len(height) - 1
left_max = right_max = 0 # 左/右已知最大高度
water = 0
while left < right:
if height[left] < height[right]:
# 左边更矮,处理左边(左边max已确定,右边至少有right高的墙)
if height[left] >= left_max:
left_max = height[left] # 更新左边最大值
else:
water += left_max - height[left] # 能积水
left += 1
else:
# 右边更矮,处理右边
if height[right] >= right_max:
right_max = height[right] # 更新右边最大值
else:
water += right_max - height[right] # 能积水
right -= 1
return water
# 测试
print(trap([0,1,0,2,1,0,1,3,2,1,2,1])) # 6
print(trap([4,2,0,3,2,5])) # 9
时间复杂度:O(n)
空间复杂度:O(1)
面试技巧¶
- 有序 + 找配对 = 对撞指针:排序后左右收缩是标准套路
- 原地操作 = 快慢指针:slow 维护结果区,fast 扫描原数组
- 回文判断 = 对撞指针:左右同时向中间走,遇到不同返回 False
- 三数之和的去重很容易错:排序+跳过重复是关键,先说思路再写代码
- 接雨水先想暴力:暴力是每个位置找左右最大值,双指针是优化这个过程
速查表¶
# 对撞指针模板
left, right = 0, len(arr) - 1
while left < right:
if condition(arr[left], arr[right]):
# 找到满足条件的
result.append(...)
left += 1; right -= 1
elif too_small:
left += 1
else:
right -= 1
# 快慢指针模板(原地操作)
slow = 0
for fast in range(len(nums)):
if keep(nums[fast]): # 满足保留条件
nums[slow] = nums[fast]
slow += 1
# 判断回文
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1; right -= 1
return True
| 题型 | 双指针类型 | 移动策略 |
|---|---|---|
| 有序数组两数之和 | 对撞 | 和小左移,和大右移 |
| 三数之和 | 固定+对撞 | 外层枚举,内层双指针 |
| 原地去重 | 快慢同向 | fast扫描,slow写入 |
| 接雨水 | 对撞 | 矮的那侧先移动 |
| 回文判断 | 对撞 | 不同返回False |