06 ~07| 链表

经典链表应用场景:LRU缓存淘汰算法。

缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的 CPU 缓存、数据库缓存、浏览器缓存等等。缓存的大小有限,当缓存被用满时候,哪些数据应该被清理,哪些数据应该被保留,需要不同的缓存淘汰策略。常见的三种策略:

  • 先进先出策略 FIFO(First In,First Out)
  • 最少使用策略 LFU(Least Frequently Used)
  • 最近最少使用策略 LRU(Least Recently Used)

数组和链表的区别:

数组必须要一块连续的内存,如果申请一个100MB的数组,但是内存中没有连续的足够大的存储空间,即使剩余可用空间大于100MB,仍然申请失败;而链表恰恰相反,它并不需要一块连续的内存空间,它通过“”指针“将一组零散的”内存串联起来使用。

三种最常见的链表结构:单链表,双向链表,循环链表。

循环链表和单链表唯一的区别就是尾结点指向链表的头结点,整个像环一样首尾相连。

循环链表的优点就是从链尾到链头比较方便,当要处理的数据具有环型数据结构时,就特别适合采用循环链表,比如著名的约瑟夫问题。

 

在实际软件开发中,更加常用的链表结构:双向链表。

双向链表支持两个方向,每个节点不止有一个后继指针next,还有一个前驱指针prev。

从图中可以看出来,双向链表需要额外的两个空间来存储后继节点和前驱节点的地址。但是支持双向遍历,这样也带来了双向链表操作的灵活性,相比于单链表,双向链表适合解决哪种问题呢?

从结构上看,双向链表可以支持O(1)时间复杂度的情况下找到前驱节点,正是这样的特点,使双向链表在某些情况下插入,删除都要比单链表高效简单。

note:刚刚说单链表的插入删除操作时间复杂度已经是O(1),双向链表还能再如何高效呢?但是这种说法实际上不是准确的,或者有先决条件,来分析一下链表的两个操作。

删除操作的两种情况:

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

第一种情况,查找到给定值的结点都是O(n),删除为O(1),所以删除给定值的时间复杂度是O(n)。单链表和双向链表没有区别。

第二种情况,双线链表由于可以知道其前向链表,所以删除O(1)就搞定了;然后单向链表还要去寻找其前向链表,遍历的话,所以时间复杂度为O(n)。

插入操作的情况:

如果我们希望在链表的某个制定结点前面插入一个结点,双向链表可以在O(1)的时间复杂度搞定,而单项链表需要O(n)的时间复杂度。

除了插入、删除操作有优势之外,对于一个有序链表,双向链表按值查询的效率也比单链表高一些。因为我们可以记录上次查找的位置p,每次查询时,根据要查找的值和p的大小关系,决定向前还是往后。

实际上,这里有一个更加重要的知识点需要你掌握,那就是用空间换时间的设计思想,当内存空间充足时候,如果为了追求代码的执行速度,可以采用空间复杂度相对高,但是时间复杂度相对很低的算法或者数据结构。

 

缓存实际上就是利用空间换时间的设计思想,如果我们把数据存储在硬盘上,会比较节省内存,但是每次查找数据都要询问一次硬盘就会比较慢,但是如果我们通过缓存技术,事先将数据加载在内存中,虽然会比较耗费内存空间,但是每次数据查询的速度就大大提高了。

如果把这两种链表整合在一起就是一个新的版本:双向循环链表

 

链表 VS 数组性能大比拼

数组和链表的对比,并不能局限于时间复杂度,而且在实际的软件开发中,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储结构。

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

如果代码对内存的使用非常苛刻,那么数据就更适合你,因为链表中每个节点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍,并且对链表进行频繁的插入删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片。

 

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

维护一个有序单链表,越靠近链表首部的结点是越早之前访问的,当有一个新的数据被访问的时候,我们从链表头开始顺序遍历链表。

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

2.如果此数据没有在缓存链表中

如果此时缓存未满,则将此节点直接插入到链表的尾部;

如果存储已满,则链表首结点删除,将新的数据结点插入链表的尾部。

因为不管缓存有没有满,我们都需要遍历一遍链表,所以基于链表的实现思路,缓存访问时间复杂度是O(n)。

改进:可以用hash table来记录每个数据的位置,将缓存访问的时间复杂度降到O(1)

 

扩展问题:

如何用数组来实现LRU缓存淘汰策略?

每次搬运的时间复杂度过大,很麻烦。

思考:

如何判断一个字符串是否是回文字符串的问题?假设该字符串是通过单向链表实现的。

快慢指针,简单题。

 

重点 :只要是涉及到添加、删除的链表问题,创建dummy结点的方法可以省去很多边界条件判断。

留意边界条件处理:

如何写出bug free的代码,以链表为例子,检查其边界条件:

  • 如果链表为空时,代码是否能正常工作?
  • 如果链表只包含一个结点时,代码是否能正常工作?
  • 如果链表只包含两个结点时,代码是否能正常工作?
  • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

写链表代码是最考验逻辑思维能力的。因为,链表代码到处都是指针的操作、边界条件的处理,稍有不慎就容易产生 Bug。链表代码写得好坏,可以看出一个人写代码是否够细心,考虑问题是否全面,思维是否缜密。所以,这也是很多面试官喜欢让人手写链表代码的原因。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值