深入理解 Python dict 与 set:从哈希表底层到高性能实战

从底层原理到实战技巧,一文搞懂 Python 中 dict 与 set 的一切。


一、从一个实际场景出发

假设你要根据同学的名字查找对应的成绩。最朴素的做法是用两个列表,下标一一对应:

python 复制代码
names  = ["A", "B", "C"]
scores = [59, 89, 90]

# 查找 B 的成绩
idx = names.index("B")  # O(n) 遍历
print(scores[idx])            # 89

这样做的缺点是:每次查找都要遍历整个列表 。如果有 100 万个名字,最坏情况下需要比较 100 万次------这就是 O(n) 的代价。

Python 提供了 dict(字典),能将查找变为 O(1)

python 复制代码
d = {"D": 100, "E": 99, "F": 99}
print(d["D"])  # 100 ------ 一步到位!

这就像字典的偏旁部首索引------你不用从第一页翻到最后一页,而是直接定位。这个"直接定位"的能力,就来自哈希表(Hash Table)


二、什么是哈希表

2.1 核心流程

哈希表是一种基于键值对(key-value)存储数据的数据结构,其工作流程如下:

vbnet 复制代码
key  ──→  哈希函数  ──→  索引  ──→  存储位置(value)
  1. key 必须唯一
  2. 对 key 执行哈希运算,得到一个整数(哈希值)
  3. 用哈希值计算出数组索引
  4. 索引指向的内存位置,存储着对应的 value
  5. 查找时,直接用 key 算出同一索引,O(1) 取出 value

2.2 哈希冲突

不同的 key 可能算出相同的索引------这就是哈希冲突 。比如 "abc""cba" 经过哈希后恰好定位到同一个槽位。

两种主流解决方式:

方式 做法 Python 采用?
链地址法 每个槽位存链表,冲突元素串在一起
开放寻址法 当前槽位被占就去下一个空位(探测)

CPython 使用的是开放寻址法 + 伪随机探测,这也是为什么 dict 需要预留大量空位------如果表塞得太满,探测路径变长,O(1) 会退化。

2.3 负载因子与扩容

  • 负载因子 = 已用槽位 / 总槽位
  • 当负载因子超过阈值(Python dict 约 2/3),就会触发扩容(rehash)
  • 扩容时分配更大的数组,重新计算所有 key 的位置

这也解释了一个重要特性:哈希表天生是"空间换时间"的------占用内存多,但查找极快。


三、Python dict 详解

3.1 基本操作

python 复制代码
d = {"D": 100, "E": 99, "F": 99}

# 取值
d["D"]          # 100(key 不存在则 KeyError)

# 安全取值
d.get("Thomas")     # None(不报错)
d.get("Thomas", -1) # -1(自定义默认值)

# 新增 / 修改
d["Adam"] = 67      # 新增
d["Adam"] = 90      # 修改(key 相同,覆盖原值)

# 判断 key 是否存在
"Thomas" in d       # False

# 删除
d.pop("Adam")       # 返回 90,key 不存在则 KeyError

3.2 常用方法速查

方法 说明 示例
d.keys() 返回所有 key 的视图 d.keys()
d.values() 返回所有 value 的视图 d.values()
d.items() 返回 (key, value) 对的视图 for k, v in d.items()
d.update(d2) 将 d2 的键值对合并进来 d.update({"x": 1})
d.setdefault(k, v) key 存在则返回其值,不存在则设为 v d.setdefault("a", 0)
d.popitem() 移除并返回最后插入的键值对(Python 3.7+ 起保证 LIFO 行为) d.popitem()
d.clear() 清空所有元素 d.clear()

3.3 插入顺序

Python 3.7 开始,dict 保证按插入顺序遍历(3.6 是 CPython 实现细节,3.7 成为语言规范)。

python 复制代码
d = {}
d["a"] = 1
d["c"] = 3
d["b"] = 2
print(list(d.keys()))  # ['a', 'c', 'b'] ------ 严格按插入顺序

注意:这里的"有序"是指插入顺序,不是排序顺序。

3.4 字典推导式

python 复制代码
# 将列表转为 {元素: 索引} 的映射
items = ["apple", "banana", "cherry"]
d = {v: i for i, v in enumerate(items)}
# {"apple": 0, "banana": 1, "cherry": 2}

# 基于条件过滤
squares = {x: x**2 for x in range(10) if x % 2 == 0}
# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

