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的链表转化为红黑树这种数据结构。我们可以认真地看一看两个版本的实现,绝对会受益匪浅。

          

  • 相关阅读:
    219. Contains Duplicate II
    189. Rotate Array
    169. Majority Element
    122. Best Time to Buy and Sell Stock II
    121. Best Time to Buy and Sell Stock
    119. Pascal's Triangle II
    118. Pascal's Triangle
    88. Merge Sorted Array
    53. Maximum Subarray
    CodeForces 359D Pair of Numbers (暴力)
  • 原文地址:https://www.cnblogs.com/JackWeTa/p/13393087.html
Copyright © 2011-2022 走看看