TS哈希表

哈希表的简介

哈希表示根据数组来进行实现的,但相对于数组,它拥有许多优势,同时也有一定的劣势;

优势:

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
    };

获取对应的值;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值