鏈表是一種用於存儲數據集合的數據結構。鏈表有以下幾個屬性:
- 相鄰元素之間通過指針進行連接
- 最后一個元素的后繼指針值為NULL
- 在程序執行的過程中,鏈表的長度可以增加或縮小
- 鏈表的空間能夠按需分配(直到系統內存耗盡)
- 沒有內存空間的浪費(但是鏈表中的指針需要一些額外的內存開銷)

一、鏈表的分類
鏈表大致可以分為這么幾類:
- 單向鏈表
- 雙向鏈表
- 存儲較為高效的雙向鏈表
- 循環鏈表
- 松散鏈表
二、LRU緩存淘汰算法
在具體介紹鏈表之前,有必要先介紹一下關於LRU緩存技術:
緩存是一種提高數據讀取性能的技術,在硬件設計,軟件開發中都有着非常廣泛的應用,比如常見的CPU緩存、數據庫緩存、瀏覽器緩存等等。
緩存的大小是有限的,當緩存被用滿的時,此時就需要對緩存做相應的清理,但是,具體哪些數據應該被清理出去?哪些數據應該被保留?這就需要緩存淘汰策略來決定。常見的策略有三種:
- 先進先出策略FIFO(First In ,First Out)
- 最少使用策略LFU(Least Frequently Used)
- 最近最少使用策略LRU(Least Recently Used)
那么接下來就有一個問題:如何使用鏈表來實現LRU緩存淘汰策略呢?
三、鏈表的底層存儲結構
相比於數組來說,鏈表是一種稍微復雜一點的數據結構,下面是鏈表和數組的內存分部對比圖:

從圖中可以看出
- 對於數組來說,它需要一塊連續的內存存儲空間來存儲,對內存的要求比較高,也就是說,如果我申請100M大小的數組的話,當內存中沒有連續的、足夠大的存儲空間的時候,即便內存中剩余總可用空間大於100M,此時仍然會申請失敗
- 對於鏈表來說,它恰恰相反,它並不需要一塊連續的內存空間,它通過“指針”將一組零散的內存塊串聯起來使用,所以,如果我們申請的是100M大小的鏈表,當內存中剩余可用空間大於100M的時候,無論是否連續,申請都不會有問題。
四、鏈表的一些基本概念:
- 結點:鏈表是通過指針將自足零散的內存塊串聯在一起,所以,將每個內存塊就是鏈表的一個"結點"
- 后繼指針:每個鏈表的結點除了存儲數據之外,還需要記錄鏈路上下一個節點的地址,將記錄下一個結點地址的指針叫做后繼指針。
- 頭結點:整個鏈路中第一個結點稱之為“頭結點”
- 尾節點:整個鏈路中最后一個結點稱之為“尾節點”
- 空地址NULL:當一個指針指向的不是下一個結點,而是空地址NULL,表示這是鏈路上的最后一個結點。
五、鏈表、數組的優缺點對比
關於數組,其實在數組這一節,做了詳細的討論,這里不做多的贅述。
鏈表與數組的優缺點對比:
鏈表、數組與動態數組的時間復雜度對比:

單向鏈表
鏈表通常是指單向鏈表,她包含了多個結點,每個結點有一個指向后繼元素的指針,表中最后一個結點next指針為NULL,表示該鏈表結束。
申明一個鏈表
public class ListNode { private int data; private ListNode next; public ListNode(int data) { this.data = data; } public void setData(int data) { this.data = data; } public int getData() { return data; } public void setNext(ListNode next) { this.next = next; } public ListNode getNext() { return this.next; } }
鏈表的主要操作(時間復雜度均為O(1)):
- 遍歷鏈表
- 插入一個元素:插入一個元素到鏈表中
- 刪除一個元素:移除並返回鏈表中指定位置的元素
鏈表的輔助操作:
- 刪除鏈表:移除鏈表中的所有元素(清空鏈表)
- 計數:返回鏈表中元素的個數
- 查找:尋找從鏈表表尾開始的第n個節點(node)
鏈表的遍歷
假設表頭指針指向鏈表中的第一個結點。遍歷鏈表需要完成以下幾個步驟:
- 沿指針遍歷
- 遍歷時顯示節點的內容
- 當next指針的值為NULL時,結束遍歷
通過遍歷鏈表來對鏈表元素進行計數:
/** * 統計鏈表節點的個數 */ public int ListLength(ListNode headNode) { int length = 0; ListNode currentNode = headNode; while (currentNode != null) { length++; currentNode = currentNode.getNext(); } return length; }
此時間復雜度為O(n),用於掃描長度為n的鏈表。
空間復雜度為O(1),僅用於創建臨時變量
單向鏈表的插入
單向鏈表的插入可以分為以下3種情況
- 在鏈表的頭前插入一個新結點(鏈表的開始出)
- 在鏈表的尾后插入一個新結點(鏈表的結尾出)
- 在鏈表的中間插入一個新結點(隨機位置)
在單向鏈表的開頭插入結點
若需要在表頭節點前插入一個新結點,只需要修改一個next指針,可通過如下兩步完成:
- 更新新節點next指針,是其指向當前結點的表頭節點。

