数据结构与算法笔记:基础篇 - 散列表(下):为什么散列表和链表经常会一起使用?
概述
已经学习了这么多章节了,你有没有发现,两种数据结构,散列表和链表,经常会被放在一起使用。你还记得,前面的章节中都有哪些地方讲到散列表和链表的组合使用吗?
在链表那一节,我讲到如何用链表来实现 LRU 缓存淘汰算法,但是链表实现的 LRU 缓存淘汰算法的时间复杂度是 O ( n ) O(n) O(n),当时提到了,通过散列表可以将这个时间复杂度降低到 O ( 1 ) O(1) O(1)。
在跳表那一节,提到 Redis 的有序集合是使用跳表来实现的,跳表可以看做这一种改进版的链表。当时我们也提到,Redis 有序集合不仅使用了链表,还用到了散列表。
此外,如果你熟悉 Java 编程语言,你会发现 LinkedHashMap
这样 一个常用的容器,也用到了散列表和链表两者数据结构。
本章,我们来看看,在这几个问题中,散列表和链表都是如何组合起来使用,以及为什么散列表和链表会经常放到一块使用。
LRU 缓存淘汰算法
在链表那一节中,我提到,借助散列表,可以将时间复杂度降低到 O ( 1 ) O(1) O(1)。现在,我们来看看它是如何做到的。
首先,我们来回顾一下当时我们是如何通过链表实现 LRU 缓存淘汰算法的。
我们需要维护一个按照访问时间从打大小有序排列的链表结构。因为缓存大小有限,当缓存空间不够,需要淘汰一个数据的时候,我们就直接将链表头部的结点删除。
当要缓存某个数据的时候,现在链表中查找这个数据。如果没有找到,则将数据放到链表的尾部;如果找到了,我们就把它移动到链表的尾部。因为查找数据需要遍历链表,所以单纯用链表实现的 LRU 缓存淘汰算法的时间复杂度很高,是 O ( n ) O(n) O(n)。
总结一下,一个缓存(cache)系统主要包含下面这几个操作:
- 往缓存中添加一个数据;
- 从缓存中删除一个数据;
- 从缓存中查找一个数据。
这三个操作都要涉及 “查找” 操作,如果单纯地采用链表的话,时间复杂度只能是 O ( n ) O(n) O(n)。如果我们将散列表和链表两种数据组合使用,可以将这三个操作的时间复杂度都降低到 O ( 1 ) O(1) O(1)。具体的结构就是下面这个样子:
我们使用双向链表存储数据,链表中的每个结点除了存储数据(data)、前驱结点(prev)、后继结点(next)之外,还新增了一个特殊的字段 hnext。这个 hnext 有什么作用呢?
因为我们的散列表是通过链表法解决冲突的,所以每个结点会在两条链中。一个链是刚刚我们提到的双向链表,另一个链是散列表中的拉链。前驱和后继指针是为了将结点串在双休链表中,hnext 指针是为了将结点串在散列表的拉链中。
了解了这个散列表和双向链表的组合存储结构之后,我们再来看,前面讲到的缓存的三个操作,是如何做到时间复杂度是 O ( 1 ) O(1) O(1)?
首先,来看如何查找一个数据。前面讲过,散列表中查找的数据的时间复杂度接近 O ( 1 ) O(1) O(1),所以通过散列表,我们可以很快地在缓存中找到一个数据。当找到数据之后,我们还需要将它移动到双向链表的尾部。
其次,来看如何删除一个数据。我们需要找到数据所在的结点,然后将结点删除。借助散列表,我们可以在 O ( 1 ) O(1) O(1) 时间复杂度里找到要删除的结点。因为我们的链表是双向链表,双向链表可以通过前驱指针 O ( 1 ) O(1) O(1) 时间复杂度获取前驱结点,所以在双向链表中,删除结点只需要 O ( 1 ) O(1) O(1) 的时间复杂度。
最后,来看如何添加一个数据。添加数据到缓存稍微有点麻烦,我们需要先看这个数据是否已经在缓存中。如果已经在其中,需要将其移动到双向链表的尾部;如果不在其中,还要看缓存有没有满。如果满了,则将双向链表头部的结点删除,然后再将数据放到链表的尾部;如果没有满,就直接将数据放到链表的尾部。
这整个过程设计的查找操作都可以通过散列表来完成。其他的操作,比如删除头结点、链表尾部插入数据等,都可以在 O ( 1 ) O(1) O(1) 的时间复杂度完成。所以,这三个操作的时间复杂度都是 O ( 1 ) O(1) O(1)。至此,我们就通过散列表和双向链表的组合使用,实现了一个高效的、支持 LRU 缓存淘汰算法的缓存系统原型。
Redis 有序集合
在跳表那一节,讲到有序集合的操作时,我稍微做了简化。实际上,在有序集合中,每个成员有两个重要的属性,key(键值)和 score(分值)。我们不仅会通过 score 来查找数据,还可以通过 key 来查找数据。
例如,用户积分排行榜这样一个功能:我们可以通过用户的 ID 来查找积分,也可以通过积分区间来查找用户 ID 或姓名信息。这里包含 ID、姓名和积分的用户信息,就是成员对象,用户 ID 就是 key,积分就是 score。
所以,如果我们细化一下 Redis 有序集合的操作,那就是下面这样:
- 添加一个成员
- 按照键值来删除一个成员对象;
- 按照键值来查找一个成员对象;
- 按照分值区间查找数据,比如查找积分在 [100, 222] 之间的成员对象;
- 按照分值从小到大排序成语变量;
如果我们仅仅按照分值将成员对象组织成跳表的结构,那按照键值来删除、查找成员对象就会很慢,解决方法与 LRU 的解决方法类似。可以在按照键值构建一个散列表,这样按照 key 来删除、查找一个成员对象的时间复杂度就变成了 O ( 1 ) O(1) O(1)。同时借助跳表,其他操作也非常高效。
实际上,Redis 有序集合的操作还有另外一类,也就是查找成员对象的排名(Rank)或者根据排名区间查找成员对象。这个功能单纯用刚刚讲的这种组合结构就无法高效实现了。这块内容,在后面的章节在讲解。
Java LinkedHashMap
如果你熟悉 Java,那你几乎天天会用到这个容器。我们之前讲过,HashMap 底层是通过散列表这种数据结构实现的。而 LinkedHashMap 前面比 HashMap 多了一个 “Linked” ,这里的 “Linked” 是不是说,LinkedHashMap 是一个通过链表法解决散列冲突的散列表呢?
实际上,LinkedHashMap 并没有那么简单,其中的 “Linked” 也不仅仅代表它是通过链表法解决散列冲突的。
先来看一段代码。你觉得这段代码会以什么样的顺序打印 3,1,5,2
这几个 key 呢?原因又是什么呢?
HashMap<Integer, Integer> map = new LinkedHashMap<>();
map.put(3,11);
map.put(1,12);
map.put(5,23);
map.put(2,22);for(Map.entry e : map.entrySet()) {System.out.println(e.getKey());
}
我先告诉你答案,上面的代码会按照数据插入的顺序依赖来打印,也就是说,打印店顺序是 3,1,5,2
。你有没有觉得奇怪?散列表中数据是经过散列函数打乱之后无规律存储的,这里是如何实现按照数据的插入顺序来遍历打印的呢?
LinkedHashMap 也是通过散列表和链表组合在一起实现的。实际上,它不仅支持按照插入顺序遍历数据,还支持按照顺序来遍历数据。你可以看下面这段代码:
// 10 是初始大小,0.75是装载因子,true是表示按照访问时间排序
HashMap<Integer, Integer> map = new LinkedHashMap<>(10, 0.75f, true);
map.put(3,11);
map.put(1,12);
map.put(5,23);
map.put(2,22);map.put(3, 26);
m.get(5)for(Map.entry e : map.entrySet()) {System.out.println(e.getKey());
}
这段代码打印的结果是 1,2,3,5
。我来具体分析一下,为什么这段代码会按照这样顺序来打印。
每次调用 put()
函数,往 LinkedHashMap 中添加数据的时候,都会往将数据添加到链表的尾部,所以,在前四个操作完成之后,链表中的数据是下面这样的。
在第 8 行代码中,再次将键值为 3 的数据放入到 LinkedHashMap 的时候,会先查找这个键值是否已经有了,然后再将已存在的 (3,11)
删除,并将心的 (3,26)
放到链表的尾部。所以,这个时候链表的尾部。所以,这个时候链表中的数据就是下面这样的:
当第 9 行代码访问到 key 为 5 的数据时,我们将被访问的数据移动到链表的尾部。所以,第 9 行代码之后,链表的数据是这样的:
所以,最后打印出来的数据是 1,2,3,5
。从上面的分析,你有没有发现,按照访问时间排序的 LinkedHashMap 本身就是一个支持 LRU 缓存淘汰策略的缓存系统?实际上,它们两个的实现原理也是一模一样的。我也就不再啰嗦了。
现在来总结一下,实际上,LinkedHashMap 是通过双向链表和散列表这两种数据结构组合实现的。LinkedHashMap 中的 “Linked” 实际上是指的是双向链表,并非指用链表法解决散列冲突。
小结
散列表这种数据结构虽然支持非常高效的数据插入、删除、查找操作,但是散列表中的数据都是通过散列函数打乱之后无规律存储的。也就是说,它无法支持按照某种顺序快速地遍历数据。如果希望按照顺序遍历散列表中的数据,那需要将散列表中的数据拷贝到数组中,然后排序,再遍历。
因为散列表是动态数据结构,不停的有数据插入、删除,所以每当我们希望按照顺序遍历散列表中的数据时,都需要先排序,那效率势必会很低。为了解决这个问题,我们将散列表和链表(或者跳表)结合在一起使用。
相关文章:
数据结构与算法笔记:基础篇 - 散列表(下):为什么散列表和链表经常会一起使用?
概述 已经学习了这么多章节了,你有没有发现,两种数据结构,散列表和链表,经常会被放在一起使用。你还记得,前面的章节中都有哪些地方讲到散列表和链表的组合使用吗? 在链表那一节,我讲到如何用…...
读AI未来进行式笔记06自动驾驶技术
1. 跃层冲击 1.1. 每个社会其实都处于不同的楼层,往往处于更低楼层的社会,要承受来自更高楼层的社会发展带来的更大冲击 2. 驾驶 2.1. 开车时最关键的不是车,而是路 2.2. 人是比机器更脆弱的生命&am…...
SpringAOP 常见应用场景
文章目录 SpringAOP1 概念2 常见应用场景3 AOP的几种通知类型分别有什么常见的应用场景4 AOP实现 性能监控4.1 首先,定义一个切面类,用于实现性能监控逻辑:4.2 定义自定义注解4.3 注解修饰监控的方法 5 AOP实现 API调用统计5.1 定义切面类&am…...
html+css示例
HTML HTML(超文本标记语言)和CSS(层叠样式表)是构建和设计网页的两种主要技术。HTML用于创建网页的结构和内容,而CSS用于控制其外观和布局。 HTML基础 HTML使用标签来标记网页中的不同部分。每个标签通常有一个开始…...
Day51 动态规划part10+Day52 动态规划part11
LC121买卖股票的最佳时机(未掌握) 暴力:双层循环寻找最优间距,每一次都确定一个起点,遍历剩余节点当作终点 贪心:取最左最小值,不断遍历那么得到的差值最最大值就是最大利润。 动态规划 dp数组…...
Wireshark自定义Lua插件
背景: 常见的抓包工具有tcpdump和wireshark,二者可基于网卡进行抓包:tcpdump用于Linux环境抓包,而wireshark用于windows环境。抓包后需借助包分析工具对数据进行解析,将不可读的二进制数转换为可读的数据结构。 wires…...
商城项目【尚品汇】07分布式锁-2 Redisson篇
文章目录 1 Redisson功能介绍2 Redisson在Springboot中快速入门(代码)2.1 导入依赖2.2 Redisson配置2.3 将自定义锁setnx换成Redisson实现(可重入锁) 3 可重入锁原理3.1 自定义分布式锁setnx为什么不可以重入3.2 redisson为什么可…...
Adobe Illustrator 矢量图设计软件下载安装,Illustrator 轻松创建各种矢量图形
Adobe Illustrator,它不仅仅是一个简单的图形编辑工具,更是一个拥有丰富功能和强大性能的设计利器。 在这款软件中,用户可以通过各种精心设计的工具,轻松创建和编辑基于矢量路径的图形文件。这些矢量图形不仅具有高度的可编辑性&a…...
Nvidia/算能 +FPGA+AI大算力边缘计算盒子:中国舰船研究院
中国舰船研究院又称中国船舶重工集团公司第七研究院,隶属于中国船舶重工集团公司,是专门从事舰船研究、设计、开发的科学技术研究机构,是中国船舶重工集团公司的军品技术研究中心、科技开发中心;主要从事舰船武器装备发展战略研究…...
双网卡配置IP和路由总结
1.在网络适配器属性IPv4中设置默认网关(记网关地址为A),将会在本地路由表中新增一条记录: 网络号子网掩码网关地址0.0.0.00.0.0.0A 2.如果有两个网卡(假设一个连接内网,一个连接互联网)&#…...
【纯血鸿蒙】——自适应布局如何实现?
界面级一多能力有 2 类: 自适应布局: 略微调整界面结构 响应式布局:比较大的界面调整 本文章先主要讲解自适应布局,响应式布局再后面文章再细讲。话不多说,开始了。 自适应布局 针对常见的开发场景,方舟开发框架提…...
Qt5学习笔记(一):Qt Widgets Application项目初探
笔者长期使用MFC开发Windows GUI软件。随着软件向Linux平台迁移的趋势越发明朗,GUI程序的跨平台需求也越来越多。因此笔者计划重新抓一下Qt来实现跨平台GUI程序的实现。 0x01. 看看Qt Widgets Application项目结构 打开Qt5,点击“ New”按钮新建项目。…...
Linux网络编程:数据链路层协议
目录 前言: 1.以太网 1.1.以太网帧格式 1.2.MTU(最大传输单元) 1.2.1.IP协议和MTU 1.2.2.UDP协议和MTU 1.2.3.TCP协议和MTU 2.ARP协议(地址解析协议) 2.1.ARP在局域网通信的角色 2.2.ARP报文格式 2.3.ARP报文…...
企业估值的三种方法
估值模型三剑客—DCF、P/E、EV /EBITDA 三种主要估值模型的优缺点: DCF 优点:通过对自由现金流的折现计算,反映了公司内在价值的本质,是最重要与最合理的估值方法。 缺点:未来自由现金流的估计不准确,受折现率影响…...
比亚迪正式签约国际皮划艇联合会和中国皮划艇协会,助推龙舟入奥新阶段
6月5日,比亚迪与国际皮划艇联合会、中国皮划艇协会在深圳共同签署合作协议,国际皮划艇联合会主席托马斯科涅茨科,国际皮划艇联合会秘书长理查德派蒂特,中国皮划艇协会秘书长张茵,比亚迪品牌及公关处总经理李云飞&#…...
宏集Panorama SCADA:个性化定制,满足多元角色需求
前言 在考虑不同人员在企业中的职能和职责时,他们对于SCADA系统的需求可能因其角色和工作职责的不同而有所差异。在SCADA系统的设计和实施过程中,必须充分考虑和解决这种差异性。 为了满足不同人员的需求, 宏集Panorama SCADA平台具备灵活的功能和定制…...
聪明人社交的基本顺序:千万别搞反了,越早明白越好
聪明人社交的基本顺序:千万别搞反了,越早明白越好 国学文化 德鲁克博雅管理 2024-03-27 17:00 作者:方小格 来源:国学文化(gxwh001) 导语 比一个好的圈子更重要的,是自己优质的能力。 唐诗宋…...
图片和PDF展示预览、并支持下载
需求 展示图片和PDF类型,并且点击图片或者PDF可以预览 第一步:遍历所有的图片和PDF列表 <div v-for"(data,index) in parerFont(item.fileInfo)" :key"index" class"data-list-item"><downloadCard :file-inf…...
图论第5天
127.单词接龙 需要cout看一下过程。 #include <iostream> #include <queue> #include <stack> #include <unordered_map> #include <unordered_set> #include <vector> using namespace ::std;class Solution { public:int ladderLength(…...
Java开发-面试题-0004-HashMap 和 Hashtable的区别
Java开发-面试题-0004-HashMap 和 Hashtable的区别 更多内容欢迎关注我(持续更新中,欢迎Star✨) Github:CodeZeng1998/Java-Developer-Work-Note 技术公众号:CodeZeng1998(纯纯技术文) 生活…...
Swift 序列(Sequence)排序面面俱到 - 从过去到现在(一)
概览 在任何语言中对序列(或集合)元素的排序无疑是一种司空见惯的常规操作,在 Swift 语言里自然也不例外。序列排序看似简单,实则“暗藏玄机”。 要想真正掌握 Swift 语言中对排序的“各种姿势”,我们还得从长计议。不如就先从最简单的排序基本功开始聊起吧。 在本篇博…...
redis 04 redis结构
1.客户端...
【原创】springboot+mysql农业园区管理系统设计与实现
个人主页:程序猿小小杨 个人简介:从事开发多年,Java、Php、Python、前端开发均有涉猎 博客内容:Java项目实战、项目演示、技术分享 文末有作者名片,希望和大家一起共同进步,你只管努力,剩下的交…...
web前端 孙俏:深度探索与实战之路
web前端 孙俏:深度探索与实战之路 在这个数字化、信息化的时代,web前端技术以其独特的魅力,吸引着越来越多的开发者投身其中。今天,我们将跟随孙俏的脚步,一同探索web前端的深度与广度,揭开其神秘的面纱。…...
opencv实战小结-银行卡号识别
实战1-银行卡号识别 项目来源:opencv入门 项目目的:识别传入的银行卡照片中的卡号 难点:银行卡上会有一些干扰项,如何排除这些干扰项,并且打印正确的号码是一个问题 最终效果如上图 实现这样的功能需要以下几个步骤…...
Windows API 开发桌面应用程序,在窗口按下鼠标左键不放可以拖图,并且拖图期间鼠标图标变成手掌
在Windows API中,要实现鼠标左键按下并拖动以移动窗口中的某个图形,并且同时改变鼠标图标为“手掌”形状(这通常指的是“拖动”或“移动”的图标),你需要执行几个步骤。 以下是一个基本的步骤指南,用于在W…...
Docker的网络管理
文章目录 一、Docker容器之间的通信1、直接互联(默认Bridge网络)1.1、Docker安装后默认的网络配置1.2、创建容器后的网络配置1.2.1、首先创建一个容器1.2.2、ip a 列出网卡变化信息1.2.3、查看新建容器后的桥接状态 1.3、容器内安装常见的工具1.4、容器间…...
【数据结构】平衡二叉树左旋右旋与红黑树
平衡二叉树左旋右旋与红黑树 平衡二叉树 定义 平衡二叉树是二叉搜索树的一种特殊形式。二叉搜索树(Binary Search Tree,BST)是一种具有以下性质的二叉树: 对于树中的每个节点,其左子树中的所有节点都小于该节点的值…...
2024蓝桥杯初赛决赛pwn题全解
蓝桥杯初赛决赛pwn题解 初赛第一题第二题 决赛getting_startedbabyheap 初赛 第一题 有system函数,并且能在bss上读入字符 而且存在栈溢出,只要过掉check函数即可 check函数中,主要是对system常规获取权限的参数,进行了过滤&…...
大模型多轮问答的两种方式
前言 大模型的多轮问答难点就是在于如何精确识别用户最新的提问的真实意图,而在常见的使用大模型进行多轮对话方式中,我接触到的只有两种方式: 一种是简单地直接使用 user 和 assistant 两个角色将一问一答的会话内容喂给大模型,…...
一个专门做字画的网站/南宁seo推广优化
一. 常用命令 1. 编辑相关 ①. awk NF:字段总数NR:第几行数据FS:分隔字符 ②. sed -n-i 直接修改4a:在第四行后添加4i:在第四行前插入1,5c sting:用sting替换1到5行的内容s/要被替换的字符串/新的字符串…...
哪些b2b网站做游戏机比较好/seo高级优化方法
1、首先他们底层数据结构不一样,ArrayList底层结构是数组,LinkedList底层结构是链表; 2、数据结构决定了,ArrayList在查询上的效率较高,而LinkedList在删除和添加上的效率更高;(需要注意的一点是…...
三门峡网站建设电话/360网站推广怎么做
初学者应该选择学习Python还是C语言 发布时间:2020-11-21 14:11:31 来源:亿速云 阅读:74 作者:小新 小编给大家分享一下初学者应该选择学习Python还是C语言,希望大家阅读完这篇文章后大所收获,下面让我们一…...
重庆搜索引擎优化/厦门seo排名
Jenkins项目团队决定在稳定性和为Kubernetes等平台提供更好的支持方面分配一些工作量。前者可能会发生一些向后不兼容的变更,将影响发布模型并提供具有更多预置选项的版本,而后者将在与现有Jenkins X项目齐头并进。\u0026#xD;\u0026#xD;Jenkins目前在处理…...
做网站的目标是什么/东莞seo项目优化方法
Linux指令篇:讯息传送与信件管理--write(转)名称 : write使用权限 : 所有使用者使用方式 :write user [ttyname]说明 : 传讯息给其他使用者参数 :user : 预备传讯息的使用者帐号ttyname : 如果使用者同时有两个以上的 tty 连线,可以自行选择合适的 tty 传讯息例子.1…...
醴陵网站开发/百度推广代理开户
java 中sleep()方法或者wait()方法的使用比如我在执行某个方法后 想停顿3秒 然后去执行另外一个 方法 (注意:不简单说:sleep由线程自动唤醒,wait必须显示用代码唤醒。 sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间…...