数据结构与算法之美
王争
前 Google 工程师
283752 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 81 讲
基础篇 (38讲)
数据结构与算法之美
15
15
1.0x
00:00/00:00
登录|注册

20 | 散列表(下):为什么散列表和链表经常会一起使用?

支持按访问顺序遍历
支持按插入顺序遍历
跳表支持高效操作
散列表解决散列冲突
成员对象的key和score
双向链表维护访问顺序
散列表解决散列冲突
散列表和双向链表组合使用
LinkedHashMap的特点
散列表和跳表结合使用
有序集合的操作
散列表和双向链表组合使用
散列表降低时间复杂度到O(1)
链表实现LRU缓存淘汰算法的时间复杂度是O(n)
散列表和链表组合使用可以兼顾高效的操作和按顺序遍历的需求
链表支持按顺序遍历数据
散列表支持高效的插入、删除、查找操作
Java LinkedHashMap
Redis有序集合
LRU缓存淘汰算法
散列表和链表的组合使用的原因
散列表和链表的组合使用

该思维导图由 AI 生成,仅供参考

我们已经学习了 20 节内容,你有没有发现,有两种数据结构,散列表和链表,经常会被放在一起使用。你还记得,前面的章节中都有哪些地方讲到散列表和链表的组合使用吗?我带你一起回忆一下。
在链表那一节,我讲到如何用链表来实现 LRU 缓存淘汰算法,但是链表实现的 LRU 缓存淘汰算法的时间复杂度是 O(n),当时我也提到了,通过散列表可以将这个时间复杂度降低到 O(1)。
在跳表那一节,我提到 Redis 的有序集合是使用跳表来实现的,跳表可以看作一种改进版的链表。当时我们也提到,Redis 有序集合不仅使用了跳表,还用到了散列表。
除此之外,如果你熟悉 Java 编程语言,你会发现 LinkedHashMap 这样一个常用的容器,也用到了散列表和链表两种数据结构。
今天,我们就来看看,在这几个问题中,散列表和链表都是如何组合起来使用的,以及为什么散列表和链表会经常放到一块使用。

LRU 缓存淘汰算法

在链表那一节中,我提到,借助散列表,我们可以把 LRU 缓存淘汰算法的时间复杂度降低为 O(1)。现在,我们就来看看它是如何做到的。
首先,我们来回顾一下当时我们是如何通过链表实现 LRU 缓存淘汰算法的。
我们需要维护一个按照访问时间从大到小有序排列的链表结构。因为缓存大小有限,当缓存空间不够,需要淘汰一个数据的时候,我们就直接将链表头部的结点删除。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

