哈喽大家好,我是咸鱼

接触过 Python 的小伙伴应该对【字典】这一数据类型都了解吧

虽然 Python 没有显式名称为“哈希表”的内置数据结构,但是字典是哈希表实现的数据结构

在 Python 中,字典的键(key)被哈希,哈希值决定了键对应的值(value)在字典底层数据存储中的位置

那么今天我们就来看看哈希表的原理以及如何实现一个简易版的 Python 哈希表

ps:文中提到的 Python 指的是 CPyhton 实现

何为哈希表?

哈希表(hash table)通常是基于“键-值对”存储数据的数据结构

哈希表的键(key)通过哈希函数转换为哈希值(hash value),这个哈希值决定了数据在数组中的位置。这种设计使得数据检索变得非常快

举个例子,下面有一组键值对数据,其中歌手姓名是 key,歌名是 value

+------------------------------+
| Key | Value |
+------------------------------+
| Kanye | Come to life |
| XXXtentacion | Moonlight |
| J.cole | All My Life |
| Lil wanye | Mona Lisa |
| Juice WRLD | Come & Go |
+------------------------------+

如果我们想要将这些键值对存储在哈希表中,首先需要将键的值转换成哈希表的数组的索引,这时候就需要用到哈希函数了

哈希函数是哈希表实现的主要关键,它能够处理键然后返回存放数据的哈希表中对应的索引

一个好的哈希函数能够在数组中均匀地分布键,尽量避免哈希冲突(两个键返回了相同的索引)

哈希函数是如何处理键的,这里我们创建一个简易的哈希函数来模拟一下(实际上哈希函数要比这复杂得多)

def simple_hash(key, size):
return ord(key[0]) % size

这个简易版哈希函数将歌手名(即 key)首字母的 ASCII 值与哈希表大小取余,得出来的值就是歌名(value)在哈希表中的索引

那这个简易版哈希函数有什么问题呢?聪明的你一眼就看出来了:容易出现碰撞。因为不同的键的首字母有可能是一样的,就意味着返回的索引也是一样的

例如我们假设哈希表的大小为 10 ,我们以上面的歌手名作为键然后执行 simple_hash(key, 10) 得到索引

可以看到,由于Juice WRLDJ.cole 的首字母都一样,哈希函数返回了相同的索引,这里就发生了哈希碰撞

虽然几乎不可能完全避免任何大量数据的碰撞,但一个好的哈希函数加上一个适当大小的哈希表将减少碰撞的机会

当出现哈希碰撞时,可以使用不同的方法(例如开放寻址法)来解决碰撞

应该设计健壮的哈希函数来尽量避免哈希碰撞

我们再来看其他的键,Kanye 通过 simple_hash() 函数返回 index 5,这意味着我们可以在索引 5 (哈希表的第六个元素)上找到 其键 Kanye 和值Come to life

哈希表优点

在哈希表中,是根据哈希值(即索引)来寻找数据,所以可以快速定位到数据在哈希表中的位置,使得检索、插入和删除操作具有常数时间复杂度 O(1) 的性能

与其他数据结构相比,哈希表因其效率而脱颖而出

不但如此,哈希表可以存储不同类型的键值对,还可以动态调整自身大小

Python 中的哈希表实现

在 Python 中有一个内置的数据结构,它实现了哈希表的功能,称为字典

Python 字典(dictionary,dict)是一种无序的、可变的集合(collections),它的元素以 “键值对(key-value)”的形式存储

字典中的 key 是唯一且不可变的,这意味着它们一旦设置就无法更改

my_dict = {"Kanye": "Come to life", "XXXtentacion": "Moonlight", "J.cole": "All My Life"}

在底层,Python 的字典以哈希表的形式运行,当我们创建字典并添加键值对时,Python 会将哈希函数作用于键,从而生成哈希值,接着哈希值决定对应的值将存储在内存的哪个位置中

所以当你想要检索值时,Python 就会对键进行哈希,从而快速引导 Python 找到值的存储位置,而无需考虑字典的大小

my_dict = {}
my_dict["Kanye"] = "Come to life" # 哈希函数决定了 Come to life" 在内存中的位置
print(my_dict["Alice"]) # "Come to life"

可以看到,我们通过方括号[key]来访问键对应的值,如果键不存在,则会报错

print(my_dict["Kanye"])  # "Come to life" 

# Raises KeyError: "Drake"
print(my_dict["Drake"])

为了避免该报错,我们可以使用字典内置的 get() 方法,如果键不存在则返回默认值

