人人都能学编程
English 开始学习
核心 · 算法第 2 课 / 共 8 课免费阅读

双指针与滑动窗口 · 数据结构与算法

怎么只走一遍就解决列表问题?

课程目录

为什么需要它

面试题里很大一部分是关于列表(数组)的:找和为目标值的一对数、反转、给有序列表去重。两个套路,双指针和滑动窗口,能把很多 O(n²) 的暴力解法变成 O(n)。

直觉理解

双指针:在有序列表的两端各放一个下标。和太小,左边的往右挪;和太大,右边的往左挪。每一步都排除一个候选,所以最多 n 步。

滑动窗口:要看每一段连续的 k 个数时,不要每次都重新把 k 个数加一遍。滑动它:加上进入窗口的数,减去离开的数。

定义

双指针

Two indexes that move toward each other (or in the same direction) so each element is visited once.
两个下标相向(或同向)移动,让每个元素只被访问一次。

滑动窗口

A range [left, right) over the list that moves forward, updating a running total instead of recomputing it.
列表上一个向前移动的区间 [left, right),每次更新累计值,而不是重新计算。

示例

找和为目标值的一对数

动手试试

列表是有序的,所以可以从两端往中间夹。

def pair_sum(xs, target):
    left, right = 0, len(xs) - 1
    while left < right:
        total = xs[left] + xs[right]
        if total == target:
            return (xs[left], xs[right])
        if total < target:
            left += 1
        else:
            right -= 1
    return None

print(pair_sum([1, 3, 4, 6, 9, 11], 13))
逐步执行
  1. L2从两端开始:1 和 11。
  2. L71 + 11 = 12 太小:左边右移。
  3. L103 + 11 = 14 太大:右边左移。
  4. L133 + 9 = 13。(3, 9)

最好的连续 3 天

动手试试

每天的销量;找出最好的连续 3 天。窗口每次滑动一天。

sales = [4, 2, 7, 1, 8, 3, 5]
k = 3
window = sum(sales[:k])
best = window
for i in range(k, len(sales)):
    window += sales[i] - sales[i - k]
    best = max(best, window)
print(best)
逐步执行
  1. L3第一个窗口:4 + 2 + 7 = 13。
  2. L6加上进来的那天,减去离开的那天。
  3. L8各个窗口:13、10、16、12、16。最好是 16。16

常见错误

在无序列表上用双指针

往中间夹之所以有效,是因为列表有序。无序的话,先排序(O(n log n)),或者用集合(下一课)。

差一错误

range(k, len(xs)) 和 xs[i - k] 必须对得上。手动走一遍前两步。

小测验

1. 这段代码会输出什么?

xs = [1, 2, 3, 4, 5]
left, right = 0, len(xs) - 1
while left < right:
    xs[left], xs[right] = xs[right], xs[left]
    left += 1
    right -= 1
print(xs)
  1. A
    [5, 4, 3, 2, 1]
  2. B
    [1, 2, 3, 4, 5]
  3. C
    [5, 2, 3, 4, 1]
查看答案

答案:A

交换两端再往里走:这是原地反转列表。

2. 在 n 个数上滑动长度为 k 的窗口,用累计值的方法大约需要多少次加法?

  1. A大约 n 次
  2. B大约 n × k 次
  3. C大约 k 次
查看答案

答案:A

第一个窗口 k 次,之后每步一加一减:O(n)。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出跟踪指针

    准确写出这段程序的输出。

  2. 改需求一遍去重

    dedupe(xs) 收到一个有序列表,返回去掉重复后的列表。里面那层循环对每个元素都把 out 扫一遍(O(n²))。既然列表有序,重复的元素只可能等于最后一个保留下来的元素。利用这一点把它改成 O(n)。

  3. 从零写最好的窗口

    写函数 best_window(xs, k),返回 xs 中任意连续 k 个数之和的最大值(假设 len(xs) ≥ k ≥ 1)。用滑动窗口:O(n)。

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

互动课按顺序解锁:前面的课还没学,会先带你到课程地图。

本课术语

术语中文英文定义
Array数组Items stored in order and reached by index; Python's list.
Two pointers双指针Two indexes moving through a list so each item is visited once.
Sliding window滑动窗口A moving range with a running total.