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) 必须是不可变的?

现在,我们就能完美地回答这个问题了。答案直接与哈希表的工作原理挂钩。

核心原因:哈希值必须始终如一。

哈希表的整个体系都建立在一个基本前提上:一个对象的哈希值在它的生命周期内必须是固定不变的。如果哈希值变了,那么通过它计算出的存储位置(桶索引)也就会变,这会导致数据丢失。

让我们来看一个“如果键是可变的”会发生什么灾难:

  1. 假设 Python 允许我们使用列表作为键。我们创建一个列表键:my_key = [1, 2]
  2. 我们用它来存储一个值:my_dict[my_key] = 'value'
    • Python 计算 hash([1, 2]),假设结果是 12345,然后将 ('value') 存入由 12345 决定的桶中。
  3. 过了一会儿,在程序的其他地方,我们修改了这个列表:my_key.append(3)。现在 my_key 变成了 [1, 2, 3]
  4. 灾难发生: 我们现在尝试去获取那个值:print(my_dict[my_key])
    • Python 再次计算 hash(my_key),但此时 my_key[1, 2, 3],它的哈希值会是一个全新的、不同的数字(比如 67890)。
    • Python 会根据这个新哈希值去一个全新的桶里查找,结果自然是“键不存在” (KeyError)。而我们真正的 'value' 还静静地躺在由 hash([1, 2]) 决定的那个旧桶里,我们再也找不回它了。

为了从根本上杜绝这种灾难,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'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰糖心书房

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值