我们昨天看了一下哈希表的几种常见的方式,以及如何解决hash
冲突,最后我们看了一下go map
的基本操作,今天我们来写一个demo
,该demo
是完成了一个简单的拉链式hash
。
什么是hash链式存储
哈希冲突
昨天已经讲过了该结构,我们再来看下,为什么有hash
链式存储这个东西,其实hash
本质是我们通过一个叫做哈希函数的东西来计算其存储点。
若多个key
都被计算到同一个点,这个就称之为hash
冲突。
拉链式解决hash冲突
我们解决hash
冲突的方法中,其中有一个就是拉链式存储。所谓拉链式存储,就是将数据使用链表给串起来。
这样在操作的时候,当定位到了下标后,就变为了链表的操作,这样,即解决了存储的问题,也解决了冲突的问题。
代码编写及逻辑
理想效果呈现
如上所述
我们可以将hash map
其抽象为如下结构,该结构中,若遇到hash
冲突,则将其值放入到链表中。
若我们将hash
数组抽象一下,可能会得到如下效果
结构体定义
如上,我们将代码编写为 每一个hash map
我们定义一个maps
maps
又键值个数(count
)、hash map
数组长度 和 包含n
个键值块(Node
)
而每一个键值块(Node
)又包含了一个键名(key
)、一个键值(value
) 以及 指向下一个键值块的指针(nextNode
)
代码如下:
初始化hash map
初始化函数我们获取一个形参i
,我们判断下i
是否小于等于0,因为小于等于0,初始化无意义,所以我们判断若小于等于0,我们就默认给长度为10。
我们申请一个maps
,并且将其count
置为0,length
为我们传入的值,然后再申请length
个Node
指针,最后将maps
给返回回去。
代码如下
定义哈希函数
在hash
存储中,最重要的就是hash
函数,通过hash
函数,我们就可以直接定位到存储点,hash
函数设计得当与否直接影响到hash
冲突的概率。
这里我们简单化了,若传入的key
为int
或则string
时,我们计算其值,并且将对maps.length
取余,其他类型直接存储数组0中,并且返回存储的下标。
新增数据
在hash map
中,key
应当唯一才是,否则会影响hash map
的正确性,所以在拿到要存储的key
和value
时,我们需要判断一下,如果该hash map
中存在该值,则直接修改其值,或则是返回错误,这里定义为若key
冲突,则为直接修改该值。
对于存储的逻辑,我们首先需要获取其数组存储的下标,在获取后,我们需要申请一个键值块(Node) ,然后将key
和value
赋值进该键值块中。
我们在进行存储的时候,相当于插入链表数据,而插入链表,我们又可以区分为3种情况
- 该链表为空
- 该链表只有一个节点
- 该链表大于一个节点
当该链表为空时,我们直接将 键值块(Node) 放入数组中即可
原始链表
插入之后数据
当只有一个链表数据时,我们直接将 键值块(Node) 插入到第一个节点之后即可
原始链表
插入后链表
当链表超过2个节点时,我们将新节点插入第一个节点后,并且将新节点的下一个节点指向第一个节点的下一个节点。
原始节点
插入后节点
我们整理为代码如下:
链表插入完毕后,我们使其count
增加1
获取数据
其中对于获取数据,我们还是需要哈希函数将key
传入,获取其下标,然后就是对链表进行查询了
如果能够查询到该key
,我们直接返回该键值块(Node)的地址,若查询不到,直接返回nil
即可
删除数据
对于删除数据,和获取数据一样,我们需要找到数据,然后再进行链表的删除
对于找到数据,我们编写如下
若找到该数据,我们将其foundok
赋值为true
,并且定义currNode
和 lastNode
来获取当前节点指针和上一个节点指针
在找到数据后,我们又有三种删除逻辑
- 尾节点
- 头结点
- 中间节点
当需要删除的节点是尾节点时
我们直接将上一个节点的指向赋值为nil
即可
当需要删除的节点是头结点时
直接将数组赋值给头结点的下一个节点即可
当需要删除的节点是中间节点时
我们只需要将上一个节点的nextNode
指针指向当前节点的下一个节点即可。
删除前
删除后
修改数据
修改数据如上新增数据逻辑
demo运行效果
我们编写主程序
编译测试
完美,赞
总结
hash表
虽然很简单,但是用的非常多,其主要优点为,查询速度非常的块,哎,昨天看了一下go map
底层实现,感觉非常复杂,我暂时还看不懂,所以先自己写了最简单的hash
实现,争取我这个运维小学生有一天,能够看到go map
实现代码吧。 哦,对了,上述代码我放在了gitee
了 gitee.com/pdudo/golea…
加油,加油。