3.5 collections 扩展

Python 标准库 collections 提供了几个常用的 dict 变体:

python 复制代码
from collections import defaultdict, Counter, OrderedDict

# defaultdict ------ 访问不存在的 key 时自动生成默认值
dd = defaultdict(list)
dd["fruits"].append("apple")   # 无需先初始化 []
print(dd["fruits"])            # ['apple']

dd = defaultdict(int)
words = ["a", "b", "a", "c", "b", "a"]
for w in words:
    dd[w] += 1                 # 无需判断 key 是否存在
print(dd)  # {'a': 3, 'b': 2, 'c': 1}

# Counter ------ 专门用于计数的字典
c = Counter(words)
print(c.most_common(2))  # [('a', 3), ('b', 2)]

3.6 时间复杂度一览

操作 平均 最坏
查找 d[k] O(1) O(n)
插入 d[k] = v O(1) O(n)
删除 del d[k] O(1) O(n)
遍历 O(n) O(n)
k in d O(1) O(n)

最坏情况 O(n) 发生在极端哈希冲突时,实际上极少出现。


四、dict 与 list 对比

维度 dict(哈希表) list(动态数组)
查找 O(1) O(n)
插入 O(1) O(n)(中间插入)
内存占用 大(需预留空位)
有序性 插入顺序(3.7+) 索引顺序
设计哲学 空间换时间 时间换空间

选择建议

  • 需要按 key 快速查找 → dict
  • 只需有序存储/遍历 → list
  • 需要去重或集合运算 → set

五、不可变对象与 hashable

5.1 dict 的 key 必须是可哈希的

python 复制代码
# ✅ 不可变类型 --- 可作 key
d = {}
d["name"] = "Alice"     # str 可哈希
d[42]      = "answer"    # int 可哈希
d[(1, 2)]  = "point"    # tuple(元素全不可变)可哈希

# ❌ 可变类型 --- 不可作 key
d[[1, 2, 3]] = "list"   # TypeError: unhashable type: 'list'

为什么? dict 依靠 key 的哈希值来决定 value 的存储位置。如果 key 可变(比如 list 可以 append),其哈希值会跟着变,dict 就找不到原来的数据了------整个表会陷入混乱。

5.2 可变 vs 不可变对象

python 复制代码
# list 是可变对象 --- 方法修改对象本身
a = ['c', 'b', 'a']
a.sort()            # sort() 原地修改,返回 None
print(a)            # ['a', 'b', 'c'] --- 同一个对象,内容变了

# str 是不可变对象 --- 方法返回新对象
s = "abc"
print(s.replace("a", "A"))  # "Abc" --- 新字符串
print(s)                    # "abc" --- 原字符串未变!

核心理解 :对于不可变对象,调用自身任意方法都不会改变它的内容 ------方法总是返回新对象。strintfloattuplefrozenset 都是不可变类型;listdictset 是可变类型。


六、Python set 详解

6.1 set 是什么

setdict 本质相同------都是一组 key 的集合。唯一的区别是 set 不存储对应的 value。因为 key 不可重复,set 天然保证元素不重复。

6.2 创建与基本操作

python 复制代码
# 两种创建方式
s = {1, 2, 3}              # 字面量
s = set([1, 2, 3, 4, 2])   # 从可迭代对象构建,自动去重 → {1, 2, 3, 4}

# 基本操作
s.add(5)        # 添加元素
s.remove(1)     # 删除元素(元素不存在则 KeyError)
s.discard(1)    # 删除元素(元素不存在也不报错)

6.3 集合运算

python 复制代码
s1 = {1, 2, 3}
s2 = {2, 3, 4}

# 交集
s1 & s2           # {2, 3}
s1.intersection(s2)

# 并集
s1 | s2           # {1, 2, 3, 4}
s1.union(s2)

# 差集
s1 - s2           # {1}  ------ 在 s1 但不在 s2
s1.difference(s2)

# 对称差集(并集 - 交集)
s1 ^ s2           # {1, 4}  ------ 在 s1 或 s2 但不同时在两者
s1.symmetric_difference(s2)

6.4 集合推导式

python 复制代码
# 找出列表中所有出现过的字符
words = ["hello", "world", "python"]
unique_chars = {c for w in words for c in w}
# {'h', 'e', 'l', 'o', 'w', 'r', 'd', 'p', 'y', 't', 'n'}

