从底层原理到实战技巧,一文搞懂 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)
- key 必须唯一
- 对 key 执行哈希运算,得到一个整数(哈希值)
- 用哈希值计算出数组索引
- 索引指向的内存位置,存储着对应的 value
- 查找时,直接用 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" --- 原字符串未变!
核心理解 :对于不可变对象,调用自身任意方法都不会改变它的内容 ------方法总是返回新对象。
str、int、float、tuple、frozenset都是不可变类型;list、dict、set是可变类型。
六、Python set 详解
6.1 set 是什么
set 和 dict 本质相同------都是一组 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 |
八、小结
- 哈希表 = 哈希函数 + 数组,key → 索引 → value,O(1) 查找
- dict = 带 value 的哈希表,空间换时间
- set = 只有 key 的哈希表,天然去重 + 集合运算
- 可变对象不可哈希 (list/set/dict 不能作 key),不可变对象可哈希(str/int/tuple/frozenset)
- Python 3.7+ dict 保持插入顺序,
collections提供defaultdict、Counter等实用扩展
从底层原理到实战技巧,一文搞懂 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)
- key 必须唯一
- 对 key 执行哈希运算,得到一个整数(哈希值)
- 用哈希值计算出数组索引
- 索引指向的内存位置,存储着对应的 value
- 查找时,直接用 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" --- 原字符串未变!
核心理解 :对于不可变对象,调用自身任意方法都不会改变它的内容 ------方法总是返回新对象。
str、int、float、tuple、frozenset都是不可变类型;list、dict、set是可变类型。
六、Python set 详解
6.1 set 是什么
set 和 dict 本质相同------都是一组 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 |
八、小结
- 哈希表 = 哈希函数 + 数组,key → 索引 → value,O(1) 查找
- dict = 带 value 的哈希表,空间换时间
- set = 只有 key 的哈希表,天然去重 + 集合运算
- 可变对象不可哈希 (list/set/dict 不能作 key),不可变对象可哈希(str/int/tuple/frozenset)
- Python 3.7+ dict 保持插入顺序,
collections提供defaultdict、Counter等实用扩展