print(my_dict.get('Drake', "Unknown")) # Unknown

在 python 中实现哈希表

首先我们定义一个 HashTable 类,表示一个哈希表数据结构

class HashTable:
def __init__(self, size):
self.size = size
self.table = [None]*size def _hash(self, key):
return ord(key[0]) % self.size

在构造函数 __init__() 中:

  • size 表示哈希表的大小
  • table是一个长度为 size 的数组,被用作哈希表的存储结构。初始化时,数组的所有元素都被设为 None,表示哈希表初始时不含任何数据

在内部函数 _hash() 中,用于计算给定 key 的哈希值。它采用给定键 key 的第一个字符的 ASCII 值,并使用取余运算 % 将其映射到哈希表的索引范围内,以便确定键在哈希表中的存储位置。

然后我们接着在 HashTable 类中添加对键值对的增删查方法

class HashTable:
def __init__(self, size):
self.size = size
self.table = [None]*size def _hash(self, key):
return ord(key[0]) % self.size def set(self, key, value):
hash_index = self._hash(key)
self.table[hash_index] = (key, value) def get(self, key):
hash_index = self._hash(key)
if self.table[hash_index] is not None:
return self.table[hash_index][1] raise KeyError(f'Key {key} not found') def remove(self, key):
hash_index = self._hash(key)
if self.table[hash_index] is not None:
self.table[hash_index] = None
else:
raise KeyError(f'Key {key} not found')

其中,set() 方法将键值对添加到表中,而 get() 该方法则通过其键检索值。该 remove() 方法从哈希表中删除键值对

现在,我们可以创建一个哈希表并使用它来存储和检索数据:

# 创建哈希表
hash_table = HashTable(10) # 添加键值对
hash_table.set('Kanye', 'Come to life')
hash_table.set('XXXtentacion', 'Moonlight') # 获取值
print(hash_table.get('XXXtentacion')) # Outputs: 'Moonlight' # 删除键值对
hash_table.remove('XXXtentacion') # 报错: KeyError: 'Key XXXtentacion not found'
print(hash_table.get('XXXtentacion'))

前面我们提到过,哈希碰撞是使用哈希表时不可避免的一部分,既然 Python 字典是哈希表的实现,所以也需要相应的方法来处理哈希碰撞

在 Python 的哈希表实现中,为了避免哈希冲突,通常会使用开放寻址法的变体之一,称为“线性探测”(Linear Probing)

当在字典中发生哈希冲突时,Python 会使用线性探测,即从哈希冲突的位置开始,依次往后查找下一个可用的插槽(空槽),直到找到一个空的插槽来存储要插入的键值对。

这种方法简单直接,可以减少哈希冲突的次数。但是,它可能会导致“聚集”(Clustering)问题,即一旦哈希表中形成了一片连续的已被占用的位置,新元素可能会被迫放入这片区域,导致哈希表性能下降

为了缓解聚集问题,假若当哈希表中存放的键值对超过哈希表长度的三分之二时(即装载率超过66%时),哈希表会自动扩容

最后总结一下:

  • 在哈希表中,是根据哈希值(即索引)来寻找数据,所以可以快速定位到数据在哈希表中的位置
  • Python 的字典以哈希表的形式运行,当我们创建字典并添加键值对时,Python 会将哈希函数作用于键,从而生成哈希值,接着哈希值决定对应的值将存储在内存的哪个位置中
  • Python 通常会使用线性探测法来解决哈希冲突问题

