前言:最近又开始重学数据结构与算法了(嗯,这么些年来确实重学N遍了,但这次和winter大神《重学前端》里讲的“重学”概念比较接近了,对自己来说应该会有比较明显的学习效果),和优秀的前辈一起交流学习果然能极速提升自己的思维水平和认知格局,比起自己一个人闷头啃书或者刷视频来的高效多了,能遇到一位乐意与自己交流技术的code-mate,绝对是自己成长道路上极其宝贵的财富。在这里首先感谢一下我身边那位科班出身见多识广的C++大佬超哥,从CPU外部三大总线结构到C++多线程与并发控制实践经验再到线程安全性与线程锁的性能权衡,从OSI七层模型的数据链路层、传输层与网络层详解到丢包对TCP和UDP应用层各协议分别的影响再到数据的大小端模式差异都能娓娓道来,给我讲的头头是道,比听德云社相声都得劲儿。如果你也可以和我一样,那么我觉得,这件事情——太酷啦!回家后果断决定趁热打铁,开始写个新的专栏。不过,这个专栏系列并没有严格遵循从哪儿到哪儿的那种逻辑次序,不定期更新,哪天看到哪个然后心血来潮了就先写哪个吧😂
打个广告:欢迎关注我的B站主页,本文也同步更新B站专栏文章
【松尾鷹志的个人空间-哔哩哔哩】 https://b23.tv/NsEL2y9
福利:首先这里强烈安利一个方便前端同学快速理解数据结构与算法的保姆级人类高质量开源项目,我不允许你不知道:The Algorithms - JavaScript,整个项目中有相当详细的注释、文档、图解,多种实现方式的代码,以及一整套测试场景覆盖齐全的jest自动化测试案例,目前GitHub上的Star数已经达到26.5k+🌟了,还愣着干嘛,快去点起来啊!👆
OK,回归正题,今天来聊一聊数据结构中的双向链表(doubly linked list),以及一种前端框架中相当常见,且日常工作中也非常实用的优化算法——LRU缓存(Least Recently Used Cache)算法,因为LRU缓存算法是我转行前端早些年接触过印象最深(这里的印象深刻仅仅指的是觉得卧槽好高大上好牛逼但就是啥啥都不懂的那种🤦♂️)的一个优化算法,Vue的keep-alive组件缓存机制、用来处理缓存逻辑的知名npm包lru-momoize,还有Redis的过期策略和内存淘汰机制,他们缓存逻辑的核心算法也都是用它来实现的,所以第一篇就先写它好了(对不住了,队列、优先队列二位兄台,只能下次有机会再聊你俩了)。
Are you ready? Let’s start!
目录
双向链表(doubly linked list)
原理篇
基本概念
基本结构
基本操作方法
代码篇
双向链表节点构造类DoublyLinkedListNode
双向链表构造类DoublyLinkedList
双向链表(doubly linked list)
原理篇
基本概念
国际惯例,先上一个相对官方的概念。以下概念整理自上述项目的说明文档中:
在计算机科学中, 一个 双向链表(doubly linked list 或者我们俗称它为双链表也行),是由一组称为节点的顺序链接记录组成的链接数据结构。每个节点包含两个字段,称为链接,它们是对节点序列中上一个节点和下一个节点的引用。开始节点和结束节点的上一个链接和下一个链接分别指向某种终止节点,通常是前哨节点或null,以方便遍历列表。如果只有一个前哨节点,则列表通过前哨节点循环链接。它可以被概念化为两个由相同数据项组成的单链表,但顺序相反。
没xx说个图,那就先看图吧:
结合这张字迹优雅的示意图(人家原作者画的,字体是真的漂亮,求同款!)来看,上面这段概念的前半段说实话一点也不难理解,node、value、pointer、previousNode、nextNode这些个概念都还挺直观的,但是后半段需要稍微多咀嚼几遍才能绕明白,总之就是不够白话,甚至有点枯燥。不过,既然这是一个主张在轻松愉悦氛围下学习的文科生专栏,当然还是说人话比较有意思。要让我说,就直接拿火车来举例好了🚂
(提示:以下内容可能有点中二,看不看随你,不想看的话从目录直接跳去看代码就行。)
基本结构
双链表这种数据结构呢,就像火车车厢一样,一节连着一节,每一节都有它的前一节(previous)和后一节(next)(注意这绝对不是废话,如果你仔细想想的话)。
假设我们现在正乘坐着一列刚从列车工厂(双链表构造类 DoublyLinkedList Class)下线,名为“双链表号”由5节车厢组成的列车组(双链表实例对象 new DoublyLikedList() ),前往目的地为数据结构与算法的极乐净土。我们分别用A-B-C-D-E(value)来表示每一节车厢(node),比如其中B车厢的前一节(previous)是A车厢,后一节(next)是C车厢。但是这些都不是重难点,因为双链表只是“看上去像”一列火车而已,它和普通的火车有一个本质的区别,这一点比前面这些概念都重要!
讲个恐怖故事,你以为这列看上去风平浪静的“双链表号”列车组的车头(head)是A,车尾(tail)是E对吗?这么理解似乎也没什么问题,因为双链表的head属性的确是A,tail属性也的确是E,但问题就出在头(head)并不是真正意义上的“头”,尾(tail)也并不是真正意义上的“尾”。这列特殊的列车组“真正的车头”(我也不知道专业术语叫啥,就用start来表示吧)根本就不是A,“真正的车尾”(用end来表示)也根本就不是E。为什么呢?因为,它是一列——无头无尾的幽灵列车!(start和end都为null)。我们肉眼能看到的,以为是车头的A车厢前面(previous)其实还挂了一节真正的车头——幽灵车厢null👻,只是出于某种原因,我们普通人根本就感知不到它的存在。同样,你以为是车尾的E车厢后面(next),其实也还挂了另一节真正的车尾——幽灵车厢null👻(null--A--B--C--D--E--null)。哪怕受到某种超自然力量的作用,整个列车组被吞噬到只剩下唯一一节车厢C,那么C车厢也是夹在两个幽灵车厢null中间的(此时C既是head又是tail,在结构上长这个样子:null--C--null),除非它也被毁灭