深入探讨一下 Python list 的底层实现
简而言之,Python 的 list 是通过动态数组(Dynamic Array) 实现的。更具体地说,在官方的 CPython 解释器中,它是一个指针数组,存储着指向列表中各个元素的指针。
下面我们将从核心数据结构、动态扩容机制和操作的时间复杂度这几个关键方面来详细解析。
1. 核心数据结构:指针数组
当你创建一个 Python list 时,其底层在 C 语言层面是一个结构体(PyListObject),这个结构体包含了几个关键信息:
ob_item: 这是一个指向指针数组的指针 (PyObject **)。这个数组才是真正存储数据的地方,但它存的不是元素对象本身,而是指向这些对象的指针。ob_size: 这是一个整数,表示list当前包含了多少个元素。也就是我们调用len(my_list)时得到的值。allocated: 这是一个整数,记录了底层为这个指针数组分配了多大的内存空间(即可以容纳多少个指针)。这个值通常会大于或等于ob_size。
为什么是指针数组?
- 支持异构数据类型:Python 的
list可以包含任意类型的对象(整数、字符串、自定义对象等)。因为数组中存储的只是指向不同对象的指针,而每个PyObject指针的类型都是相同的,从而轻松实现了在同一个列表中存储不同类型的数据。 - 内存效率:如果直接存储对象本身,当对象大小不一时,数组的管理会变得异常复杂且低效。存储统一大小的指针则简单高效得多。
内存示意图:
假设我们有这样一个列表:my_list = [10, "hello", [1, 2]]
它的底层内存结构大致如下:
my_list (PyListObject)
|
|---- ob_size: 3
|---- allocated: 4 (或其他大于3的数字)
|---- ob_item: 指向一个指针数组
|
|---- [ 指针0 | 指针1 | 指针2 | (空闲) ]
| | |
| | `---------> PyListObject (另一个列表对象 [1, 2])
| |
| `-----------------> PyStringObject ("hello")
|
`-------------------------> PyLongObject (整数对象 10)
2. 动态扩容与缩容机制 (Amortized O(1))
动态数组的精髓在于它可以根据需要自动调整大小。
扩容 (Append)
当你不断向 list 中添加元素 (append) 时,ob_size 会增加。当 ob_size 即将超过 allocated 时,Python 必须为列表分配更大的存储空间。这个过程大致如下:
- 申请新空间:Python 会申请一块比当前所需更大的新内存空间。这个“更大”不是简单地+1,而是采用一种超额分配 (over-allocation) 的策略。
- 复制元素:将旧数组中所有的指针复制到新的内存空间中。
- 添加新元素:在新空间的末尾放入指向新元素的指针。
- 释放旧空间:释放掉旧的、较小的内存空间。
增长策略:
为了避免频繁地进行内存分配和复制(这是一个成本很高的操作),Python 的 list 扩容策略大致是每次将容量变为原来的 1.125 倍左右,并加上一个小的常数。这个增长因子确保了连续 append 操作的摊还时间复杂度 (Amortized Time Complexity) 为 O(1)。
虽然偶尔一次 append 会因为触发扩容而导致 O(n) 的时间成本,但由于扩容后会留出很多空闲空间,接下来多次 append 操作都将是 O(1) 的。将高成本操作的耗时分摊到多次低成本操作上,平均下来每次操作的成本接近于一个常数。
缩容 (Pop / Remove)
当从列表中删除元素时,如果 allocated 的空间远大于 ob_size,为了节省内存,Python 也会进行缩容。但缩容策略相对保守,通常是当列表中的元素数量少于已分配空间的一半时,才会触发缩容,将数组大小减小。
3. 主要操作的时间复杂度
理解了底层实现后,我们就能很自然地推导出 list 主要操作的时间复杂度:
| 操作 | 示例 | 平均时间复杂度 | 最坏时间复杂度 | 解释 |
|---|---|---|---|---|
| 索引访问 | my_list[i] |
O(1) | O(1) | 数组通过下标直接计算内存地址,所以访问是常数时间。 |
| 末尾添加 | my_list.append(x) |
O(1) | O(n) | 大部分情况是 O(1),但如果触发扩容,则需要 O(n) 来复制所有元素。 |
| 末尾弹出 | my_list.pop() |
O(1) | O(1) | 只需移动 ob_size 指针,通常不涉及缩容。 |
| 插入元素 | my_list.insert(i, x) |
O(n) | O(n) | 需要将被插入位置之后的所有元素的指针向后移动一位。 |
| 删除元素 | del my_list[i] |
O(n) | O(n) | 需要将被删除位置之后的所有元素的指针向前移动一位。 |
| 迭代 | for x in my_list: |
O(n) | O(n) | 需要遍历所有元素。 |
| 切片 | my_list[i:j] |
O(k) | O(k) | 其中 k = j-i。需要创建一个新列表并复制 k 个元素的指针。 |
| 成员检查 | x in my_list |
O(n) | O(n) | 需要线性扫描列表,逐个比较元素。 |
总结
- 数据结构:Python
list的底层是动态数组,在 CPython 中具体实现为指针数组 (PyObject **)。 - 核心优势:
- 通过指针支持异构数据类型。
- 通过索引实现**O(1)**的快速访问。
- 动态性:
- 通过超额分配和动态扩容机制,实现了摊还 O(1) 的尾部添加效率。
- 当空间浪费过多时会自动缩容。
- 性能注意点:在列表的开头或中间进行插入和删除操作是低效的(O(n)),因为需要移动大量元素。如果你的应用场景需要频繁地在序列两端进行添加和删除,那么
collections.deque(双向队列)会是更好的选择,因为它在两端的操作都是 O(1) 的。