js数据结构4--哈希表

哈希表

认识哈希表

哈希表是基于数组实现的,数组相对的劣势有:

  • 插入和删除效率低;

  • 基于内容查找效率也不高;

  1. 哈希表的优势 :
  • 提供快速的插入-删除-查找操作

  • O(1)的时间级,只需要几个机器指令完成;

  • 速度比树还快,编码更容易。

  1. 哈希表的劣势:
  • 数据是没有顺序的,不能用特定顺序的方式遍历元素;
  • 哈希表的key不能重复

那么哈希表到底是什么呢?它的结构就是数组,它神奇的地方就在于对下标值的一种变换,即哈希函数,通过哈希函数得到HashCode。

探索字符转换成数字的方案

for example :

  • 所有字母幂相乘 -> 空间太大

  • 所有字母对应数字相加 -> 重复太多

answer :

压缩大空间 (巨大的整数范围 -> 可接受的数组范围)

measure :

比如: 0 ~ 199 largeNunber 0 ~ 9 smallNunmber

下标 index = largeNunber% 10;

question :

12,32,122他们的下标都是2,出现了重复;

但是假如在0 ~ 199 中取出5个数字,放进0 ~ 9 的数组中,很不容易重复(但重复的概率很小)

哈希表的一些重要概念

  • 哈希化:将大数字转化成数组范围内下标的过程,我们就称之为哈希化;

  • 哈希函数:将单词转成大数字,再进行哈希化的代码实现放在函数中,这个函数就是哈希函数

  • 哈希表: 最终将数据插入到的数组,对整个结构的封装,就是一个哈希表

如何解决下标值重复的问题?

冲突的解决方法

冲突:两个单词经哈希函数转化的下标相同的情况

解决方法:1. 链地址法;2. 开放地址法;

  1. 链地址法
  • 每个数组单元中存储的不是单个数据,而是一个链条(可以是数组或链表)

  • 查询时,可根据下标找到位置,取出链条,依次查询数据;

  • 数组和链表哪个好?两者效率都差不多,都是线性查找,但是要把新插入的数据放在链条前面时,最好用链表;(数组插入需要把数据后移,操作更复杂)

  1. 开放地址法
  • 寻找空白位置来防止冲突的数据

  • 探索方式: 线性探测、二次探测、再哈希法

线性探测:

  • 插入:空的位置就是合适的位置

  • 查询:查询到空位置就停止,因为不可能跳到空位置去其他的地方;

  • 删除:删除完以后地址上的要使value = -1,若设置为null则无法找到其他下标冲突的数据。

二次探测

如果数据是连续插入的,那么新插入的数据要探测很长的时间,这里就用优化的二次探测。

  • 探测步长线性探测步长为一个常数,而二次探测是可以将要探测步长优化。

  • 比如,下标值从x+ (1,2,3) -> ( 1^2 , 2^2 , 3^2)

  • 但是如果冲突的数据过多,比如12,22,32,112,132那么会造成步长不一的一种聚焦

再哈希化

  • 产生一种依赖关键字的探测序列,把关键字用另一个哈希函数,再做一次哈希化,结果作为步长;

  • 对于特定关键字,虽然再哈希化后的步长不变,但是不同关键字步长不同,减小了聚焦发生。

  • 第二次哈希化的特点:1.输出不能为0(否则探测原地踏步陷入死循环);2.与第一次哈希函数不同。

  • 解决方案 :stepSize = constant(质数) - (key % constant)

哈希化的效率

  • 在没有冲突时,哈希表的效率是非常高的;

  • 如果发生冲突,存取时间就依赖于探测长度;

  • 平均探测长度以及存取时间,取决于填装因子,越大,探测长度越长,效率越低。

什么是填装因子?

  • 填装因子 = 总数据数 / 哈希表长度 ;

  • 开放地址法的填装因子最大等于1;

  • 链地址法可以大于1,甚至无限延伸下去;

优秀的哈希函数

优点在于

  1. 能快速的计算,获得hashCode,所以在函数中要尽量少的乘法和除法
  2. 均匀的分布,能将元素映射在不同的位置;
  • 快速的计算:霍纳法则(秦九韶算法),将时间复杂度从O(N^2)降到O(N)

  • 均匀分布:在使用常量的地方(哈希表的长度和N次幂的底数)使用质数

封装一个哈希函数

    function hashFunc(str,size) {
        var hashCode = 0;
        //霍纳算法,计算hashCode
        for(var i = 0; i < str.length; i++) {
            hashCode = 37 * hashCode + str.charCodeAt(i);
        }
        //取余
        var index = hashCode % size;
        return index;
    }

