赞
踩
哈希表的简介
哈希表示根据数组来进行实现的,但相对于数组,它拥有许多优势,同时也有一定的劣势;
优势:
1、它可以提供非常快速的 查找——删除——插入操作
2、实现较为容易(相对于树)
劣势:
1、它是无序的
2、key是不允许重复的
123=1*10**3+2*10**2+3
作用:保证key的唯一性,但同时也占据了内存
那么如何解解决占据内存大的问题呢?
答:给最终答案再规定范围内去进行取余。
而规定范围就是你所要存储内存的范围:比如你最多要存储100个,那么你的规定范围就是0~100。
而得到的结果则是123%100=23;
这时,它的最终结果就会小很多,但届时,又会有其他的问题,比如3取余也是3,1023取余也是23,等等...
也就是说,还是会存在重复的问题;
那么,此时给了其他的解决办法:
1、链地址法
2、开放地址法
链地址法
为了解决哈希表的冲突,链地址法则是将每一个下标值中再创建一个数组或链表,而这个数组或链表中存储的都是下标值相同的值,就如同我们小学时用字典搜索字的那种检索;
开放地址法
此时我们数组上存储的已有1——8——9;
此时我们在想存储个2然后再存储个21;
首先,会将2插入到正确的位置,其次我们的算法得知(21%10=1),它的下标值为1;但此时1已被占用,但开放地址法它会往下顺延,直到找到一个空位置,来存放21的地方,这个方法就叫做线性探测;
例如:21——下标值为1,而1已被占用,那么它就会继续寻找下一个位置,也就是2;但此时的2已被刚才插入的2占用,此时只能再找下一个位置,而3是空的,没有被占用,那此时就将21插入下标识3中;
那如果我在想插入3呢?
答:当然与插入21时差不多啦;
算法就是:index+1;
那这样顺序打乱了,我如何查找我想要的值呢?
当他搜索21时,他会优先找到下标值为1的位置,然后比对值,如果是,就返回值,如果不是,则会按着index+1的算法依次往下找,但只要发现有一个位置为null,则就返回false(表示没有此内容),反之找到了对应的匹配值则就会返回其匹配值;
那我假设:我存的有1,21,31,41,然后我删除的31,然后我又准备查找41的时候,那我检测的过程中,31的位置为null,那我岂不是返回的就是false;
那这就涉及到删除的问题了,当我们删除的时候则用一种软删除,也就是说,我们在删除31的时候,用一个特殊的字符站用这个位置,比如“||”这个字符,当我们检测到这个字符的时候,就不是检测到为nul,就可以继续往下查找。
同时,当我们再次插入51的时候,我们检测顺序是1——21——“||”,此时我们就知道“||”此符号表示占位的意思,我们就顺理成章的将51插入其中,此时的数字排序则是【1——21——51——41】
而这种寻找空位的方法就被称为“线性探测”,而这种找空位放置内容的方式,显然弊端很大,如果这里1,2,3,4,5,都有值了,那么21只能插入到6上,如果这个空位间隔更大呢?那还不如用一个链表呢。
那有没有可以优化的呢?
当然有,那就是我们接下来所说的探索中的二次哈希;
二次哈希
1、二次哈希是对哈希化过后的哈希值再次哈希;
2、二次哈希的算法不可与第一次哈希算法一样;
3、已有一套合理的二次哈希的算法:stepSize=constant-(key%constant)
3.1constant是一个质数,而key就是一次哈希的值
我们这里依旧用21来举例;
21的一次哈希;
(21%10)=1;
21的二次哈希且假设contant===5;
5-(1%5)=4;
填充因子
填充因子=总数据项/哈希表长度
两种哈希法则都可以,但我们选择更加人性化的链地址法,谁叫咱和字典感情深呢,而在实现链地址法的前提下,我们来介绍一下,霍尔法则
霍尔法则
我们假设 a=1,b=2,c=3,d=4;
输入:abcda
设x=10;
那么根据我们上面提到的密乘,最终得到的结果应该是
result=1*x^4+2*x^3+3*x^2+4*x+1=12341
我们可以看到这样是非常繁琐的,而且毫不避讳的说,我是用算计器算出来的,而计算机尤其是面对密乘的时候,计算机远远不如直接的加载乘除来的快捷;那么有什么方式进行优化呢?
废话短说,至于霍尔法则是怎样的,自己应该在网上都可以查询出来,我这里就将它简单的优化一下,实现一下霍尔法则;
result=x(1*x^3+2*x^2+3*x+4)+1
=x(x(1*10^2+2*x+3)+4)+1
=x(x(x(1*x+2)+3)+4)+1
=10(10(10(1*10+2)+3)+4)+1
=10(10(10(12)+3)+4)+1
=10(10(123)+4)+1
=10(1234)+1
=12341
看上去简单许多;那么如何将它华为一个完整的算法呢
由于我们还没有设计哈希表无法找到对应得值,所以我们简单的就用num=12341来分装一个霍尔法则;
通过上面的式子可以总结过程
let result=【第一位数乘以10+第二位数】
result=[result*10+第三位数]
result=[result*10+第四位数]
result=[result*10+第五位数]
.....
直到最后一位数
- function Horner(num: string): Number {
- let x = 10;
- let result = Number(num[0])
- let index = 0
- while (num[index + 1] !== undefined) {
- index = index + 1
- console.log(num[index]);
-
- result = result * x + Number(num[index])
- }
- return result
- }
- let res = Horner(num)
- console.log(res);
设计好了霍尔法则以后,我们就可以去进行功能上的开发了
哈希表的代码实现
-
- //霍尔法则
- function Horner(str: string): number {
- let x = 37;
- let result = str.charCodeAt(0)
- let index = 0
- while (str[index + 1] !== undefined) {
- index = index + 1
- result = result * x + str.charCodeAt(index)
- }
- return result
- }
-
-
- class HashTable {
- storage: any[]
- count: number//用于记录数组中已经存放的元素
- limit: number//用于记录数组中的长度
- constructor() {
- this.storage = []
- this.count = 0
- this.limit = 7
- }
- // 哈希函数
- hashWatch(str: string, ListSize: number) {
- let hashCode = Horner(str);
- // 取存放的下标值
- let index = hashCode % ListSize;
- return index
- }
-
- // 插入与修改操作
- put() {}
- // 获取
- get() {};
- remove() { }
- // 扩容
- dilatation() {}
- // 判断是否为质数
- getPrime() {}
-
- }
-
- let hs = new HashTable()
-
这里面一眼望去,可以看见一个函数和一个类,开头的函数就是最开始设计的霍尔法则,由于我们最终传递的是一个个字母,所以这里我们进行了一个小小的改造,但原理还是未变;
然后类是哈希表的真正载体,里面我们定义了一些变量以及一些方法,而其中有一个【哈希函数】我们是已经定义好了,它的作用就是将霍尔函数最后计算出来的数值通过计算,取出对应元素的存放地址,这里可以清晰的看见,我们使用的是取模。
接下来,我们会探讨具体的方法实现
getPrime方法
- getPrime(num: number) {
- for (let i = 2; i < Math.floor(Math.sqrt(num)); i++) {
- if (num % i === 0) {
- return this.getPrime(num + 1)
- }
- }
-
- return num
- }
得到质数:传入一个数先判断是否是一个质数,如果是,则返回,如果不是则不反回,并且以此+1,直到返回的是一个质数
首先,我们要了解什么是质数,就是除了1与它本身之外,没有任何数的乘积可以得到它,例如:3,5,7,11,17....
为什么要有这一步?
要想知道,必须要了解质数对于计算机的好处
使数列取模分布相对均匀,可以减少冲突
具体可看这片文章:(6条消息) 算法分析:哈希表的大小为何是素数_夜行625的博客-CSDN博客_hash 素数
这里主要检测的是数组中的长度是否是质数,用于扩容;
dilatation方法
- // 扩容
- dilatation(newLimit) {
- this.limit = newLimit;
- //olderStorage用于存放以前的哈希表中的数组
- let olderStorage = this.storage;
- //将哈希表的数组置空,以方便扩容
- this.storage = [];
- //同时存放的数量也需要清空
- this.count = 0;
-
- //这里运用的是双重for循环,将老的哈希表中每一个数组中的元素,依次使用put方法放置于新的哈希表中
- for (let i = 0; i < olderStorage.length; i++) {
- let bucket: { key?: any, value?: { [vaule: string]: any } }[] = olderStorage[i];
- if (!bucket) {
- continue
- }
- for (let j = 0; j < bucket.length; j++) {
- //这里调用的是已定义的put方法
- this.put(bucket[j].key, bucket[i].value)
- }
- }
- return true
- }
这一段作用是将哈希表进行扩容。
为什么扩容?是因为哈希表的长度不够用吗?
这里不是的,按道理来说,我们先将我们创建的哈希表的模式给介绍下,然后在解释为什么需要进行扩容:
我们的哈希表设计成链地址,所以先将数据模型给写出来:
- [
- [{key:string,value:any},{key:string,value:any}],
- [{key:string,value:any},{key:string,value:any}],
- [{key:string,value:any},{key:string,value:any}],
- [{key:string,value:any},{key:string,value:any}]
- ]
从本质上来说,我们的哈希表是一个二维数组,我们的最外面一层的数组长度是由limit来进行限制其长度的,主要是更据对应得【哈希函数】将值插入对应的内部一层的数组,像我们打比方的这个数组,此时的limit=4。而内部一层的数组我们有设置长度吗?显然是没有设置任何的长度。所以从某种角度来说,哈希表可以是无限的。
还是打个比方吧,还是以字典为例:字典26个字母是有限的,但26个字母之下每个字母对用的字数可以是无限的。
那既然是无限的,那为什么还有扩容呢?
我会告诉你是出于性能考虑:
还是用数据来说话吧
假设此时我们limit去5,而拥有的数有[5,10,15,20,6] 这几个数
根据【哈希函数】取对应的下标值
我们可以清晰的看到大部分得数值都被分到index=0下面去了,而其他的基基本上没有,那么相对于其他,我们检索20,与检测6的速度是不是不一样呢?毕竟index=0下面的for循环更久一些。这几个数都是如此,那么更多的数值呢?
那接下来还是这几个数,我们来进行一下扩limit=7;
如此看来是不是检索20起来是不是速度要快上很多,
那什么时候扩容呢?
PUT方法里面介绍
PUT方法
- put(key: string, vaule: { [vaule: string]: any }) {
- let index = this.hashWatch(key, this.limit);
- // 取出对应索引的bucket的一个下标值;
- let bucket: { key?: any, value?: { [vaule: string]: any } }[] = this.storage[index];
- // 先判断对用的bucken是否存在,如果没有,则添加
- if (!bucket) {
- bucket = [];
- this.storage[index] = bucket
- }
- // 判断是否为修改
- for (let i = 0; i < bucket.length; i++) {
- if (bucket[i].key === key) {
- // 判断是修改某一个值还是插入一个新值
- let keys = Object.keys(bucket[i].value);
- keys.forEach(item => {
- for (let key in vaule) {
- if (key === item) {
- console.log(vaule, "vaule");
-
- bucket[i].value[item] = vaule[key]
- }
- bucket[i].value[key] = vaule[key]
- }
- })
-
- }
- return true
- }
- bucket.push({ key, value: vaule })
- this.count += 1;
- if (bucket.length > this.limit * 0.75) {
- let newLimit = this.getPrime(this.limit * 2)
- this.dilatation(newLimit)
- }
- return true
- }
put方法中主要实现的便是哈希表元素的储存与修改;
前面的我就不在介绍了,主要是介绍这一段
- if (bucket.length > this.limit * 0.75) {
- let newLimit = this.getPrime(this.limit * 2)
- this.dilatation(newLimit)
- }
判断什麽时候进行扩容
当所填充的bucket(也就是所填充对应下标值的长度)大于限制长度的0.75倍开始扩容;
remove
- remove(key: string) {
- let index = this.hashWatch(key, this.limit);
- let bucket: { key?: any, value?: { [vaule: string]: any } }[] = this.storage[index];
- if (!bucket) {
- return false
- };
- bucket.forEach((item, index) => {
- if (key === item) {
- delete bucket[index];
- this.count -= 1
- }
- return true
- })
- if (bucket.length < this.limit * 0.45 && this.limit > 7) {
- let newLimt = this.getPrime(this.limit / 2)
- this.dilatation(newLimt)
- }
- return false
- }
这是删除操作,同样的,这里有个减容操作
- if (bucket.length < this.limit * 0.45 && this.limit > 7) {
- let newLimt = this.getPrime(this.limit / 2)
- this.dilatation(newLimt)
- }
但我们这里有个条件,就是应该拥有最低的容器长度;
get
- // 获取
- get(key: string) {
- let index = this.hashWatch(key, this.limit);
- let bucket: { key?: any, value?: { [vaule: string]: any } }[] = this.storage[index];
- if (!bucket) {
- return false
- }
-
-
- for (let i = 0; i < bucket.length; i++) {
- if (bucket[i].key === key) {
- return bucket[i]
- }
- }
-
- return false
- };
获取对应的值;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。