数据结构之快速认识哈希表

154 阅读5分钟

哈希表是一个怎样的数据结构呢

我的理解是整体上来看,哈希表是一个数组,但是数组的每一项不只有一项,而是有很多项又组成一个数组,我们也可以想象成一个数组里每一项都是一个水桶

为什么要设计这样的哈希表结构呢?

​ 那我们就先要讲讲数组的弊端,数组其实在我们的使用中很是便利了,但是当数据庞大到几千万的量级上时,劣势就很明显了(因为数组时临时存放在内存中的,这里补充一个知识点,你知道两千万大小的数组占的内存大小是多少吗?先卖个关子)

​ 回到数组的弊端,数组在有下标值的情况下查找的效率时很高的,O(1)就能解决;举个栗子吧,如果我们有一个数组,数组每一项都有某一个同学的家庭地址,现在我们打算要去小天家里玩,但是呢,我们只知道小天的名字,但是我们不知道小天住在哪里,这个时候只能通过数组的遍历,一个个的对不查找到的是不是小天的家庭地址,所以如果我们要找到小天,起码要对比四次;想象一下,如果这个数组存放的是全校同学的家庭地址,那我们需要做的工作就多很多了

所以设计出哈希表的初衷是为了提高数组的查找效率,无论多少的数据,插入,删除,查找的操作都接近常量的事件,最好能达到O(1)

那哈希表是怎么实现的呢

哈希表主要的实现是通过哈希函数,将要查找的信息转换成数字,并讲这个数字作为存放在数组里位置的索引,需要查找的时候,通过这个下标去查找

在哈希函数里,你可以自己设置成你想要的映射规则,有很多中方式,比如把信息转换成对应的ASCII编码,或者设计一个自己的编码系统,有了编码系统,还需要组合方式得出一个数字,这里的方式也多样,你可以选择把所有字符转换的数字求和、幂的来连乘等等

这里幂的连乘,涉及到霍纳准则,霍纳准则是通过提取同类项的方法实现了乘法运算的降维

这样你就会得到一个很大的编码后的数字索引,这时候就需要进行压缩的操作了,因为这样的编码方式,面对只有几个信息的时候,是很浪费内存空间的,比如 有 [1 5 29 9] 这样一个数组,9进行编码后 (编码方式是幂的连乘)9*37+9=384,这样转换的下标就是384,但是数组的数据实际最多只有四个,所以光是幂的连乘还不够,需要压缩。伟大的数学家已经找到了很好的办法,那就是进行霍纳准则后取余,使用霍纳准则得到一个大的数字,在进行取余压缩,因为取余操作的结果的范围(0-9),当数据很多的时候,冲突的情况是必然会发生的

hash(str, size) { //hash(9,11) 
        let hashCode = 0;

        for (let i = 0; i < str.length; i++) {
            hashCode = 37 * hashCode + str.charCodeAt(i);
        }
  //一般采用一个质数37相乘,为什么呢?我的理解是,这可以让我们最后生成的索引值分散,减少冲突的可能,我后面会说冲突的具体情况
        hashCode = hashCode % size;
        return hashCode;
    }

什么是冲突呢?

冲突是指两个信息转换成的数字是一样的,又因为哈希表是不可以存放重复的key(信息)的,那这样怎么实现一个索引能有多个信息呢,有两个方法,链地址法和开放地址法,我这里主要介绍常用的链地址法,看到图片你大概就懂了这个方法的含义了

就是给每个下标值对应的位置,都设计为数组的结构,这种方法也会有弊端,可能某一项的数组特别多,这样就会失衡,但是也没什么坏处吧,在数组里采用的是线性的查找方式,这种方式想必大家都很熟悉了

下面我们就来实现一下哈希表的操作吧(哈希表的难点主要在理论哈,只要思路清晰,代码还是很好实现的)

封装哈希表结构【ES6实现】

class HashTable {
    constructor() {
        this.storage = []; 	//定义存储的数组表
        this.count = 0;			//记录已经存放的数据量
        this.limit = 7;			//定义数组的长度
    }
}

哈希函数

 //哈希函数 霍纳算法
    hash(str, size) {
        let hashCode = 0;

        for (let i = 0; i < str.length; i++) {
            hashCode = 37 * hashCode + str.charCodeAt(i);
        }
        hashCode = hashCode % size;
        return hashCode;
    }

这里的37可以换成你想要的质数,一般为31、37、43、41...

添加修改操作

//添加或者修改操作
    put(key, value) {
        let index = this.hash(key, this.limit);//利用hash函数处理信息key得到索引值
        let bucket = this.storage[index];
        //判断桶为空的情况
        if (bucket == null) {
            bucket = [];  //定义每一项都是一个数组
            this.storage[index] = bucket;
        }

        //判断是否要修改值
        for (let i = 0; i < bucket.length; i++) {
            let tuple = bucket[i];
            if (tuple[0] == key) {
                tuple[1] = value;
            }
        }

        //增加操作
        bucket.push([key, value]); //存放的形式类似python的元组
        this.count += 1;
    }

查询操作

 //查询操作
    find(key) {
        let index = this.hash(key, this.limit);
        let bucket = this.storage[index];
        if (bucket == null) return null;
        for (let i = 0; i < bucket.length; i++) {
            let tuple = bucket[i];
            if (tuple[0] == key) {
                return tuple[1];
            }
        }
        return false;
    }

删除操作

 //删除操作
    delete(key) {
        let index = this.hash(key, this.limit);
        let bucket = this.storage[index];
        if (bucket == null) return null
        for (let i = 0; i < bucket.length; i++) {
            let tuple = bucket[i];
            if (tuple[0]==key){
                bucket.splice(i,1);
                this.count-=1;
                return tuple[1];
            }
        }
        return "抱歉,哈希表不存在" + key;
    }

其他方法

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

//哈希表大小
    size(){
        return this.count;
    }

讲的还没有很全面,这部分的理论知识相对其他的结构多一些,文章如有不对的地方,欢迎批评指正!

参考:

coderwhy老师的数据结构与算法讲解视频

几百万数据放入内存不会把系统撑爆吗?