笔记:数据结构与算法之美 06 | 链表(上):如何实现LRU缓存淘汰算法?

LRU缓存淘汰算法

优先淘汰最近最少使用的数据

  • Least 最少
  • Recently 最近
  • Used 使用

链表和数组底层存储结构不同

  • 数组需要一块连续的内存空间来存储
  • 链表不需要,他通过指针将一组零散的内存块串联起来使用

五花八门的链表结构

  • 单链表
  • 双向链表
  • 循环链表

单链表

每一组零散的内存块称之为结点
记录下个结点地址的指针叫作后继指针next
有两个特殊结点

  • 第一个结点 头结点,记录链表的基地址
  • 最后一个结点 尾结点,指针不是指向下一个结点,而是指向一个空地址NULL
    笔记:数据结构与算法之美 06 | 链表(上):如何实现LRU缓存淘汰算法?_第1张图片

插入和删除操作

  • 链表时间复杂度是O(1),只需要考虑相邻结点的指针改变
  • 数组的是O(n),因为为了保持内存数据连续性,需要做大量的数据搬移
    笔记:数据结构与算法之美 06 | 链表(上):如何实现LRU缓存淘汰算法?_第2张图片

随机访问第k个元素,链表需要根据指针对应结点依次遍历,所以时间复杂度是O(n)

循环链表

循环链表是一种特殊的单链表
和单链表唯一的区别在于尾结点指向头结点
笔记:数据结构与算法之美 06 | 链表(上):如何实现LRU缓存淘汰算法?_第3张图片
当数据具有环形结构特点时,适合采用循环链表

著名的约瑟夫问题,用循环链表解决,代码会简介很多(这里不展开讨论,浅尝辄止即可)

双向链表

实际开发中,比较常用
结构角度

  • 单向链表只有一个方向,结点只有一个后继指针next指向后面的结点
  • 双向链表有两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点
    笔记:数据结构与算法之美 06 | 链表(上):如何实现LRU缓存淘汰算法?_第4张图片

空间角度
因此双向链表需要额外的两个空间来存粗后续和前驱结点的地址,占用更多的内存空间

时间复杂度

前提芝士:理论上单链表的增删时间复杂度为O(1),但实际操作中,由于删除和新增需要先定位到具体的位置,所以整个操作时间复杂度是O(n)

一句话:双向链表增删更高效,有序情况下,按值查找效率也比单链表高(不是很确定)

举例论证增删
ex:删除操作
情况:

  • 删除结点中“值等于某个给定值”的结点
  • 删除给定指针指向的结点

第一种情况:
不管是单链表还是双向链表,为了查到“值等于某个给定值”的结点,都需要从头结点开始一个一个依次遍历对比,直到找到,然后通过指针操作将其删除。
尽管删除操作时间复杂度O(1),但是遍历查找时间复杂度是O(n),所以此情况单链表和双向链表增删总时间复杂度为O(n)

第二种情况:
已经找到要删除的结点,但是删除某个结点q需要知道其前驱结点,

  • 而单链表并不支持直接获取前驱结点,所以为了找到前驱结点还是需要从头遍历,直到p->next=q,说明p是q的前驱结点,此情况单链表增删增删总时间复杂度是O(n)
  • 但是双向链表的结点已经保存了前驱结点的指针,不需要遍历,所以此情况双向链表增删增删总时间复杂度是O(1)

举例论证查找
有序链表情况下,可以记录上次查找的位置p(这个很关键,我理解可能是数据结构里存储了最后一次查找的数据),每次查询时,根据要查找的值与p的大小关系,决定往前还是往后(单链表只能往后,所以这里单链表会有缺陷),所以平均只需要查找一半的数据。

链表vs数组

笔记:数据结构与算法之美 06 | 链表(上):如何实现LRU缓存淘汰算法?_第5张图片
选择用哪个,不能局限于时间复杂度
数组连续内存空间,可以借助cpu的缓存机制,预读数组中数据,所以访问效率高
链表内存不连续,没办法有效预读

数组大小固定,声明时过大过小都不太友好
链表没有大小限制,天然支持动态扩容

解答开篇

问:如何基于链表实现LRU缓存淘汰算法?
答:维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的
当有新数据被访问时,从头开始顺序遍历
1.如果数据已存在链表中,遍历得到对应结点,并将其从原来位置删除,然后再插入到链表头部
2.如果数据不存在

  • 缓存未满:此结点直接插入到头部
  • 缓存已满:删除链表尾部结点,将新数据插入链表头部
    总结:不管缓存有没有满,都要遍历一遍链表,所以缓存访问时间复杂度是O(n)

你可能感兴趣的:(链表,数据结构,算法)