目錄
鏈表:隨機存儲,順序訪問(讀取)
前言
本博文的大部分插圖來自於《漫畫算法——小灰》,也複製了該書部分文字
我加了一些自己的總結、代碼(我代碼實現是參考了本書以及java自帶LinkedList的源代碼)
我發現本書有關鏈表的代碼存在錯誤,已經向作者反饋
建議有能力的同學直接去看java自帶LinkedList的源代碼,寫的真的好
應本書作者要求,加上本書公衆號《程序員小灰》二維碼
一、單向鏈表
鏈表(linked list)是一種在物理上非連續、非順序的數據結構,由若干節點(node)所組成。
單向 鏈表的每一個節點又包含兩部分,一部分是存放數據的變量data,另一部分是指向下一個節點的指針next。
鏈表的第1個節點被稱爲頭節點,最後1個節點被稱爲尾節點,尾節點的next指針指向空。
什麼叫隨機存儲呢?
如果說數組在內存中的存儲方式是順序存儲,那麼鏈表在內存中的存儲方式則是隨機存儲 。
上一節我們講解了數組的內存分配方式,數組在內存中佔用了連續完整的存儲空間。而鏈表則採用了見縫插針的方式,鏈表的每一個節點分佈在內存的不同位置,依靠next指針關聯起來。這樣可以靈活有效地利用零散的碎片空間。
圖中的箭頭代表鏈表節點的next指針。
鏈表的基本操作
1. 查找節點
在查找元素時,鏈表不像數組那樣可以通過下標快速進行定位,只能從頭節點開始向後一個一個節點逐一查找。
/**
* 鏈表查找元素
*
* @param index 查找的位置
* @return index位置的Node對象
*/
public Node get(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出鏈表的節點的範圍!");
}
Node temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
鏈表中的數據只能按順序進行訪問,最壞的時間複雜度是O(n)
2. 更新節點
如果不考慮查找節點的過程,鏈表的更新過程會像數組那樣簡單,直接把舊數據替換成新數據即可。
如果不考慮查找元素的過程,只考慮純粹的更新節點操作,時間複雜度是O(1)
/**
* 更新節點 將列表中指定位置的節點的data替換爲指定的data。
*
* @param index 需要更新的節點的位置
* @param data 新data
* @return 舊data
*/
public int set(int index, int data) {
Node x = get(index);
int oldVal = x.data;
x.data = data;
return oldVal;
}
3. 插入節點
只要內存空間允許,能夠插入鏈表的元素是無窮無盡的,不需要像數組那樣考慮擴容的問題。
與數組類似,鏈表插入節點時,同樣分爲3種情況。
- 尾部插入
- 頭部插入
- 中間插入
3.1. 尾部插入
尾部插入,是最簡單的情況,把最後一個節點的next指針指向新插入的節點即可。
3.2. 頭部插入
頭部插入,可以分成兩個步驟。
- 第1步,把新節點的next指針指向原先的頭節點。
- 第2步,把新節點變爲鏈表的頭節點。
3.3. 中間插入
中間插入,同樣分爲兩個步驟。
- 第1步,新節點的next指針,指向插入位置的節點。
- 第2步,插入位置前置節點的next指針,指向新節點。
三鍾情況的代碼合到一起
/**
* 鏈表插入元素
*
* @param index 插入位置
* @param data 插入元素 被插入的鏈表節點的數據
*/
public void insert(int index, int data) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出鏈表節點範圍!");
}
Node insertedNode = new Node(data);
if (size == 0) {
//空鏈表
head = insertedNode;
last = insertedNode;
} else if (index == 0) {
//插入頭部
insertedNode.next = head;
head = insertedNode;
} else if (size == index) {
//插入尾部
last.next = insertedNode;
last = insertedNode;
} else {
//插入中間
Node prvNode = get(index - 1);
insertedNode.next = prvNode.next;
prvNode.next = insertedNode;
}
size++;
}
4. 刪除元素
鏈表的刪除操作同樣分爲3種情況。
- 尾部刪除
- 頭部刪除
- 中間刪除
4.1. 尾部刪除
尾部刪除,是最簡單的情況,把倒數第2個節點的next指針指向空即
可。
4.1. 頭部刪除
頭部刪除,也很簡單,把鏈表的頭節點設爲原先頭節點的next指針即可。
4.1. 中間刪除
中間刪除,同樣很簡單,把要刪除節點的前置節點的next指針,指向要
刪除元素的下一個節點即可。
這裏需要注意的是,許多高級語言,如Java,擁有自動化的垃圾回收機制,所以我們不用刻意去釋放被刪除的節點,只要沒有外部引用指向它們,被刪除的節點會被自動回收。
如果不考慮插入、刪除操作之前查找元素的過程,只考慮純粹的插入和刪除操作,時間複雜度都是O(1)
/**
* 鏈表刪除元素
*
* @param index 刪除的位置
* @return 被刪除的節點
*/
public Node remove(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出鏈表節點範圍");
}
Node removeNode;
if (index == 0) {
if (size == 0) {
throw new NullPointerException("當前鏈表爲空,不可以進行刪除操作");
}
//刪除頭節點
removeNode = head;
head = head.next;
} else if (index == size - 1) {
//刪除尾節點
Node preNode = get(index - 1);
removeNode = preNode.next;
preNode.next = null;
last = preNode;
} else {
//刪除中間節點
Node prevNode = get(index - 1);
removeNode = prevNode.next;
prevNode.next = prevNode.next.next;
}
size--;
return removeNode;
}
Java實現鏈表的完整代碼
package chapter2.part2;
/**
* Created by IntelliJ IDEA.
*
* @Author: 張志浩 Zhang Zhihao
* @Email: [email protected]
* @Date: 2020/5/3
* @Time: 13:39
* @Version: 1.0
*/
public class MyLinkedList2 {
private Node head; //頭節點
private Node last; //尾節點
private int size; //鏈表實際長度
public static void main(String[] args) {
MyLinkedList2 myLinkedList = new MyLinkedList2();
// myLinkedList.remove(0); // java.lang.NullPointerException: 當前鏈表爲空,不可以進行刪除操作
// myLinkedList.remove(3); // java.lang.IndexOutOfBoundsException: 超出鏈表節點範圍
myLinkedList.insert(0, 3);
myLinkedList.insert(1, 7);
myLinkedList.insert(2, 9);
myLinkedList.insert(3, 5);
myLinkedList.insert(1, 6);
myLinkedList.remove(0);
myLinkedList.set(0, 23);
myLinkedList.output();
}
/**
* 鏈表插入元素
*
* @param index 插入位置
* @param data 插入元素 被插入的鏈表節點的數據
*/
public void insert(int index, int data) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出鏈表節點範圍!");
}
Node insertedNode = new Node(data);
if (size == 0) {
//空鏈表
head = insertedNode;
last = insertedNode;
} else if (index == 0) {
//插入頭部
insertedNode.next = head;
head = insertedNode;
} else if (size == index) {
//插入尾部
last.next = insertedNode;
last = insertedNode;
} else {
//插入中間
Node prvNode = get(index - 1);
insertedNode.next = prvNode.next;
prvNode.next = insertedNode;
}
size++;
}
/**
* 鏈表刪除元素
*
* @param index 刪除的位置
* @return 被刪除的節點
*/
public Node remove(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出鏈表節點範圍");
}
Node removeNode;
if (index == 0) {
if (size == 0) {
throw new NullPointerException("當前鏈表爲空,不可以進行刪除操作");
}
//刪除頭節點
removeNode = head;
head = head.next;
} else if (index == size - 1) {
//刪除尾節點
Node preNode = get(index - 1);
removeNode = preNode.next;
preNode.next = null;
last = preNode;
} else {
//刪除中間節點
Node prevNode = get(index - 1);
removeNode = prevNode.next;
prevNode.next = prevNode.next.next;
}
size--;
return removeNode;
}
/**
* 更新節點 將列表中指定位置的節點的data替換爲指定的data。
*
* @param index 需要更新的節點的位置
* @param data 新data
* @return 舊data
*/
public int set(int index, int data) {
Node x = get(index);
int oldVal = x.data;
x.data = data;
return oldVal;
}
/**
* 鏈表查找元素
*
* @param index 查找的位置
* @return index位置的Node對象
*/
public Node get(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出鏈表的節點的範圍!");
}
Node temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
/**
* 輸出鏈表
*/
public void output() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + " ");
temp = temp.next;
}
}
/**
* 鏈表節點
*/
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
}
二、雙向鏈表
雙向鏈表比單向鏈表稍微複雜一些,它的每一個節點除了擁有data和next指針,還擁有指向前置節點的prev 指針。