• 前端数据结构--散列表(哈希表)


    散列表的由来

    前面说了数组、链表,他们各自有自己的特点:

    1. 数组:具有随机访问的特点,可以快速的根据下标访问到数据,缺点是插入、删除需要移动数据
    2. 链表:插入、删除只需要改变结点之间的引用,缺点是查找数据需要从根结点遍历访问

     散列表是组合了数组和链表的优势,规避它们的不足而产生新的一种数据结构。散列表是一种常用的数据存储技术,散列后的数据可以快速地插入或取用。

    什么是散列表

      散列表英文叫 Hash table,也叫哈希表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度。这个映射函数叫做散列函数,存放记录的数组叫散列表 如下图所示

    上面的定义可能不那么清晰,可以尝试这样理解, 散列表就是通过散列函数(也叫哈希函数)将元素的键映射为数组下标(转化后的值叫做散列值或哈希值),然后在对应下标的位置存储记录值、或者查找记录值,这种数据结构称为散列表。

      如图散列表用的是数组支持下标随机访问特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。

    散列函数

      从上面可以看出散列函数在散列表中起着非常核心的作用,散列函数,顾名思义,它是一个函数。我们可以把它定义成 hash(key),其中 key 表示键值,hash(key) 的值表示经过散列函数计算得到的散列值,即数组的下标。

    基本特点

    1. 散列函数计算得到的散列值是一个非负整数
    2. 如果 key1 = key2,那 hash(key1) == hash(key2)
    3. 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)

    第一点:因为数组下标是从 0 开始的,所以散列函数生成的散列值也要是非负整数。

    第二点:相同的 key,经过散列函数得到的散列值也应该是相同的。

    第三点:理论上key和散列值是一一对应的,但是种现实是很有可能一个key对应了多个散列值的情况,这就会存在冲突的情况,这取决于散列函数的设计。

    设计散列函数

      散列函数设计的好坏,决定了散列表冲突的概率大小,也直接决定了散列表的性能。一个好的散列函数基本满足两个原则

    1、计算hash值简单

      过于复杂的散列函数,会消耗很多计算时间,也就间接地影响到散列表的性能,因此散列涵的计算要简单、快速。

    2、散列函数计算出来的地址要分布均匀

      散列函数生成的值要尽可能随机并且均匀分布,这样才能避免或者最小化散列冲突,即便出现冲突,散列到每个槽里的数据也会比较平均,这样可以保证存储空间的合理使用。

    实际工作中,我们还需要综合考虑各种因素。这些因素有关键字的长度、特点、分布、还有散列表的大小等。

    常用设计散列函数基本思路:

    1、直接地址法

    1 hash(key) = a * key + b  // a、b为常数

    这种方法计算最简单,不会产生冲突,适合关键字的分布比较连续,而且长度较小的情况,如果关键字不连续,空位就会比较多,就会造成存储空间的浪费。

    假如我们有 20 名选手参加学校运动会。为了方便记录成绩,每个选手胸前都会贴上自己的参赛号码。这 20 名选手的号码依次是 1 到 20。现在希望实现这样一个功能,通过号码快速找到对应的选手信息。

    我们可以把号码为 1 的选手,我们放到数组中下标为 1 的位置;号码为 2 的选手,我们放到数组中下标为 2 的位置。以此类推,号码为 k 的选手放到数组中下标为 k 的位置。即我们的哈希函数只要返回对应的key 即可;

    1 function hash (key) {
    2    return key
    3 }

    2、数字分析法

      上面号码太简单了,如果把1-20的号码增加了年级、班级,如1 变成了202103001, 2 变成了202103002,那么此时我们上面那个哈希函数就不适用了。尽管我们不能直接把号码作为数组下标,我们可以用号码的后两位做为数组的下标,即我们的哈希函数可以改为
    1 function hash (key) {
    2    return String(key).substring(6)
    3 }

    3、平方取中法

    4、折叠法

    5、除留余数法

      除留余数法是使用的比较多的一种,公式为:

    1 hash(key) = key % p  

    如果散列表的表长为m,p为小于等于m的最大的质数,在一般情况下,对质数取余会让冲突更少,数据元素在散列表分布的更均匀。

    质数又称素数,除了1和自身,不能被其他自然数整除的数 如(2,3,5,7,11,13,17,...)

    如有数据 { 10,15,20,25,30,35,40,45,50 },表长为10,那么我们对 7 取余如下,其中 ^ 表示为空的链表:

    6、随机数法

    选择一个随机函数,用关键字作为随机函数的种子,返回值作为散列地址,即

    hash(key) = radmom(key) 

    可结合除留余数

    总结散列函数基本设计原则

    散列函数设计没有固定的方法,需要结合实际情况考虑如下因素:

    1. 要清楚关键字分布的情况、范围、规律,结合上面常用几种方法,写出散列函数
    2. 散列表的大小要合理,太大浪费空间太小则容易产生冲突
    3. 散列表的数据分布要均匀,不要一些下标中有很多元素,其他的没有或者很少
    4. 散列函数代码要精简,追求的是简单高效、分布均匀

    散列冲突

      再好的散列函数也无法避免散列冲突,因为散列值是非负整数,总量是有限的,但是现实世界中要处理的键值是无限的,将无限的数据映射到有限的集合,肯定避免不了冲突。即便像业界著名的MD5、SHA、CRC等哈希算法,也无法完全避免这种散列冲突。而且,因为数组的存储空间有限,也会加大散列冲突的概率。

      我们常用的散列冲突解决方法有两类,开放寻址法(open addressing)和链表法(chaining)。下面简单介绍下链表法

    链表法

      链表法是一种更加常用的散列冲突解决办法,在散列表中,每个下标会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。

    每一个数组下标对应的链表可以是单链表也可以是双链表。

    当插入的时候,我们只需要通过散列函数计算出对应的下标,将其插入到对应链表中即可,所以插入的时间复杂度是 O(1)。

    当查找、删除一个元素时,我们同样通过散列函数计算出对应下标,然后遍历链表查找或者删除。

    前端哈希数据结构

      javascript 中的ObjectSetWeakSetMapWeakMap 都是哈希结构。

     

  • 相关阅读:
    【BZOJ 1370】 团伙
    【BZOJ 1590】 Secret Message
    【BZOJ 2288】 生日礼物
    【POJ 3630】 Phone List
    【BZOJ 1398】 Necklace
    platform驱动之probe函数
    linux输入子系统之按键驱动
    linux输入子系统概念介绍
    操盘策略:KDJ三线合一 必定孕育大牛股
    nandflash裸机程序分析
  • 原文地址:https://www.cnblogs.com/longbensong/p/14702970.html
Copyright © 2020-2023  润新知