6.5 判断关系

python 复制代码
a = {1, 2}
b = {1, 2, 3, 4}

a.issubset(b)     # True  --- a 是 b 的子集
b.issuperset(a)   # True  --- b 是 a 的超集
a.isdisjoint({5}) # True  --- 没有交集

6.6 frozenset------不可变集合

python 复制代码
fs = frozenset([1, 2, 3])
# fs.add(4)          # AttributeError ------ 不可修改
d = {fs: "valid"}   # ✅ frozenset 可作 dict 的 key(因为不可变)

6.7 set 时间复杂度

操作 平均
添加 add O(1)
删除 remove O(1)
成员判断 x in s O(1)
并集 ` `
交集 & O(min(len(s1), len(s2)))
差集 - O(len(s1))

七、实战场景总结

场景 使用 理由
快速查找 / 映射关系 dict O(1) 查找
去重 set 元素自动唯一
成员判断(是否存在) set O(1) vs list 的 O(n)
交集 / 并集 / 差集运算 set 原生支持集合运算
计数统计 Counter 专为此场景优化
缓存 / 记忆化 dict 快速存取
需要按索引顺序访问 list dict 插入顺序不替代索引
不可变的常量集合 frozenset 可哈希,可作 key

八、小结

  1. 哈希表 = 哈希函数 + 数组,key → 索引 → value,O(1) 查找
  2. dict = 带 value 的哈希表,空间换时间
  3. set = 只有 key 的哈希表,天然去重 + 集合运算
  4. 可变对象不可哈希 (list/set/dict 不能作 key),不可变对象可哈希(str/int/tuple/frozenset)
  5. Python 3.7+ dict 保持插入顺序,collections 提供 defaultdictCounter 等实用扩展

从底层原理到实战技巧,一文搞懂 Python 中 dict 与 set 的一切。


一、从一个实际场景出发

假设你要根据同学的名字查找对应的成绩。最朴素的做法是用两个列表,下标一一对应:

python 复制代码
names  = ["A", "B", "C"]
scores = [59, 89, 90]

# 查找 B 的成绩
idx = names.index("B")  # O(n) 遍历
print(scores[idx])            # 89

这样做的缺点是:每次查找都要遍历整个列表 。如果有 100 万个名字,最坏情况下需要比较 100 万次------这就是 O(n) 的代价。

Python 提供了 dict(字典),能将查找变为 O(1)

python 复制代码
d = {"D": 100, "E": 99, "F": 99}
print(d["D"])  # 100 ------ 一步到位!

这就像字典的偏旁部首索引------你不用从第一页翻到最后一页,而是直接定位。这个"直接定位"的能力,就来自哈希表(Hash Table)


二、什么是哈希表

2.1 核心流程

哈希表是一种基于键值对(key-value)存储数据的数据结构,其工作流程如下:

vbnet 复制代码
key  ──→  哈希函数  ──→  索引  ──→  存储位置(value)
  1. key 必须唯一
  2. 对 key 执行哈希运算,得到一个整数(哈希值)
  3. 用哈希值计算出数组索引
  4. 索引指向的内存位置,存储着对应的 value
  5. 查找时,直接用 key 算出同一索引,O(1) 取出 value

2.2 哈希冲突

不同的 key 可能算出相同的索引------这就是哈希冲突 。比如 "abc""cba" 经过哈希后恰好定位到同一个槽位。

两种主流解决方式:

方式 做法 Python 采用?
链地址法 每个槽位存链表,冲突元素串在一起
开放寻址法 当前槽位被占就去下一个空位(探测)

CPython 使用的是开放寻址法 + 伪随机探测,这也是为什么 dict 需要预留大量空位------如果表塞得太满,探测路径变长,O(1) 会退化。

2.3 负载因子与扩容

  • 负载因子 = 已用槽位 / 总槽位
  • 当负载因子超过阈值(Python dict 约 2/3),就会触发扩容(rehash)
  • 扩容时分配更大的数组,重新计算所有 key 的位置

这也解释了一个重要特性:哈希表天生是"空间换时间"的------占用内存多,但查找极快。


三、Python dict 详解

3.1 基本操作

python 复制代码
d = {"D": 100, "E": 99, "F": 99}

# 取值
d["D"]          # 100(key 不存在则 KeyError)

# 安全取值
d.get("Thomas")     # None(不报错)
d.get("Thomas", -1) # -1(自定义默认值)

