当前位置: 首页 > news >正文

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'
http://www.dtcms.com/a/277758.html

相关文章:

  • S7-1200 与 ET200SP:PROFINET 设备关键数据 IP 地址、MAC 地址及 MRP 环状态获取
  • Datawhale AI 夏令营2025科大讯飞AI大赛<夏令营:用AI做带货视频评论分析>
  • 什么是渐进式框架
  • OpenVela 之 UI 应用开发
  • kettle从入门到精通 第102课 ETL之kettle xxl-job调度kettle的两种方式
  • 【Linux系统】进程状态 | 进程优先级
  • 手写std::optional:告别空指针的痛苦
  • java + groovy : 动态解析groovy脚本,并与java交互
  • MacBook Air M4 安装 VMware Fusion Pro
  • 问题记录:Fastjson序列化-空值字段处理
  • CA复习功课
  • Appdynamic 配置 PostgreSQL 收集器
  • 复习笔记 34
  • 【VSCode+LaTeX】科研写作环境搭建
  • 内存池(C++)v3 | 简历写法 | 相关面试题
  • 浏览器本地存储——使用localStorage实现电商系统商品收藏功能实战
  • 在网站学装机
  • SCTP协议网络编程
  • 从源码看Nginx:Nginx事件驱动架构深度拆解来了
  • linux上的软挂载操作方法
  • Docker 快速上手
  • 【elementUI踩坑记录】解决 el-table 固定列 el-table__fixed 导致部分滚动条无法拖动的问题
  • 全星质量管理QMS软件系统——汽车零部件制造业数字化转型的质量管理中枢
  • 【设计模式】备忘录模式(标记(Token)模式)
  • 设计模式:软件开发的高效解决方案(单例、工厂、适配器、代理)
  • 从 Intel MacBook 迁移到 ARM MacBook 的完整指南
  • Cursor的使用
  • Pandas 中 stack 和 unstack 方法在数据重塑中的应用
  • 日记-生活随想
  • 信号量机制,互斥的避免自旋锁的实现方法(操作系统)