性能优化的第一步,往往不是调参数、换解释器,而是选对数据结构和算法。同样的功能,用错容器或算法,复杂度可能从 O(n) 直接跳到 O(n²),代码再花哨也救不回来。这一节聚焦 Python 中最实用、最容易被忽视的选型策略,帮助你写出天生就更快的代码。
查询与去重:列表、集合、字典怎么选
场景:判断某个元素是否存在
# 列表查询:O(n)
items = [1, 2, 3, ..., 100000]
if 99999 in items: # 遍历整个列表
pass
# 集合查询:O(1)
items_set = set(items)
if 99999 in items_set: # 直接哈希查找
pass
- 列表的
in操作是线性扫描,数据量越大越慢。 - 集合(set)和字典(dict) 基于哈希表,平均 O(1) 查询速度,在百万级数据下的性能差异可达数万倍。
- 规则:凡是需要反复判断“是否存在”的场景,优先用集合;如果需要关联键值对,用字典;只在必须保持顺序且不需要高频查询时才考虑元组或列表。
场景:去重
# 手工去重:O(n²)
unique = []
for item in data:
if item not in unique:
unique.append(item)
# 利用集合:O(n)
unique = list(dict.fromkeys(data)) # 保持原始顺序
# 或
unique = list(set(data)) # 不保持顺序
- 利用哈希集合的特性去重,不仅代码短,复杂度也低。当需要保留原始顺序时,可以用
dict.fromkeys()(Python 3.7+ 字典保序)。
list 变体:array 与 deque
list 不是万能容器
list底层是一个动态数组,在尾部插入/删除是 O(1),但头部插入/删除是 O(n)(因为需要移动所有元素)。- 如果需要频繁从两端操作数据,换成
collections.deque:
from collections import deque
q = deque()
q.appendleft(1) # O(1)
q.popleft() # O(1)
- 如果需要存储大量同类型数值且追求内存效率,用
array.array或 NumPy 数组,比用list存储同样数据更紧凑、更快。
计数与分组:Counter 和 defaultdict
统计词频
# 用字典手动计数
counts = {}
for word in words:
if word in counts:
counts[word] += 1
else:
counts[word] = 1
# 用 collections.Counter
from collections import Counter
counts = Counter(words)
Counter 底层是字典,但针对计数场景做了优化,并且支持直接取 Top N(most_common(k))、减法等运算。
分组聚合
# 用普通字典分组(需要判空)
groups = {}
for item in data:
key = item['category']
if key not in groups:
groups[key] = []
groups[key].append(item)
# 用 defaultdict
from collections import defaultdict
groups = defaultdict(list)
for item in data:
groups[item['category']].append(item)
defaultdict 省去了重复的 if key not in ... 分支,代码更清爽,底层同样是 C 实现的字典操作。
排序与查找的场景化选择
- 只需最大/最小几个元素:用
heapq.nlargest()/nsmallest(),复杂度 O(n log k),而不是对整个序列排序(O(n log n))。 - 需要维护动态有序序列:用
heapq维护堆,插入/删除 O(log n),取最小值 O(1)。 - 需要快速查找但数据基本不变:用
bisect模块维护有序列表,搜索 O(log n);或者在构建时直接排序,之后用二分查找。 - 键值表快速查找:字典本身就是哈希查找的首选,但若键有序且不需要频繁修改,可以考虑
bisect配合两个列表维护“有序映射”,但复杂场景通常还是“字典优先”。
典型错误:多层循环与不必要的“造轮子”
交集、并集、差集
# 错误:双重循环 O(n*m)
common = []
for a in list_a:
if a in list_b:
common.append(a)
# 正确:用集合操作 O(n+m)
common = list(set(list_a) & set(list_b))
- Python 的集合运算符(
&、|、-、^)都是 C 实现的,远比纯 Python 循环快。
在序列上频繁删除元素
# 效率低:每次 remove 都是 O(n) 查找 + O(n) 移动
while target in lst:
lst.remove(target)
# 好很多:列表推导式或直接过滤
lst = [x for x in lst if x != target]
哪怕逻辑上需要原地修改,也可以考虑用列表推导式生成新列表,内存换时间在绝大多数业务场景中是划算的。
算法复杂度自查清单
在动手优化前,先问自己几个问题:
- 这段代码的循环嵌套了几层?能否用集合/字典将内层循环降维?
- 有没有在循环里反复计算同一个值?可以提到循环外或用缓存。
- 数据结构是否允许 O(1) 的查询/修改?是否有更适合的容器(如 deque、heap、tree)?
- 是否使用了设计好的算法(排序、二分、哈希、动态规划)而非暴力枚举?
大多数时候,数据结构选对了,算法复杂度自然就降下来了,后续的代码级优化才更有意义。