# 新增 / 修改
d["Adam"] = 67      # 新增
d["Adam"] = 90      # 修改(key 相同,覆盖原值)

# 判断 key 是否存在
"Thomas" in d       # False

# 删除
d.pop("Adam")       # 返回 90,key 不存在则 KeyError

3.2 常用方法速查

方法 说明 示例
d.keys() 返回所有 key 的视图 d.keys()
d.values() 返回所有 value 的视图 d.values()
d.items() 返回 (key, value) 对的视图 for k, v in d.items()
d.update(d2) 将 d2 的键值对合并进来 d.update({"x": 1})
d.setdefault(k, v) key 存在则返回其值,不存在则设为 v d.setdefault("a", 0)
d.popitem() 移除并返回最后插入的键值对(Python 3.7+ 起保证 LIFO 行为) d.popitem()
d.clear() 清空所有元素 d.clear()

3.3 插入顺序

Python 3.7 开始,dict 保证按插入顺序遍历(3.6 是 CPython 实现细节,3.7 成为语言规范)。

python 复制代码
d = {}
d["a"] = 1
d["c"] = 3
d["b"] = 2
print(list(d.keys()))  # ['a', 'c', 'b'] ------ 严格按插入顺序

注意:这里的"有序"是指插入顺序,不是排序顺序。

3.4 字典推导式

python 复制代码
# 将列表转为 {元素: 索引} 的映射
items = ["apple", "banana", "cherry"]
d = {v: i for i, v in enumerate(items)}
# {"apple": 0, "banana": 1, "cherry": 2}

# 基于条件过滤
squares = {x: x**2 for x in range(10) if x % 2 == 0}
# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

3.5 collections 扩展

Python 标准库 collections 提供了几个常用的 dict 变体:

python 复制代码
from collections import defaultdict, Counter, OrderedDict

# defaultdict ------ 访问不存在的 key 时自动生成默认值
dd = defaultdict(list)
dd["fruits"].append("apple")   # 无需先初始化 []
print(dd["fruits"])            # ['apple']

dd = defaultdict(int)
words = ["a", "b", "a", "c", "b", "a"]
for w in words:
    dd[w] += 1                 # 无需判断 key 是否存在
print(dd)  # {'a': 3, 'b': 2, 'c': 1}

# Counter ------ 专门用于计数的字典
c = Counter(words)
print(c.most_common(2))  # [('a', 3), ('b', 2)]

3.6 时间复杂度一览

操作 平均 最坏
查找 d[k] O(1) O(n)
插入 d[k] = v O(1) O(n)
删除 del d[k] O(1) O(n)
遍历 O(n) O(n)
k in d O(1) O(n)

最坏情况 O(n) 发生在极端哈希冲突时,实际上极少出现。


四、dict 与 list 对比

维度 dict(哈希表) list(动态数组)
查找 O(1) O(n)
插入 O(1) O(n)(中间插入)
内存占用 大(需预留空位)
有序性 插入顺序(3.7+) 索引顺序
设计哲学 空间换时间 时间换空间

选择建议

  • 需要按 key 快速查找 → dict
  • 只需有序存储/遍历 → list
  • 需要去重或集合运算 → set

五、不可变对象与 hashable

5.1 dict 的 key 必须是可哈希的

python 复制代码
# ✅ 不可变类型 --- 可作 key
d = {}
d["name"] = "Alice"     # str 可哈希
d[42]      = "answer"    # int 可哈希
d[(1, 2)]  = "point"    # tuple(元素全不可变)可哈希

# ❌ 可变类型 --- 不可作 key
d[[1, 2, 3]] = "list"   # TypeError: unhashable type: 'list'

为什么? dict 依靠 key 的哈希值来决定 value 的存储位置。如果 key 可变(比如 list 可以 append),其哈希值会跟着变,dict 就找不到原来的数据了------整个表会陷入混乱。

5.2 可变 vs 不可变对象

python 复制代码
# list 是可变对象 --- 方法修改对象本身
a = ['c', 'b', 'a']
a.sort()            # sort() 原地修改,返回 None
print(a)            # ['a', 'b', 'c'] --- 同一个对象,内容变了

# str 是不可变对象 --- 方法返回新对象
s = "abc"
print(s.replace("a", "A"))  # "Abc" --- 新字符串
print(s)                    # "abc" --- 原字符串未变!

