数据结构与算法--哈希表--JS

数据结构与算法–哈希表–JS

字典主要特点:

一一对应;key不可以重复;key是无序的

哈希表,通常是基于数组实现的,但是相对于数组,有很多优势:

可以快速插入、删除、查找,O(1)的时间级,实际上,只需要几个机器指令即可;
哈希表的速度比树还要快,基本可以瞬间查找到想要的元素;
哈希表相对于树来说编码要容易很多

哈希表相对于数组的一些不足:

数据是没有顺序的,所以不能以一种固定的方式(从小到大)来遍历其中的元素
key是不允许重复的,不能放置相同的key用于保存不同的元素。

哈希表的结构就是数组,但是对下标进行变换,这种变换叫做哈希函数,通过哈希函数可以获取到HashCode。
哈希化:将大数字转化为数组范围内下标的过程称为哈希化。
哈希函数:将单词转为大数字,大数字再进行哈希化的代码实现放在一个函数内,这个函数叫哈希函数
哈希表:最终将数据插入到的这个数组,对整个结构的封装,称之为哈希表
哈希表里面一旦哈希化时下标值产生了冲突,一般采用的方案:

链地址法(拉链法);开放地址法。

链地址法是每个数组单元中存储的不再是单个数据,而是一个链条,这个链条常用的数据结构是数组或者链表。

用数组或者链表的效率差不多,都是需要线性查找。假如数组每个单元内新加入的数据要放在前面的话,使用链表,效率更高一些。

开放地址法主要通过寻找空白的单元格来添加重复的数据。
在寻找空白单元格的时候有三种方法:

线性探测;二次探测;再哈希法

线性探测:线性地查找空白单元格,一旦遇到空的位置就停止,这样的话,如果删除一个数据项时,不可以将这个位置的下标的内容设置为null,因为会影响之后的查询操作,所以如果删除的话,将该位置进行特殊化处理(例如设置为-1),线性探测有个严重的问题,就是聚集。

如果在没有任何数据的时候,插入的是22 23 24 25,那么意味着下标值为 2 3 4 5的位置都有元素,这种一连串填充单元就叫做聚集
聚集会影响哈希表的性能,无论是插入、查询、删除。
解决这个问题,用二次探测

二次探测主要优化的是探测的步长,线性探测可以看做步长为1的探测,x+1,x+2,x+3依次探测

二测探测,对步长做了优化,比如从下标值x开始,x+(1的平方)、x+(2的平方)、x+(3的平方),一次性探测比较长的距离,避免聚集带来的影响。
二次探测的问题:加入再加入32 112 82 2 192,会造成步长不一的一种聚集,影响效率,但这种情况比连续数字的可能性小,
怎么解决每个的步长不一样,使用再哈希法
产生一种依赖关键字的探测序列,而不是每个关键字都一样。不同关键字即使映射到相同的数组下标,也可以使用不同的探测序列。

再哈希法的做法是:把关键字用另一个哈希函数再做一次哈希化,用这次哈希化的结果作为步长。

对于指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。
第二次哈希化需要具备的特点:1.和第一个哈希函数不同;2.不能输出为0
哈希函数为stepSize = constant - (key % constant),其中constant是质数且小于数组的容量。

哈希表中执行插入和搜索操作效率是非常高的,如果没有冲突,那么效率就会更高。发生冲突,存取时间就依赖后来的探测长度。
平均探测长度以及平均存取时间,取决于填装因子,随着填装因子变大,探测长度也越长。

填装因子:表示当前哈希表中包含的数据项和整个哈希表长度的比值。填装因子=总数据项/哈希表长度。

开放地址法的填装因子为1;链地址法的填装因子可以大于1。

效率

二次探测和在哈希探测的性能比线性探测略好,都是指数方式进行增长;链地址法是比较平缓的,成线性增长。
随着填装因子变大,效率下降的情况在不同开放地址法方案中比链地址法更严重。在开发中,使用链地址法的情况更多一些。

哈希表的主要优点就是速度

好的哈希表应该具备两个优点:快速的计算;均匀的分布

快速的计算:调高速度的一个办法就是让哈希函数中尽量少的有乘法和除法,因为乘除的效率是比较低的。
均匀的分布:无论是链地址法还是开发地址法,当多个元素映射到同一个位置的时候,都会影响效率,所以需要让元素在哈希表中均匀的分布。

快递计算:霍纳法则/秦九昭算法,时间复杂度O(n)
均匀分布:在使用常量的地方,尽可能使用质数。比如哈希表的长度,n次幂的底数。

Java中的哈希表采用的是链地址法,HashMap为了提高效率,采用的是位运算方式,index=HashCode(Key)&(Length - 1),相对于取模运算来说性能是高的。

//设计哈希函数
//1.将字符串(第一个参数)转成为比较大的数字hashCode
//2.将大的数字hashCode压缩到数组范围(大小,第二个参数)之内
function hashFunc(str, size) {
    //定义hashCode变量
    var HashCode = 0;
    //霍纳算法,计算hashCode的值
    //charCodeAt是把字符转为Unicode编码
    for (var i = 0; i < str.length; i++) {
        HashCode = 37 * HashCode + str.charCodeAt(i); //37是经常使用的质数
    }
    //取余操作
    var index = HashCode % size;
    return index;
}

//测试哈希函数
alert(hashFunc('abc', 7));
//判断一个数是否是质数
//一个数若可以因数分解,这两个数一定是一个小于sqrt(n),一个大于等于sqrt(n)
function isPrime(num) {
    //获取平方根
    var temp = parseInt(Math.sqrt(num));

    for (var i = 2; i <= temp; i++) {
        if (num % i == 0) {
            return false;
        }
    }
    return true;
}

