1.概念:链表不需要在内存中有连续的内存空间,他是将零散的内存块通过 “指针” 的形式串联在一起,每个节点中需要保存下一个节点所在的地址。
链表与数组的区别是:数组在内存中需要一块连续的空间来存储数据,如果数组需要100MB的存储空间,虽然内存中剩余的存储空间大于100MB,但是连续的存储空间小于100MB,那么该数组同样也会创建失败。
2.常见的三种链表结构:单向链表,双向链表,循环链表
(1)单向链表:
在单向链表中,通常把第一个节点称之为 “头节点”,头节点中不存放数据,头节点用来记录整条链表的基地址,把最后一个节点称之为 “尾节点”。
根据头节点就可以便利整条链表。尾节点的指针,指向一个NULL,代表这是链表上的最后一个节点。
在链表中的某一个位置,删除一个节点数据时,只需要改变相邻节点的指针即可,所以它的时间复杂度为O(1),但是如果在链表中随机访问某一个元素,因为他在内存中不是连续存储的,所以无法像数组那样,可以根据首地址和下标索引,根据寻址公式计算出当前元素的地址,只能通过便利链表的形式来查找,所以它的时间复杂度为O(n)。
(2)双向链表:
双向链表在实际开发中也是最为常用的,它存在两个指针,(1)前驱指针(2)后继指针,所以在某些特殊的情况下,它的插入,删除的效率要比单向链表高。在存储相同的数据时,双向链表比单向链表更占内存空间。
实际开发中,从链表中删除一个数据时,最常见的两种情况:(1)根据指定的值去删除(2)根据指定的指针去删除
其实说单向链表中,插入和删除的时间复杂度是O(1),指的是单纯的插入和删除操作,没有算上之前的遍历运算。
在第一种情况下,无论是单向链表还是双向链表,都需要遍历链表,然后找到对应的值去删除,时间复杂度都为O(n),
第二种情况下,因为单向链表没有前驱指针,即使知道要删除哪一个节点,还是需要找到它的前驱节点,所以还需要遍历链表,根据时间复杂度的加法法则,复杂度还是O(n)。但是双向链表就会不一样,它存在前驱指针,所以在知道确定删除哪一个节点之后,就会找到它的前驱和后继,进行指针的变换,所以复杂度为O(1).
同理:在某一个节点的前面插入节点时,也会出现该现象。
在实际开发中,即使双向链表消耗更多的内存,也使用的比较多,是因为里面有 “用空间换时间的思想”,对于执行较慢的程序,可以采用,用空间换时间。对于内存比较紧张的情况下,可以采用,以时间换空间的思想。LinkedHashMap。
(3)循环链表:
循环链表与单向链表的区别在于,单向链表的末尾指针,指向NULL,但是循环链表的末尾指针,指向当前链表的头节点,如同一个环形结构,比如著名的 “约瑟夫问题” 就可以使用循环链表来解决。
3.基于链表实现缓存淘汰算法:
我的思路是这样的:我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。
1.如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
2.如果此数据没有在缓存链表中,又可以分为两种情况:
-
如果此时缓存未满,则将此结点直接插入到链表的头部;
-
如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
这样我们就用链表实现了一个LRU缓存,
public class LRUCacheLinkedList<T> {
/*
* 缓存链表的默认容量*/
private int DEFAULT_CAPACITY = 10;
/*
* 缓存链表头节点*/
private CacheNode headNode;
/*
* 缓存链表长度*/
private int length;
/*
* 缓存链表容量*/
private int capacity;
public LRUCacheLinkedList(int capacity) {
this.headNode = new CacheNode();
this.capacity = capacity;
this.length = 0;
}
public LRUCacheLinkedList() {
this.length = 0;
this.capacity = DEFAULT_CAPACITY;
this.headNode = new CacheNode();
}
/*
* 遍历缓存链表,判断在链表中是否能查找到指定的数据,如果能查到,那么返回当前节点的前驱节点,
* 如果查不到,那么返回null
*/
public CacheNode findPreNode(T data) {
CacheNode node = headNode;
while(node.nextNode != null) {
if(node.nextNode.getValue().equals(data)) {
return node;
}
node = node.nextNode;
}
return null;
}
/*
* 增加节点*/
public void lruAdd(T data) {
//增加节点时,需要先判断该数据在缓存链表中,是否存在,如果存在,需要先删除,然后加到头节点
CacheNode node = findPreNode(data);
if(node != null) {
lruRemove(node);
insertByHead(data);
}else {
if(length >= this.capacity) {
removeEndNode();
}
insertByHead(data);
}
}
/*
* 将数据添加到头节点*/
public void insertByHead(T data) {
CacheNode node = headNode.getNextNode();
headNode.setNextNode(new CacheNode(data,node));
length++;
}
/*
* 删除节点*/
public void lruRemove(CacheNode preNode) {
CacheNode node = preNode.getNextNode();
preNode.setNextNode(node.getNextNode());
node = null;
//因为删除节点了,所以长度需要减一
length--;
}
/*
* 删除缓存链表中尾部节点*/
public void removeEndNode() {
CacheNode node = headNode;
//如果是空链表,那么直接返回
if(node.getNextNode() == null) {
return;
}
//获得缓存链表的倒数第二个节点
while ( node.getNextNode().getNextNode()!= null) {
node = node.getNextNode();
}
CacheNode temp = node.getNextNode();
node.setNextNode(null);
temp = null;
length--;
}
/*
* 打印缓存链表中的元素*/
public void printAll() {
CacheNode node = headNode.getNextNode();
while (node != null) {
System.out.print(node.getValue()+", ");
node = node.getNextNode();
}
System.out.println();
System.out.println("打印完毕!");
}
/*
* 内部类,缓存链表中的节点元素*/
private class CacheNode<T> {
private T value;
private CacheNode nextNode;
/*
* 该构造方法在链表头部添加节点时,需要用到*/
public CacheNode(T value,CacheNode nextNode) {
this.value = value;
this.nextNode = nextNode;
}
public CacheNode(T value) {
this.value = value;
}
public CacheNode() {
this.nextNode = null;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public CacheNode getNextNode() {
return nextNode;
}
public void setNextNode(CacheNode nextNode) {
this.nextNode = nextNode;
}
}
public static void main(String[] args) {
LRUCacheLinkedList lru = new LRUCacheLinkedList();
Scanner scanner = new Scanner(System.in);
while(true) {
lru.lruAdd(scanner.nextInt());
lru.printAll();
}
}
}