核心理解 :对于不可变对象,调用自身任意方法都不会改变它的内容 ------方法总是返回新对象。strintfloattuplefrozenset 都是不可变类型;listdictset 是可变类型。


六、Python set 详解

6.1 set 是什么

setdict 本质相同------都是一组 key 的集合。唯一的区别是 set 不存储对应的 value。因为 key 不可重复,set 天然保证元素不重复。

6.2 创建与基本操作

python 复制代码
# 两种创建方式
s = {1, 2, 3}              # 字面量
s = set([1, 2, 3, 4, 2])   # 从可迭代对象构建,自动去重 → {1, 2, 3, 4}

# 基本操作
s.add(5)        # 添加元素
s.remove(1)     # 删除元素(元素不存在则 KeyError)
s.discard(1)    # 删除元素(元素不存在也不报错)

6.3 集合运算

python 复制代码
s1 = {1, 2, 3}
s2 = {2, 3, 4}

# 交集
s1 & s2           # {2, 3}
s1.intersection(s2)

# 并集
s1 | s2           # {1, 2, 3, 4}
s1.union(s2)

# 差集
s1 - s2           # {1}  ------ 在 s1 但不在 s2
s1.difference(s2)

# 对称差集(并集 - 交集)
s1 ^ s2           # {1, 4}  ------ 在 s1 或 s2 但不同时在两者
s1.symmetric_difference(s2)

6.4 集合推导式

python 复制代码
# 找出列表中所有出现过的字符
words = ["hello", "world", "python"]
unique_chars = {c for w in words for c in w}
# {'h', 'e', 'l', 'o', 'w', 'r', 'd', 'p', 'y', 't', 'n'}

6.5 判断关系

python 复制代码
a = {1, 2}
b = {1, 2, 3, 4}

a.issubset(b)     # True  --- a 是 b 的子集
b.issuperset(a)   # True  --- b 是 a 的超集
a.isdisjoint({5}) # True  --- 没有交集

6.6 frozenset------不可变集合

python 复制代码
fs = frozenset([1, 2, 3])
# fs.add(4)          # AttributeError ------ 不可修改
d = {fs: "valid"}   # ✅ frozenset 可作 dict 的 key(因为不可变)

6.7 set 时间复杂度

操作 平均
添加 add O(1)
删除 remove O(1)
成员判断 x in s O(1)
并集 ` `
交集 & O(min(len(s1), len(s2)))
差集 - O(len(s1))

七、实战场景总结

场景 使用 理由
快速查找 / 映射关系 dict O(1) 查找
去重 set 元素自动唯一
成员判断(是否存在) set O(1) vs list 的 O(n)
交集 / 并集 / 差集运算 set 原生支持集合运算
计数统计 Counter 专为此场景优化
缓存 / 记忆化 dict 快速存取
需要按索引顺序访问 list dict 插入顺序不替代索引
不可变的常量集合 frozenset 可哈希,可作 key

八、小结

  1. 哈希表 = 哈希函数 + 数组,key → 索引 → value,O(1) 查找
  2. dict = 带 value 的哈希表,空间换时间
  3. set = 只有 key 的哈希表,天然去重 + 集合运算
  4. 可变对象不可哈希 (list/set/dict 不能作 key),不可变对象可哈希(str/int/tuple/frozenset)
  5. Python 3.7+ dict 保持插入顺序,collections 提供 defaultdictCounter 等实用扩展
相关推荐
带派擂总1 小时前
Python全栈开发 Day10_用户管理系统
python
databook1 小时前
用 SymPy 解决 Manim 曲线绘制速度不均的问题
python·数学·动效
宇宙无敌程序员菜鸟1 小时前
浅玩CRUD Agent
python
程序大视界1 小时前
【Python系列课程】Python入门教程
开发语言·人工智能·python
morning_judger1 小时前
Agent系列(二)-记忆系统的设计
开发语言·python·机器学习
RSTJ_16251 小时前
PYTHON+AI LLM DAY SIXTY-ONE
开发语言·python
TickDB1 小时前
智谱GLM-4 接金融数据:工具描述多写三个字,模型少犯一类错
人工智能·python·websocket·行情数据 api·行情 api
用户0332126663672 小时前
使用 Python 在 Excel 中查找并高亮显示
python
sugar__salt2 小时前
Prompt工程实战指南:规范设计、LLM接口封装与避坑技巧
人工智能·python·prompt