第 4 章:Python 数据结构实现
4.1 list (动态数组)
原理讲解
Python 的 list 是过度分配的动态数组。
┌─────────────────────────────────────────────────────────┐
│ list 内存布局 │
├─────────────────────────────────────────────────────────┤
│ │
│ PyListObject 结构: │
│ ┌──────────────────────────────────────────┐ │
│ │ ob_refcnt (引用计数) │ │
│ │ ob_type (类型指针) │ │
│ │ ob_size (元素数量) │ │
│ │ allocated (分配的容量) │ │
│ │ *items (指向元素数组的指针) │ │
│ └──────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────┐ │
│ │ [item0][item1][item2][item3][ ][ ]... │ │
│ │ ↑ ↑ │ │
│ │ ob_size=2 allocated=8 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 过度分配策略: │
│ new_allocated = (new_size >> 3) + (3 if new_size < 9 else 6) │
│ │
└─────────────────────────────────────────────────────────┘
过度分配策略:
python
# CPython 源码中的增长策略
# 新容量 = 当前大小 + (当前大小 >> 3) + 常数
# 大约是 1.125 倍增长
# 实际容量增长序列:
# 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
append 的摊销复杂度:
┌─────────────────────────────────────────────────────────┐
│ append 操作分析 │
├─────────────────────────────────────────────────────────┤
│ │
│ 情况 1: 有剩余空间 │
│ ┌─────────────────────────────────┐ │
│ │ [A][B][C][D][ ][ ][ ][ ] │ │
│ │ ↑ │ │
│ │ append(E) │ │
│ │ 操作:直接放入,O(1) │ │
│ └─────────────────────────────────┘ │
│ │
│ 情况 2: 需要扩容 │
│ ┌─────────────────────────────────┐ │
│ │ [A][B][C][D][E][F][G][H] │ 满了! │
│ │ ↓ │ │
│ │ 1. 分配新数组 (1.125 倍) │ │
│ │ 2. 复制所有元素 │ │
│ │ 3. 添加新元素 │ │
│ │ 操作:O(n), 但摊销后 O(1) │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
list 的内存布局
c
// CPython 源码 (Include/cpython/listobject.h)
typedef struct {
PyObject_VAR_HEAD // ob_refcnt, ob_type, ob_size
PyObject **ob_item; // 指向元素数组
Py_ssize_t allocated; // 分配的容量
} PyListObject;
查看 list 内部信息:
python
import sys
lst = [1, 2, 3]
print(f"list 对象大小:{sys.getsizeof(lst)}")
print(f"元素数量:{len(lst)}")
# 计算容量(通过添加元素测试)
test_list = []
for i in range(1000):
size_before = sys.getsizeof(test_list)
test_list.append(i)
size_after = sys.getsizeof(test_list)
if size_after != size_before:
print(f"添加第 {i} 个元素时扩容:{size_before} → {size_after}")
4.2 dict (哈希表)
哈希表实现
Python 3.6+ 的紧凑 dict:
┌─────────────────────────────────────────────────────────┐
│ Python 3.6+ dict 结构 │
├─────────────────────────────────────────────────────────┤
│ │
│ 传统 dict (3.5 及之前): │
│ ┌─────────────────────────────────────────┐ │
│ │ [hash1, key1, value1] │ │
│ │ [hash2, key2, value2] │ │
│ │ [hash3, key3, value3] │ │
│ │ ... (稀疏,浪费空间) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 紧凑 dict (3.6+): │
│ ┌─────────────────────────────────────────┐ │
│ │ indices (索引数组) │ │
│ │ [2, 0, 1, -1, -1, ...] │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ entries (紧凑数组) │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ [hash0, key0, value0] │ │ │
│ │ │ [hash1, key1, value1] │ │ │
│ │ │ [hash2, key2, value2] │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 优势: │
│ - 节省 20-25% 内存 │
│ - 保持插入顺序 │
│ - 迭代更快 │
│ │
└─────────────────────────────────────────────────────────┘
哈希冲突解决:开放寻址法
python
# 伪代码展示查找过程
def dict_lookup(key):
hash_value = hash(key)
index = hash_value & mask # mask = size - 1
while True:
entry = table[index]
if entry is EMPTY:
return NOT_FOUND
if entry.key == key:
return entry.value
# 冲突!使用扰动函数找下一个位置
index = perturb(index, hash_value)
dict 结构:
c
// CPython 源码 (Include/cpython/dictobject.h)
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used; // 使用中的条目数
Py_ssize_t ma_version; // 版本号(用于迭代检查)
PyDictKey *ma_keys; // 键数组
PyObject **ma_values; // 值数组
} PyDictObject;
哈希冲突解决
┌─────────────────────────────────────────────────────────┐
│ 哈希冲突解决示例 │
├─────────────────────────────────────────────────────────┤
│ │
│ 假设:hash("apple") % 8 = 3 │
│ hash("orange") % 8 = 3 (冲突!) │
│ │
│ 使用扰动函数找新位置: │
│ perturb = perturb >> 5 | perturb * 0x9e3779b9 │
│ │
│ 索引表: │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ │
│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │
│ │ │ │ │ 0 │ │ │ 1 │ │ │
│ │ │ │ │↑ │ │ │↑ │ │ │
│ │ │ │ ││ │ │ ││ │ │ │
│ │ │ │ │apple │ │orange │ │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ │
│ "apple" 在位置 3 │
│ "orange" 冲突,扰动后放到位置 6 │
│ │
└─────────────────────────────────────────────────────────┘
4.3 set 和 tuple
set 的哈希表实现
set 与 dict 的关系:
python
# set 本质上是只有 key 的 dict
my_set = {1, 2, 3}
# 内部实现类似:
# {1: None, 2: None, 3: None}
set 的特性:
- 无序(Python 3.7+ 保持插入顺序用于迭代,但不保证)
- 元素必须可哈希
- 成员测试 O(1)
python
# set 的内存效率
import sys
lst = [1, 2, 3, 4, 5]
st = {1, 2, 3, 4, 5}
print(f"list 大小:{sys.getsizeof(lst)}")
print(f"set 大小:{sys.getsizeof(st)}")
# set 通常比 list 大,因为有哈希表开销
tuple 的不可变性
tuple 结构:
c
// CPython 源码
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1]; // 变长数组
} PyTupleObject;
不可变性的优势:
┌─────────────────────────────────────────────────────────┐
│ tuple vs list │
├─────────────────────────────────────────────────────────┤
│ │
│ tuple (不可变): │
│ - 内存紧凑(不需要额外空间用于增长) │
│ - 可哈希(可用作 dict 的 key) │
│ - 创建更快 │
│ - 线程安全(不需要锁) │
│ │
│ list (可变): │
│ - 需要过度分配 │
│ - 不可哈希 │
│ - 支持增删改 │
│ - 更灵活 │
│ │
│ 性能对比: │
│ - tuple 创建比 list 快约 30% │
│ - tuple 迭代略快 │
│ - tuple 内存占用更小 │
│ │
└─────────────────────────────────────────────────────────┘
4.4 实践:对比不同数据结构的性能
实验代码
python
# examples/chapter-04/data_structure_performance.py
import sys
import timeit
import random
print("=" * 70)
print("Python 数据结构性能对比实验")
print("=" * 70)
# ========== 1. 内存对比 ==========
print("\n【实验 1】内存占用对比")
print("-" * 50)
sizes = [100, 1000, 10000]
for n in sizes:
data = list(range(n))
lst = list(data)
tpl = tuple(data)
st = set(data)
dct = {i: i for i in data}
print(f"\nn = {n}:")
print(f" list: {sys.getsizeof(lst):>8} bytes")
print(f" tuple: {sys.getsizeof(tpl):>8} bytes")
print(f" set: {sys.getsizeof(st):>8} bytes")
print(f" dict: {sys.getsizeof(dct):>8} bytes")
# ========== 2. 查找性能 ==========
print("\n【实验 2】查找性能对比 (10000 次查找)")
print("-" * 50)
n = 10000
data = list(range(n))
search_targets = [random.randint(0, n-1) for _ in range(10000)]
lst = data
st = set(data)
dct = {i: i for i in data}
# list 查找
time_list = timeit.timeit(
lambda: [x for x in search_targets if x in lst],
number=1
)
# set 查找
time_set = timeit.timeit(
lambda: [x for x in search_targets if x in st],
number=1
)
# dict 查找
time_dict = timeit.timeit(
lambda: [dct[x] for x in search_targets if x in dct],
number=1
)
print(f" list: {time_list*1000:>8.2f} ms (O(n) 查找)")
print(f" set: {time_set*1000:>8.2f} ms (O(1) 查找)")
print(f" dict: {time_dict*1000:>8.2f} ms (O(1) 查找)")
print(f" 加速比:set 比 list 快 {time_list/time_set:.0f}x")
# ========== 3. append 性能 ==========
print("\n【实验 3】append 性能对比 (追加 10000 个元素)")
print("-" * 50)
time_list_append = timeit.timeit(
lambda: [lst := []] + [lst.append(i) for i in range(10000)],
number=10
) / 10
print(f" list.append: {time_list_append*1000:>8.2f} ms")
print(f" 平均每次 append: {time_list_append*1000/10000:.4f} ms")
# ========== 4. 创建性能 ==========
print("\n【实验 4】创建性能对比")
print("-" * 50)
time_list_create = timeit.timeit(
lambda: list(range(10000)),
number=100
)
time_tuple_create = timeit.timeit(
lambda: tuple(range(10000)),
number=100
)
time_set_create = timeit.timeit(
lambda: set(range(10000)),
number=100
)
time_dict_create = timeit.timeit(
lambda: {i: i for i in range(10000)},
number=100
)
print(f" list: {time_list_create*10:>8.2f} ms")
print(f" tuple: {time_tuple_create*10:>8.2f} ms (快 {time_list_create/time_tuple_create:.2f}x)")
print(f" set: {time_set_create*10:>8.2f} ms")
print(f" dict: {time_dict_create*10:>8.2f} ms")
# ========== 5. 迭代性能 ==========
print("\n【实验 5】迭代性能对比 (遍历 10000 个元素)")
print("-" * 50)
n = 10000
lst = list(range(n))
tpl = tuple(range(n))
st = set(range(n))
time_list_iter = timeit.timeit(
lambda: sum(lst),
number=100
)
time_tuple_iter = timeit.timeit(
lambda: sum(tpl),
number=100
)
time_set_iter = timeit.timeit(
lambda: sum(st),
number=100
)
print(f" list: {time_list_iter*10:>8.2f} ms")
print(f" tuple: {time_tuple_iter*10:>8.2f} ms")
print(f" set: {time_set_iter*10:>8.2f} ms")
# ========== 6. dict 扩容测试 ==========
print("\n【实验 6】dict 扩容观察")
print("-" * 50)
d = {}
prev_size = sys.getsizeof(d)
for i in range(1000):
d[i] = i
curr_size = sys.getsizeof(d)
if curr_size != prev_size:
print(f" 插入第 {i} 个元素时扩容:{prev_size} → {curr_size} bytes")
prev_size = curr_size
实验练习
练习 1:观察 list 扩容行为
python
import sys
def observe_list_growth():
"""观察 list 的过度分配"""
lst = []
prev_size = sys.getsizeof(lst)
prev_capacity = 0
for i in range(100):
lst.append(i)
curr_size = sys.getsizeof(lst)
if curr_size != prev_size:
# 估算容量(每个指针 8 字节)
curr_capacity = (curr_size - 56) // 8
print(f"长度={i+1:>3} | 大小={curr_size:>4} | "
f"容量≈{curr_capacity:>3} | 利用率={100*(i+1)/curr_capacity:.1f}%")
prev_size = curr_size
observe_list_growth()
练习 2:测试哈希冲突对性能的影响
python
import timeit
# 创建一个有哈希冲突的场景
class BadHash:
"""所有对象哈希值相同"""
def __init__(self, val):
self.val = val
def __hash__(self):
return 42 # 故意返回相同值
def __eq__(self, other):
return isinstance(other, BadHash) and self.val == other.val
# 创建 dict
bad_dict = {BadHash(i): i for i in range(1000)}
normal_dict = {i: i for i in range(1000)}
# 测试查找性能
bad_time = timeit.timeit(
lambda: sum(bad_dict.get(BadHash(500), 0)),
number=1000
)
normal_time = timeit.timeit(
lambda: sum(normal_dict.get(500, 0)),
number=1000
)
print(f"哈希冲突 dict: {bad_time*1000:.2f} ms")
print(f"正常 dict: {normal_time*1000:.2f} ms")
print(f"性能下降:{bad_time/normal_time:.1f}x")
练习 3:比较不同大小 dict 的内存效率
python
import sys
def dict_memory_efficiency():
"""测试 dict 的内存效率"""
sizes = [10, 100, 1000, 10000]
print("dict 内存效率分析:")
print("大小\tdict 大小\t每条目\t负载因子")
print("-" * 50)
for n in sizes:
d = {i: i for i in range(n)}
size = sys.getsizeof(d)
per_item = size / n
# 估算容量(简化)
# 实际需要使用更复杂的方法
print(f"{n}\t{size}\t{per_item:.1f}\t-")
dict_memory_efficiency()
常见问题
Q1: 为什么 list 查询是 O(n) 而 dict 是 O(1)?
A:
- list 是数组,需要线性搜索
- dict 是哈希表,通过哈希函数直接计算位置
python
# list: 需要遍历
5 in [1, 2, 3, 4, 5] # 最坏检查所有元素
# dict: 直接计算位置
5 in {1, 2, 3, 4, 5} # 计算 hash(5),直接访问
Q2: 为什么 tuple 比 list 快?
A:
- tuple 不需要过度分配
- tuple 不需要检查可变性
- tuple 内存布局更紧凑
- tuple 可以被优化(如常量折叠)
Q3: dict 保持顺序是从哪个版本开始的?
A:
- Python 3.6: CPython 实现细节
- Python 3.7: 语言规范保证
Q4: 如何选择使用 list 还是 tuple?
A:
python
# 使用 tuple 当:
- 数据不应该改变
- 用作 dict 的 key
- 需要哈希
- 性能敏感
# 使用 list 当:
- 需要增删元素
- 需要排序
- 需要切片赋值
Q5: set 和 dict 的哈希表实现有什么区别?
A:
- set 只存储 key(类似 dict 的 keys)
- dict 存储 key-value 对
- 底层哈希算法相同
- set 的 value 是固定的(存在性)
本章小结
- list 是过度分配的动态数组,append 摊销 O(1)
- dict 在 3.6+ 使用紧凑布局,节省内存并保持顺序
- 哈希表使用开放寻址法解决冲突
- set 是只有 key 的哈希表
- tuple 是不可变数组,更紧凑更高效
- 选择合适的数据结构对性能至关重要
下一章预告
第 5 章将深入探讨 字符串与编码,包括 Unicode、UTF-8 和字符串驻留机制。