学习顺序主线第 01 / 22 课 · 阶段 1

为什么现在学:从具体数据开始,先建立后面所有数组动画的共同语言。

前置知识这是起点,无需其他算法课程
基础课程

数组:下标、遍历与修改Arrays & Indices

先把下标和元素值分开,再解释为什么访问快、插入慢。

25 分钟 · 含手算与练习

学完这一课,你应该能做到

  • 能说出 a[2] 中 2 和返回值分别代表什么
  • 能手算中间插入时哪些元素必须移动
  • 能区分读取一个已知位置与寻找一个未知位置

01先读懂这个概念

先约定一份看得见的数据

设 a = [10, 20, 30, 40]。数组长度 n=4,有效下标是 0、1、2、3。下标描述位置,元素值描述内容,所以 a[2] 是 30,而不是 20。n 表示元素数量,最后一个下标则是 n−1。

用一排等宽格子理解连续数组:知道起点和格子宽度,就能直接定位第 i 格,不必先经过前面的格子。这是按下标访问 O(1) 的原因。Python list 可以理解为连续保存对象引用的动态数组;对象本身不必相邻,扩容也不是每次都发生。

一次循环究竟在做什么

如果问题是『30 在哪里』,我们尚不知道下标。最直接的办法从 i=0 开始,依次检查 a[i] 是否等于 30:10 不是,20 不是,30 是,于是返回 2。目标不存在时必须看完所有 n 个位置,最坏 O(n)。

插入与赋值也不同。a[1]=15 只是把 20 改成 15,长度仍为 4;在下标 1 插入 15 则必须保留 20,最终长度为 5。假设容量充足,需要先移动 40,再移动 30、20,最后写入 15。先从左侧移动会覆盖还没有保存的值。

02从具体数据开始手算

拿一个小输入,逐步手算

在 [10, 20, 30, 40] 的下标 1 插入 15;末尾已有一个空位。

动作格子内容为什么
初始[10, 20, 30, 40, 空]长度 4,容量至少 5
40 右移[10, 20, 30, 空, 40]先保住最右端元素
30 右移[10, 20, 空, 30, 40]继续腾位置
20 右移[10, 空, 20, 30, 40]下标 1 可供插入
写入 15[10, 15, 20, 30, 40]移动 3 次,写入 1 次

表格把移走的位置显示为空,便于理解位置变化;实际赋值实现可能暂时留下重复值。插入结束后顺序和所有原元素都要保留。

亲手试一下:访问与插入有什么不同?

点击格子,用下标直接取值。下标从 0 开始;末尾「空」表示预留的容量。这是连续数组的移动示意。

a[2] = 30。已知下标时不用从头扫描,按下标访问是 O(1)。

假设还剩一个空位。在下标 1 插入 15,必须先把右侧元素向右移动。

想一想:如果只知道要找 30,却不知道下标呢?无序数组通常要逐个检查,最坏需要 O(n)。

03把正确性的理由说清楚

为什么线性查找不会漏掉目标

  1. 开始时还没检查任何位置,候选范围是整个数组。
  2. 如果 a[i] 不等于目标,就只排除这个位置;之前检查过的位置都已确认不是答案。
  3. 命中时返回的下标确实对应目标;循环结束仍未命中,说明全部位置都检查过,可以返回 -1。

04把刚才的思路写成代码

基础课用 Python 表达,先对照变量含义与执行顺序。后续算法课提供 Python、C++ 与 JavaScript 三种实现。

def find_index(a, target):
    for i in range(len(a)):
        if a[i] == target:
            return i
    return -1

代码里的每个关键决定

range(len(a))
长度为 4 时生成 0、1、2、3,不包含 4。循环变量 i 是下标,不是元素值。
if a[i] == target:
每一步只回答当前位置是否匹配;return i 返回位置。如果只需元素,可以直接遍历 a,但那时没有这个下标变量。
return -1
放在循环外,表示所有位置都失败。若放进循环的 else 中,遇到第一个不匹配的元素就会错误结束。

05这些操作要付出多少代价

把工作量具体数出来

按下标读取或覆盖只定位一个格子,按定长元素模型计为 O(1)。按值查找最坏检查 n 次,是 O(n)。

在下标 k 插入需要移动 n−k 个元素;最坏在开头插入,要移动 n 个。动态数组偶尔还要重新分配并复制,因此尾部追加通常描述为摊还 O(1)。

查找函数仅用一个下标,是 O(1) 辅助空间。复制 a 得到新数组需要 O(n);不要因为调用只有一行,就把复制当成常数成本。

06用一个反例检查理解

用反例检查前提

把下标和长度混为一谈

a=[10,20,30,40] 时读取 a[4] 越界,因为第 4 个元素的下标是 3。查找不存在的 99 时也不能随便返回 0,0 是一个合法答案位置。

怎样修正:统一写有效范围 0 ≤ i < n;找不到时使用约定的 -1,并在调用处检查它,避免把 Python 的 a[-1] 误当成失败。

07自己推一次,再做自测

合上答案,自己推一次

先自己手算

a=[8,3,6]。find_index(a,6) 返回什么,比较了几次?如果要在下标 0 插入 5,哪些值按什么顺序移动?

需要一点提示

先写下下标 0、1、2;插入时从最右边开始移动。

查看完整推导与答案
  1. 返回下标 2,依次比较 8、3、6,共 3 次。
  2. 先移动 6,再移动 3,再移动 8,最后写入 5;结果是 [5,8,3,6]。
  3. 改成 a[0]=5 的结果是 [5,3,6],这叫覆盖,不是插入。

01长度为 5 的数组,最后一个有效下标是多少?

02无序数组中查找一个不存在的值,最坏检查几项?

03为什么中间插入通常从右往左移动?

继续阅读

本课例子与推导为本站编写。需要另一种表述或进一步学习时,可对照这些公开课程与文档:

MIT 6.006 · 算法讲义Python 官方教程 · 数据结构

能独立完成本课练习,再标记掌握。也可以直接用上面的链接继续学习。