📄 一句话概括
数组=挨着放;地址=基址 + 下标 × 元素长度,所以按下标取永远一样快,按值找却仍要一个个看。
📝 本节和前面几节的关系
4.3 有了指针这个工具,这一节用它摆出第一个形状:数组。
一、数组就是把数据挨着放
数组的规矩只有一条:所有元素紧挨着排,中间不留缝。 正因为挨着,才有那个著名的换算公式: 元素地址 = 基址 + 下标 × 每个元素占几字节。 比如基址 1000、每个元素 4 字节,那么下标 5 的地址就是 1000 + 5 × 4 = 1020。
二、为什么下标从 0 开始
因为这样算最省事:下标直接就是【要跳几格】。 若从 1 开始,公式得多减一次:基址 + (下标 - 1) × 长度。 省下的这一步乘法不算大事,但它每天要被算几亿亿次,所以约定就这么定下来了。 所以【从 0 开始】不是怪癖,是让换算少一步的设计。
三、快在哪,慢在哪
按下标取快:不管下标是 0 还是 100 万,都是乘一下加一下,花的步骤一样多。 按值找慢:数组自己不会排序也不会建索引,只能从头一个个比,平均要翻一半。 中间插删慢:插一个进去,后面所有元素都得往后挪一格——这是数组最贵的一项操作。 所以数组适合元素个数确定、以【按下标取】为主的场合。
四、它是别的结构的地基
栈、队列、环形缓冲区、堆、哈希表——底层几乎都是数组。 它们在数组之上只加了一条规矩:从哪一头进出、怎么算下标。 这也呼应了章名:内存是方方正正的一串格子,你要什么形状,靠规矩去摆。 下一节先看最简单的一种摆法:进出顺序不同的三个容器。
五、动手 Demo:用好内存先从数组开始
数组=把一串数据挨着放,于是求地址只要一步乘法: 元素地址 = 基址 + 下标 × 每个元素占几字节。
import array
arr = array.array("i", [10, 20, 30, 40, 50, 60])
base, size = 1000, arr.itemsize
for i, v in enumerate(arr):
print("下标 %d 值 %d 地址 %d" % (i, v, base + i * size))
实跑输出:
--------------------------------------------------------------------
用好内存先从数组开始
--------------------------------------------------------------------
**数组=把一串数据挨着放。** 挨着放之后,求第 n 个元素在哪儿只要一步乘法:
元素地址 = 基址 + 下标 × 每个元素占几字节
下标 元素值 地址 = 1000 + 下标 × 4
-------------------------------------------
0 10 1000
1 20 1004
2 30 1008
3 40 1012
4 50 1016
5 60 1020
两个要点:
**按下标取数据,不管下标多大多小都一样快**——都是乘一下加一下。
**但按值找数据仍然要一个个看**——数组自己不会排序、也不建索引。
这就解释了二分查找的两个前提:**排好序**,以及**能随便跳**。
数组一跳就到中间;链表却得从头一格一格爬——这正是下一节和再下一节的区别。
每个下标对应的地址都是等差的,公差正好是元素长度。 这就是【按下标取永远一样快】的原因——它只是乘一下加一下。
30 秒实验
- 把基址改成 2000、元素大小改成 8,重新算一遍各下标的地址。
- 想想一个 4 行的二维数组,第 3 行第 2 列(下标 2、1)该怎么算。
常见误区
- 以为数组会帮你找值。它只会按位置给,找值仍要自己遍历。
- 以为数组插入和读取一样快。读取是乘一下,插入要搬一串。
自测三问(点开对答案)
1. 数组元素地址怎么算? —— 基址 + 下标 × 每个元素占的字节数。 2. 为什么下标通常从 0 开始? —— 这样下标直接就是要跳的格数,换算公式少一步减法。 3. 数组最快和最慢的操作分别是什么? —— 按下标读取最快(一步乘法);在中间插入或删除最慢(要搬动后续元素)。- ☐ 下一节(4.5 栈与队列,以及环形缓冲区)
相关笔记
- 指针其实很简单 — 数组名为什么能当指针用
- 栈与队列,以及环形缓冲区 — 在数组上加一条规矩
- 用基址和变址实现数组 — 机器码层面的同一套算法
- ⌨️ 编程原理 · 索引 — 把编程全貌理一遍
- ⚙️ 程序是怎样跑起来的 · 索引