# 雙向鏈結串列(Doubly Linked List) **雙向鏈結串列示意圖** ![Doubly Linked List](https://www.namepluto.com/wp-content/uploads/2021/06/doubly.png) 可以很明顯的發現,查找的方向從單向變為雙向。 ## 遍歷、添加、修改、刪除的操作思路 1. **遍歷**方式和單向鏈結串列一樣,只是可以向前也可以向後查找 2. **添加**(默認加到雙向鏈結串列的最後) -- 先找到雙向鏈結串列的最後節點 -- `temp.next = newHeroNode` -- `newHeroNode.prev = temp` 3. **修改**的思路和原來的單向鏈結串列一樣 4. **刪除** -- 因為是雙向,因此我們可以實現自我刪除某個節點 -- 直接找到要刪除的節點,比如 temp -- `temp.prev.next = temp.next` -- `temp.next.prev = temp.prev` ## 代碼實現 因為基本和單向鏈結串列差不多,僅有刪除和新增的部分有些微的修改,因此不再多做解釋。 ```java= public class DoublyLinkedListDemo { public static void main(String[] args) { System.out.println("雙向鏈結串列的測試"); HeroNode2 hero1 = new HeroNode2(1, "宋江", "及時雨"); HeroNode2 hero2 = new HeroNode2(2, "盧俊義", "玉麒麟"); HeroNode2 hero3 = new HeroNode2(3, "吳用", "智多星"); HeroNode2 hero4 = new HeroNode2(4, "林沖", "豹子頭"); DoublyLinkedList doublyLinkedList = new DoublyLinkedList(); doublyLinkedList.add(hero1); doublyLinkedList.add(hero4); doublyLinkedList.add(hero2); doublyLinkedList.add(hero3); doublyLinkedList.list(); System.out.println("===修改測試==="); HeroNode2 newHeroNode = new HeroNode2(4, "公孫勝", "入雲龍"); doublyLinkedList.updata(newHeroNode); doublyLinkedList.list(); System.out.println("===刪除測試==="); doublyLinkedList.delete(2); doublyLinkedList.list(); } } class DoublyLinkedList { //先初始化一個頭節點,頭節點不要動,不存放具體數據 private HeroNode2 head = new HeroNode2(0, "", ""); public HeroNode2 getHead() { return head; } //遍歷雙向鏈結串列 public void list() { if(head.next == null) { System.out.println("鏈結串列為空"); return; } HeroNode2 temp = head.next; while(true) { if(temp == null) { break; } //輸出節點訊息 System.out.println(temp); //將temp後移, 不然是死循環 temp = temp.next; } } //添加數據到雙向鏈結串列的最後 public void add(HeroNode2 heroNode) { //因為head節點不能動,因此我們需要一個輔助遍歷 temp HeroNode2 temp = head; //遍歷鏈結串列,找到最後 while(true) { if (temp.next == null) { break; } temp = temp.next; //沒找到就將temp後移一個節點 } //當退出while循環時,temp就指向了鏈結串列的最後 //形成一個雙向鏈結串列 temp.next = heroNode; heroNode.prev = temp; } //修改節點內容 public void updata(HeroNode2 newHeroNode) { if(head.next == null) { System.out.println("鏈結串列為空"); return; } //找到需要修改的節點 //定義一個輔助變數 HeroNode2 temp = head.next; boolean flag = false; //是否找到該節點 while(true) { if (temp == null) break; //已遍歷結束 if (temp.no == newHeroNode.no) { //找到 flag = true; break; } temp = temp.next; } //根據flag 判斷是否找到 if(flag) { temp.name = newHeroNode.name; temp.nickname = newHeroNode.nickname; }else { System.out.printf("沒有找到編號 %d 的節點, 不能修改\n",newHeroNode.no); } } //刪除節點 public void delete(int no) { HeroNode2 temp = head.next; boolean flag = false; if(head.next == null) { System.out.println("鏈結串列為空,無法刪除"); return; } while(true) { if (temp == null) break; //已遍歷結束 if(temp.no == no) { //要刪除的節點為第一個節點 flag = true; break; } temp = temp.next; } if(flag) { temp.prev.next = temp.next; //如果是最後一個節點就不需要執行下面這句話,否則會出現空指針異常 if(temp.next != null) { temp.next.prev = temp.prev; } System.out.printf("刪除了 %d 節點.\n",no); }else { System.out.printf("沒有找到編號 %d 的節點, 不能刪除\n",no); } } } class HeroNode2 { public int no; public String name; public String nickname; public HeroNode2 next; //指向下一個節點, 默認為null public HeroNode2 prev; //指向前一個節點, 默認為null //構造函數 public HeroNode2(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } //重寫toString @Override public String toString() { return "HeroNode [no = " + no + ", name = " + name + ", nickname = " + nickname + "]"; } } ``` ![](https://www.namepluto.com/wp-content/uploads/2021/06/image-42.png) ### 有序添加 ```java= public void addByOrder(HeroNode2 heroNode) { HeroNode2 temp = head; boolean flag = false; //添加的編號是否已經存在 while(true) { if(temp.next == null) break; if(temp.next.no > heroNode.no) { //位置找到, 就在temp的後面插入 break; }else if(temp.next.no == heroNode.no) { //編號已經存在 flag = true; break; } temp = temp.next; } //判斷flag的值 if(flag) { System.out.printf("準備添加的英雄編號 %d 已經存在,不能添加\n",heroNode.no); }else{ heroNode.next = temp.next; temp.next = heroNode; heroNode.prev = temp; } } ``` ![](https://www.namepluto.com/wp-content/uploads/2021/06/image-41.png) ## 約瑟夫問題(Josephus Problem) 設編號為1,2,...n的n個人圍坐一圈,約定編號為k(1<=k<=n)的人從1開始報數,數到m的人出列,依此類推直到所有人出列為止,由此產生一個出隊編號的序列。 **提示:** 用一個不帶頭結點的循環鏈結串列來處理,先構成一個有n個結點的單循環鏈結串列,然後由k結點起從1開始計數,計到m時,對應結點從鏈結串列中刪除,然後再從被刪除結點的下一個結點又從1開始報數,直到最後一個結點從鏈結串列中刪除演算法結束。 **設 n = 5 , k = 1(從1開始數), m = 2(數到第2個出隊列)** ![](https://i.imgur.com/131wjA8.png) 如下圖所示,出隊列順序為: **2 -> 4 -> 1 -> 5 -> 3** ![](https://www.namepluto.com/wp-content/uploads/2021/06/%E7%B4%84%E7%91%9F%E5%A4%AB%E5%95%8F%E9%A1%8C.png) ### 分析圖解和實現 這部分建議看 [EP28](https://www.bilibili.com/video/BV16t411g7wa?p=28) 聽老師講解思路,用文字說明不太容易懂。 ### 構建一個單項的環形鏈結串列 **思路** 1. 先創建第一個節點,讓 first 指向該節點,並形成一個環形 2. 後面當我們每創建一個新的節點,就把該節點加入到已有的環形鏈結串列中 (每個節點的next指向下一個節點,最後一個節點的next指向第一個節點形成一個環) #### 遍歷環形鏈結串列 1. 先讓一個輔助變數指向 first 節點 2. 然後通過一個 while 循環遍歷該環形鏈結串列 `cur.next == first` 根據使用者的輸入,生成一個節點出隊列的順序: 1. 需求創建一個輔助變數 helper,事先應該指向環形鏈結串列的最後節點 2. 先讓 first 和 helper移動 k-1 次 3. 當節點報數時,讓first和helper指針同時移動 m-1 次 4. 這時就可以將first 指向節點出隊列 -- `first = first.next` -- `helper.next = first` 原先first 指向的節點就沒有任何引用(出隊列了),被垃圾回收。 ```java= public class JosephusDemo { public static void main(String[] args) { CircularSinglyLinkedList csll = new CircularSinglyLinkedList(); csll.addNode(5); // 加入5個節點 csll.showNode(); System.out.println("===出隊列順序==="); csll.countNode(1, 2, 5); } } // 創建一個環形的單向鏈結串列 class CircularSinglyLinkedList { // 創建一個first節點, 當前沒有編號 private Node first = new Node(-1); // 添加節點, 構建成一個環形的鏈結串列 public void addNode(int nums) { // nums 數據校驗 if (nums < 1) { System.out.println("nums值不正確! "); return; } Node cur = null; // 輔助變數 幫助構建環形鏈結串列 // 使用for循環創建環形鏈結串列 for (int i = 1; i <= nums; i++) { // 根據編號創建節點 Node child = new Node(i); // 若為第一個節點 if (i == 1) { first = child; first.setNext(first); // 構成環 cur = first; // 讓cur 指向第一個節點 } else { cur.setNext(child); child.setNext(first); cur = child; } } } // 遍歷當前環形鏈結串列 public void showNode() { // 判斷鏈結串列是否為空 if (first == null) { System.out.println("鏈結串列為空"); return; } // 因為 first不能動,因此需要使用一個輔助指針 Node cur = first; while (true) { System.out.printf("節點編號 %d \n", cur.getNo()); if (cur.getNext() == first) { // 遍歷完畢 break; } cur = cur.getNext(); // 往後遍歷 } } /** * * @param startNo 表示從第幾個node開始數 * @param countNum 表示數幾次 * @param nums 表示最初有多少個node在環中 */ // 根據使用者的輸入,計算出節點出隊列的順序 public void countNode(int startNo, int countNum, int nums) { if (first == null || startNo < 1 || startNo > nums) { System.out.println("參數輸入有誤,請重新輸入"); return; } // 創建一個輔助指針,幫助完成node出隊列 Node helper = first; while (true) { if (helper.getNext() == first) { // helper指向最後一個節點 break; } helper = helper.getNext(); } //先讓first和helper 移動startNo-1次 for (int j = 0; j < startNo - 1; j++) { first = first.getNext(); helper = helper.getNext(); } //當節點報數時,讓first和helper指針同時移動 countNum- 1次 然後出隊列 while(true) { if(helper == first) { //說明環中只有一個node break; } //讓 first和helper同時移動 countNum -1次 for (int i = 0; i < countNum -1; i++) { first = first.getNext(); helper = helper.getNext(); } //這時 first 指向的節點就是 要出隊列 的節點 System.out.printf("節點 %d 出隊列\n",first.getNo()); //將first指向的節點出隊列 first = first.getNext(); //出隊列後往下一個節點繼續數 helper.setNext(first); //把helper歸回第一個數的節點繼續遍歷 } System.out.printf("最後留在環中的節點為 %d \n",first.getNo()); } } // 創建一個Node類,表示一個節點 class Node { private int no; // 編號 private Node next; // 指向下一個節點 public Node(int no) { this.no = no; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } } ``` ![](https://i.imgur.com/IeJlTCF.png) **個人理解** * first 指向第一個數的節點 * helper 用於遍歷節點 假設今天**n = 5 (總共5個節點), k = 1 (從1開始數) , m = 2 (數到2出隊列)** 1. 那麼first要指向第一個數的節點,所以是 1,helper 也先設為 first 2. 然後將 first,helper 遍歷 2-1次 **為什麼first也要跟著遍歷,它不是要指向第一個節點嗎?** 那是因為當你數到 2 節點出隊列後,下一次就是從出隊列的後一個節點開始數, 所以 first 也需要跟著 helper 一起遍歷,如此一來,當 2 號節點出隊列後,first會指向 3,而helper也指向3 重新開始遍歷。 以此類推。 **為什麼明明要數 2 次(m = 2),卻只需要遍歷 2-1(m - 1)次?** 如果會有這個疑惑就是對約瑟夫問題還沒理解,這裡的 m 代表數到幾出隊列,而開始數的那個節點也要計算,所以從 1節點 開始數,數到 2 就是 2號節點。 這些問題搞清楚之後回去看程式碼應該會很容易理解。