1、什麼是鏈表
單向鏈表
鏈表(linkedlist)是一種在物理上非連續、非順序的數據結構,由若干節點(node)所組成;單向鏈表的每一個節點又包含兩部分,一部分是存放數據的變量 data,另一部分是指向下一個節點的指針 next
結構圖:
代碼實現:
private static class Node {
int data;
Node next;
}
鏈表的第1個節點被稱爲頭節點,最後1個節點被稱爲尾節點,尾節點的 next 指針指向空;
與數組按照下標來隨機尋找元素不同,對於鏈表的其中一個節點A,只能根據節點A的 next 指針來找到該節點的下一個節點B,再根據節點B的next指針找到下一個節點C……一級一級,單線傳遞!想讓每個節點都能回溯到它的前置節點,可以使用雙向鏈表
雙向鏈表
雙向鏈表比單向鏈表稍微複雜一些,它的每一個節點除了擁有data和next指 針,還擁有指向前置節點的prev指針;
鏈表的存儲方式
說數組在內存中的存儲方式是順序存儲,那麼鏈表在內存中的存儲方式則是隨機存儲
數組在內存中佔用了連續完整的存儲空間,而鏈表則採用了見縫插針的方式,鏈表的每一個節點分佈在內存的不同位置,依靠 next 指針關聯起來,這樣可以靈活有效地利用零散的碎片空間。
- 數組的內存分配方式圖:
- 鏈表的內存分配方式圖:
圖中的箭頭代表鏈表節點的 next 指針
2、鏈表的基本操作
【1】查找節點
在查找元素時,鏈表不像數組那樣可以通過下標快速進行定位,只能從頭節點開始向後一個一個節點逐一查找。
例如給出一個鏈表,需要查找從頭節點開始的第3個節點:
查找步驟:
- 第1步,將查找的指針定位到頭節點
- 第2步,根據頭節點的next指針,定位到第2個節點
- 第3步,根據第2個節點的next指針,定位到第3個節點,查找完畢
鏈表中的數據只能按順序進行訪問,最壞的時間複雜度是O(n)
【2】更新節點
如果不考慮查找節點的過程,鏈表的更新過程會像數組那樣簡單,直接把舊數據替換成新數據即可
【3】插入節點
鏈表插入節點時,分爲3種情況:
- 尾部插入
尾部插入把最後一個節點的next指針指向新插入的節點即可
- 頭部插入
頭部插入可以分成兩個步驟:
第1步,把新節點的next指針指向原先的頭節點
第2步,把新節點變爲鏈表的頭節點
- 中間插入
中間插入同樣分爲兩個步驟:
第1步,新節點的 next 指針,指向插入位置的節點
第2步,插入位置前置節點的 next 指針,指向新節點
只要內存空間允許,能夠插入鏈表的元素是無窮無盡的,不需要像數組那樣考慮擴容的問題
【4】刪除元素
鏈表的刪除操作同樣分爲3種情況:
- 尾部刪除
尾部刪除把倒數第2個節點的 next 指針指向空即可:
- 頭部刪除
頭部刪除把鏈表的頭節點設爲原先頭節點的next指針即可:
- 中間刪除
中間刪除把要刪除節點的前置節點的 next 指針,指向要刪除元素的下一個節點即可:
這裏需要注意的是,許多高級語言,如Java,擁有自動化的垃圾回收機制,所以不用刻意去釋放被刪除的節點,只要沒有外部引用指向它們,被刪除的節點 會被自動回收
鏈表的插入和刪除操作中如果不考慮插入、刪除操作之前查找元素的過程,只考慮純粹的插入和刪除操作,時間複雜度都是O(1)
實現鏈表的完整代碼:
public class MyLinkedList {
// 頭節點指針
private Node head;
// 尾節點指針
private Node last;
// 鏈表實際長度
private int size;
//鏈表插入元素; data插入元素 ;index插入位置
public void insert(int data, int index) throws Exception{
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 prevNode = get(index-1);
insertedNode.next = prevNode.next;
prevNode.next = insertedNode;
}
size++;
}
//鏈表刪除元素 ; index刪除的位置
public Node remove(int index) throws Exception{
if (index<0 || index>=size) {
throw new IndexOutOfBoundsException(" 超出鏈表節點範圍!");
}
Node removedNode = null;
if(index == 0){
//刪除頭節點
removedNode = head;
head = head.next;
}else if(index == size-1){
//刪除尾節點
Node prevNode = get(index-1);
removedNode = prevNode.next;
prevNode.next = null;
last = prevNode;
}else {
//刪除中間節點
Node prevNode = get(index-1);
Node nextNode = prevNode.next.next;
removedNode = prevNode.next;
prevNode.next = nextNode;
}
size--;
return removedNode;
}
//鏈表查找元素 ; index查找的位置
public Node get(int index) throws Exception {
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.println(temp.data);
temp = temp.next;
}
}
// 鏈表節點
private static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
public static void main(String[] args) throws Exception {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.insert(3,0);
myLinkedList.insert(7,1);
myLinkedList.insert(9,2);
myLinkedList.insert(5,3);
myLinkedList.insert(6,1);
myLinkedList.remove(0);
myLinkedList.output();
}
}
輸出:
以上是對單鏈表相關操作的代碼實現。爲了尾部插入的方便,代碼中額外增加 了指向鏈表尾節點的指針 last
3、數組VS鏈表
數組和鏈表相關操作的性能對比如下圖:
數組的優勢在於能夠快速定位元素,對於讀操作多、寫操作少的場景來說,用數組更合適一些;
鏈表的優勢在於能夠靈活地進行插入和刪除操作,如果需要在尾部頻繁插入、刪除元素,用鏈表更合適一些。
—————————————————————————————————————————
內容來源:《漫畫算法》