1. 程式人生 > >『資料結構與演算法』連結串列(單鏈表、雙鏈表、環形連結串列)

『資料結構與演算法』連結串列(單鏈表、雙鏈表、環形連結串列)

> 微信搜尋:碼農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