Python 哈希表的实现——字典的更多相关文章

  1. 04.python哈希表

    python哈希表 集合Set 集合,简称集.由任意个元素构成的集体.高级语言都实现了这个非常重要的数据结构类型. Python中,它是可变的.无序的.不重复的元素的集合. 初始化 set() -&g ...

  2. 索引器、哈希表Hashtabl、字典Dictionary(转)

    一.索引器 索引器类似于属性,不同之处在于它们的get访问器采用参数.要声明类或结构上的索引器,使用this关键字. 示例:   索引器示例代码 /// <summary> /// 存储星 ...

  3. Python哈希表的例子:dict、set

    dict(字典) Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度. 和list比较,dic ...

  4. Python哈希表和解析式

    目录 1. 封装和解构 1.1 封装 1.2 解构 2. 集合Set 2.1 初始化 2.2 增加 2.3 删除 2.4 遍历 2.5 并集&交集&差集&对称差集 3.字典 3 ...

  5. Python 中的哈希表

    Python 中的哈希表:对字典的理解   有没有想过,Python中的字典为什么这么高效稳定.原因是他是建立在hash表上.了解Python中的hash表有助于更好的理解Python,因为Pytho ...

  6. 数据结构图解(递归,二分,AVL,红黑树,伸展树,哈希表,字典树,B树,B+树)

    递归反转 二分查找 AVL树 AVL简单的理解,如图所示,底部节点为1,不断往上到根节点,数字不断累加. 观察每个节点数字,随意选个节点A,会发现A节点的左子树节点或右子树节点末尾,数到A节点距离之差 ...

  7. [PHP内核探索]PHP中的哈希表

    在PHP内核中,其中一个很重要的数据结构就是HashTable.我们常用的数组,在内核中就是用HashTable来实现.那么,PHP的HashTable是怎么实现的呢?最近在看HashTable的数据 ...

  8. 使用python实现哈希表、字典、集合

    哈希表 哈希表(Hash Table, 又称为散列表),是一种线性表的存储结构.哈希表由一个直接寻址表和一个哈希函数组成.哈希函数h(k)将元素关键字k作为自变量,返回元素的存储下标. 简单哈希函数: ...

  9. 自己动手实现 HashMap(Python字典),彻底系统的学习哈希表(上篇)——不看血亏!!!

    HashMap(Python字典)设计原理与实现(上篇)--哈希表的原理 在此前的四篇长文当中我们已经实现了我们自己的ArrayList和LinkedList,并且分析了ArrayList和Linke ...

  10. Python 字典和集合基于哈希表实现

    哈希表作为基础数据结构我不多说,有兴趣的可以百度,或者等我出一篇博客来细谈哈希表.我这里就简单讲讲:哈希表不过就是一个定长数组,元素找位置,遇到哈希冲突则利用 hash 算法解决找另一个位置,如果数组 ...

随机推荐

  1. java位运算及移位运算你还记得吗

    本文中所提到的运算都是基于整数来说的,因为只有整数(包括正数和负数)在操作系统中是以二进制的补码形式运算的,关于原码.反码.补码.位运算.移位运算的背景这里不再介绍,网上资料很多,感兴趣的可自行搜索. ...

  2. 配置DHCP

    配置DHCP 条件:关闭防火墙 和selinux 1,安装dhcp服务 [root@localhost ~]#yum install dhcp -y#安装dhcp服务 2,查看配置文件 [root@l ...

  3. quarkus依赖注入之七:生命周期回调

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本篇的知识点是bean的生命周期回调:在be ...

  4. [golang]获取本机IP

    前言 方便在内网环境中获取服务器本机IP,省了在脚本中过滤ip或ifconfig的结果. 如果内网中有nginx的话,通过nginx获取本机IP也很方便,可参考 借助nginx自动获取本机IP 示例代 ...

  5. 基于consul实现docker跨主机网络通信

    前言 IP: 192.168.0.10 192.168.0.11 系统版本:ubuntu 20.04 consul版本:1.11.1 官网下载地址: https://www.consul.io/dow ...

  6. Thinkphp 5.x 远程代码执行漏洞利用小记

    Thinkphp 5.x远程代码执行漏洞存在于Thinkphp 5.0版本及5.1版本,通过此漏洞,可以在远程执行任意代码,也可写入webshell 下面是对其进行的漏洞利用! 漏洞利用: 1,利用s ...

  7. Jni GetMethodID中函数标识sig的详细解释

    在 JNI(Java Native Interface)中,GetMethodID 函数用于获取 Java 类的方法的标识符.这个函数的详细解释如下: cCopy code jmethodID Get ...

  8. do_fork(一)

    fork 是linux创建进程的系统调用,相关的函数(不只是系统调用)还有 vfork,clone,sys_frok等.这些函数会整理不同参数,再调用到 do_fork 中. 本篇文章主要介绍do_f ...

  9. Jmeter插件PerfMon Metrics Collector安装使用及报错解决

    Jmeter作为一个轻量级的性能测试工具,开源.小巧.灵活的特性使其越来越受到测试人员喜爱.在实际的 项目中,特别是跨地区项目,排除合作方指定要求使用Loadrunner作为性能测试工具外,Jmete ...

  10. skynet的timer似乎有问题

    skynet.timeout 传进去 number 范围内的数值但是会溢出, 调查发现 skynet.timeout 调用的是 c 的方法: c.intcommand("TIMEOUT&qu ...