06.链表(上)

06.链表(上):如何实现LRU缓存淘汰算法?

markdown文件已上传至github

缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比较常见的有CPU缓存、数据库缓存、浏览器缓存等等。

当缓存被用满时哪些数据应该被清理出去,哪些数据应该被保留,是由缓存淘汰策略来决定的,常见策略有三种:先进先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used)。

1.五花八门的链表结构

img

从图中很直观的地可以看到数组需要一块连续的内存空间来存储,即使剩余空间足够大,但是不连续,仍然会申请失败。

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

链表结构五花八门,如:单链表、双链表、循环链表。

1.1单链表

每个链表的节点存储:**数据、后继指针next(**下一个节点的地址)

img

如图,有两个结点是比较特殊的。链表第一个结点称为头结点,用来记录链表的基地址,有了它就可以遍历得到整个链表。链表最后一个结点称为尾结点,它的next指针指向一个空地址NULL。

单链表的插入和删除

链表中插入或者删除一个数据,不用为了保持内存的连续性而搬移结点,所以插入和删除时非常快速的。

img

数组插入和删除因为需要做大量数据搬移,所以时间复杂度为O(n)。而链表插入和删除只需考虑相邻结点的指针改变,所以时间复杂度为O(1).

数组根据下标查找时间复杂度为O(1),不知道下标的情况下,如果数组是有序的,用二分查找时间复杂度为O(logn)。链表的随机访问没有数组好,当我们需要知道第k个数据是什么的时候,我们需要从头节点一个一个往下查找,时间复杂度为O(n).

1.2 循环链表

循环链表是一种特殊的单链表,将单链表的尾结点的指针指向链表的头节点,就成了循环链表。

img

**循环链表的优点:**从链尾到链头比较方便。当要处理的数据具有环形结构特点时,就适合采用循环链表。如著名的约瑟夫问题。尽管用单链表也可以实现,但是用双链表的话代码要简洁很多。

1.3 双向链表

img

双向链表的结点比单链表的结点多一个前驱指针prev(指向前一个结点)。虽然双向链表比单链表占用更多的内存空间,但可以支持双向遍历,带来了操作的灵活性。

1.3.1 删除操作

从结构上看,双向链表能在O(1)时间复杂度的情况下找到前驱结点,这个特点使它在某些情况下的插入、删除操作都比单链表简单、高效。

单链表删除、插入已经是O(1)时间复杂度了,双链表为什么比它还高效?

在实际软件开发中,从链表中删除一个数据无非两种情况:

​ 1.删除”值等于某个给定值”的结点。

​ 2.删除给定指针指向的结点。

第一种情况,单链表和双链表都需要从头结点开始一个一个开始遍历。删除操作复杂度为O(1),但是遍历查找的时间复杂度为O(n)。

第二种情况,我们已经知道要删除结点的指针了,就不用从头开始一个一个遍历。但是删除某个结点需要知道前驱节点这样才能把链表连起来。而单链表找前驱结点需要从头开始往后遍历(时间复杂度O(n)),双链表只需用前驱指针(时间复杂度(O(1))。

同理,在某个结点前插入一个节点双向链表时间复杂度O(1),单链表O(n).

除了插入、删除操作有优势之外,对于一个有序链表,双向链表的按值查询效率也要比单链表高一些。我们可以记录上次查找的位置p,将下次查找的值与其值比较大小,决定往前还是往后查找,所以平均只需要查找一般的数据。

以上就是为什么双向链表尽管比较废内存,但仍然比单链表使用更加广泛的原因。Java中的LinkHashMap容器就使用到了双向链表数据结构。

**这也是一个用空间换时间的设计思想。**实际开发时按照实际情况进行取舍。缓存就是利用了空间换时间这种设计思想,每次查找数据都要访问一次硬盘,会比较慢,通过缓存技术,将数据加载至内存,虽然会比较消耗内存空间,但每次数据查询的速度就大大提高了。

1.4双向循环链表

img

2.链表VS数组性能大比拼

img

数组简单易用,在实现上使用的是连续的存储空间,可以借助CPU缓存机制预读数组中的数据,所以访问效率更高。链表不是连续存储,对缓存机制不友好,没办法有效预读。

**数组缺点:**大小固定,一经声明就要占用整块连续内存空间,如果声明的数组过大,可能会内存不足,如果国小,可能出现不够用的情况,这时只能申请一个更大的内存空间(如:容器),把原数组拷贝进去,很费时。

链表本身没有大小限制,天然支持动态扩容,这是与数组最大的区别。

如果开发中代码对内存的使用非常苛刻,那么数组更加适合,因为链表没个结点需要存储指针,造成内存消耗翻倍。

对链表进行频繁的插入、删除操作会导致频繁的内存申请和释放,容易造成内存碎片。

3.解答开篇:如何基于链表实现最近最少使用策略LRU算法?

**思路:**维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当要访问一个数据时,从链表头开始遍历链表。

1.如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来位置删除,然后插入到链表的头部。

2.如果此数据没有在缓存链表中,又可以分为两种情况:

  • 如果此时缓存未满,则将此节点直接插入到链表的头部。
  • 如果此时缓存已满,则链表尾结点删除,将新数据结点插入链表头部。

这样就用链表实现了一个LRU缓存。

**缓存访问的时间复杂度:**不管缓存满没满,我们都要遍历一遍链表,所以缓存访问的时间复杂度为O(n)。

我们可以继续优化这个实现思路,比如引入散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到O(1)。

4.思考题

如果字符串是基于单链表来存储的,如何判断该字符串是一个回文串?

方法一:

1.利用双指针技巧,一个指针p一次走两个结点,一个指针q一次走一个结点,同时将链表进行反转。当p走到末尾时,q刚好在中间结点。

2.然后遍历前半段、后半段并进行比较。

时间复杂度O(n),空间复杂度O(1)。

方法二:

1.第一次遍历将数据压入栈。

2.第二次遍历链表同时栈顶元素出栈并进行比较。

时间复杂度O(n),空间复杂度O(n)。

5.参考

这个是我学习王争老师的《数据结构与算法之美》所做的笔记,王争老师是前谷歌工程师,该课程截止到目前已有87244人付费学习,质量不用多说。

在这里插入图片描述

截取了课程部分目录,课程结合实际应用场景,从概念开始层层剖析,由浅入深进行讲解。本人之前也学过许多数据结构与算法的课程,唯独王争老师的课给我一种茅塞顿开的感觉,强烈推荐大家购买学习。课程二维码我已放置在下方,大家想买的话可以扫码购买。

在这里插入图片描述

本人做的笔记并不全面,推荐大家扫码购买课程进行学习,而且课程非常便宜,学完后必有很大提高。

nLmNzZG4ubmV0L3N1cHJlbWVfMQ==,size_16,color_FFFFFF,t_70" alt=“在这里插入图片描述” style=“zoom:50%;” />

本人做的笔记并不全面,推荐大家扫码购买课程进行学习,而且课程非常便宜,学完后必有很大提高。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/supreme_1/article/details/107587375