『資料結構與演算法』連結串列(單鏈表、雙鏈表、環形連結串列)
阿新 • • 發佈:2021-02-14
> 微信搜尋:碼農StayUp
> 主頁地址:[https://gozhuyinglong.github.io](https://gozhuyinglong.github.io/)
> 原始碼分享:[https://github.com/gozhuyinglong/blog-demos](https://github.com/gozhuyinglong/blog-demos/)
### 1. 前言
通過前篇文章《[陣列](https://blog.csdn.net/gozhuyinglong/article/details/109702860)》瞭解到陣列的儲存結構是一塊連續的記憶體,插入和刪除元素時其每個部分都有可能整體移動。為了避免這樣的線性開銷,我們需要保證資料可以不連續儲存。本篇介紹另一種資料結構:連結串列。
### 2. 連結串列(Linked List)
連結串列是一種線性的資料結構,其物理儲存結構是零散的,資料元素通過指標實現連結串列的邏輯順序。連結串列由一系列結點(連結串列中每一個元素稱為節點)組成,節點可以在記憶體中動態生成。
連結串列的特性:
* 連結串列是以節點(Node)的方式來儲存,所以又叫鏈式儲存。
* 節點可以連續儲存,也可以不連續儲存。
* 節點的邏輯順序與物理順序可以不一致
* 表可以擴充(不像陣列那樣還得重新分配記憶體空間)
連結串列分為單鏈表、雙鏈表和環形連結串列,下面通過例項逐個介紹。
### 3. 單鏈表(Singly Linked List)
單鏈表又叫單向連結串列,其節點由兩部分構成:
* `data`域:資料域,用來儲存元素資料
* `next`域:用於指向下一節點
單鏈表的結構如下圖:
![單鏈表.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092847165-711074127.png)
#### 3.1 單鏈表的操作
單鏈表的所有操作都是從`head`開始,`head`本身不儲存元素,其`next`指向第一個節點,然後順著`next`鏈進行一步步操作。其尾部節點的`next`指向為空,這也是判斷尾部節點的依據。
這裡主要介紹插入和刪除節點的操作。
##### 3.1.1 插入節點
向單鏈表中插入一個新節點,可以通過調整兩次`next`指向來完成。如下圖所示,X為新節點,將其`next`指向為A2,再將A1的`next`指向為X即可。
若是從尾部節點插入,直接將尾部節點的`next`指向新節點即可。
![單鏈表-插入節點.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092847506-1421564514.png)
##### 3.1.2 刪除節點
從單鏈表中刪除一個節點,可以通過修改`next`指向來實現,如下圖所示,將A1的`next`指向為A3,這樣便刪除A2,A2的記憶體空間會自動被垃圾回收。
若是刪除尾部節點,直接將上一節點的`next`指向為空即可。
![單鏈表-刪除節點.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092847697-126416291.png)
#### 3.2 程式碼實現
我們使用Java程式碼來實現一個單鏈表。其中`Node`類儲存單鏈表的一個節點,`SinglyLinkedList`類實現了單鏈表的所有操作方法。
`SinglyLinkedList`類使用帶頭節點的方式實現,即`head`節點,該節點不儲存資料,只是標記單鏈表的開始。
```java
public class SinglyLinkedListDemo {
public static void main(String[] args) {
Node node1 = new Node(1, "張三");
Node node2 = new Node(3, "李四");
Node node3 = new Node(7, "王五");
Node node4 = new Node(5, "趙六");
SinglyLinkedList singlyLinkedList = new SinglyLinkedList();
System.out.println("-----------新增節點(尾部)");
singlyLinkedList.add(node1);
singlyLinkedList.add(node2);
singlyLinkedList.add(node3);
singlyLinkedList.add(node4);
singlyLinkedList.print();
System.out.println("-----------獲取某個節點");
Node node = singlyLinkedList.get(3);
System.out.println(node);
singlyLinkedList.remove(node3);
System.out.println("-----------移除節點");
singlyLinkedList.print();
System.out.println("-----------修改節點");
singlyLinkedList.update(new Node(5, "趙六2"));
singlyLinkedList.print();
System.out.println("-----------按順序新增節點");
Node node5 = new Node(4, "王朝");
singlyLinkedList.addOfOrder(node5);
singlyLinkedList.print();
}
private static class SinglyLinkedList {
// head節點是單鏈表的開始,不用來儲存資料
private Node head = new Node(0, null);
/**
* 將節點新增到尾部
*
* @param node
*/
public void add(Node node) {
Node temp = head;
while (true) {
if (temp.next == null) {
temp.next = node;
break;
}
temp = temp.next;
}
}
/**
* 按順序新增節點
*
* @param node
*/
public void addOfOrder(Node node) {
Node temp = head;
while (true) {
if (temp.next == null) {
temp.next = node;
break;
} else if(temp.next.key > node.getKey()){
node.next = temp.next;
temp.next = node;
break;
}
temp = temp.next;
}
}
/**
* 獲取某個節點
*
* @param key
* @return
*/
public Node get(int key) {
if (head.next == null) {
return null;
}
Node temp = head.next;
while (temp != null) {
if (temp.key == key) {
return temp;
}
temp = temp.next;
}
return null;
}
/**
* 移除一個節點
*
* @param node
*/
public void remove(Node node) {
Node temp = head;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.key == node.key) {
temp.next = temp.next.next;
break;
}
temp = temp.next;
}
}
/**
* 修改一個節點
*
* @param node
*/
public void update(Node node) {
Node temp = head.next;
while (true) {
if (temp == null) {
break;
}
if (temp.key == node.key) {
temp.value = node.value;
break;
}
temp = temp.next;
}
}
/**
* 列印連結串列
*/
public void print() {
Node temp = head.next;
while (temp != null) {
System.out.println(temp.toString());
temp = temp.next;
}
}
}
private static class Node {
private final int key;
private String value;
private Node next;
public Node(int key, String value) {
this.key = key;
this.value = value;
}
public int getKey() {
return key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Node getNext() {
return next;
}
@Override
public String toString() {
return "Node{" +
"key=" + key +
", value='" + value + '\'' +
'}';
}
}
}
```
輸出結果:
```
-----------新增節點(尾部)
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------獲取某個節點
Node{key=3, value='李四'}
-----------移除節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=5, value='趙六'}
-----------修改節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=5, value='趙六2'}
-----------按順序新增節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=4, value='王朝'}
Node{key=5, value='趙六2'}
```
#### 3.3 單鏈表的缺點
通過對單鏈表的分析,可以看出單鏈表有如下缺點:
(1)單鏈表的查詢方法只能是一個方向
(2)單鏈表不能自我刪除,需要靠上一節點進行輔助操作。
而這些缺點可以通過雙鏈表來解決,下面來看詳細介紹。
### 4. 雙鏈表(Doubly Linked List)
雙鏈表又叫雙向連結串列,其節點由三部分構成:
* `prev`域:用於指向上一節點
* `data`域:資料域,用來儲存元素資料
* `next`域:用於指向下一節點
雙鏈表的結構如下圖:
![雙鏈表.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092847911-973593170.png)
#### 4.1 雙鏈表的操作
雙鏈表的操作可以從兩端開始,從第一個節點通過`next`指向可以一步步操作到尾部,從最後一個節點通過`prev`指向可以一步步操作到頭部。
這裡主要介紹插入和刪除節點的操作。
##### 4.1.1 插入節點
向雙鏈表中插入一個新節點,需要通過調整兩次`prev`指向和兩次`next`指向來完成。如下圖所示,X為新節點,將A1的`next`指向X,將X的`next`指向A2,將A2的`prev`指向X,將X的`prev`指向A1即可。
![雙鏈表-插入節點.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092848138-1660447517.png)
##### 4.1.2 刪除節點
從雙鏈表中刪除一個節點,需要通過調整一次`prev`指向和一次`next`指向來完成。如下圖所示,刪除A2節點,將A1的`next`指向A3,將A3的 `prev`指向A1即可。
![雙鏈表-刪除節點.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092848330-1290815184.png)
#### 4.2 程式碼實現
我們使用Java程式碼來實現一個雙鏈表。其中 `Node`類儲存雙鏈表的一個節點,`DoublyLinkedList`類實現雙鏈表的所有操作方法。
`DoublyLinkedList`類使用不帶頭節點的方式實現,其中`first`為第一個節點,`last`為最後一個節點。這兩個節點預設都為空,若只有一個元素時,則兩個節點指向同一元素。
```java
public class DoublyLinkedListDemo {
public static void main(String[] args) {
DoublyLinkedList doublyLinkedList = new DoublyLinkedList();
System.out.println("-----------從尾部新增節點");
doublyLinkedList
.addToTail(new Node(1, "張三"))
.addToTail(new Node(3, "李四"))
.addToTail(new Node(7, "王五"))
.addToTail(new Node(5, "趙六"))
.print();
System.out.println("-----------從頭部新增節點");
doublyLinkedList
.addToHead(new Node(0, "朱開山"))
.print();
System.out.println("-----------獲取某個節點");
System.out.println(doublyLinkedList.get(3));
System.out.println("-----------移除節點");
doublyLinkedList
.remove(new Node(3, "李四"))
.print();
System.out.println("-----------修改節點");
doublyLinkedList
.update(new Node(5, "趙六2")).print();
System.out.println("-----------按順序新增節點");
doublyLinkedList
.addOfOrder(new Node(4, "王朝"))
.print();
}
private static class DoublyLinkedList {
private Node first = null; // first節點是雙鏈表的頭部,即第一個節點
private Node last = null; // tail節點是雙鏈表的尾部,即最後一個節點
/**
* 從尾部新增
*
* @param node
*/
public DoublyLinkedList addToTail(Node node) {
if (last == null) {
first = node;
} else {
last.next = node;
node.prev = last;
}
last = node;
return this;
}
/**
* 按照順序新增
*
* @param node
*/
public DoublyLinkedList addOfOrder(Node node) {
if (first == null) {
first = node;
last = node;
return this;
}
// node比頭節點小,將node設為頭節點
if (first.key > node.key) {
first.prev = node;
node.next = first;
first = node;
return this;
}
// node比尾節點大,將node設為尾節點
if (last.key < node.key) {
last.next = node;
node.prev = last;
last = node;
return this;
}
Node temp = first.next;
while (true) {
if (temp.key > node.key) {
node.next = temp;
node.prev = temp.prev;
temp.prev.next = node;
temp.prev = node;
break;
}
temp = temp.next;
}
return this;
}
/**
* 從頭部新增
*
* @param node
*/
public DoublyLinkedList addToHead(Node node) {
if (first == null) {
last = node;
} else {
node.next = first;
first.prev = node;
}
first = node;
return this;
}
/**
* 獲取節點
*
* @param key
* @return
*/
public Node get(int key) {
if (first == null) {
return null;
}
Node temp = first;
while (temp != null) {
if (temp.key == key) {
return temp;
}
temp = temp.next;
}
return null;
}
/**
* 移除節點
*
* @param node
*/
public DoublyLinkedList remove(Node node) {
if (first == null) {
return this;
}
// 要移除的是頭節點
if (first == node) {
first.next.prev = null;
first = first.next;
return this;
}
// 要移除的是尾節點
if (last == node) {
last.prev.next = null;
last = last.prev;
return this;
}
Node temp = first.next;
while (temp != null) {
if (temp.key == node.key) {
temp.prev.next = temp.next;
temp.next.prev = temp.prev;
break;
}
temp = temp.next;
}
return this;
}
/**
* 修改某個節點
*
* @param node
*/
public DoublyLinkedList update(Node node) {
if (first == null) {
return this;
}
Node temp = first;
while (temp != null) {
if (temp.key == node.key) {
temp.value = node.value;
break;
}
temp = temp.next;
}
return this;
}
/**
* 列印連結串列
*/
public void print() {
if (first == null) {
return;
}
Node temp = first;
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
}
private static class Node {
private final int key;
private String value;
private Node prev; // 指向上一節點
private Node next; // 指向下一節點
public Node(int key, String value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"key=" + key +
", value='" + value + '\'' +
'}';
}
}
}
```
輸出結果:
```
-----------從尾部新增節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------從頭部新增節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------獲取某個節點
Node{key=3, value='李四'}
-----------移除節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------修改節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=7, value='王五'}
Node{key=5, value='趙六2'}
-----------按順序新增節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=4, value='王朝'}
Node{key=7, value='王五'}
Node{key=5, value='趙六2'}
```
### 5. 環形連結串列(Circular Linked List)
環形連結串列又叫迴圈連結串列,本文講述的是單向環形連結串列,其與單鏈表的唯一區別是尾部節點的`next`不再為空,則是指向了頭部節點,這樣便形成了一個環。
環形連結串列的結構如下圖:
![環形連結串列.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092848531-71366558.png)
#### 5.1 約瑟夫問題
約瑟夫問題:有時也稱為約瑟夫斯置換,是一個電腦科學和數學中的問題。在計算機程式設計的演算法中,類似問題又稱為約瑟夫環。又稱“丟手絹問題”。
引自[百度百科](https://baike.baidu.com/item/%E7%BA%A6%E7%91%9F%E5%A4%AB%E9%97%AE%E9%A2%98/3857719?fr=aladdin):
> 據說著名猶太曆史學家Josephus有過以下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡為止。然而Josephus 和他的朋友並不想遵從。首先從一個人開始,越過k-2個人(因為第一個人已經被越過),並殺掉第*k*個人。接著,再越過k-1個人,並殺掉第*k*個人。這個過程沿著圓圈一直進行,直到最終只剩下一個人留下,這個人就可以繼續活著。問題是,給定了和,一開始要站在什麼地方才能避免被處決。Josephus要他的朋友先假裝遵從,他將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。
>
> 17世紀的法國數學家加斯帕在《數目的遊戲問題》中講了這樣一個故事:15個教徒和15 個非教徒在深海上遇險,必須將一半的人投入海中,其餘的人才能倖免於難,於是想了一個辦法:30個人圍成一圓圈,從第一個人開始依次報數,每數到第九個人就將他扔入大海,如此迴圈進行直到僅餘15個人為止。問怎樣排法,才能使每次投入大海的都是非教徒。
>
> 問題分析與演算法設計
>
> 約瑟夫問題並不難,但求解的方法很多;題目的變化形式也很多。這裡給出一種實現方法。
>
> 題目中30個人圍成一圈,因而啟發我們用一個迴圈的鏈來表示,可以使用結構陣列來構成一個迴圈鏈。結構中有兩個成員,其一為指向下一個人的指標,以構成環形的鏈;其二為該人是否被扔下海的標記,為1表示還在船上。從第一個人開始對還未扔下海的人進行計數,每數到9時,將結構中的標記改為0,表示該人已被扔下海了。這樣迴圈計數直到有15個人被扔下海為止。
![自殺順序.jpg](https://img2020.cnblogs.com/blog/2179262/202102/2179262-20210214092848767-44946225.jpg)
#### 5.2 程式碼實現
我們使用Java程式碼來實現一個環形連結串列,並將節點按約瑟夫問題順序出列。
```java
public class CircularLinkedListDemo {
public static void main(String[] args) {
CircularLinkedList circularLinkedList = new CircularLinkedList();
System.out.println("-----------新增10個節點");
for (int i = 1; i <= 10; i++) {
circularLinkedList.add(new Node(i));
}
circularLinkedList.print();
System.out.println("-----------按約瑟夫問題順序出列");
circularLinkedList.josephusProblem(3);
}
private static class CircularLinkedList {
private Node first = null; // 頭部節點,即第一個節點
/**
* 新增節點,並將新新增的節點的next指向頭部,形成一個環形
*
* @param node
* @return
*/
public void add(Node node) {
if (first == null) {
first = node;
first.next = first;
return;
}
Node temp = first;
while (true) {
if (temp.next == null || temp.next == first) {
temp.next = node;
node.next = first;
break;
}
temp = temp.next;
}
}
/**
* 按約瑟夫問題順序出列
* 即從第1個元素開始報數,報到num時當前元素出列,然後重新從下一個元素開始報數,直至所有元素出列
*
* @param num 表示報幾次數
*/
public void josephusProblem(int num) {
Node currentNode = first;
// 將當前節點指向最後一個節點
do {
currentNode = currentNode.next;
} while (currentNode.next != first);
// 開始出列
while (true) {
// 當前節點要指向待出列節點的前一節點(雙向環形佇列不需要)
for (int i = 0; i < num - 1; i++) {
currentNode = currentNode.next;
}
System.out.printf("%s\t", currentNode.next.no);
if(currentNode.next == currentNode){
break;
}
currentNode.next = currentNode.next.next;
}
}
/**
* 輸出節點
*/
public void print() {
if (first == null) {
return;
}
Node temp = first;
while (true) {
System.out.printf("%s\t", temp.no);
if (temp.next == first) {
break;
}
temp = temp.next;
}
System.out.println();
}
}
private static class Node {
private final int no;
private Node next; // 指向下一節點
public Node(int no) {
this.no = no;
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
'}';
}
}
}
```
輸出結果:
```
-----------新增10個節點
1 2 3 4 5 6 7 8 9 10
-----------按約瑟夫問題順序出列
3 6 9 2 7 1 8 5 10 4
```
### 推薦閱讀
* 《 [陣列](https://mp.weixin.qq.com/s/YVbahU_0fzmyEX-JBvcnqQ) 》
* 《 [稀疏陣列](https://mp.weixin.qq.com/s/YYemaomm10HiKs9MoKHKIw) 》
* 《 [連結串列(單鏈表、雙鏈表、環形連結串列)](https://mp.weixin.qq.com/s/46ShChMslDGsV6xSObh5nQ) 》
* 《 [棧](https://mp.weixin.qq.com/s/dfv4WM_-agLpygCuzqQUTA) 》
* 《 [佇列](https://mp.weixin.qq.com/s/64oTQJatNcBsfvrJKMQOWA) 》
* 《 [樹](https://mp.weixin.qq.com/s/Ui5p4RQRwEHv4a_HWeXJYQ) 》
* 《 [二叉樹](https://mp.weixin.qq.com/s/XkeEyUCCvQ_AtMLBUYTH0Q) 》
* 《 [二叉查詢樹(BST)](https://mp.weixin.qq.com/s/6S8M6r-EY4IMF3UUvZ7_AA) 》
* 《 [AVL樹(平衡二叉樹)](https://mp.weixin.qq.com/s/eeXi_11illdVqMnkse_mhQ) 》
* 《 [B樹](https://mp.weixin.qq.com/s/Cx03l-ezvYjAKrmedup-aQ) 》
* 《 [散列表(雜湊表)](https://mp.weixin.qq.com/s/oX28uyCbbaYQErT6RE-t