python数据结构与算法-07_哈希表

news2024/11/20 11:43:02

哈希表

不知道你有没有好奇过为什么 Python 里的 dict 和 set 查找速度这么快呢,用了什么黑魔法吗?
经常听别人说哈希表(也叫做散列表),究竟什么是哈希表呢?这一章我们来介绍哈希表,后续章节我们会看到 Python 中的字典和集合是如何实现的。

哈希表的工作过程

前面我们已经讲到了数组和链表,数组能通过下标 O(1) 访问,但是删除一个中间元素却要移动其他元素,时间 O(n)。
循环双端链表倒是可以在知道一个节点的情况下迅速删除它,但是吧查找又成了 O(n)。

难道就没有一种方法可以快速定位和删除元素吗?似乎想要快速找到一个元素除了知道下标之外别无他法,于是乎聪明的计算机科学家又想到了一种方法。
能不能给每个元素一种『逻辑下标』,然后直接找到它呢,哈希表就是这种实现。它通过一个哈希函数来计算一个元素应该放在数组哪个位置,当然对于一个
特定的元素,哈希函数每次计算的下标必须要一样才可以,而且范围不能超过给定的数组长度。

我们还是以书中的例子说明,假如我们有一个数组 T,包含 M=13 个元素,我们可以定义一个简单的哈希函数 h

h(key) = key % M

这里取模运算使得 h(key) 的结果不会超过数组的长度下标。我们来分别插入以下元素:

765, 431, 96, 142, 579, 226, 903, 388

先来计算下它们应用哈希函数后的结果:

M = 13
h(765) = 765 % M = 11
h(431) = 431 % M = 2
h(96) = 96 % M = 5
h(142) = 142 % M = 12
h(579) = 579 % M = 7
h(226) = 226 % M = 5
h(903) = 903 % M = 6
h(388) = 388 % M = 11

下边我画个图演示整个插入过程(纯手工绘制,原谅我字写得不太优雅):

在这里插入图片描述

哈希冲突 (collision)

这里到插入 226 这个元素的时候,不幸地发现 h(226) = h(96) = 5,不同的 key 通过我们的哈希函数计算后得到的下标一样,
这种情况成为哈希冲突。怎么办呢?聪明的计算机科学家又想到了办法,其实一种直观的想法是如果冲突了我能不能让数组中
对应的槽变成一个链式结构呢?这就是其中一种解决方法,叫做 链接法(chaining)。如果我们用链接法来处理冲突,后边的插入是这样的:

在这里插入图片描述

这样就用链表解决了冲突问题,但是如果哈希函数选不好的话,可能就导致冲突太多一个链变得太长,这样查找就不再是 O(1) 的了。
还有一种叫做开放寻址法(open addressing),它的基本思想是当一个槽被占用的时候,采用一种方式来寻找下一个可用的槽。
(这里槽指的是数组中的一个位置),根据找下一个槽的方式不同,分为:

  • 线性探查(linear probing): 当一个槽被占用,找下一个可用的槽。 $ h(k, i) = (h^\prime(k) + i) % m, i = 0,1,…,m-1 $
  • 二次探查(quadratic probing): 当一个槽被占用,以二次方作为偏移量。 $ h(k, i) = (h^\prime(k) + c_1 + c_2i^2) % m , i=0,1,…,m-1 $
  • 双重散列(double hashing): 重新计算 hash 结果。 $ h(k,i) = (h_1(k) + ih_2(k)) % m $

我们选一个简单的二次探查函数 $ h(k, i) = (home + i^2) % m $,它的意思是如果
遇到了冲突,我们就在原始计算的位置不断加上 i 的平方。我写了段代码来模拟整个计算下标的过程:

inserted_index_set = set()
M = 13

def h(key, M=13):
    return key % M

to_insert = [765, 431, 96, 142, 579, 226, 903, 388]
for number in to_insert:
    index = h(number)
    first_index = index
    i = 1
    while index in inserted_index_set:   # 如果计算发现已经占用,继续计算得到下一个可用槽的位置
        print('\th({number}) = {number} % M = {index} collision'.format(number=number, index=index))
        index = (first_index +  i*i) % M   # 根据二次方探查的公式重新计算下一个需要插入的位置
        i += 1
    else:
        print('h({number}) = {number} % M = {index}'.format(number=number, index=index))
        inserted_index_set.add(index)

