链表
基于链表实现LRU缓存淘汰算法
常见的缓存淘汰算法有:FIFO 先进先出,LFU最少使用,LRU最近最少使用
链表的底层逻辑
链表和数组不同,不需要一块连续的内存空间,通过指针将一组零散的内存块(也称为节点)串联起来使用
链表中为了将所有节点串联起来,不仅存储了数据本身还记录了下一个节点的地址的指针叫做next指针;
并且把第一个节点称为头节点,记录链表的基地址;最后一个节点是为节点,记录最后一个节点它的next指针指向null
定义一个链表
1 public class LinkedList {
2 public class Node {
3 public int data; //假设存储是int类型的数据
4 public Node next;
5 }
6 private Node head=null;
7 }
链表的查找插入删除
查找
1 public Node find (int val){
2 Node p=head;
3 while (null!=p&&p.data!=val){
4 p=p.next
5 }
6 return p;
7 }
插入
1 void insert(Node b,Node x){ //在节点b后面插入节点x
2 if (b==null){ //在链表头部
3 x.next=head;
4 head=x;
5 }else{
6 x.next=b.next;
7 b.next=x;
8 }
9 }
删除
1 void remove (Node a,Node b){
2 if (a==null){ //如果待删除的是头节点
3 head=head.next;
4 }else {
5 a.next=a.next.next;
6 }
7 }
remove方法的时候,在我们知道b的前驱节点a的情况下,能快速删除节点,时间复杂度是O(1)
由于链表的数据是非连续的,想要查找到第k个元素,我们需要从链表的头节点开始,依次遍历,直到找到为止。
1 public Node get(int k){
2 Node p=head;
3 int i=0;
4 while (p!=null&&i!=k){
5 i++;
6 p=p.next
7 }
8 return p;
9 }
因此在链表中访问第k个数据的操作的时候,没有数组高效,时间复杂度是O(n)
循环链表
它与单链表的唯一区别就是尾节点,单链表的尾节点的next指向null循环链表指向了头节点。
循环链表的优点是从链尾遍历到链头比较方便。
双向链表
双向链表支持两个遍历方向有prev指针(前驱节点)和next指针
定义一个双向链表
1 public class DoublyLinkedList{
2 public class Node {
3 public int data;
4 public Node prev;
5 public Node next;
6 }
7 private Node head=null;
8 }
删除值等于给定值的节点
无论单链表还是双向链表,都需要从链表的头节点开始遍历依次对比直到找到值等于给定值的节点;
1 //单向链表
2 public void remove(int val){
3 Node q=head;
4 Node p=null; //q的前驱节点
5 while (q!=null&&q.data!=val){
6 p=q;
7 q=q.next;
8 }
9 if(q!=null){ //找到值等于val的节点q
10 if (p===null){ //如果找到的是头节点
11 head=q.next
12 }else{
13 p.next=q.next
14 }
15 }
16 }
17
18 //双向链表
19 public void remove (int val){
20 Node q=head;
21 while (q!=null&&q.data!=val){
22 q=q.next;
23 }
24 if (q!=null){ //找到值等于val的节点q
25 if (q.prev==null){ //如果找到的是头节点
26 head=q.next;
27 }else{
28 q.prev.next=q.next;
29 }
30 }
31 }
尽管单纯的删除操作的时间复杂度是0(1),但是遍历是主要的耗时点。这两种链表对于删除给定值的节点,时间复杂度都是O(n)
对于删除给定指针的节点
1 //在单链表中删除
2 void remove(Node q){
3 if (q==null){
4 return ;
5 }
6 if (head==q){
7 head=q.next;
8 return ;
9 }
10 Node p=head;
11 while (p.next!=null&&p.next!=q){
12 p=p.next;
13 }
14 if (p.next!=null){
15 p.next=q.next;
16 }
17 }
18 //在双向链表删除节点q
19 void remove(Node q){
20 if (q==null){
21 return ;
22 }
23 if (q.prev==null){ //q是头节点
24 head=q.next;
25 return ;
26 }
27 }
同理,对于指定节点前面插入一个节点这样的操作,显然双向链表更快,时间复杂度是O(1)
事实上这就是典型的空间换时间的设计思想。
还有一种链表就是把循环链表和双向链表整合在一起就是双向循环链表
对于LRU缓存淘汰算法
我们维护一个有序链表,越是靠近链表尾部的节点存储越早访问的数据,当有一个新数据被访问时,我们次部分链表头节点开始遍历链表
如果此数据之前被缓存在链表中,那么我们遍历得到这个数据对应的节点,将其从原来的位置删除,插入到链表头部
如果没有被缓存在链表中,并且缓存未满,则将新的数据节点直接插入到链表头部
如果缓存满了,则删除链表尾节点,将新的数据节点插入链表头部。
目前缓存访问时间复杂度无论满不满都是O(n)
实际上我们可以引入哈希表来记录每个数据在链表中的位置,就可以把时间复杂度降到O(1)
避免指针丢失
我们需要在节点a和相邻的节点b中间插入x节点
1 a.next=x;
2 x.next=a.next;
第一行代码执行玩之后,a节点的next指针已经不再指向节点b了,而是x
第二行代码相当于把(a.next)也就是x赋值给x.next,就是自己指向自己。
正确做法是这两行代码换位置。所以我们要先记录后一个节点的位置,防止数据丢失。