javascript数据结构 -- 哈希表(三)

2,964 阅读5分钟

哈希表

对哈希表有一些基本的了解之后,下面开始着手实现。首先,使用相对简单的链地址法实现此功能。

结构介绍

哈希表中应该具有下面的属性:

  1. storage: 是一个数组,其下标由哈希函数产生
  2. count: 表示哈希表中现有的元素个数
  3. limit: 表示的是哈希表的容量

哈希表中的方法有:

  1. hashFunc: 用来产生哈希值的函数
  2. put: 向storage中新增元素的方法
  3. get: 根据key从哈希表中取值的方法
  4. remove: 根据key从哈希表中删除某个元素
  5. isEmpty: 判断哈希表是否为空
  6. size: 获取哈希表中元素的个数

hash表中存储的数据结构

  • key ---> value
  • 存储的时候: key ---> index ---> insert value
  • 取值的时候: key ---> index ---> get value

桶(bucket)

链地址方法中,使用哈希函数计算并得到的index首次索引哈希表得到的结果叫做桶,下标相同的element放在同一个桶中。

基本实现

type IElement = {
    key: string;
    value: any;
}

class _Hash {
    storage: Array<Array<IElement>> = [];
    count = 0;
    limit = 7; // 容量取质数,但是对于链地址法要求不严格

    // 用来产生哈希值的哈希函数
    hashFunc (str: string, size: number) {
        let hashcode = 0; // 哈希函数计算值
        // 使用霍纳算法计算hashcode
        for (let i = 0; i < str.length; i++) {
            hashcode = hashcode * 37 + str.charCodeAt(i);
        }
        // 下标收紧至数组容量范围内
        const index = hashcode % size;
        return index;
    }

    // 向storage中增加新的元素
    put (key: string, value: any) {
        // 使用哈希函数计算出下标之后收紧
        const index = this.hashFunc(key, this.limit);
        // 使用计算得到的下标值找到storage此下标的数组
        let bucket = this.storage[index];
        // 如果这个下标还未曾有元素插入过,则为undefined,这个时候先初始化一个数组出来,然后将此元素放进去
        if(!bucket){
            bucket = this.storage[index] = [];
        }
        // 对于哈希表来说修改元素和新插入一个元素没有区别的
        const oldItemIndex = bucket.findIndex(
            item => item.key === key
        )
        if(oldItemIndex !== -1){
            bucket[oldItemIndex] = value;
        } else {
            bucket.push({
                key,
                value,
            })
            // 插入新元素之后不要忘记更新长度
            this.count++;
        }
    }

    // 通过key获取storage中的元素
    get (key: string): null | IElement{
        // 使用哈希函数计算下标然后收紧(对于相同的key,哈希函数计算出来的值总是一样的)
        const index = this.hashFunc(key, this.limit);
        // 前几布和插入/修改元素的时候是完全相同的
        const bucket = this.storage[index];
        // 如果bucket是undefined,说明还没有元素在这个位置上,这个时候直接返回一个null
        if(!bucket) return null;
        const target = bucket.find(
            item => item.key === key
        )
        return target || null;
    }

    // 通过key删除某个元素
    remove (key: string): null | IElement {
        let target: null | IElement = null;
        const index = this.hashFunc(key, this.limit);
        const bucket = this.storage[index];
        if(!bucket) return target;
        const targetIndex = bucket.findIndex(
            item => item.key === key
        )
        if(targetIndex !== -1){
            target = bucket.splice(targetIndex, 1)[0];
            // 删除元素之后一定要记得将长度减1
            this.count--;
            return target;
        } else {
            return target;
        }
    }

    // 判断哈希表是否为空
    isEmpty (): boolean {
        return this.count === 0;
    }

    // 返回哈希表中的数目
    size (): number {
        return this.count;
    }
}

哈希表扩容(基本)

loadFactor(负载因子)

  • 显然,使用链地址方法实现的哈希表可以存储无限多的元素,因为每一个bucket中都可以放入无限多的元素;
  • 但是问题在于,操作bucket中的元素使用的是遍历数组的方式,如果一个桶中放入了过多的元素,那么这种哈希表还不如数组,因为数组的缺点它都有,优点也被舍弃了;
  • 必须使用一个标准来衡量一个桶中有过多元素的现象,使用的是loadFactor(负载因子),其定义为:已经存储的元素的数量 / 哈希表中桶的个数;
  • 最理想的情况就是一个桶中放一个元素,此时loadFactor = 1.

扩容时机

一般来说为了保证哈希表的性能,在loadFactor > 0.75之后就需要进行扩容了。这种判断只会出现在新加元素之后,也就是put方法中:

// 插入新元素之后不要忘记更新长度
this.count++;

// 判断是否需要扩容
if(this.count > this.limit * 0.75) this.resize(this.limit * 2);

扩容方法

简单的将容量增加到原来的两倍就可以了,但复杂一点的是将容量增加到原来两倍附近的一个质数上

扩容之后

扩容是一件非常耗能的事情,扩容之后需要遍历哈希表中的每一个元素,然后逐个插入到新的哈希表中去。这是必要的!

扩容实现

// 哈希表改变容量大小
resize (newLimit: number): void {
    // 暂存原始数据(由于遍历的时候不需要用到count和limit所以只需要存thie.storage就可以了)
    const oldStorage = this.storage;
    // 格式化数据
    this.count = 0;
    this.limit = newLimit;
    // 遍历原来哈希表中的每一个元素,逐个插入到新的哈希表中去
    oldStorage.forEach(
        bucket => {
            bucket?.forEach(
                item => {
                    const { key, value } = item;
                    this.put(key, value);
                }
            )
        }
    )
}

