鏈表是線性表的一種。線性表是最基本、最簡單、也是最常用的一種數據結構。
線性表中數據元素之間的關系是一對一的關系,即除了第一個和最后一個數據元素之外,其它數據元素都是首尾相接的。
線性表有兩種存儲方式:
- 順序存儲結構
- 鏈式存儲結構
我們常用的數組就是一種典型的順序存儲結構。
在存儲一大波數據的時候,我們通常是使用數組來進行存儲,但是有的時候數組會顯得不夠靈活
比如,現在有 [1, 2, 3, 5, 6] 這樣一個數組,如果要在數字 3 的后面添加一個數字 4, 那就需要將數字 3 后面的所有數字都進行移動才可以插入數字 4,如圖
這樣操作顯然很浪費時間
相反,鏈式存儲結構就是兩個相鄰的元素在內存中可能不是相鄰的,每一個元素都有一個指針域,指針域一般是存儲著到下一個元素的指針。
此時如果在數字 3 和數字 5 中間插入一個數字 4,鏈表的操作如下圖所示
鏈式存儲方式的優點是
插入和刪除的時間復雜度為 O(1),不會浪費太多內存,添加元素的時候才會申請內存,刪除元素會釋放內存。
缺點是訪問的時間復雜度最壞為 O(n)
順序表的特性是隨機讀取,也就是訪問一個元素的時間復雜度是O(1)
鏈式表的特性是插入和刪除的時間復雜度為O(1)
鏈表就是鏈式存儲的線性表。根據指針域的不同,鏈表分為單向鏈表、雙向鏈表、循環鏈表等等。
鏈表是一種非?;A的數據結構。在鏈表中的每個節點都包含其數據內容以及一個指向下一個元素的指針。我們可以通過這些指針來遍歷鏈表。
以下代碼使用 Kotlin 編寫
單鏈表
單鏈表顧名思義就是一個鏈式數據結構,它有一個表頭,并且除了最后一個節點外,所有節點都有其后繼節點。如下圖。
鏈表節點
首先,我寫出鏈表節點的類。單鏈表中的每一個節點,都保存其數據域和后驅指針。
class Node<T> (value: T){
val data = value
var next: Node<T>? = null
}
單例表實現
節點出來以后,現將 LinkedList
的基本框架搭建出來,其中包括單鏈表的初始化函數,單鏈表的節點訪問及修改和單鏈表的節點插入與刪除。
public class LinkedList<T> {
class Node {
T data;
Node next;
Node(T data) {
this.data = data;
}
}
private Node header;
public LinkedList(List<T> list) {
}
public int size() {
return 0;
}
public void setData(int index, T data) {
}
public T getData(int index) {
return null;
}
public void insert(int index, T data) {
}
public T delete(int index) {
}
public void clear() {
}
public void isEmpty() {
}
}
框架搭建完成之后,首先是初始化一個鏈表,傳入一個 list
并將其所有元素依次串聯成一個鏈表。 注意,鏈表對象并不持有所有對象,它只保存了表頭。
public LinkedList(List<T> list) {
if (null == list || list.size() == 0) {
return;
}
header = new Node(list.get(0));
Node pointer = header;
int size = list.size();
for (int i = 1; i < size; i++) { // index 從 1 開始,因為 0 的 data 已經賦予了 header
pointer.next = new Node(list.get(i));
pointer = pointer.next; // 將指針由 header 移到 next
}
}
想要知道鏈表有多長,只能是對當前鏈表進行遍歷,時間復雜對為 O(n)
public int size() {
int length = 0;
if (null != header) {
Node pointer = header;
while (null != pointer) {
pointer = pointer.next; // 指針按照鏈表依次移到
length++;
}
}
return length;
}
如果想要索引鏈表中的某個元素,還是需要一個個的遍歷過去,因為鏈表只保留了第一個元素的引用。
public void setData(int index, T data) {
if (index < 0 || index > size() || null == data) {
return;
}
if (index == 0) {
header = new Node(data);
return;
}
Node pointer = header;
int currentIndex = 0;
while (currentIndex < index) {
pointer = pointer.next; // 指針按照鏈表依次移到
currentIndex++;
}
pointer.data = data;
}
public T getData(int index) {
if (index < 0 || index > size() || null == header) {
return null;
}
Node pointer = header;
int currentIndex = 0;
while (currentIndex < index) {
pointer = pointer.next;
currentIndex++;
}
return pointer.data;
}
鏈表中還有兩個特別重要的方法,插入和刪除。插入需要找到插入的位置,把前一個元素的 next 指針指向被插入的節點,并將被插入節點的 next 指針指向后一個節點,如下圖左側所示。而刪除則是把前一個節點的 next 指針指向后一個節點,并返回被刪除元素的數據內容,如下圖右側所示。
[圖片上傳失敗...(image-8f71c7-1513179298327)]
public void insert(int index, T data) {
if (index < 0 || index > size() || null == header || null == data) {
return;
}
if (index == 0) {
header = new Node(data);
return;
}
Node currentPointer = header;
Node prevPointer;
int currentIndex = 0;
while (currentIndex < index) {
prevPointer = currentPointer;
currentPointer = currentPointer.next;
currentIndex++;
if (currentIndex == index) {
// 把前一個元素的 next 指針指向被插入的節點,并將被插入節點的 next 指針指向后一個節點
Node insertNode = new Node(data);
prevPointer.next = insertNode;
insertNode.next = currentPointer;
}
}
}
public T delete(int index) {
if (index < 0 || index >= size() || null == header) {
return null;
}
if (index == 0) {
T result = header.data;
header = header.next;
return result;
}
Node currentPointer = header;
Node prevPointer;
int currentIndex = 0;
T data = null;
while (currentIndex < index) {
prevPointer = currentPointer;
currentPointer = currentPointer.next;
currentIndex++;
if (currentIndex == index) {
// 把前一個節點的 next 指針指向后一個節點,并返回被刪除元素的數據內容
data = currentPointer.data;
prevPointer.next = currentPointer.next;
}
}
return data;
}
以上就是單鏈表數據結構的簡單實現
注意: 鏈表對象并不持有所有元素,它只保存了表頭。
雙向鏈表
雙向鏈表和單鏈表不同之處在于,鏈表中的每一個節點,都保存其數據域和前/后驅指針。這就意味著如果你想刪除鏈表的最后一個元素,你不需要從表頭開始遍歷到最后一個元素了。你可以直接從表尾開始直接刪除這個元素。顯然,雙向鏈表在效率上要高于單鏈表,不過其數據結構更復雜,占用了更多的空間。
[圖片上傳失敗...(image-a0a60c-1513179298327)]
還是先來定義節點類。包含數據以及 prev & next 兩個指針
class Node {
T data;
Node prev;
Node next;
Node(T data) {
this.data = data;
}
}
整個雙鏈表的整體框架如下所示
public class RoundLinkedList<T> {
class Node {
T data;
Node prev;
Node next;
Node(T data) {
this.data = data;
}
}
private Node head;
private Node tail;
public RoundLinkedList(List<T> list) {
}
public int size() {
return 0;
}
public T getData(int index) {
return null;
}
public void setData(int index, T data) {
}
public void insert(int index, T data) {
}
public T delete(int index) {
return null;
}
}
我們寫出初始化函數,我們可以讀入一個數組并將其生成一個鏈表,注意雙端鏈表要從頭到尾從尾到頭都可以查找。
public RoundLinkedList(List<T> list) {
if (null == list) {
return;
}
int size = list.size();
if (size == 1) {
head = new Node(list.get(0));
tail = head;
return;
}
// 初始化首&末節點
head = new Node(list.get(0));
tail = new Node(list.get(size - 1));
Node pointer = head;
for (int i = 1; i < size - 1; i++) { // 排除 index 為 0 和 size - 1 的情況
Node node = new Node(list.get(0));
pointer.next = node; // 當前節點指向下個節點
node.prev = pointer; // 下一個節點指回上一個節點
pointer = node; // 移動指針從當前節點到下一個節點
}
// 連接末節點
pointer.next = tail;
tail.prev = pointer;
}
主要需要注意的是插入和刪除,我們需要判斷插入位置是靠近頭還是尾。 如果靠近頭,我們就從頭開始遍歷找到操作位置,否則就從尾部開始遍歷
public void insert(int index, T data) {
if (null == data) {
return;
}
Node node = null;
if (null == head && index == 0) {
node = new Node(data);
head = node;
tail = head;
return;
}
// 在頭部插入
if (index == 0) {
node = new Node(data);
head.prev = node;
head = node;
return;
}
// 在尾部出入
if (index == size()) {
node = new Node(data);
tail.next = node;
node.prev = tail;
tail = node;
return;
}
Node currentPointer;
Node prevPointer;
int currentIndex = 0;
// 靠近頭部,從頭開始
if (index <= size() / 2) {
currentPointer = head;
prevPointer = currentPointer;
while (currentIndex < index) {
prevPointer = currentPointer;
currentPointer = currentPointer.next;
currentIndex++;
}
node = new Node(data);
prevPointer.next = node;
node.prev = prevPointer;
node.next = currentPointer;
currentPointer.prev = node;
return;
}
// 靠近尾部,從尾開始
if (index > size() / 2) {
currentPointer = tail;
prevPointer = currentPointer;
currentIndex = size();
while (index < currentIndex) {
prevPointer = currentPointer;
currentPointer = currentPointer.prev;
currentIndex--;
}
node = new Node(data);
currentPointer.next = node;
node.prev = currentPointer;
node.next = prevPointer;
prevPointer.prev = node;
}
}
public T delete(int index) {
if (index < 0 || index >= size()) {
return null;
}
T data = null;
// 刪除鏈表頭
if (index == 0) {
data = head.data;
head = head.next;
}
// 刪除鏈表尾
if (index == size() - 1) {
data = tail.data;
tail = tail.prev;
}
Node currentPointer;
Node prevPointer;
int currentIndex = 0;
// 刪除靠近頭部的元素
if (index <= size() / 2) {
currentPointer = head;
prevPointer = currentPointer;
while (currentIndex < index) {
prevPointer = currentPointer;
currentPointer = currentPointer.next;
currentIndex++;
}
data = currentPointer.data;
prevPointer.next = currentPointer.next;
currentPointer.next.prev = prevPointer;
}
// 刪除靠近尾部的元素
if (index > size() / 2) {
currentPointer = tail;
prevPointer = currentPointer;
currentIndex = size() - 1;
while (index < currentIndex) {
prevPointer = currentPointer;
currentPointer = currentPointer.prev;
currentIndex--;
}
data = currentPointer.data;
prevPointer.prev = currentPointer.prev;
currentPointer.prev.next = prevPointer;
}
return data;
}
LeetCode 實戰
檢查鏈表中是否有環
題目分析: 我們可以用兩個指針從表頭開始,一快一慢的遍歷鏈表,快的一次走兩步,慢的一次走一步。如果單鏈表有環,則不存在表尾(所有節點都有后繼節點),當指針進入環后,將在環里面一直轉,兩個指針由于一快一慢,快的指針必然會在某一個時刻”追上”慢的指針,兩個指針達到同一個點。如下圖中,快慢兩個指針將在 5-10 這幾個節點形成的環中進行追擊,直到相遇。所以,如果兩個指針在出發后可以到達同一節點,我們就可以判斷這個鏈表有環。
[圖片上傳失敗...(image-9a916d-1513179298327)]
假設該鏈表中存在環,并且設慢指針走過的路程為 K, 環的長度為 R, 則可以得出公式 2K - K = nR, K = nR (n 為走過的環的圈數), 現在設鏈表的頭節點到環開始節點之間的距離為 X, 而鏈表頭節點到快慢兩指針第一次相遇節點之間的距離,也就是慢指針所走過的距離為 K, 設快慢兩指針第一次相遇節點到環開始節點的距離為 M, 則可以得出等式 X = K - (R - M) = nR - R + M = (n - 1)R + M, 取 n = 1, 則鏈表頭節點到環開始節點的距離等于快慢兩指針第一次相遇節點到環開始節點的距離。
以下為相關代碼:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (null == head || null == head.next) {
return null;
}
boolean isCycle = false;
ListNode fast = head;
ListNode slow = head;
while (fast != null && slow != null) {
fast = fast.next; // fast 領先走一步
if (null == fast) { // 鏈表走到了頭,說明不存在環
return null;
}
// fast 和 slow 各自走一步,但 fast 每次比 slow 多一步
fast = fast.next;
slow = slow.next;
if (fast == slow) { // fast 最終與 slow 相遇說明存在環
isCycle = true;
break;
}
}
if (!isCycle) {
return null;
}
// 鏈表頭節點到環開始節點的距離等于快慢兩指針第一次相遇節點到環開始節點的距離
while (head != slow) {
head = head.next;
slow = slow.next;
}
return head;
}
}
刪除當前節點
題目分析: 從前面的小節中我們已經得知,想要刪除一個節點,需要把這個節點前驅節點的 next 指針知道其后面的節點。但是如下圖,我們要刪除 "hello" 節點,卻不知道它的前驅節點的。笨辦法是從鏈表頭開始遍歷找到待刪除的節點。好的辦法是,我們把當前節點的后繼節點的數據域復制到當前節點,然后刪掉后繼節點。還是以下圖為例,我們把第二個節點的數據域復制到第一個節點,然后刪除第二個節點。
[圖片上傳失敗...(image-438de7-1513179298327)]
以下是相關代碼:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val; // 將下一個節點的值賦予此節點
node.next = node.next.next; // 將此節點連向下一個節點的下一個節點
}
}
刪除從尾部數起第 N 個節點
題目分析: 我們沒有辦法從尾部開始遍歷單鏈表,而且我們也不知道單鏈表的長度(除非我們遍歷一次)。一個比較取巧的方法是用兩個指針指向表頭,一個先走 n 步,然后兩個指針一起出發,當前面那個指針到達表尾的時候,后面那個指針正好處在待刪除節點的前驅節點。下面就很簡單啦。
以下圖為例,我們想刪除鏈表的倒數第二個節點。首先在表頭設定兩個指針 p1,p2,并讓 p2 先走兩步,然后兩個指針一同出發直到 p2 到達表尾。然后刪除p1的后繼節點(就是倒數第二個節點)。
[圖片上傳失敗...(image-aaa72b-1513179298327)]
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if (null == head) {
return null;
}
ListNode fast = head;
ListNode slow = head;
int index = 0;
while (index < n) { // fast 與 slow 間隔 n 步
fast = fast.next;
index++;
}
if (null == fast) {
return head.next;
}
// 當前面那個指針到達表尾的時候,后面那個指針正好處在待刪除節點的前驅節點
while (null != fast.next) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return head;
}
}
參考
- 鏈表
- [數據結構與算法/leetcode/lintcode題解](https://www.kancloud.cn/kancloud/data-structure-and-algorithm-note