介绍链表之前,先说下三种缓存策略:
1,先进先出策略 FIFO(First In, First Out)
2,最少使用策略 LFU(Least Frequently Used)
3,最近最少使用策略 LRU(Least Recently Used)
个人理解,第三种LRU是前两种策略:FIFO 和 LFU 的结合体。
数组和链表的区别:
数组在内存中是连续存储,初始化时即确定了所占空间大小(当存满扩容时,需要重新开辟更大的连续存储空间,并进行数据迁移,也成动态扩容);另外,数组的随机访问时间复杂度是O(1),数组的随机插入、删除操作,因为往往需要做大量的数据迁移,所以时间复杂度是O(n)。
链表在内存中不需要必须是连续存储,可以是分散存储,链表数据所占空间大小是动态变化的;
链表的随机访问时间复杂度是O(n);链表单纯的插入、删除操作时间复杂度是O(1)。之所以说是单纯的,因为如果不单纯的话,还需要考虑确定插入、删除位置的消耗,我个人觉得,总体来说,链表的随机插入、删除操作的时间复杂度和数组是差不多的,属一个量级。
数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,因此对CPU的缓存不友好,没有办法有效预读。
三种常见的链表结构:
1,单链表
2,循环链表
3,双向链表
举个例子说明某些情况下,双向链表相对于单链表的优势:
从链表中删除一个数据可以分为两种情况:
- 删除节点中“值等于某个给定值”的节点
- 删除给定指针指向的节点
第一种情况:不管是单链表还是双向链表,为了查找到值等于给定值的节点,都需要从头结点开始逐一遍历对比,尽管单纯的删除操作时间复杂度为O(1),但是遍历查找是主要的耗时点,对应的时间复杂度为O(n)。根据时间复杂度分析中的加法准则,这种情况下,单链表和双向链表的总时间复杂度都是O(n)
第二种情况:这种情况,对于单链表,在删除之前,需要找到待删节点的前驱节点,这就额外需要时间复杂度为O(n)的遍历操作;而对于双向链表,由于待删节点已保存了前驱节点的指针,因此不需要像单链表那样去遍历。
基于链表实现LRU缓存淘汰算法:
我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问,我们从链表头开始顺序遍历链表:
1,如果此数据之前已经被缓存到链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
2,如果此数据没有缓存在链表中,又可分为两种情况:
- 如果此时缓存未满,则将此结点直接插入到链表头部;
- 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
这样就实现了一个LRU缓存。
思考题:
如何判断一个字符串是否是回文字符串:
如果是通过单链表来存储的,那该如何来判断是一个回文串?相应的时间空间复杂度又是多少?
答:
1,快慢指针定位中间节点
1.1 奇数情况,中点位置不需要矫正
1.2 偶数情况,使用偶数定位中点策略,要确定是返回上中位数或下中位数
1.2.1 如果是返回上中位数,后半部分串头取next
1.2.2 如果是返回下中位数,后半部分串头即是当前结点位置,但前半部分串尾要删除掉当前结点
2,从中间节点对后半部分逆序,或者将前半部分逆序
3,前后半部分逐个比较,判断是否为回文
4,逆序恢复现场
时间复杂度O(n)
空间复杂度O(1)