本文目录导读:
- 核心结论(先记住)
- 案例:模拟一个电话本查找系统
- 第一步:准备模拟数据
- 第二步:实现三种查找数据结构
- 第三步:测试比较(核心)
- 运行结果(预期输出,近似值)
- 关键发现与总结
- 为什么会有这么大差距?
- 实战建议
- 极端测试:你可以在本地试试
这是一个很经典的问题,它直观地展示了数据结构选择对算法性能的巨大影响,尤其是当数据量变大时。
下面通过一个具体的“电话本查找”案例,来对比列表、集合和字典在不同查找场景下的效率,我们将使用Python的 timeit 模块来精确测量时间。
核心结论(先记住)
- 列表(List): 查找慢,平均
O(n)(线性时间),数据量越大,查找越慢。 - 集合(Set): 查找极快,平均
O(1)(常数时间)。无序,适合判断“是否存在”。 - 字典(Dict): 查找极快,平均
O(1)(常数时间)。有映射关系(键值对),适合“根据键找值”。
案例:模拟一个电话本查找系统
假设我们有一个存储了 (姓名, 电话号码) 的电话本,长度为 n,需要完成两种常见操作:
- 场景A:判断“张三”是否在电话本中?(判断存在性)
- 场景B:查找“李四”的电话号码是多少?(查找关联值)
我们将用列表、集合、字典三种方式分别实现,并测试它们在不同数据规模下的表现。
第一步:准备模拟数据
定义一个函数,生成一个包含 n 个模拟联系人的数据。
import timeit
import random
import string
import sys
def generate_phonebook(n):
"""生成一个包含n个联系人的模拟电话本"""
names = []
phones = []
for i in range(n):
# 生成随机8位名字
name = ''.join(random.choices(string.ascii_lowercase, k=8))
phone = f"138-{str(i).zfill(7)}" # 保证唯一
names.append(name)
phones.append(phone)
return names, phones
第二步:实现三种查找数据结构
列表(List)
def create_list(phone_book):
# phone_book: (name_list, phone_list)
names, phones = phone_book
# 直接用列表存储元组
return list(zip(names, phones))
def list_search(phone_data, target_name):
"""列表查找:遍历所有元素"""
for name, phone in phone_data:
if name == target_name:
return phone
return None
def list_exists(phone_data, target_name):
"""列表判断存在性"""
for name, phone in phone_data:
if name == target_name:
return True
return False
集合(Set)
def create_set(phone_book):
names, phones = phone_book
# 集合只存储姓名,用于快速判断存在性
return set(names)
def set_search(phone_data, target_name):
"""集合无法直接查询电话号码(只能判断在不在)"""
# 此函数用于演示场景A(存在性)
pass
def set_exists(phone_data, target_name):
"""集合判断存在性:直接使用 in 操作符"""
return target_name in phone_data
字典(Dict)
def create_dict(phone_book):
names, phones = phone_book
# 字典存储 姓名->电话 的映射
return dict(zip(names, phones))
def dict_search(phone_data, target_name):
"""字典查找:直接通过键获取值"""
return phone_data.get(target_name)
def dict_exists(phone_data, target_name):
"""字典判断存在性:使用 in 操作符"""
return target_name in phone_data
第三步:测试比较(核心)
我们将用 timeit 分别测试三种数据结构在 n=1000, 10000, 100000 时,查找一个未在数据集中(最坏情况) 的目标的时间。
def benchmark():
# 测试不同规模
sizes = [1000, 10000, 100000]
results = []
for n in sizes:
print(f"\n===== 数据规模: {n} 条 =====")
phone_book = generate_phonebook(n)
# 创建一个肯定不在数据中的名字(保证遍历到末尾)
fake_name = "!not_exist_!"
# ---------- 场景A:判断存在性 ----------
# 列表
list_data = create_list(phone_book)
t_list = timeit.timeit(
lambda: list_exists(list_data, fake_name),
number=100 # 重复100次取平均
)
# 集合
set_data = create_set(phone_book)
t_set = timeit.timeit(
lambda: set_exists(set_data, fake_name),
number=100
)
# 字典
dict_data = create_dict(phone_book)
t_dict = timeit.timeit(
lambda: dict_exists(dict_data, fake_name),
number=100
)
print(f"【判断存在性】")
print(f" 列表: {t_list:.4f} 秒")
print(f" 集合: {t_set:.4f} 秒")
print(f" 字典: {t_dict:.4f} 秒")
print(f" 集合/列表 耗时比: {t_set/t_list:.2%}")
# ---------- 场景B:查找电话号码 ----------
# 字典
t_dict_get = timeit.timeit(
lambda: dict_search(dict_data, fake_name),
number=100
)
# 列表
t_list_get = timeit.timeit(
lambda: list_search(list_data, fake_name),
number=100
)
print(f"【查找电话号码】")
print(f" 列表: {t_list_get:.4f} 秒")
print(f" 字典: {t_dict_get:.4f} 秒")
print(f" 字典/列表 耗时比: {t_dict_get/t_list_get:.2%}")
results.append((n, t_list, t_set, t_dict, t_dict_get, t_list_get))
return results
运行结果(预期输出,近似值)
如果你运行上面的代码,输出会类似于:
===== 数据规模: 1000 条 =====
【判断存在性】
列表: 0.0052 秒
集合: 0.0001 秒
字典: 0.0001 秒
集合/列表 耗时比: 1.92%
【查找电话号码】
列表: 0.0053 秒
字典: 0.0001 秒
字典/列表 耗时比: 1.89%
===== 数据规模: 10000 条 =====
【判断存在性】
列表: 0.0510 秒
集合: 0.0001 秒
字典: 0.0001 秒
集合/列表 耗时比: 0.20%
【查找电话号码】
列表: 0.0518 秒
字典: 0.0001 秒
字典/列表 耗时比: 0.19%
===== 数据规模: 100000 条 =====
【判断存在性】
列表: 0.5120 秒
集合: 0.0001 秒
字典: 0.0001 秒
集合/列表 耗时比: 0.02%
【查找电话号码】
列表: 0.5021 秒
字典: 0.0001 秒
字典/列表 耗时比: 0.02%
关键发现与总结
| 操作 | 数据结构 | 时间复杂度 | 解释 |
|---|---|---|---|
| 判断存在性 | 列表 | O(n) |
需要从头到尾遍历,直到找到目标 |
| 判断存在性 | 集合 / 字典 | O(1) |
基于哈希表,直接计算位置 |
| 获取关联值 | 列表 | O(n) |
必须找到元素才能获取值 |
| 获取关联值 | 字典 | O(1) |
键直接映射到值 |
| 获取关联值 | 集合 | 不支持 | 集合不存储值,只能回答“有没有” |
为什么会有这么大差距?
- 数组(列表):相当于一个按顺序排列的盒子,你需要一个一个打开查看,直到找到你需要的那个,最坏情况下,要看完所有盒子。
- 哈希表(集合、字典):相当于一个魔法书,当你给出“张三”这个名字时,它直接通过一个“哈希函数”计算出“张三”对应的存储位置,直接抵达目标,无论这本书多厚(数据多大),这个计算过程几乎一样快。
实战建议
| 场景 | 推荐数据结构 | 原因 |
|---|---|---|
| 判断某个元素是否在数据集里 | set |
最快,且最省空间(不需要存值) |
| 需要根据键查找值 | dict |
最快,直接映射 |
| 需要保持插入顺序 | list 或 dict (Python 3.7+) |
列表有序,字典现在也保留插入顺序 |
| 数据量极小(< 100 条) | 三者差异很小,用列表即可 | 简单直观 |
| 数据量大 + 频繁查找 | 坚决不用列表 | 性能差异可达几百到上万倍 |
极端测试:你可以在本地试试
修改 benchmark 中的 n 到 1,000,000(100万条),你会看到:
- 列表查找可能需要 5秒以上。
- 集合/字典查找仍然在 001秒 以内。
这充分说明了:对于查找操作,选择哈希表(集合、字典)而非列表,是程序性能提升最直接、最有效的策略之一。
一句话总结:如果你需要快速知道“这个人在不在?”用集合;如果你需要知道“这个人的电话是多少?”用字典;除非你明确需要顺序遍历,否则别用列表做查找。
标签: 性能对比