数据结构与算法之美第四天

118 阅读6分钟

链表:如何实现LRU缓存淘汰算法?

链表:LRU缓存淘汰算法

缓存是一种提高数据读取性能的技术,常见的有CPU缓存,数据库缓存。浏览器缓存等等。

当缓存满的时候哪些数据应该被清理,哪些应该被保留?常见的策略有三种:
1.先进先出策略FIFO
2.最少使用策略LFU
3.最近最少使用策略LRU

例子:书房书满了,需要大扫除,我们会选择的策略似乎和以上策略神似

数组与链表

区别
底层的存储结构

image.png

从上图分析,数组需要一块连续的内存空间来存储,如果申请100MB的内存空间如果不是连续的,数组申请就会失败;链表不会,链表他是通过“指针”,将一组零散的内存块串起来使用的。如果申请100MB的大小的链表,根本不会有问题

常见的三中链表结构-单链表、双链表和循环链表

单链表

image.png

链表:通过指针将一个个不连续的内存块连接在一起,每一个内存块称为结点,内存块中不光有存储数据,还要记住下一个结点的地址,我们称为后继指针next 第一个结点叫做头结点,最后一个结点叫做尾结点,尾结点指向一个空地址null,表示链表上最后一节点

链表也支持数据的查找、插入和删除操作

数组的插入删除,因为数据内存的连续性,操作起来他的时间复杂度为O(n),链表本身就不是连续的,所以他插入和删除数据的时候特别快,只考虑相邻结点的指针改变,所以对应的时间复杂度是O(1)

image.png

链表的查询比较复杂,因为存储不连续,不想数组使用寻址公式就可以得到计算对应内存地址。需要一个结点一个结点的去找。

例子:

一个队伍,每个人只知道他前面的人和后面的人是谁,但是不知道多少号,那么找第K位人的时候就需要从头一个个的遍历报号,链表的随机访问性没有数组好,它的时间复杂度是O(n)

循环链表

image.png 如图所示:循环链表只是将最后一个结点不是执行空地址null,而是指向第一个结点头结点,这样收尾相连叫做循环链表

双向链表

image.png 双向链表它是支持两个方向,每一个结点都有一个后继指针next,还会有一个前驱指针prev指向前面结点,所以双向链表需要额外的两个空间存储后继指针next,和前驱指针prev,所以双向链表比单项链表占的空间多,但它的优势在于可以双向遍历。

思考问题,那么单链表和双向链表适合女解决哪些问题?
删除操作

从链表中删除一个数据外无乎这两种情况: 删除结点中“值等于给定值”的结点
删除给定指针指向的结点\

对于第一种情况:不管是单链表还是双向链表,都是要去遍历找到值等于给定值得结点然后删除,这个过程时间复杂度是O(n),删除操作的时间复杂度是O(1),所以不管是单链表还是双向链表,根据时间复杂度分析中的加法法则。总的时间复杂度=查询时间复杂度+删除时间复杂度=O(n); 对于第二种情况:我们已经找到要删除的结点,但是删除某个结点q需要知道其前驱结点,而单链表并不支持获取前驱结点,所以为了找到前驱结点,还是要从头遍历,知道p->next=q(备注:假设p指针指向A,q指针指向B,p->next=q,表示将A与B连接起来)那就说明p是q的前驱结点
但是对于双向链表来说删除给定指针指向的结点,因为双向链表本身已经存储了前驱指针prev和后继指针next,所以不用再去遍历找prev,所以删除的时间复杂度O(1)同理插入也是一样道理

查询效率

对于一个有序链表,双向链表要比单链表效率更好,因为我们可以记录上一次查询的位置p,每一次查询的值都会与p的大小关系决定是网签查询还是往后查询,所以平均只需要查询一般的数据。

空间换时间

当内存充足的时候我们就要优化代码,选择空间复杂度相对复杂度相对比较高,但时间复杂度比较低的算法或者数据结构,相反,如果内存紧缺,比如代码跑在手机上,或者单片机上。这个时候我们就要反过来用时间换空间

总结

对于程序执行比较慢,可以通过消耗内存来进行优化,而消耗内存比较大的可以通过消耗时间来降低内存消耗。 #####双向链表

image.png

链表VS数组性能大比拼

image.png 数组因为是连续的所以可以借助cpu的缓存机制,预读数组中的数据,所以访问效率更高,而链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。 数组由于占用连续空间,所以分配的空间必须预先指定且固定,扩容时候只能拷贝,同时容易OOM,而链表本身没有大小限制,天然支持动态扩容
ArrayList虽然支持动态扩容,但他是被封装了的数组,当一个数组没有空闲空间时,就会申请更大的空间,将数据拷贝过去,而拷贝数据操作也是非常耗时的
但链表的每一个结点都会消耗额外的内存空间存储一个指向下一个结点的指针,内存消耗会翻倍,而且链表进行频繁的插入,删除操作,会导致频繁的内存申请和释放,容易造成内存碎片,会导致频繁的GC

开篇解答 如何基于链表实现LRU缓存淘汰算法?(最近最少是用策略)

假设维护一个有序单链表,越靠近链表尾的结点是越早之前访问的,当有一个新的数据被访问时,我们从链表投开始顺序遍历链表。 LRU的算法思路是真的简单,概括下: 使用定长链表来保存所有缓存的值,并且最老的值放在链表最后面 当访问的值在链表中时: 将找到链表中值将其删除,并重新在链表头添加该值(保证链表中 数值的顺序是从新到旧) 当访问的值不在链表中时: 当链表已满:删除链表最后一个值,将要添加的值放在链表头 当链表未满:直接在链表头添加