链表
一.常见的缓存淘汰策略:
1.先进先出策略FIFO
2.最少使用策略LFU
3.最近最少使用策略LRU
二.链表
(一)链表的定义:链表是物理存储单元上非连续的、非顺序的存储结构,它由一个个结点,通过指针联系起来的,每个结点包括数据和指针。
(二)链表结构:
1.单链表
通过“指针”将一组零散的内存块串联起来使用。内存块称为结点;记录下个结点地址的指针称为后继指针next;第一个结点为头结点,用来记录链表的基地址;最后一个结点为尾结点,指针指向NULL。
链表的插入、删除操作,只需考虑相邻结点的指针变化,不需要数据搬移,时间复杂度为O(1)。随机访问的时间复杂度为O(n)。
2.循环链表
是一种特殊的单链表。尾结点的指针指向链表的头结点。
从链尾到链头比较方便。当要处理的数据具有环形结构特点时,适合采用循环链表。例如约瑟夫问题。
3.双向链表
每个结点有两个指针,分别为后继指针next和前驱指针prev。找到前驱结点的时间复杂度为O(1)。
4.双向循环链表
三.
1.删除操作
(1)删除结点中“值等于某个给定值”的结点
从头结点开始一个一个遍历,直到找到值等于给定值的结点,再删除。删除操作时间复杂度为O(1),查找操作时间复杂度为O(n);总时间复杂度为O(n)。
(2)删除给定指针指向的结点
对于单链表来说,要从头结点开始遍历找到给定结点的前驱结点,再删除。时间复杂度为O(n)。
对于双向链表来说,可以直接找到前驱结点。时间复杂度为O(1)。
2.插入操作
在某个结点前插入一个结点,单链表要从头遍历,找到给定结点再在其前面插入,时间复杂度为O(n);双向链表的时间复杂度为O(1)。
3.有序链表的按值查询
可以记录上次查找的位置q,每次查询时根据查找的值与q相比较就知道了接下来是向前查还是向后查,平均只需要查找一半的数据。
四.用空间换时间/用时间换空间
当内存空间充足时,如果更追求代码执行速度,可以选择空间复杂度较高,时间复杂度较低的数据结构。相反则用时间换空间。
五.数组与链表比较
1.数组在实现上使用连续的内存空间,可以借助CPU缓存机制,预读数组中的数据,效率更高。链表在内存中不连续存储,不能使用CPU缓存机制。
CPU缓存机制:CPU运行速度非常快,每次去内存中取数据很耗时。所以CPU设置了缓存。提前把用到的数据存储在缓存里,下次再用时从缓存中取能减少耗时。什么样的情况下数据会提前加载到缓存呢?即当某个元素被用到的时候,那么这个元素地址附近的元素也会被加载到缓存。例如数组1,2,3,4,当1被用到时,CPU认为既然1被用到了,那么2,3,4也很大概率可能被用到,所以都被加载到缓存里,当用到2,3,4的时候,直接从缓存里取。
2.数组大小固定,链表支持动态扩容。
六.基于链表实现LRU缓存淘汰算法
用一个有序链表,越靠近尾部的结点是越早之前访问的。当有一个新的数据被访问时,从头结点开始顺序遍历链表
(1)如果此数据已经存在链表中了,遍历此数据对应的结点,删除,再插入到链表头。
(2)如果链表中无此数据,分两种情况
<1>此时缓存未满,将此数据插入到链表头
<2>此时缓存已满,删除链表尾结点,将此数据插入到链表头。
时间复杂度为O(n)
七.代码
class Node{ /** * 结点的值 */ int data; /** * 结点引用,指向下一个结点 */ Node next = null; public Node(int data){ this.data = data; } /** * 哨兵结点 */ Node head = new Node(0); /** * 链表长度 */ int length = 0; /** * 1.从尾结点开始添加结点 * @param val */ public void tailInsert(int val){ Node temp = head; //遍历链表,直到到达尾结点 while (temp.next!=null){ temp = temp.next; } //新建结点 temp.next = new Node(val); } /** * 2.从头结点插入 * @param val */ public void headInsert(int val){ Node temp = head; //新结点 Node newNode = new Node(val); //新节点的指针指向头结点的下一个结点 newNode.next = temp.next; //头结点指针指向新结点 temp.next = newNode; }
参考:王争《数据结构与算法之美》