散列表和链表是常用的数据结构,它们经常被放在一起使用。本文从LRU缓存淘汰算法和Redis有序集合两个实际应用场景出发,详细介绍了散列表和链表的组合使用。在LRU缓存淘汰算法中,通过将散列表和双向链表结合使用,实现了高效的缓存系统,将查找、删除和添加数据的时间复杂度都降低到O(1)。而在Redis有序集合中,通过将成员对象组织成跳表的结构,并结合散列表来按照键值进行删除和查找,实现了高效的操作。文章通过实际应用场景的介绍,生动地展示了散列表和链表的组合使用的重要性和优势,为读者提供了深入理解和应用这一技术的指导。 此外,文章还介绍了Java中的LinkedHashMap,它通过双向链表和散列表组合实现,支持按照插入顺序和访问顺序遍历数据,甚至实现了LRU缓存淘汰策略。这些例子展示了散列表和链表结合使用的重要性,散列表虽然高效,但无法支持按顺序快速遍历数据,而链表的加入解决了这一问题。 总之,散列表和链表的组合使用在实际应用中具有重要意义,能够提高数据操作的效率,并且在实现缓存系统等方面发挥重要作用。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《数据结构与算法之美》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(278)

  • 最新
  • 精选
  • Smallfly
    通过这 20 节课学习下来,个人感觉其实就两种数据结构,链表和数组。 数组占据随机访问的优势,却有需要连续内存的缺点。 链表具有可不连续存储的优势,但访问查找是线性的。 散列表和链表、跳表的混合使用,是为了结合数组和链表的优势,规避它们的不足。 我们可以得出数据结构和算法的重要性排行榜:连续空间 > 时间 > 碎片空间。 PS:跟专业的书籍相比,老师讲的真的是通俗易懂不废话,篇篇是干货。如果这个课程学不下去,学其它的会更加困难。暂时不懂的话反复阅读复习,外加查阅,一定可以的!

    作者回复: 👍 大牛

    2018-11-05
    26
    1029
  • Smallfly
    1. 在删除一个元素时,虽然能 O(1) 的找到目标结点,但是要删除该结点需要拿到前一个结点的指针,遍历到前一个结点复杂度会变为 O(N),所以用双链表实现比较合适。 (但其实硬要操作的话,单链表也是可以实现 O(1) 时间复杂度删除结点的)。 iOS 的同学可能知道,YYMemoryCache 就是结合散列表和双向链表来实现的。 2. 以积分排序构建一个跳表,再以猎头 ID 构建一个散列表。 1)ID 在散列表中所以可以 O(1) 查找到这个猎头; 2)积分以跳表存储,跳表支持区间查询; 3)这点根据目前学习的知识暂时无法实现,老师文中也提到了。

    作者回复: 👍 其他同学可以看看这条留言

    2018-11-05
    44
    449
  • HunterYuan
    看好些人询问LRU中设计的到pre,next和hnext的具体含义,将自己的理解说下,pre和next组成双向链表,这个链表是按照缓存的时间由大到小,组成的一个缓存队列;对于hnext作用是,在最新时间插入缓存数据时,通过哈希函数得出的冲突,用其连接。 总结:在双向链表中,时间是从大到小;在hnext组成的拉链中,时间从左到右依次变小。 核心:数据结构的设计,一定是建立应用场景之上,根据最新时间加入缓存。 这是自己的见解,若是有错误,希望争哥不吝赐教,thanks

    作者回复: 对的 👍

    2019-04-15
    18
    71
  • Keep-Moving
    LRU查找数据,查找到之后,不是应该把数据放到链表的头部吗?为什么这里说是尾部?

    作者回复: 两种方式都可以的

    2018-11-05
    8
    45
  • 莫问流年
    怎么判断缓存已满,是要维护一个计数变量吗

    作者回复: 是的

    2018-11-05
    38
  • 邸志惠
    我不是科班出身,三年前准备学数据结构,却从没坚持下来,一度怀疑自己的智商有问题,但是看了老师的课程,篇篇都不由自主点赞啊,真是通俗易懂,鞭辟入里啊!感恩老师!

    作者回复: 多谢赞美啊😁

    2019-10-08
    2
    30
  • P@tricK
    老师我想问下,散列表和双向链表结构中的散列值,是用链表中的data哈希的吗?因为这样才能用O(1)查找… 那问题来了,那我要在链表尾部插入数据时,根据什么方法用O(1)定位到尾部呢?

    作者回复: 需要维护一个尾指针的

    2018-11-07
    10
    27
  • 微秒
    通过散列表遍历后不用在遍历双向链表了,那怎么以o(1)的时间查找定位链表中的节点???除非,散列表的尺寸很大,使得散列表的节点中只有少量数据的链表????

    作者回复: 是的 理论上散列表查找数据的时间复杂度是O(1)

    2018-11-06
    4
    19
  • ITACHI
    第一个图我是这样理解的,不知道对不对: 通俗地将,浅色的线(对应pre、next指针)维系的是链表中用于存储缓存数据的节点位置关系。 而黑色的线(对应hnext指针)维系的是添加节点时发生冲突后,在某个桶的节点位置关系。 比如第一个桶的最后一个节点,应该是在添加时,发现有冲突,且应属于第一个桶,所以用hnext指针和第一个桶的最后一个节点连起来了。 而它又是链表的最后一个节点,所以同时也是和第五个桶的最后一个节点通过pre、next连在一起的。 也就是说实际上只有一个双向链表,灰色线(pre、next指针)维系节点在链表中的位置,新指针hnext维系的是发生冲突的节点在某个桶中的位置。 (这个理解比较笨,而且可能不准确,不过感觉这么想能说通这个图,请问大佬们,这样对么。。。)

    作者回复: 你理解的完全正确哈。你可以看下这篇文章: https://mp.weixin.qq.com/s/PdvdZoa-SGk_Ojkv2pC2tQ

    2019-09-12
    8
    17
  • Zeng Shine
    “一个节点会存在两条拉链中,一条是双向链表,另一条是散列表中的拉链”,这句话描述的结构,怎么都想不明白。。

    作者回复: 图能不能看懂呢 你结合图看下

    2018-11-06
    17
    15
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部