js数据结构-散列表(哈希表)

散列表

散列表(Hash table,也叫哈希表),是依据键(Key)而直接接见在内存存储位置的数据结构。也就是说,它经由过程盘算一个关于键值的函数,将所需查询的数据映照到表中一个位置来接见纪录,这加快了查找速率。这个映照函数称做散列函数,寄存纪录的数组称做散列表。

《js数据结构-散列表(哈希表)》

我们从上图最先剖析

  • 有一个鸠合U,内里离别是1000,10,152,9733,1555,997,1168
  • 右边是一个10个插槽的列表(散列表),我们须要把鸠合U中的整数寄存到这个列表中
  • 如何寄存,离别存在哪一个槽里?这个题目就是须要经由过程一个散列函数来处置惩罚了。我的寄存体式格局是取10的余数,我们对应这图来看

    • 1000%10=0,10%10=0 那末1000和10这两个整数就会被存储到编号为0的这个槽中
    • 152%10=2那末就寄存到2的槽中
    • 9733%10=3 寄存在编号为3的槽中

经由过程上面简朴的例子,应当会有一下几点一个大抵的明白

  • 鸠合U,就是可以会涌现在散列表中的键
  • 散列函数,就是你本身设想的一种如何将鸠合U中的键值经由过程某种盘算寄存到散列表中,如例子中的取余数
  • 散列表,寄存着经由过程盘算后的键

那末我们在接着看平常我们会如何去取值呢?

比方我们存储一个key为1000,value为’张三’ —> {key:1000,value:’张三’}
从我们上述的诠释,它是不是是应当寄存在1000%10的这个插槽里。
当我们经由过程key想要找到value张三,是不是是到key%10这个插槽里找就可以了呢?到了这里你可以停下来思索一下。

散列的一些术语(可以简朴的看一下)

  • 散列表中所有可以涌现的键称作全集U
  • 用M示意槽的数目
  • 给定一个键,由散列函数盘算它应当涌现在哪一个槽中,上面例子的散列函数h=k%M,散列函数h就是键k到槽的一个映照。
  • 1000和10都被存到了编号0的这个槽中,这类状况称之为碰撞。

看到这里不晓得你是不是大抵明白了散列函数是什么了没。经由过程例子,再经由过程你的思索,你可以转头在读一遍文章头部关于散列表的定义。假如你能读懂了,那末我预计你应当是懂了。

经常运用的散列函数

  • 处置惩罚整数 h=>k%M (也就是我们上面所举的例子)
  • 处置惩罚字符串:

        function h_str(str,M){
            return [...str].reduce((hash,c)=>{
                hash = (31*hash + c.charCodeAt(0)) % M
            },0)
        }

hash算法不是这里的重点,我也没有很深切的去研讨,这里重要照样去明白散列表是个如何的数据结构,它有哪些长处,它详细做了如何一件事。
而hash函数它只是经由过程某种算法把key映照到列表中。

构建散列表

经由过程上面的诠释,我们这里做一个简朴的散列表

散列表的构成

  • M个槽
  • 有个hash函数
  • 有一个add要领去把键值增加到散列表中
  • 有一个delete要领去做删除
  • 有一个search要领,依据key去找到对应的值

初始化

- 初始化散列表有多少个槽
- 用一个数组来建立M个槽
    class HashTable {
        constructor(num=1000){
            this.M = num;
            this.slots = new Array(num);
        }
    }

散列函数

处置惩罚字符串的散列函数,这里运用字符串是由于,数值也可以传换成字符串比较通用一些

  • 先将通报过来的key值转为字符串,为了可以严谨一些
  • 将字符串转换为数组, 比方’abc’ => […’abc’] => [‘a’,’b’,’c’]
  • 离别对每一个字符的charCodeAt举行盘算,取M余数是为了恰好对应插槽的数目,你统共就10个槽,你的数值%10 肯定会落到 0-9的槽里
    h(str){
        str = str + '';
        return [...str].reduce((hash,c)=>{
            hash = (331 * hash + c.charCodeAt()) % this.M;
            return hash;
        },0)
    }

增加

  • 挪用hash函数获得对应的存储地点(就是我们之间类比的槽)
  • 由于一个槽中可以会存多个值,所以须要用一个二维数组去示意,比方我们盘算得来的槽的编号是0,也就是slot[0],那末我们应当往slot[0] [0]里存,背面进来的同样是编号为0的槽的话就接着往slot[0] [1]里存
    add(key,value) {
        const h = this.h(key);
        // 推断这个槽是不是是一个二维数组, 不是则建立二维数组
        if(!this.slots[h]){
            this.slots[h] = [];
        }
        // 将值增加到对应的槽中
        this.slots[h].push(value);
    }  

删除

  • 经由过程hash算法,找到地点的槽
  • 经由过程过滤来删除
    delete(key){
        const h = this.h(key);
        this.slots[h] = this.slots[h].filter(item=>item.key!==key);
    }

查找

  • 经由过程hash算法找到对应的槽
  • 用find函数去找同一个key的值
  • 返回对应的值
    search(key){
        const h = this.h(key);
        const list = this.slots[h];
        const data = list.find(x=> x.key === key);
        return data ? data.value : null;    
    }

总结

讲到这里,散列表的数据结构已讲完了,实在我们每学一种数据结构或算法的时刻,不是去照搬完成的代码,我们要学到的是头脑,比方说散列表它终究做了什么,它是一种存储体式格局,可以疾速的经由过程键去查找到对应的值。那末我们会思索,假如我们设想的槽少了,在同一个槽里寄存了大批的数据,那末这个散列表它的搜刮速率肯定是会大打折扣的,这类状况又应当用什么体式格局去处置惩罚,又或许是不是用其他的数据结构的替代它。

补充一个小知识点

v8引擎中的数组 arr = [1,2,3,4,5] 或 new Array(100) 我们都晓得它是拓荒了一块一连的空间去存储,而arr = [] , arr[100000] = 10 如许的操纵它是运用的散列,由于这类操纵假如一连拓荒100万个空间去存储一个值,那末显然是在糟蹋空间。

后续

后续可以会去引见一下二叉树,别的关于文章有什么写错或许写的不好的处所人人都可以提出来。我会延续的去写关于前端的一些技术文章,假如人人喜好的话可以关注一下,点个赞哦感谢

    原文作者:accord
    原文地址: https://segmentfault.com/a/1190000017578404
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