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把正确性的理由说清楚
为什么线性查找不会漏掉目标
- 开始时还没检查任何位置,候选范围是整个数组。
- 如果 a[i] 不等于目标,就只排除这个位置;之前检查过的位置都已确认不是答案。
- 命中时返回的下标确实对应目标;循环结束仍未命中,说明全部位置都检查过,可以返回 -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;插入时从最右边开始移动。
查看完整推导与答案
- 返回下标 2,依次比较 8、3、6,共 3 次。
- 先移动 6,再移动 3,再移动 8,最后写入 5;结果是 [5,8,3,6]。
- 改成 a[0]=5 的结果是 [5,3,6],这叫覆盖,不是插入。