zoukankan      html  css  js  c++  java
  • 散列表

    1.什么是散列表?

      散列表也叫哈希表,这种数据结构提供了键(key)和值(value)的映射关系,。只要给出一个key,就可以高效的查找到它所匹配的value,时间复杂度接近于O(1)。

    2.基本原理

      2.1 散列表在本质上也是一个数组,,我们需要一个“中转站”,通过某种方式,把key和数组下标进行转换。这个中转站就叫做哈希函数。

      2.2 哈希函数

        哈希函数是怎么实现的呢?

        这里以java的常用集合HashMap为例,看一看哈希函数在java中的实现。

        在java及大多数面向对象的语言中,每一个对象都有属于自己的hashcode,这个hashcode是区分不同对象的重要标识。无论对象自身的类型是什么,它们的hashcode都是一个整型变量。

        既然都是整形变量,想要转换成数组的下标也就不难实现了。最简单的转换方式就是按照数组长度进行取模运算:

          index = HashCode(key)% Array.length

        实际上,JDK中的哈希函数并没有直接采用取模运算,而是利用了位运算的方式来优化性能。

    3.基本操作

      有了哈希函数,就可以在散列表中进行读写操作了。

      3.1 写操作

        写操作就是散列表中插入新的键值对(在JDK中叫做Entry)。

        如调用hashMap.put("002931","王五"),意思是插入一组key为002931,value为王五的键值对。

        具体该怎么做呢?

           第一步,通过哈希函数,将key转化为数组下标5。

          第二步,如果数组下标5对应的位置没有元素,就把这个Entry填入到数组下标5的位置。

        但是,由于数组的长度是有限的,当插入的Entry越来越多时,不同的key通过哈希函数获得的下标有可能是相同的。例如002936这个key对应的数组下标是2,002947这个key对应的数组下标也是2。

        这种情况就叫做哈希冲突。

        哈希冲突是无法避免的,因此只能想办法解决,解决哈希冲突的方法主要有两种:一种是开放寻址法,一种是链表法。

          ① 开放寻址法的原理很简单,当一个key通过哈希函数获得对应的数组下标已被占用时,我们可以“另谋高就”,寻找下一个空挡位置。

            以上面两个重复的键值对为例,通过哈希函数得到的数组下标为2,该下标在数组中已经有了其他元素,那么就向后移动一位,看看数组下标3的位置是否有空,如果3也被占用,那么就再向后移动1位,看看数组下标4的位置是否有空,如果没有被占用,九八这个键值对存入数组下标为4的位置。

            这其实就是开放寻址法的基本思路。当然在遇到哈希冲突时,寻址方式有很多种,并不一定只是简单的寻找当前元素的后一个元素,这里只是举一个简单的示例而已。在Java中,ThreadLocal所使用的就是开放寻址法。

          ② 链表法—被应用在Java的集合类HashMap中

            HashMap数组中的每一个元素不仅是一个Entry对象,还是一个链表的头结点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可。

      3.2 读操作

        读操作就是通过给定的key,在散列表中查找到对应的value。

        例如调用HashMap.get("003936"),意思是查找ke为002936的Entry在散列表中所对应的值。

        具体该怎么做呢?

          第一步,通过哈希函数,把key转换成数组下标2。

          第二步,找到数组下标2所对应的元素,如果这个元素的key是002936,那么就找到了;如果key不是002936也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢往下找,看看能否找到与key相匹配的节点。

      3.3 扩容

        3.3.1 什么时候需要扩容呢?

          当经过多次元素插入,散列表达到一定饱和度时,key映射位置发生冲突的概率会逐渐提高。这样一来,大量元素拥挤在相同的数组下标位置,形成很长的链表,对后续插入操作和查询操作的性能都有很大的影响。这时,散列表就需要扩展它的长度,也就是进行扩容。

        对于JDK中的散列表实现类HashMap来说,影响其扩容的因素有两个:

          ① Capacity,即HashMap的当前长度。

          ② LoadFactor,即HashMap的负载因子,默认值为0.75f。

        衡量HashMap需要进行扩容的条件如下:

          HashMap.Size >= Capacity * LoadFactor

        3.3.2 散列表的扩容操作具体做了什么事情?

          扩容不是简单的把散列表的长度扩大,二是经历了下面两个步骤:

            ① 扩容,创建一个新的Entry空数组,长度是原来的2倍。

            ② 重新Hash,遍历Entry数组,把所有的Entry重新Hash到新的数组中。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。

        经过扩容,原本拥挤的散列表重新变得稀疏,原有的Entry也重新得到了尽可能均匀的分配。

    4.总结

      以上就是散列表各种基本操作的原理,实现代码可以直接参考JDK中HashMap类的源码。

      需要注意的是,关于HashMap的实现,JDK8和以前的版本有着很大的不同。当多个Entry被Hash到同一个数组下标位置时,为了提升插入和查找的效率,HahsMap会把Entry的链表转化为红黑树这种数据结构。我们可以认真地看一看两个版本的实现,绝对会受益匪浅。

          

  • 相关阅读:
    LG3626 [APIO2009]会议中心(倍增+树状数组)
    LG3624 [APIO2008]DNA(DP+前缀和)
    CF1516C
    PKUSC2021 口胡题解
    THUSC2021 Day1口胡题解
    2021.4
    2021.3
    2021 暑假 sxyz 集训做题记录
    【做题记录】CF746F Music in Car
    KMP
  • 原文地址:https://www.cnblogs.com/JackWeTa/p/13393087.html
Copyright © 2011-2022 走看看