跳转至

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)


面试技巧

  1. 有序 + 找配对 = 对撞指针:排序后左右收缩是标准套路
  2. 原地操作 = 快慢指针:slow 维护结果区,fast 扫描原数组
  3. 回文判断 = 对撞指针:左右同时向中间走,遇到不同返回 False
  4. 三数之和的去重很容易错:排序+跳过重复是关键,先说思路再写代码
  5. 接雨水先想暴力:暴力是每个位置找左右最大值,双指针是优化这个过程

速查表

# 对撞指针模板
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