学习链表有什么用?引出用链表实现LRU缓存淘汰算法
链表:通过指针将一组零散的内存块串联在一起,其中我们把内存块叫做链表的节点
1、缓存:是一种提高数据读取性能的技术
缓存淘汰策略:FIFO先进先出策略,LFU(Least Frequently Used)最少使用策略,LRU最近最少使用策略(Least Recently Used)
LRU:活在当下。比如在公司中,一个新员工做出新业绩,马上会得到重用。
LFU:以史为镜。还是比如在公司中,新员工必须做出比那些功勋卓著的老员工更多更好的业绩才可以受到老板重视,这样的方式比较尊重“前辈”。
2..双向链表支持O(1)时间复杂度找到前驱节点,所以比单链表插入删除更加高效。单链表插入删除已经是O(1)的时间复杂度是理论上的我们来看删除的二种情况
1、删除节点中 “值等于给定值”的节点:单,双链表都需要 从头遍历,找到值等于给定值的节点,然后删除 。主要耗时在查找,所以时间复杂度位O(n)
2、删除给定指针指向的节点: 我们已经知道要删除的节点p,单链表需要从头遍历找到其前驱节点q->next=q,在删除节点时间复杂度为O(n); 双向链表就可以直接获取其前驱节点,在删除,时间复杂度为o(1)
双向链表查找速度也优于单向链表,有序链表中,双向链表可以前后遍历,查找数据,只需要查找一半数据即可
3、链表 VS 数组性能大比拼
数组:
优点:简单易用,在实现上使用的是连续的内存空间,可以借助 CPU的缓存机制,预读数组中的数据,所以访问效率高,
缺点:数组一经声明就需要占据大量连续内存空间,如果声明数组过大,系统可能没有连续内存空间分配
链表:
缺点:在内存中并不是连续存储,所以对CPU缓存不友好,没办法预读
总结: 如果对内存 的使用非常苛刻,建议使用数组,链表每个节点都需要耗费额外空间存储下一个节点的指针,内存消耗翻倍,频繁删除插入容易造成内存碎片
思想:
对于 执行较慢的程序可以通过消耗更多的内存(空间换时间)来进行优化
而消耗过多的内存 程序,可以消耗更多的时间(时间换空间)来降低内存的消耗
4、如何基于链表实现LRU缓存淘汰算法?
我们维护一个有序单链表,越靠近底部的节点是越早之前访问的,当有一个新数据被访问时,我们从链表头开始顺序遍历链表
1.如果该数据之前已经在链表中,遍历得到该数据,删除其所在位置,然后插入链表头部
2.如果该数组没有在缓存链表中,
1)缓存未满,直接插入链表头部
2)缓存已满,删除链表尾部数据,把该数据插入头部
5、如何利用数组实现 LRU 缓存淘汰策略呢?
方式一:首位置保存最新访问数据,末尾位置优先清理
当访问的数据未存在于缓存的数组中时,直接将数据插入数组第一个元素位置,此时数组所有元素需要向后移动1个位置,时间
复杂度为O(n);当访问的数据存在于缓存的数组中时,查找到数据并将其插入数组的第一个位置,此时亦需移动数组元素,时间
复杂度为O(n)。缓存用满时,则清理掉末尾的数据,时间复杂度为O(1)。
方式二:首位置优先清理,末尾位置保存最新访问数据
当访问的数据未存在于缓存的数组中时,直接将数据添加进数组作为当前最有一个元素时间复杂度为O(1);当访问的数据存在于
缓存的数组中时,查找到数据并将其插入当前数组最后一个元素的位置,此时亦需移动数组元素,时间复杂度为O(n)。缓存用满
时,则清理掉数组首位置的元素,且剩余数组元素需整体前移一位,时间复杂度为O(n)。(优化:清理的时候可以考虑一次性清
理一定数量,从而降低清理次数,提高性能。)
6、如何写出链表代码
技巧一:理解指针或引用的含义
指针中存储了这个变量的内存地址,指向这个变量,通过指针可以找到这个变量
示例:
p—>next = q; 表示p节点的后继指针存储了q节点的内存地址。
p—>next = p—>next—>next; 表示p节点的后继指针存储了p节点的下下个节点的内存地址。
技巧二:警惕指针丢失和内存泄漏
1、插入节点
在节点a和节点b之间插入节点x,b是a的下一节点,,p指针指向节点a,则造成指针丢失和内存泄漏的代码:p—>next =
x;x—>next = p—>next; 显然这会导致x节点的后继指针指向自身。
正确的写法是2句代码交换顺序,即:x—>next = p—>next; p—>next = x;
2、删除节点
在节点a和节点b之间删除节点b,b是a的下一节点,p指针指向节点a:p—>next = p—>next—>next;
技巧三:利用哨兵简化实现难度
“哨兵”节点不存储数据,无论链表是否为空,head指针都会指向它,作为链表的头结点始终存在。这样,插入第一个节点和插入其他节点,删除最后一个节点和删除其他节点都可以统一为相同的代码实现逻辑
四、重点留意边界条件处理
经常用来检查链表是否正确的边界4个边界条件:
1、如果链表为空时,代码是否能正常工作?
2、如果链表只包含一个节点时,代码是否能正常工作?
3、如果链表只包含两个节点时,代码是否能正常工作?
4、代码逻辑在处理头尾节点时是否能正常工作?
五、举例画图,辅助思考
核心思想:释放脑容量,留更多的给逻辑思考,这样就会感觉到思路清晰很多。
六、多写多练,没有捷径
5个常见的链表操作:
1、单链表反转
// 1) 单链表反转
public static Node reverse(Node list) {
Node prev = null;
Node head = null;
Node current = list;
while (current != null) {
Node next = current.next; // 暂存下一个节点
if(next==null) {
head =current;
}
current.next = prev;
prev = current;
current = next;
}
return head;
}
2、链表中环的检测
// 2) 链表中环的检测
public static boolean checkCircle(Node list) {
if (list == null) {
return false;
}
Node slow = list;
Node fast = list.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
3、两个有序链表合并
/**
* 3) 两个有序的链表合并
*
* @param la 链表a
* @param lb 链表b
* @return
*/
public static Node mergeSortedLists(Node la, Node lb) {
// 判断两个单链表是否为空
if (la == null)
return lb;
if (lb == null)
return la;
Node head = null;
Node p = la;
Node q = lb;
// 找头节点
if (p.ele < q.ele) {
head = p;
p = p.next;
} else {
head = q;
q = q.next;
}
Node r = head;
while (p != null && q != null) {
if (p.ele < q.ele) {
r.next = p;
p = p.next;
} else {
r.next = q;
q = q.next;
}
r = r.next;
}
// 判断 p、q 两个链表哪一个先遍历结束,最后将剩余的链表拼接到合成链表的最后
if (p != null) {
r.next = p;
} else {
r.next = q;
}
return head;
}
4、删除链表倒数第n个节点
// 4) 删除链表倒数第n个结点
public static Node deleteLastKth(Node list, int k) {
//将fast指针指向list单链表的开始的第一个结点
Node fast = list;
//i 用来进行计数
int i = 1;
//通过 while 循环,正想找到第 K 个结点(这里有两个判断条件,fast != null 条件的作用是要删除倒数第k个结点
//的单链表不能小于k的长度)
while (fast != null && i < k) {
fast = fast.next;
++i;
}
//如果单链表的长度小于 k ,就返回 list 单链表
if (fast == null) return list;
//前边找到第 k 个结点之后,让 slow 指向第一个结点
Node slow = list;
Node prev = null;
//判断fast指针也就是最前边的指针下一个节点是否为 null(如果为null相当于到尾部了)
while (fast.next != null) {
//如果不为 null , fast 指针向后移动一个指针,slow 指针也要移动一个
fast = fast.next;
//prev 指向移动前的结点,为了区别下方单链表的长度和 k 相等
prev = slow;
//slow 向后移动一个
slow = slow.next;
}
//这个判断是,如果单链表的长度正好等于 k ,删除倒数第K个结点也就是删除头结点。
if (prev == null) {
list = list.next;
} else {
//不为上述情况就可以用单链表的删除思想了
prev.next = prev.next.next;
}
//返回已经删除结点的链表
return list;
}
5、求链表的中间节点
// 求链表中间节点
public static Node findMidde(Node list) {
if (list == null) {
return null;
}
Node slow = list;
Node fast = list.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}