哈希表缩容

有扩容就有缩容,缩容的时机为:在每一次删除元素之后,如果哈希表的元素数量小于桶的四分之一,就需要进行缩容以释放不用的空间。

this.count--;
// 判断是否需要缩容(这里保证了一个最小长度)
if(this.limit > 7 && this.count < this.limit * 0.25) this.resize(Math.floor(this.limit / 2));

扩(缩)容优化(使用质数扩容)

上面使用直接将容量扩展为原来两倍的做法扩容,更好的做法是:在两倍容量附近找一个最近的质数,以此质数作为容量

判断是否为质数

// 判断是否为质数
isPrime (limit: number): boolean {
    const _tmp = ~~Math.sqrt(limit);
    for (let index = 2; index < _tmp; index++) {
        if( limit % index === 0 ) return false;
    }
    return true;
}

找到附近的质数

// 找到附近的质数
findPrime (nearby: number): number {
    let prime = nearby;
    while(!this.isPrime(prime)){
        prime++;
    }
    return prime;
}

最终代码

type IElement = {
    key: string;
    value: any;
}

class _Hash {
    storage: Array<Array<IElement>> = [];
    count = 0;
    limit = 7; // 容量取质数,但是对于链地址法要求不严格

    // 用来产生哈希值的哈希函数
    hashFunc (str: string, size: number) {
        let hashcode = 0; // 哈希函数计算值
        // 使用霍纳算法计算hashcode
        for (let i = 0; i < str.length; i++) {
            hashcode = hashcode * 37 + str.charCodeAt(i);
        }
        // 下标收紧至数组容量范围内
        const index = hashcode % size;
        return index;
    }

    // 向storage中增加新的元素
    put (key: string, value: any) {
        // 使用哈希函数计算出下标之后收紧
        const index = this.hashFunc(key, this.limit);
        // 使用计算得到的下标值找到storage此下标的数组
        let bucket = this.storage[index];
        // 如果这个下标还未曾有元素插入过,则为undefined,这个时候先初始化一个数组出来,然后将此元素放进去
        if(!bucket){
            bucket = this.storage[index] = [];
        }
        // 对于哈希表来说修改元素和新插入一个元素没有区别的
        const oldItemIndex = bucket.findIndex(
            item => item.key === key
        )
        if(oldItemIndex !== -1){
            bucket[oldItemIndex] = {key, value};
        } else {
            bucket.push({
                key,
                value,
            })
            // 插入新元素之后不要忘记更新长度
            this.count++;

            // 判断是否需要扩容
            if(this.count > this.limit * 0.75) this.resize(this.limit * 2)
        }
    }

    // 通过key获取storage中的元素
    get (key: string): null | IElement{
        // 使用哈希函数计算下标然后收紧(对于相同的key,哈希函数计算出来的值总是一样的)
        const index = this.hashFunc(key, this.limit);
        // 前几布和插入/修改元素的时候是完全相同的
        const bucket = this.storage[index];
        // 如果bucket是undefined,说明还没有元素在这个位置上,这个时候直接返回一个null
        if(!bucket) return null;
        const target = bucket.find(
            item => item.key === key
        )
        return target || null;
    }

    // 通过key删除某个元素
    remove (key: string): null | IElement {
        let target: null | IElement = null;
        const index = this.hashFunc(key, this.limit);
        const bucket = this.storage[index];
        if(!bucket) return target;
        const targetIndex = bucket.findIndex(
            item => item.key === key
        )
        if(targetIndex !== -1){
            target = bucket.splice(targetIndex, 1)[0];
            // 删除元素之后一定要记得将长度减1
            this.count--;
            // 判断是否需要缩容(这里保证了一个最小长度)
            if(this.limit > 7 && this.count < this.limit * 0.25) this.resize(Math.floor(this.limit / 2));
            return target;
        } else {
            return target;
        }
    }

    // 判断哈希表是否为空
    isEmpty (): boolean {
        return this.count === 0;
    }

    // 返回哈希表中的数目
    size (): number {
        return this.count;
    }

    // 哈希表改变容量大小
    resize (newLimit: number): void {
        // 暂存原始数据(由于遍历的时候不需要用到count和limit所以只需要存thie.storage就可以了)
        const oldStorage = this.storage;
        // 格式化数据
        this.count = 0;
        this.limit = newLimit;
        // 遍历原来哈希表中的每一个元素,逐个插入到新的哈希表中去
        oldStorage.forEach(
            bucket => {
                bucket?.forEach(
                    item => {
                        const { key, value } = item;
                        this.put(key, value);
                    }
                )
            }
        )
    }

    // 判断是否为质数
    isPrime (limit: number): boolean {
        const _tmp = ~~Math.sqrt(limit);
        for (let index = 2; index < _tmp; index++) {
            if( limit % index === 0 ) return false;
        }
        return true;
    }

    // 找到附近的质数
    findPrime (nearby: number): number {
        let prime = nearby;
        while(!this.isPrime(prime)){
            prime++;
        }
        return prime;
    }
}
const h = new _Hash();
h.put('a', 132);
h.put('a', 700);
h.remove('a');
console.log(h.size()); // 0
console.log(h.isEmpty()); // true