python 字典(Dictionary) vs. 集合(Set):它们是如何做到快速查找的?为什么字典的键(key)必须是不可变的?
字典 (Dictionary) vs. 集合 (Set):基本区别
首先,我们回顾一下它们在功能上的区别:
-
字典 (
dict
): 是一个键值对 (key-value pairs) 的集合。它用于存储有关联关系的数据,可以通过一个唯一的键 (key) 来快速查找、获取、修改或删除对应的值 (value)。- 例子:
phone_book = {'Alice': '123-4567', 'Bob': '987-6543'}
- 核心用途: 建立映射关系。
- 例子:
-
集合 (
set
): 是一个无序且不含重复元素的集合。它主要用于成员测试(检查一个元素是否存在于集合中)以及执行数学上的集合运算(如并集、交集、差集)。- 例子:
unique_tags = {'python', 'data', 'web'}
- 核心用途: 去重和成员测试。
- 例子:
如何做到快速查找:哈希表 (Hash Table) 的魔力
列表和元组通过索引 [i]
来访问元素,而字典和集合能够实现闪电般快速查找的秘密武器,就是它们的底层数据结构——哈希表(也叫哈希映射或散列表)。
假设,你有一本非常厚的、没有按字母排序的字典。如果要找一个单词,你只能从第一页翻到最后一页,这就是列表的查找方式,效率很低(时间复杂度为 O(n))。
而哈希表就像一个智能的图书馆管理员,你想找一本书,他能不假思索地告诉你书在哪一排的哪一个架子上。他是如何做到的呢?
这个过程主要分为三步:
1. 计算哈希值 (Hashing)
- 当你尝试向字典或集合中添加一个元素(对于字典来说是键),Python 会首先调用内置的
hash()
函数来计算这个元素的哈希值。 - 哈希值是一个整数,它由元素的内容唯一确定。关键在于,对于同一个元素,
hash()
函数总是返回相同的整数。print(f"整数 100 的哈希值: {hash(100)}") # 输出: 100 print(f"字符串 'python' 的哈希值: {hash('python')}") # 输出: 一个固定的、很大的整数 print(f"元组 (1, 2) 的哈希值: {hash((1, 2))}") # 输出: 另一个固定的、很大的整数
2. 确定存储位置 (桶/Bucket)
- 哈希表的内部其实是一个类似列表的数组,里面有很多**“桶” (buckets)**,每个桶都有一个编号(索引)。
- Python 使用这个哈希值(通常是对桶的数量取模)来直接计算出元素应该被放入哪个桶中。
bucket_index = hash(key) % number_of_buckets
- 因为这个计算过程非常快,所以无论哈希表有多大,找到正确的桶几乎是瞬时的。
3. 存储和查找
- 存储: 当你执行
my_dict['name'] = 'Alice'
时,Python 计算hash('name')
,得到桶的索引,然后将('name', 'Alice')
这个键值对存入该桶。 - 查找: 当你执行
my_dict['name']
时,Python 重复同样的过程:计算hash('name')
,得到桶的索引,然后直接去那个桶里查找。它完全不需要检查其他任何桶。
这就是为什么字典和集合的查找、插入和删除操作的平均时间复杂度能达到 O(1),即常数时间——操作所需的时间不随容器内元素的数量增加而增加。
关于哈希冲突: 偶尔,两个不同的键可能会计算出相同的桶索引,这被称为“哈希冲突”。Python 有高效的机制来解决这个问题(通常是在同一个桶里用一个类似链表的结构来存储冲突的元素),所以即使发生冲突,性能也依然非常高。
为什么字典的键 (Key) 必须是不可变的?
现在,我们就能完美地回答这个问题了。答案直接与哈希表的工作原理挂钩。
核心原因:哈希值必须始终如一。
哈希表的整个体系都建立在一个基本前提上:一个对象的哈希值在它的生命周期内必须是固定不变的。如果哈希值变了,那么通过它计算出的存储位置(桶索引)也就会变,这会导致数据丢失。
让我们来看一个“如果键是可变的”会发生什么灾难:
- 假设 Python 允许我们使用列表作为键。我们创建一个列表键:
my_key = [1, 2]
。 - 我们用它来存储一个值:
my_dict[my_key] = 'value'
。- Python 计算
hash([1, 2])
,假设结果是12345
,然后将('value')
存入由12345
决定的桶中。
- Python 计算
- 过了一会儿,在程序的其他地方,我们修改了这个列表:
my_key.append(3)
。现在my_key
变成了[1, 2, 3]
。 - 灾难发生: 我们现在尝试去获取那个值:
print(my_dict[my_key])
。- Python 再次计算
hash(my_key)
,但此时my_key
是[1, 2, 3]
,它的哈希值会是一个全新的、不同的数字(比如67890
)。 - Python 会根据这个新哈希值去一个全新的桶里查找,结果自然是“键不存在” (KeyError)。而我们真正的
'value'
还静静地躺在由hash([1, 2])
决定的那个旧桶里,我们再也找不回它了。
- Python 再次计算
为了从根本上杜绝这种灾难,Python 规定:只有那些值永远不会改变的对象,即不可变对象,才能保证其哈希值永远不变。
因此,只有不可变类型 (immutable types) 的对象才能被用作字典的键或集合的元素。
- 合法的键/元素类型: 整数 (
int
), 浮点数 (float
), 字符串 (str
), 元组 (tuple
), 布尔值 (bool
) 等。 - 非法的键/元素类型: 列表 (
list
), 字典 (dict
), 集合 (set
) 等可变类型。
# 合法
valid_dict = {1: 'integer_key','text': 'string_key',(1, 2): 'tuple_key'
}# 尝试用列表做键,会立即报错
try:invalid_dict = {[1, 2]: 'list_key'}
except TypeError as e:print(f"错误: {e}") # 输出: 错误: unhashable type: 'list'