人人都会AI编程

26.3 算法与数据结构选型优化

更新时间:2026-07-12

性能优化的第一步,往往不是调参数、换解释器,而是选对数据结构和算法。同样的功能,用错容器或算法,复杂度可能从 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)?
  • 是否使用了设计好的算法(排序、二分、哈希、动态规划)而非暴力枚举?

大多数时候,数据结构选对了,算法复杂度自然就降下来了,后续的代码级优化才更有意义。