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

哈希表

不知道你有没有好奇过为什么 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第28期 | GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区,集成了生成预训练Transformer(GPT)、人工智能生成内容(AIGC)以及大型语言模型(LLM)等安全领域应用的知识。在这里,您可以…

什么是深度学习

一、深度学习的发展历程 1.1 Turing Testing (图灵测试) 图灵测试是人工智能是否真正能够成功的一个标准,“计算机科学之父”、“人工智能之父”英国数学家图灵在1950年的论文《机器会思考吗》中提出了图灵测试的概念。即把一个人和一台计算机分别放在两个隔离的房…

idea Maven Helper插件使用方法

idea Maven Helper插件使用方法 文章目录 idea Maven Helper插件使用方法📆1.安装mavenhelper🖥️2.使用教程📌3.解决冲突📇4.列表展示依赖🧣5.tree展示依赖🖥️6.搜索依赖🖊️7.最后总结 &…

Mac使用unrar和rar解压文件

WinRAR archiver, a powerful tool to process RAR and ZIP files 下载 tar -xzvf rarmacos-x64-624.tar.gz cd rar # 安装rar命令 sudo install -c -o $USER rar /usr/local/bin/ # 安装unrar命令 sudo install -c -o $USER unrar /usr/local/bin/ 压缩rar a test.rar readme…

PPT幻灯片里的图片,批量提取

之前分享过如何将PPT文件导出成图片,今天继续分享PPT技巧,如何提取出PPT文件里面的图片。 首先,我们将PPT文件的后缀名,修改为rar,将文件改为压缩包文件 然后我们将压缩包文件进行解压 最好是以文件夹的形式解压出来…

脱离form表单校验input(校验单个input输入框)提交时边框变红

把需要自定义校验的数据放在一个对象中&#xff0c;方便以后多个字段校验 customVerifyInps:{communityInp2:"",asPathInp:"",}, 在输入框中绑定id <el-inputid"communityInp2"placeholder""v-model"customVerifyInps.commu…

API给电商带来了哪些变化?

随着数字化和网络化程度的不断加深&#xff0c;电商行业在过去的几年里经历了翻天覆地的变化。其中&#xff0c;API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&#xff09;在电商领域的应用&#xff0c;不仅改变了电商行业的运作方式&#xf…

5-Nacos环境搭建

本文介绍nacos集群环境的搭建。 1、基础环境 机器&#xff1a;mac&#xff0c;intel版本jdk&#xff1a;1.8数据库&#xff1a;mysql 8.029nacos&#xff1a;2.03 2、下载 nacos点击这里下载。 3、开始配置 这里搭建在自己机器上搭建两台nacos集群。下载完成后&#xff0…

windows 查看防火墙设置命令使用方法

点击键盘上windows键&#xff0c;输入cmd&#xff0c;选择以管理员身份运行 输入下面命令查看使用说明 netsh advfirewall firewall add rule ? 发现显示不全&#xff0c;不方便看 可以输入下面命令&#xff0c;生成文件&#xff0c;方便查看 netsh advfirewall firewall ad…

浙大恩特客户资源管理系统fileupload.jsp,machord_doc.jsp接口任意文件上传漏洞复现 [附POC]

文章目录 浙大恩特客户资源管理系统fileupload.jsp,machord_doc.jsp接口任意文件上传漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 浙大恩特客户资源管理系统fileupload.jsp,machord_doc.jsp接…

Shell脚本:Linux Shell脚本学习指南(第二部分Shell编程)一

第二部分&#xff1a;Shell编程&#xff08;一&#xff09; 这一章我们正式进入 Shell 脚本编程&#xff0c;重点讲解变量、字符串、数组、数学计算、选择结构、循环结构和函数。 Shell 的编程思想虽然和 C、Java、Python、C# 等其它编程语言类似&#xff0c;但是在语法细节方…

白炽灯护眼还是LED护眼?眼科专家都推荐的护眼台灯分享

白炽灯和LED灯相比&#xff0c;我认为还是LED灯会更护眼一些。因为LED灯长时间照射&#xff0c;温度也不会变得很高&#xff0c;这就说明了LED灯的散热效果好&#xff0c;安全性高&#xff0c;而且光线散发会比较均匀。 白炽灯是通过发热发光的&#xff0c;大部分能量都转化为了…

NX二次开发UF_CAM_ask_lower_limit_plane_usage 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;里海NX二次开发3000例专栏 UF_CAM_ask_lower_limit_plane_usage Defined in: uf_cam_planes.h int UF_CAM_ask_lower_limit_plane_usage(tag_t object_tag, UF_PARAM_lwplane_usage_t * usage ) overview 概述 Query the usa…

【2021集创赛】IEEE杯一等奖:一种28GHz高能效Outphasing PA设计

本作品参与极术社区组织的有奖征集|秀出你的集创赛作品风采,免费电子产品等你拿~活动。 团队介绍 参赛单位&#xff1a;电子科技大学 队伍名称&#xff1a;PA调得队 指导老师&#xff1a;王政 参赛队员&#xff1a;倪梦虎、杨茂旋、张振翼 总决赛奖项&#xff1a;一等奖 1.项…

ADE XL 工艺角corner仿真

在ADE L界面打开ADE XL 建立一个新的ADE XL 点击click to add corner 添加工艺角 点击图标添加三个工艺角 点击model files里面的click to add 添加model 文件。点击import from tests&#xff0c;点击ok 填好红框内容&#xff0c;点击ok 可以看到添加好的工艺角&#xff0c;双…

滚雪球学Java(09-6):Java中的条件运算符,你真的掌握了吗?

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

亿图图示9.4——办公人士的瑞士刀

今天博主将带来一款强悍的制图软件——亿图图示。相信接触过万兴喵影的同学们&#xff0c;对万兴科技都不陌生。今天学长带来的亿图图示&#xff0c;也是其旗下产品哦。亿图图示&#xff0c;即亿图图示专家(EDraw Max)&#xff0c;是一款基于矢量的绘图工具&#xff0c;包含大量…

Linux安装rabbitMq(亲测可用)解决只能本地访问的问题

安装er https://blog.csdn.net/laterstage/article/details/131513793?spm1001.2014.3001.5501下载mq wget --content-disposition "https://packagecloud.io/rabbitmq/rabbitmq-server/packages/el/7/rabbitmq-server-3.10.0-1.el7.noarch.rpm/download.rpm?distro_v…

从字典到 CookieJar 的转换技巧

在使用requests库进行HTTP请求时&#xff0c;经常需要传递cookies参数来实现一些特定的功能&#xff0c;例如保持用户会话状态或者进行身份验证。 在HTTP请求中&#xff0c;Cookie是一种用来在客户端和服务器之间传递状态信息的方式&#xff0c;通常用于记录用户的身份验证信息…

智慧城市科普:最近很火的概念“智慧城市 ”到底是啥?

在当今飞速发展的数字时代&#xff0c;智慧城市的兴起成为城市管理与科技创新的焦点。本文将深入科学原理和技术细节&#xff0c;揭示智慧城市的奥秘&#xff0c;以及它对城市未来发展的深远影响。 1. 智慧城市的概念&#xff1a; 智慧城市并非抽象的未来愿景&#xff0c;而是…