这一节讲一下如何用链表实现 LRU 缓存淘汰算法。
Linked list is a linear collection of data elements, whose order is not given by their physical placement in memory. Instead, each element points to the next.
这是维基百科给出的链表的定义,链表是一种存储结构上非连续、非顺序的数据结构,链表元素遍历依靠每个节点上的指针。每个节点分为数据域和指针域两部分。
常见链表结构
常见的链表结构有四种种:单链表、循环链表、双向链表和双向循环链表。
单链表和循环链表的每个节点分为两部分,一是存放数据的部分,二是后继指针(用于存放另一节点的地址),如图所示。
单链表的每个节点只知道后面一个节点的地址,而不知道前面节点的地址,因此,如果查找一个节点的上一节点地址,需要再遍历一次整个链表。需要注意的是:单链表的尾节点指向一个空指针(NULL)。
循环链表是在单链表的基础上,尾节点不指向空指针,而是指向头节点,形成一个环形回路,可以用在解决约瑟夫问题上。
双向链表的每个节点(除了头节点)有三部分:前驱指针、数据域、后继指针。双向链表的优点在于,它让链表中的每个节点,既能知道上一节点的地址,也能知道下一节点的地址,双向链表的结构如图所示。
特别的,循环链表和双向链表可以组合成双向循环链表,如图所示。
常见的缓存算法
这篇文章写得很详细,这里我就偷个懒,先不再写了。
比较数组和链表
在时间复杂度方面,数组和链表在不同的应用场景下有差异。
对于数组,插入、删除操作的时间复杂度为 O(n),根据下标随机访问的时间复杂度为 O(1)。
对于链表,插入、删除操作的时间复杂度为 O(1),随机访问的时间复杂度为 O(n)。
除了时间复杂度方面的比较,在实际的软件开发中,数组比较简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,访问效率更高。而链表在内存中并不是连续存储,对 CPU 缓存不友好,没办法实现有效的预读。
数组的缺点是大小固定,一经声明就要占用整块连续的内存空间。如果声明的数组过大,可能导致内存不足。如果声明的内存过小,可能出现不够用的情况,这时再申请一个更大的内存空间,就会涉及到数据的拷贝,非常耗时。而链表没有大小的限制,支持动态扩容。
那什么时候用数组或者链表比较好呢?如果代码对内存的要求比较高,就选用数组,因为存储相同的数据,链表的每个节点要多存储一份或两份指向其他节点的指针,内存消耗会翻倍,并且在进行频繁的插入或删除操作时,会导致频繁的内存申请和释放,容易造成内存碎片。
用链表实现 LRU 缓存淘汰算法
LRU 的全称是 Least Recently Used,直译就是最近最少使用,就是最近一段时间没有被访问到,那么之后被访问的概率就会比较小。下面是 LRU 缓存淘汰算法的描述。
1、当有一个新的数据被访问时,如果该数据在缓存链表中,就删除该数据所在的节点,然后将该节点插入到表头。
2、如果该数据不在缓存链表中,会有两种情况。
- 链表未满时,将该数据节点插入到表头
- 链表满时,先将链表的尾节点删除,再讲新数据节点插入到头部。
这就是 LRU 缓存淘汰算法实现的大体思路,看着是挺简单,但是手写这个算法还是感觉有点懵,目前还写不出来,容我想一晚上。
今天先这样。