- 更新表頭指針的值,使其指向新結點。

在單向量表的結尾插入結點
如果需要在表尾部插入新結點,則需要修改兩個next指針
- 新結點的next指針指向NULL

- 最后一個結點的指針指向新結點

在單向鏈表的中間插入結點
假設給定插入新結點的位置,在這種情況下,需要修改兩個next指針:
- 如果位置3增加一個元素,則需要將指針定位於鏈表的位置2,。即需要從表頭開始經過兩個結點,然后插入新結點。假設第二個結點為位置結點,新結點的next指針指向位置結點(我們要在此處增加新結點)的下一個結點

- 位置結點的next指針指向新結點

代碼實現:
/** * 單向鏈表List節點進行插入操作 */ public ListNode InsertInLinkedList(ListNode headNode, ListNode nodeToInsert, int position) { //如果鏈表為空,則插入的節點即為頭結點 if (headNode == null) { return nodeToInsert; } //獲取該鏈表的節點數 int size = ListLength(headNode); if (position < 1 || position > size + 1) { System.out.println("Position of node to insert is invalid.The valid input are 1 to " + (size + 1)); return headNode; } //否則,插入元素要么是在頭插入,要么是在尾節點,或是中間 if (position == 1) { nodeToInsert.setNext(headNode); return nodeToInsert; } else { //在鏈表的中間或尾部插入 ListNode previousNode = headNode; int count = 1; while (count < position - 1) { previousNode = previousNode.getNext(); count++; } ListNode currentNode = previousNode.getNext(); nodeToInsert.setNext(currentNode); previousNode.setNext(nodeToInsert); } return headNode; }
時間復雜度為O(n)。在最壞情況下,可能需要在鏈表尾部插入結點。
空間復雜度為O(1),僅用於創建一個臨時變量。
單向鏈表的刪除
單向鏈表的刪除操作,也分為三種情況:
- 刪除鏈表的表頭(第一個)結點
- 刪除鏈表的表尾(最后一個)節點
- 刪除鏈表的中間的節點
刪除單向鏈表表頭結點
刪除鏈表的第一個結點,可以通過兩步實現:
- 創建一個臨時結點,它指向表頭指針所指的結點。

- 修改表頭指針的值,使其指向下一個結點,並移除臨時結點。

刪除單向鏈表的最后一個結點
這種情況下,操作比刪除第一個結點要麻煩一點,因為算法需要找到表尾節點的前驅節點。這需要三步來實現:
- 遍歷鏈表,在遍歷時還要保存前驅(前一次經過)結點的地址。當遍歷到鏈表的表尾時,將有兩個指針,分別是表尾結點的指針tail(表尾)即指向表尾結點的錢去結點的指針

- 將表尾的前驅節點的next指針更新為NULL

- 移除表尾節點。

刪除單向鏈表中間一個結點
在這種情況下,刪除的結點總是位於兩個結點之間,因此不需要更新表頭和表尾的指針。該刪除操作通過兩步實現:
- 在遍歷時保存前驅(前一次經過的)結點的地址。一旦找到被刪除的結點,將前驅結點next指針的值更新為被刪除結點的next指針的值

- 移除需要刪除的當前結點

代碼實現:
/** * 單向鏈表List的刪除操作 */ public ListNode deleteNodeFromeLinkedList(ListNode headNode, int position) { int size = ListLength(headNode); if (position > size || position < 1) { System.out.println("Postition of node to delete is invalid.The valid inputs are 1 to " + size); return headNode; } if (position == 1) { ListNode currentNode = headNode.getNext(); headNode = null; return currentNode; } else { ListNode preivousNode = headNode; int count = 1; while (count < position) { preivousNode = preivousNode.getNext(); count++; } ListNode currentNode = preivousNode.getNext(); preivousNode.setNext(currentNode.getNext()); currentNode = null; } return headNode; }
時間復雜度為O(n)。在最差情況下,可能需要刪除鏈表的表尾節點。
空間復雜度為O(1),僅用於創建一個臨時變量
刪除單向鏈表
該操作通過將當前結點存儲在臨時變量中,然后釋放當前結點(空間)的方式來完成。當時放完當前結點(空間)后,移動到下一個結點並將其存儲在臨時變量中,然后不斷重復該過程直至釋放所有結點。
代碼實現:
/** * 刪除單向鏈表 */ public void deleteLinkedList(ListNode headNode) { ListNode auxilaryNode,iterator = headNode; while (iterator != null){ auxilaryNode = iterator.getNext(); iterator = null; iterator = auxilaryNode; } }
時間復雜度為O(n),用掃描大小為n的整個建鏈表
空間復雜度為O(1),用於創建臨時變量