//封装哈希表类
//链地址法
function HashTable() {
    //属性
    this.storage = []; //存放所有元素的数组
    this.count = 0; //当前数组的元素个数,需要用它来求填装因子loadFactor,
    //loadFactor<0.25时,进行缩容;loadFactor>0.75时需要时,再插入元素时性能变低,对数组进行扩容,
    this.limit = 7; //哈希表数组当前的总长度
    //方法
    //哈希函数
    HashTable.prototype.hashFunc =  function (str, size) {
        //定义hashCode变量
        var HashCode = 0;
        //霍纳算法,计算hashCode的值
        //charCodeAt是把字符转为Unicode编码
        for (var i = 0; i < str.length; i++) {
            HashCode = 37 * HashCode + str.charCodeAt(i); //37是经常使用的质数
        }
        //取余操作
        var index = HashCode % size;
        return index;
    }
    //插入和修改操作
    HashTable.prototype.put = function (key, value) {
        //根据key获取对应的index
        //一个index可能对应很多个key,所以会有链式
        var index = this.hashFunc(key, this.limit);
        //根据index取出对应的桶bucket
        var bucket = this.storage[index];
        //判断该bucket是否为null
        if (bucket == null) {
            bucket = [];
            this.storage[index] = bucket;
        }
        //判断是否是修改数据
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i];
            if (tuple[0] == key) {
                tuple[1] = value;
                return;
            }
        }
        //不是修改数据,就是添加操作
        bucket.push([key, value]);
        this.count += 1;
        //判断是否需要扩容操作(在下面)
        if (this.count > this.limit * 0.75) {
            var newSize = this.limit * 2; //长度乘以2
            var newPrime = this.getPrime(newSize); //扩展的长度需要是个质数,因为总容量为质数更有利于数据在哈希表中均匀的分布
            this.resize(newSize);
        }
    }
    //获取操作
    HashTable.prototype.get = function (key) {
        //根据key获取对应的index
        var index = this.hashFunc(key, this.limit);
        //根据index获取对应的bucket
        var bucket = this.storage[index];
        //判断bucket是否为null
        if (bucket == null) {
            return null;
        }
        //有bucket,那么进行线性查找
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i];
            if (tuple[0] == key) {
                return tuple[1];
            }
        }
        //依然没有找到,那么返回null
        return null;
    }
    //删除操作
    HashTable.prototype.remove = function (key) {
        //根据key获取对应的index
        var index = this.hashFunc(key, this.limit);
        //根据index获取对应的bucket
        var bucket = this.storage[index];
        //判断bucket是否为null
        if (bucket == null) return null;
        //有bucket,那么进行线性查找并删除
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i];
            if (tuple[0] == key) {
                bucket.splice(i, 1);
                this.count--;

                //缩小容量
                //如果整体容量大于7且当前小于整体容量的0.25
                if (this.limit > 7 && this.count < this.limit * 0.25) {
                    var newSize = Math.floor(this.limit / 2);
                    var newPrime = this.getPrime(newSize);
                    this.resize(newPrime);
                }
                
                return tuple[1];
            }
        }
        //依然没有找到,那么返回null
        return null;
    }
    //其他方法
    //判断哈希表是否为null
    HashTable.prototype.isEmpty = function () {
        return this.count;
    }
    //获取哈希表中元素的个数
    HashTable.prototype.size = function () {
        return this.count;
    }

    //哈希表扩容思想
    //为什么需要扩容?目前,是将所有数据项都放在长度为7的数组中,因为使用的是链地址法,loadFactor可以大于1,所以这个哈希表可以无限制地添加新数据,
    //但是随着数据量增多,每个index对应的bucket会越来越长,就会造成效率低下,所以应该在合适的情况下对数组进行扩容,比如扩容两倍。
    //如何进行扩容?扩容是将容量增大二倍,这种情况下,所有数据项一定要同时进行修改,因为每个数据项对应的index值已经都发生改变了,重新修改是一个耗时的过程,但是如果数组需要扩容,此阶段是必要的。
    //什么情况下需要扩容?比较常见的是loadFactor>0.75的时候,比如Java的哈希表就是在填装因子大于0.75的时候,对哈希表进行扩容。
    //哈希表扩容/缩容
    HashTable.prototype.resize = function (newLimit) {
        //保留旧的数组内容
        var oldStorage = this.storage;
        //重置所有的属性
        this.storage = [];
        this.count = 0;
        this.limit = newLimit;
        //遍历oldStorage中所有的bucket
        for (var i = 0; i < oldStorage.length; i++) {
            //取出对应的bucket
            var bucket = oldStorage[i];
            //判断bucket是否为null
            if (bucket == null) {
                continue;
            }
            //bucket中有数据,那么取出数据,重新插入
            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) {
        //从num开始向后查找最近的一个质数
        while(!this.isPrime(num)) {
            num++;
        }
        return num;
    }
}
//测试哈希表
//创建哈希表
var ht = new HashTable();
//插入数据
ht.put('abc', '123');
ht.put('cba', '321');
//获取数据
alert(ht.get('abc'));
//修改方法
ht.put('abc', '111');
alert(ht.get('abc'));
//删除方法
ht.remove('abc');
alert(ht.get('abc'));
//isEmpty
alert(ht.isEmpty());
//个数
alert(ht.size());
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值