图解数据结构(04) -- 哈希表

1、什么是哈希表

哈希表(hash table),这种数据结构提供了键(Key)和值 (Value)的映射关系;只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)
在这里插入图片描述

2、哈希函数

散列表在本质上也是一个数组,可是数组只能根据下标,像a[0]、a[1]、a[2]、a[3]、a[4]这样来访问,而散列表的Key则是以字符串类型为主的,例如以学生的学号作为Key,输入002123,查询到李四;或者以单词为Key,输入by,查询到数字46……所以需要一个“中转站”,通过某种方式,把Key和数组下标进行转换,这个中转站就叫作哈希函数。
在这里插入图片描述

哈希函数的实现

以Java的常用集合HashMap为例,来讲解哈希函数在Java中的实现:
在Java及大多数面向对象的语言中,每一个对象都有属于自己的hashcode,这个hashcode是区分不同对象的重要标识,无论对象自身的类型是什么,它们的 hashcode都是一个整型变量。
既然都是整型变量,想要转化成数组的下标简单的转化方式就是按照数组长度进行取模运算

index = HashCode (Key) % Array.length

通过哈希函数**可以把字符串或其他类型的Key,转化成数组的下标 index;**例如给出一个长度为8的数组,则当 key=001121时,index = HashCode (“001121”) % Array.length = 1420036703 % 8 = 7
而当key=this时,index = HashCode (“this”) % Array.length = 3559070 % 8 = 6

3、哈希表的读写操作

写操作(put)

写操作就是在散列表中插入新的键值对,如调用 hashMap.put(“002931”, “王五”),意思是插入一组Key为002931、 Value为王五的键值对;具体步骤如下:

  • 第1步,通过哈希函数,把Key转化成数组下标5;
  • 第2步,如果数组下标5对应的位置没有元素,就把这个Entry填充到数组下标5的位置;
    在这里插入图片描述
    由于数组的长度是有限的,当插入的Entry越来越多时,不同的Key通过哈希函数获得的下标有可能是相同的;例如002936这个Key对应的数组下标是2; 002947这个Key对应的数组下标也是2;这种情况称为哈希冲突
    在这里插入图片描述
    哈希冲突是无法避免的,解决哈希冲突的方法主要有两种,一种是开放寻址法,一种是链表法;
  • 开放寻址法
    当一个Key通过哈希函数获得对应的数组下标已被占用时,我们可以“另谋高就”,寻找下一个空档位置,以上面的情况为例,Entry6通过哈希函数得到下标 2,该下标在数组中已经有了其他元素,那么就向后移动1位,看看数组下标3的位置是否有空:
    在这里插入图片描述
    很不巧,下标3也已经被占用,那么就再向后移动1位,看看数组下标4的位置是否有空:
    在这里插入图片描述
    幸运的是,数组下标4的位置还没有被占用,因此把Entry6存入数组下标4的位置:
    在这里插入图片描述
    这就是开放寻址法的基本思路!在Java中,ThreadLocal 所使用的就是开放寻址法。
  • 链表法
    链表法被应用在了Java的集合类HashMap当中,HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点,每 一个Entry对象通过 next 指针指向它的下一个Entry节点,当新来的 Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可。
    在这里插入图片描述

读操作(get)

读操作就是通过给定的Key,在散列表中查找对应的Value;例如调用 hashMap.get(“002936”),意思是查找Key为002936的Entry在散列表中所对应的值,步骤如下:

  • 第1步,通过哈希函数,把Key转化成数组下标2;
  • 第2步,找到数组下标2所对应的元素,如果这个元素的Key是002936,那么就找到了;如果这个Key不是002936也没关系,由于数组的每个元素都与一个链表对应,可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。
    在这里插入图片描述
    在上图中,首先查到的节点Entry6的Key是002947,和待查找的Key002936不符;接着定位到链表下一个节点Entry1,发现Entry1的Key 002936正是要寻找的,所以返回Entry1的Value即可。

扩容(resize)

什么时候需要进行扩容呢?
当经过多次元素插入,散列表达到一定饱和度时,Key映射位置发生冲突的概率会逐渐提高;大量元素拥挤在相同的数组下标位置,形成很长的链表, 对后续插入操作和查询操作的性能都有很大影响
在这里插入图片描述
这种情况下,散列表就需要扩展它的长度,也就是进行扩容;对于JDK中的散列表实现类HashMap来说,影响其扩容的因素有两个:

  • Capacity,即HashMap的当前长度;
  • LoadFactor,即HashMap的负载因子,默认值为0.75f

衡量HashMap需要进行扩容的条件:HashMap.Size >= Capacity × LoadFactor
注意:扩容不是简单地把散列表的长度扩大,而是经历了下面两个步骤:

  • 1.扩容,创建一个新的Entry空数组,长度是原数组的2倍;
  • 2.重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中;
  • 为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变
    经过扩容,原本拥挤的散列表重新变得稀疏,原有的Entry也重新得到了尽可能均匀的分配,扩容前HashMap如下:
    在这里插入图片描述
    扩容后的 HashMap 如下:
    在这里插入图片描述

关于HashMap的实现,JDK 8和以前的版本有着很大的不同。当多个Entry被Hash到同一个数组下标位置时,为了提升插入和查找的效率,HashMap 会把Entry的链表转化为红黑树这种数据结构

4、总结

  • 什么是数组
    数组是由有限个相同类型的变量所组成的有序集合,它的物理存储方式是顺序存储,访问方式是随机访问;利用下标查找数组元素的时间复杂度是O(1),中间插入、删除数组元素的时间复杂度是O(n)
  • 什么是链表
    链表是一种链式数据结构,由若干节点组成,每个节点包含指向下一节点的指针,链表的物理存储方式是随机存储,访问方式是顺序访问。查找链表节点的时间复杂度是O(n),中间插入、删除节点的时间复杂度是O(1)
  • 什么是栈
    栈是一种线性逻辑结构,可以用数组实现,也可以用链表实现。栈包含入栈和出栈操作,遵循先入后出的原则(FILO)。
  • 什么是队列
    队列也是一种线性逻辑结构,可以用数组实现,也可以用链表实现。队列包含 入队和出队操作,遵循先入先出的原则(FIFO)。
  • 什么是哈希表
    哈希表是存储Key-Value映射的集合;对于某一个Key,散列表可以在接近O(1)的时间内进行读写操作。散列表通过哈希函数实现Key和数组下标的 转换,通过开放寻址法和链表法来解决哈希冲突。
    —————————————————————————————————————————————
    内容来源:《漫画算法》
  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值