4. 链表和缓存淘汰策略
Posted export
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4. 链表和缓存淘汰策略相关的知识,希望对你有一定的参考价值。
几个常见的缓存淘汰策略
缓存是提高数据读取性能的技术,在硬件软件领域都有广泛的应用。如 CPU 缓存,数据库缓存,浏览器缓存...
缓存的大小空间占满的时候,需要作出清理,需要一个清理数据的规则。
常见的缓存清理有三种方式:
-
先进先出。理解为时间排序,时间最早的最先清理。 -
最少使用。按照出现频次清理。 -
最近最少使用,一个范围内最少使用清理。
避免出现英文名称不认识,也方便记忆,对应的英文为:
FIFO(First in, sirst out)
LFU(Least Frequently Used)
「LRU(Least Recently Used)」
可联想实际的收藏品对比策略,如小时候的玩具丢弃决策,收藏书丢弃决策...
这是一个引入使用场景,可以用单链表来解决这个问题。
链表结构
链表结构,与数组的关键性区别:
-
存储结构 -
访问和操作复杂度
单链表及插入和删除
链表也支持数据的查找,插入和删除。
在数组插入和删除的时候,为了保证内存空间的连续,需要做大量的数组搬移。
插入包含数据的结点 newnode 位置到 Km 结点和 Kn 个结点之间,那么操作就是:Km 结点的 next 指向 newnode,newnode 的 next 指向 Kn。
对应的删除,修改上一个元素的 next 指向,直接指向原本的下一个结点。
「插入和删除只需要修改相邻节点的指针改变,所以时间复杂度就是O(1)」。
场景:从一个没有编号的队伍找出某人,每个人只知道后面的人是谁。
那么只能从第一个人开始报后一个人的名字,挨个进行,才能找到目标。
「访问元素的时候只能挨个遍历,所以链表的随机访问的时间复杂度就是O(n)」。
循环链表
循环链表是一种特殊的单链表,区别就是尾结点指向了链表的头结点。
也就是说,对循环链表,从链表的头结点进,按照每个结点的nex t指针遍历,会一直循环访问链表中的元素。
双向链表
单链表只有下一个元素的指针,那么要找前面的元素就会很麻烦。所以,就有了双向链表,对应的除了后继指针 next,还有一个前驱指针 prev,指向前一个元素结点。
将单链表的访问从单向变成了双向,使得在链表中找到前驱结点的时间复杂度也是O(1)。
实际问题
单链表的插入和删除的时间复杂度是O(1),但是这只是在单纯的讨论这一个删除和插入操作。实际的情况一般是
-
删除“值等于某个值”的结点 -
删除给点指针指向的结点
对于情况一,需要遍历判断找到满足条件的值,才能进行删除。而链表遍历的复杂度是O(n),删除操作是O(1),所以实际的总时间复杂度是O(n),不论是单链表和双向链表。
而对于情况二,单链表依然需要遍历,找到 p->next == q ,找到前驱结点,才能改变前驱结点的后继指针来完成删除,复杂度依然是O(n)。双链表则可以直接找到前驱结点,然后修改后继指针实现删除,复杂度直接将为O(1)。
同样的,插入操作也是一样。综合来说,双向链表虽然每个结点要存储两个指针,但时间效率要比单链表高。
这其实也是一种「空间换取时间」的思想。
对应的还有一种双向循环列表,头结点的 prev 指针指向尾结点,尾结点的 next 指针指向头结点。
链表 & 数组对比
对比两种数据结构
时间复杂度 | 数组 | 链表 |
---|---|---|
插入删除 | O(n) | O(1) |
随机访问 | O(1) | O(n) |
数组使用连续内存空间,CPU缓存机制可以预读数据,访问效率高。但可能存在需要扩容的情况,对数据操作也不够友好。
而链表不需要整块内存,支持动态扩容,数据操作更方便。但招用内存更高。两者各有优缺。
链表版本的 LRU 缓存淘汰策略
维护一个有序单链表,越靠近尾部的结点是越早之前访问的。一个新的数据被访问的时候,从链表头开始遍历:
-
如果数据已经在链表的话,删除对应的结点,并重新插入到链表头部。 -
如果没有在链表中的话: -
缓存未满的时候,插入到链表的头部(表示最近使用一条数据) -
缓存已满的时候,先删除链表尾结点,再将新数据插入到链表头部
每次访问新数据的时候,都必然伴随着一次遍历,然后才是删除和插入操作,所以复杂度就是O(n)。
- END -链表只是引入这个问题模型,后续使用「散列表」,对应的复杂度是O(1)。
以上是关于4. 链表和缓存淘汰策略的主要内容,如果未能解决你的问题,请参考以下文章