作为线性结构家族的又一个成员,链表终于闪亮登场了。那么链表这个数据结构到底有什么用呢? 链表到底长什么样子呢?链表有什么特点呢? 不要着急 ,我一一的解答。
首先来我们来了解一下链表的作用,抱着对链表时使用场景来学习它。提到链表我们肯定能想到LinkedList里面的底层是链表,什么增删快,查询慢,这是背烂了的面试题吧。链表除了这个还有一些使用场景,比如LUR缓存淘汰算法。
缓存,这个词在这个时代已经不再陌生了吧,jdk自带的cache, hibernate的缓存,mybatis缓存,redis作为缓存等等。
缓存的大小有限,当缓存满了之后,怎么清理数据就是一个问题了。 大致有三种策略: 1 FIFO(先入先出) 2 LFU(最少使用策略) 3 LRU(最近最少使用策略)。
这上面三个策略好像不那么好理解,给大家举例子吧,我们生活中 人人离不开手机,手机里面装了很多的软件,先入先出,就是当手机内存不足的时候,卸载掉最早安装的软件。LFU(最少使用策略) 卸载掉使用频率最少的手机软件。 LRU是卸载掉最近几天使用最少的软件,这样解释大家应该可以看懂了吧。
好了回到链表上来。链表的结构也分为几种 单链表、循环链表、双向链表。
提到链表了 自然要和数组进行比较
链表 | 数组 |
不需要连续内存空间 | 需要连续内存空间 |
增删快 | 随机访问快 |
单链表
链表通过指针将不连续的内存块串联在一起,其中把内存块成为链表的结点。为了把所有节点串联起来,每个结点除了存储数据之外,还需要继续下一个结点的地址,把这个记录下一个结点地址的指针称为后继指针next
从图中可以看出来其中有两个结点很特殊,一个是头结点,一个是尾结点。头结点是用来记录链表地址,有了它,我们可以遍历整个链表,尾结点的next指向的不是下一个结点,而是指向一个空值null。表示这个是链表的最后一个节点。
接下来我们来看看链表是如何进行插入操作的,插入一个新的元素,只需要将前一个元素的next结点地址指向插入元素,然后将插入元素的next指针地址指向后一个节点
插入和删除的操作时间复杂度为O(1),但是查询便没有那么方便了,比如我们要查询第K个元素,只能从首结点以此遍历到第K个结点。
循环链表
循环链表和单向链表唯一的区别就是在于,尾部指针不是指向NULL而是指向头结点,形成一个环状。
双向链表
单向链表只有一个方向,结点只有一个next指向后面的结点。而双向链表支持两个方向,每个结点有执行下一个结点的next后继指针,也有指向前面结点的前驱指针prev。
双向链表需要额外的两个空间,用来存放后继结点和前驱结点地址。所有同样多的数据双向链表要比单向链表更浪费内存空间。删除给定指针指向的结点,我们找到了要删除的结点,但是删除某个节点,我们必须要知道它的前驱结点,这个时候双向链表的时间复杂度为O(1),而单向链表无法获取前驱结点,必须遍历循环所有结点。
双向链表对于单向链表 相当于用空间换取了时间。
如何基于链表实现 LRU 缓存淘汰算法?
我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的,当有一个新的数据被访问的时候,我们从链表的头部开始循环遍历
1 如果此数据之前已经被缓存到链表中,我们遍历得到这个数据的结点,并从原来的位置进行删除,然后再插入到链表头部
2 如果此数据不在链表中
1)如果缓存未满,则直接将此结点插入到链表头部
2)如果缓存已满,则将链表尾部删除,将新数据插入链表头部
本文参考自极客时间王争老师的课程讲解。