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();
}
}
}