这段代码输出的结果如下:

h(765) = 765 % M = 11
h(431) = 431 % M = 2
h(96) = 96 % M = 5
h(142) = 142 % M = 12
h(579) = 579 % M = 7
	h(226) = 226 % M = 5 collision
h(226) = 226 % M = 6
	h(903) = 903 % M = 6 collision
	h(903) = 903 % M = 7 collision
h(903) = 903 % M = 10
	h(388) = 388 % M = 11 collision
	h(388) = 388 % M = 12 collision
	h(388) = 388 % M = 2 collision
	h(388) = 388 % M = 7 collision
h(388) = 388 % M = 1

遇到冲突之后会重新计算,每个待插入元素最终的下标就是:

在这里插入图片描述
在这里插入图片描述

Cpython 如何解决哈希冲突

如果你对 cpython 解释器的实现感兴趣,可以参考下这个文件 dictobject.c。
不同 cpython 版本实现的探查方式是不同的,后边我们自己实现 HashTable ADT 的时候会模仿这个探查方式来解决冲突。

The first half of collision resolution is to visit table indices via this
recurrence:

    j = ((5*j) + 1) mod 2**i

For any initial j in range(2**i), repeating that 2**i times generates each
int in range(2**i) exactly once (see any text on random-number generation for
proof).  By itself, this doesn't help much:  like linear probing (setting
j += 1, or j -= 1, on each loop trip), it scans the table entries in a fixed
order.  This would be bad, except that's not the only thing we do, and it's
actually *good* in the common cases where hash keys are consecutive.  In an
example that's really too small to make this entirely clear, for a table of
size 2**3 the order of indices is:

    0 -> 1 -> 6 -> 7 -> 4 -> 5 -> 2 -> 3 -> 0 [and here it's repeating]

哈希函数

到这里你应该明白哈希表插入的工作原理了,不过有个重要的问题之前没提到,就是 hash 函数怎么选?
当然是散列得到的冲突越来越小就好啦,也就是说每个 key 都能尽量被等可能地散列到 m 个槽中的任何一个,并且与其他 key 被散列到哪个槽位无关。
如果你感兴趣,可以阅读后边提到的一些参考资料。视频里我们使用二次探查函数,它相比线性探查得到的结果冲突会更少。

装载因子(load factor)

如果继续往我们的哈希表里塞东西会发生什么?空间不够用。这里我们定义一个负载因子的概念(load factor),其实很简单,就是已经使用的槽数比哈希表大小。
比如我们上边的例子插入了 8 个元素,哈希表总大小是 13, 它的 load factor 就是 $ 8/13 \approx 0.62 $。当我们继续往哈希表插入数据的时候,很快就不够用了。
通常当负载因子开始超过 0.8 的时候,就要新开辟空间并且重新进行散列了。

重哈希(Rehashing)

当负载因子超过 0.8 的时候,需要进行 rehashing 操作了。步骤就是重新开辟一块新的空间,开多大呢?感兴趣的话可以看下 cpython 的 dictobject.c 文件然后搜索
GROWTH_RATE 这个关键字,你会发现不同版本的 cpython 使用了不同的策略。python3.3 的策略是扩大为已经使用的槽数目的两倍。开辟了新空间以后,会把原来哈希表里
不为空槽的数据重新插入到新的哈希表里,插入方式和之前一样。这就是 rehashing 操作。

HashTable ADT

实践是检验真理的唯一标准,这里我们来实现一个简化版的哈希表 ADT,主要是为了让你更好地了解它的工作原理,有了它,后边实现起 dict 和 set 来就小菜一碟了。
这里我们使用到了定长数组,还记得我们在数组和列表章节里实现的 Array 吧,这里要用上了。

解决冲突我们使用二次探查法,模拟 cpython 二次探查函数的实现。我们来实现三个哈希表最常用的基本操作,这实际上也是使用字典的时候最常用的操作。

  • add(key, value)
  • get(key, default)
  • remove(key)
class Slot(object):
    """定义一个 hash 表 数组的槽
    注意,一个槽有三种状态,看你能否想明白
    1.从未使用 HashMap.UNUSED。此槽没有被使用和冲突过,查找时只要找到 UNUSED 就不用再继续探查了
    2.使用过但是 remove 了,此时是 HashMap.EMPTY,该探查点后边的元素扔可能是有key
    3.槽正在使用 Slot 节点
    """
    def __init__(self, key, value):
        self.key, self.value = key, value

class HashTable(object):
    pass

具体的实现和代码编写在视频里讲解。这个代码可不太好实现,稍不留神就会有错,我们还是通过编写单元测试验证代码的正确性。

思考题

  • 请你分析下哈希表插入和删除元素的平均时间复杂度是多少?我们都实现代码了,相信这个问题你可以回答上来
  • Slot 在二次探查法里为什么不能直接删除?为什么我们要给它定义几个状态?

延伸阅读

  • 《Data Structures and Algorithms in Python》11 章 Hash Tables
  • 《算法导论》第三版 11 章散列表,了解几种哈希冲突的解决方式,以及为什么我们选择二次探查而不是线性探查法?
  • 介绍 c 解释器如何实现的 python dict对象:Python dictionary implementation
  • Python hash function implement

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1232905.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

申请注册苹果iOS企业级开发者证书需要公司拥有什么规模条件

在全球范围内,iOS应用市场的规模和影响力不断增长,企业级应用在其中扮演着重要角色。为了在苹果设备上开发和发布内部应用,企业需要获取苹果的iOS企业级开发者证书。文章旨在探讨企业在申请此证书时需要满足的条件、注册流程,以及…

QT 搭建opencv 环境

1. 准备工具CMake 一、CMake介绍 CMake是一个被广泛使用的、开源免费并且完全跨平台的构建工具,可以用简单的语句来描述所有平台的安装(编译过程)。它能够输出各种各样的makefile或者project文件,能测试编译器所支持的C特性,类似UNIX下的aut…

代码随想录 11.20 || 单调栈 LeetCode 503.下一个更大元素Ⅱ、42.接雨水

503.下一个更大元素Ⅱ 本题与 496.下一个更大元素Ⅰ 如出一辙,求在给定一维数组中,每个元素右起第一个更大元素。不同之点在于,本题要求将一维数组视为首尾相连的,最后一个元素的下一个元素是第一个元素。问题的重点在于 如何使用…

正版软件|PhotoPad 照片/图像编辑工具

编辑照片/图像从未如此简单! 关于PhotoPad PhotoPad是本世纪最稳定、最易用且功能最全面的照片编辑器之一。 轻松编辑数码照片和其他图片。支持所有流行的图像格式快速裁剪、旋转、调整大小和翻转照片。 PhotoPad旨在快速打开和编辑照片,轻松无忧&…

Echarts设置环形图中心文字

方式一:title 通过在最外层设置title 配置项 title 同时也是环形图标题的配置项,只是通过left和top强制让其移动到环形图中间位置;提前设置好环形图 title: {// 图形标题(如果想要换行则使用ES6 模板字符串)// 例如: 示例// 这…

ros2工作空间

我们先不管ros2工作空间是什么样子的,如果是我自己来搞一个工作空间,我一定是这样安排 一个文件夹用来放自己存放的文件,。。。。。。。。。。对应src文件夹 一个文件夹用来放编译后的文件,。。。。。。。。。。。对应intall文件…

U4_1:图论之DFS/BFS/TS/Scc

文章目录 一、图的基本概念二、广度优先搜索(BFS)记录伪代码时间复杂度流程应用 三、深度优先搜索(DFS)记录伪代码时间复杂度流程时间戳结构BFS和DFS比较 四、拓扑排序一些概念有向图作用拓扑排序 分析伪代码时间复杂度彩蛋 五、强…

OpenAI一朝领导者山姆·奥尔特曼被解除职务,领导能力不再被认可?

原创 | 文 BFT机器人 在11月17日,OpenAI Inc.宣布CEO山姆奥尔特曼 (Sam Altman) 离职。掌舵人发生重大转变,董事会却在这个时候将公司首席技术官 Mira Murati推举为临时首席执行官,这一操作顿时引起了热议。 这突如其来的变化标志着OpenAI的…

C++实战学习笔记

文章目录 erase()uniquevector的insert()std::string::npos erase() (1)erase(pos,n); 删除从pos开始的n个字符,比如erase(0,1)就是删除第一个字符 (2)erase(position);删除position处的一个字符(position是个string类…

element表格分页+数据过滤筛选

目录 前言效果展示分页效果展示搜索效果展示 代码分析分页功能过滤数据功能 全部代码 前言 在el-element的标签里的tableData数据过多时,会导致表格页面操作卡顿。为解决这一问题,有以下解决方法: 分页加载: 将大量数据进行分页&…

Android File Transfer(安卓文件传输工具)

Android File Transfer 是一款安卓文件传输工,它允许在Mac操作系统和Android设备之间进行文件传输。 该软件通过USB连接将文件从Mac电脑传输到连接的Android设备,或者反过来从Android设备传输文件到Mac电脑。这包括照片、视频、音乐、文档和其他文件类型…

最新企业服务总线ESB的国内主要厂商和开源厂商排名,方案书价格多少

企业服务总线ESB是什么? ESB平台(企业服务总线,Enterprise Service Bus)是一种企业级集成平台,它提供了一种开放的、基于标准的消息机制,通过简单的标准适配器和接口,来完成粗粒度应用&#xff…

Qt程序打包成.exe可执行文件

1.使用Release进行编译 2.找到编译成功的地址: 找到对应的目录 3.把SerialTool.exe文件单独复制到一个文件夹,这里我直接在桌面创建一个SerialTool文件夹,这时候直接运行是不行的,我们需要把库都导进去 4. 在安装目录找到如下这个文件,点击打开,找到你电脑对应的版本即可,我这…

数据仓库:架构之详解Kappa和Lambda

目录 一、前言 二、架构详解 1 Lambda 架构 1.1 Lambda 架构组成 1.2 Lambda 特点 1.3 Lambda 架构的优点 1.4 Lambda 架构的不足 2 Kappa 架构 2.1 Kappa 架构的核心组件 2.2 Kappa 架构优点 2.3 Kappa 架构的注意事项 三、区别对比 四、选择时考虑因素 一、前言 …

录屏软件自动开启录视频,是如何实现的?

工作要留痕,作为职场人的一项必备技能,因此许多人在做一些重要操作的时候,就会提前开启录屏软件,把操作的每一个步骤进行录制,以避免在出现问题的时候进行检查。当每天都需要在固定的时间点重复某项工作的时候&#xf…

【AI实用技巧】GPT写sql统计语句

编写sql的统计语句是一项复杂的任务,特别是涉及多表的情况下。但有了GPT的帮助,一切变得轻松愉快。 AI7号 - 最强人工智能(GPT)中文版https://ai7.pro/s/9v2um 举例说明 有表结构如下: users(user_id, name) bills(…

家具生产ERP有哪几种?如何选择家具生产ERP

不同种类的家具产品有差异化的原材料和制造工艺,每道生产工艺又有不同的管理模式和难点,有些家具生产企业内部还存在各个业务环节信息传递不畅、财务核算不清、不能实时获取库存动态数据等情况。 形成对比的则是部分家具生产企业引入ERP系统完成数字化全…

「C++」虚函数与多态

💻文章目录 📄前言虚函数概念虚函数重写虚函数的协变 重载、覆盖(重写)、隐藏(重定义)的对比 多态多态的概念多态的定义与实现多态的类型多态的构成条件抽象类 多态的原理虚函数表 多继承中的虚函数表 &…

数字化转型背景下,企业如何做好知识管理?

在当今数字化转型的时代,企业面临着日益复杂和快速变化的商业环境。知识管理成为了企业成功的关键之一。有效地管理和利用知识资源可以提升企业的创新能力、决策质量和竞争力。以下我列了一些关键的点,讲讲在数字化转型背景下,企业如何可以做…

21款奔驰GLE350升级香氛负离子车载香薰

香氛负离子系统是由香氛系统和负离子发生器组成的一套配置,也可以单独加装香氛系统或者是负离子发生器,香氛的主要作用就是通过香氛外壳吸收原厂的香水再通过空调管输送到内饰中,而负离子的作用就是安装在空气管中通过释放电离子来打击空气中…