封装一个哈希类

    //基于数组实现的哈希表类,冲突用链地址法
    function HashTable() {
        this.storage = [];
        this.count = 0; // 数组元素个数
        this.limit = 7; // 数组总长度

             //哈希函数
        HashTable.prototype.hashFunc = function(str,size) {
            var hashCode = 0;
            //霍纳算法,计算hashCode
            for(var i = 0; i < str.length; i++) {
                hashCode = 37 * hashCode + str.charCodeAt(i);
            }
            //取余
            var index = hashCode % size;
            return index;
        }

        //在哈希表中添加&修改数据
        HashTable.prototype.put = function(key,value) {
            //1.根据key获取index
            var index = this.hashFunc(key,this.limit);
            //2.根据索引值取出bucket(篮子)
            var bucket = this.storage[index];
            //3.判断bucket是否为空
            if(bucket == null) {
                bucket = [];
                this.storage[index] = bucket;
            }
            //4.遍历bucket看是否修改数据
            for(var i = 0; i < bucket.length; i++) {
                var tuple = bucket[i];
                if(tuple[0] == key) {
                    tuple[1] = value;
                    return;           
                }
            }
            //5.添加数据
            bucket.push([key,value]);
            this.count ++;

            //6.扩容操作:如果数据过多扩大表内存
            // if(this.count > 0.75 * this.limit) {
            //     this.resize(this.limit * 2);
            // }
            if(this.count > 0.75 * this.limit) {
                var newSize = this.limit * 2;
                var newPrime = this.getPrime(newSize);
                this.resize(newPrime);
            }
        }

        //获取数据
        HashTable.prototype.get = function(key) {
            var index = this.hashFunc(key,this.limit);
            var bucket = this.storage[index];
            if(bucket == null) {return null;}
            for(var i = 0; i < bucket.length; i++) {
                var tuple = bucket[i];
                if(tuple[0] == key) {
                    return tuple[1];
                }
            }
            return null;
        }

        //删除数据
        HashTable.prototype.remove = function(key) {
            var index = this.hashFunc(key,this.limit);
            var bucket = this.storage[index];
            if(bucket == null) {return null;}
            for(var i = 0; i < bucket.length; i++) {
                var tuple = bucket[i];
                if(tuple[0] == key) {
                    bucket.splice(i-1,1);
                    this.count --;
                    return bucket[i];

                     //如果数据较少就需要缩容
                    // if(this.count < this.limit * 0.25) {
                    //     this.resize(Math.floor(this.limit / 2));
                    // }
                    if(this.limit > 7 && count < this.limit * 0.25) {
                        var newSize = Math.floor(this.limit / 2);
                        var newPrime = this.getPrime(newSize);
                        this.resize(newPrime);
                    } 
                }
            }
            return null;
        }

        
        //哈希表的扩容操作
        HashTable.prototype.resize = function(newLimit) {
            //1.保存旧表的内容
            var oldStorage = this.storage;
            //2.重置属性
            this.storage = [];
            this.count = 0;
            this.limit = newLimit; //扩大哈希表内存
            //3 遍历oldStorage的每一个bucket
            for(var i = 0 ;i < oldStorage.length ;i++) {
                //3.1 获取bucket
                var bucket = oldStorage[i];
                //3.2 bucket是否为空
                if(bucket == null) {
                    continue;
                }
                //3.3 将数据重新插入哈希表
                for(var j = 0; j < bucket.length; j++) {
                    var tuple = bucket[j];
                    this.put(tuple[0],tuple[1]);
                }
            }
        }
        
		//判断质数
        HashTable.prototype.isPrime = function(num) {
            var temp = parseInt(Math.sqrt(num));
            for(var i =2; i <= temp; i++) {
                if(num % i == 0) {
                    return false;
                }
            }
            return true;
        }
        
        //向前获取质数
        HashTable.prototype.getPrime = function(num) {
            while(!this.isPrime(num)){
                num ++;
            }
            return num;
        }
    
    }

哈希表的扩容

当数据过多时,为了提高哈希表的效率,我们要对哈希表的内存进行扩容,这种情况下所有的数据都要进行重组(重新调用哈希函数),这是一个耗时的过程,但是却是很有必要的。

  • loadFactor(填装因子)要在0.25 ~ 0.75之间

  • 扩容的方法和思路已经在封装的哈希表中

实现容量恒为质数

我们前面说到要使哈希表的容量为质数,这样哈希表的效率最高,那么怎么使limit恒为质数?

  1. 封装一个判断质数的函数;

  2. 封装一个获取质数的函数;

  3. 使扩容时limit变化的数依然是质数;

具体的方法在封装哈希表中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值