---
# System prepended metadata

title: 雙向鏈結串列(Doubly Linked List)

---

# 雙向鏈結串列(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號節點。

這些問題搞清楚之後回去看程式碼應該會很容易理解。