# Data Structure / Doubly Linked List ## 1. 基本概念 雙向鏈表的每個節點包含三個主要部分: - 數據(data) - 指向前一個節點的指標(prev) - 指向下一個節點的指標(next) **`prev` 指標的有無,就是DLL與SLL的主要差別。** --- **雙向鏈結在實際應用中很有用ㄛ,例如:** - 瀏覽器的歷史記錄(前進 / 後退功能) - 音樂播放器的播放清單 - 文字編輯器中的撤銷 / 重做功能 - 快取系統中的 LRU(最近最少使用)演算法 ### 1.1 節點結構 基本的節點類定義如下(等等會解釋不用擔心): ```java= class IntNode { int info; IntNode next = null, prev = null; IntNode() { } IntNode(int info) { this.info = info; } IntNode(int info, IntNode next, IntNode prev) { this.info = info; this.next = next; this.prev = prev; } } ``` ### 1.2 節點結構 - 逐行解釋 #### 1.2.1 類別定義 ```java class IntNode { ``` 這行程式碼宣告了一個名為 `IntNode` 的類別,用來表示雙向鏈結串列中的節點。雙向鏈結串列是一種資料結構,節點包含資料(`info`)、指向前一個節點的變數(`prev`)、以及指向下一個節點的變數(`next`)。 #### 1.2.2 變數宣告 ```java int info; ``` 這行宣告一個整數變數 `info`,用來存放節點的資料。預設的存取控制是僅能在同一個類別或套件中使用。 ```java IntNode next = null, prev = null; ``` 這行宣告兩個變數 `next` 和 `prev`,它們的型別是 `IntNode`,分別用來指向下一個節點和前一個節點。這兩個變數的初始值為 `null`,表示目前節點不連接到其他節點。 #### 1.2.3 建構子:無參數版本 ```java IntNode() { } ``` 這是類別的無參數建構子,當你建立一個節點物件時,如果不傳入任何參數,就會使用這個建構子,它不進行任何初始化操作。 #### 1.2.4 建構子:單參數版本 ```java IntNode(int el) { info = el; } ``` 這個建構子接受一個參數 `el`,並將它賦值給節點的 `info` 變數,用來初始化節點的資料。此時,`next` 和 `prev` 的值仍然是 `null`。 #### 1.2.5 建構子:完整版本 ```java IntNode(int info, IntNode next, IntNode prev) { this.info = info; this.next = next; this.prev = prev; } ``` 這個建構子接受三個參數: - `info`:用來初始化節點的資料,賦值給 `this.info`,即當前物件的 `info` 變數。 - `next`:下一個節點,賦值給 `this.next`,即當前物件的 `next` 變數。 - `prev`:前一個節點,賦值給 `this.prev`,即當前物件的 `prev` 變數。 ### 1.3 DLL 結構 雙向鏈表類包含指向頭節點和尾節點的指標: ```java= class IntDLList { private IntNode head, tail; public IntDLList() { head = tail = null; } } ``` **(逐行解釋就不再寫ㄌ,[點我回到SLL複習](https://hackmd.io/Fyn8LvpQTH-Bu95CJD4rYQ#14-SLL-%E7%B5%90%E6%A7%8B---%E9%80%90%E8%A1%8C%E8%A7%A3%E9%87%8B))** ## 2. 基本操作詳解 ### 2.0 Helpers 輔助函數 ```java= public boolean isEmpty() { // 檢查鏈表是否為空 return head == null; } public void setToNull() { // 將鏈表設為空 head = tail = null; } public int firstEl() { // 獲取頭節點資料 if (!isEmpty()) { return head.info; } else { return 0; // 或拋出異常 } } ``` ### 2.1 插入操作 #### 2.1.1 在頭部插入 在頭部插入是最簡單的插入操作,時間複雜度為 O(1)。 ```java= public void addToHead(int el) { if (!isEmpty()) { head = new IntNode(el, head, null); head.next.prev = head; } else { head = tail = new IntNode(el); } } ``` 這個操作的步驟如下: 1. 如果鏈表不為空,建立一個新的節點 `IntNode`,將新節點的 `next` 設為當前的 `head`,並將新節點設為新的 `head`。隨後,將原本的 `head` 節點的 `prev` 設為新節點,建立雙向鏈結。 2. 如果鏈表為空,則直接將 `head` 和 `tail` 都指向新建立的節點。 #### 2.1.2 在尾部插入 在尾部插入的時間複雜度也是 O(1),因為我們維護了一個 tail 指標。 ```java= public void addToTail(int el) { if (!isEmpty()) { tail = new IntNode(el, null, tail); tail.prev.next = tail; } else { head = tail = new IntNode(el); } } ``` 這個操作的步驟如下: 1. 如果鏈表不為空,建立一個新節點 `IntNode`,將新節點的 `prev` 設為當前的 `tail`,並將新節點設為新的 `tail`。隨後,將原本的 `tail` 節點的 `next` 設為新節點,完成雙向鏈結。 2. 如果鏈表為空,則將 `head` 和 `tail` 都指向新建立的節點。 #### 2.1.3 在中間插入 在中間插入需要先找到插入位置,因此時間複雜度為 O(n)。 ```java= public void insertAfter(int el, int afterEl) { IntNode tmp = head; while (tmp != null && tmp.info != afterEl) { tmp = tmp.next; } if (tmp != null) { // 找到目標節點 IntNode newNode = new IntNode(el, tmp.next, tmp); if (tmp.next != null) { tmp.next.prev = newNode; } else { tail = newNode; // 如果 tmp 是尾節點,更新 tail } tmp.next = newNode; } } ``` 這個操作的步驟如下: 1. 遍歷鏈表找到值為 `afterEl` 的節點。 2. 如果找到目標節點,創建新節點並插入到該節點之後。 3. 調整新節點與相鄰節點的連結,並根據情況更新 `tail`。 ### 2.2 刪除操作 #### 2.2.1 刪除頭部節點 刪除頭部節點的時間複雜度為 O(1)。 ```java= public int deleteFromHead() { if (!isEmpty()) { int el = head.info; if (head == tail) { head = tail = null; } else { head = head.next; head.prev = null; } return el; } else { return 0; // 或拋出異常 } } ``` 這個操作的步驟如下: 1. 如果鏈表不為空,則取得頭部節點的資料值。 2. 如果鏈表只有一個節點,將 `head` 和 `tail` 同時設為 `null`。 3. 如果鏈表有多個節點,將 `head` 移動到下一個節點,並將新的頭節點的 `prev` 設為 `null`。 4. 返回刪除的資料值。 #### 2.2.2 刪除尾部節點 刪除尾部節點只需調整 `tail` 和 `tail.prev` 之間的連結,不需要遍歷整個鏈表,因此時間複雜度為 O(1)。**BUT! SLL的時間複雜度為 O(n),因為我們需要遍歷到倒數第二個節點。** ```java= public int deleteFromTail() { if (!isEmpty()) { int el = tail.info; if (head == tail) { head = tail = null; } else { tail = tail.prev; tail.next = null; } return el; } else { return 0; // 或拋出異常 } } ``` 這個操作的步驟如下: 1. 如果鏈表不為空,取得尾部節點的資料值。 2. 如果鏈表只有一個節點,將 `head` 和 `tail` 同時設為 `null`。 3. 如果鏈表有多個節點,將 `tail` 移動到前一個節點,並將新的尾節點的 `next` 設為 `null`。 4. 返回刪除的資料值。 #### 2.2.3 刪除特定節點 ```java= public int delete(int el) { IntNode tmp = head; while (tmp != null && tmp.info != el) { tmp = tmp.next; } if (tmp != null) { // 找到目標節點 if (tmp == head) { return deleteFromHead(); } else if (tmp == tail) { return deleteFromTail(); } else { tmp.prev.next = tmp.next; tmp.next.prev = tmp.prev; return tmp.info; } } return 0; // 或拋出異常 } ``` 這個操作的步驟如下: 1. 遍歷鏈表找到值為 `el` 的節點。 2. 如果找到節點,根據其位置(頭部、尾部或中間)進行刪除操作: - 如果是頭部節點,使用 `deleteFromHead()`。 - 如果是尾部節點,使用 `deleteFromTail()`。 - 如果在中間,調整前後節點的 `next` 和 `prev` 連結,將其從鏈表中移除。 ### 2.3 搜索操作 ```java= public int find(int el) { IntNode tmp; for (tmp = head; tmp != null && tmp.info != el; tmp = tmp.next); if (tmp == null) { return 0; // 或拋出異常 } else { return tmp.info; } } ``` 這個操作的步驟如下: 1. 從 `head` 開始遍歷鏈表,每次移動到下一個節點 `tmp.next`,直到找到與 `el` 相等的資料值或遍歷到鏈表尾部(即 `tmp == null`)。 2. 如果找不到節點,返回 `0`(或拋出異常),如果找到則返回節點的 `info` 值。 ### 2.4 完整程式碼 Demo ```java= class IntNode { int info; IntNode next = null, prev = null; IntNode() { } IntNode(int info) { this(info, null, null); } IntNode(int info, IntNode next, IntNode prev) { this.info = info; this.next = next; this.prev = prev; } } class DLL { private IntNode head, tail; // constructor public DLL() { head = tail = null; } // helpers public boolean isEmpty() { return head == null; } public void setToNull() { head = tail = null; } public int firstInfo() { if (!isEmpty()) { return head.info; } else { return 0; } } public void printing() { if (isEmpty()) { System.out.println("List is empty."); return; } IntNode temp = head; while (true) { if (temp == null) { break; } System.out.print(temp.info + " "); temp = temp.next; } System.out.println(); } // ADD METHODS public void addToHead(int info) { if (!isEmpty()) { head = new IntNode(info, head, null); head.next.prev = head; } else { head = tail = new IntNode(info); } } public void addFromTail(int info) { if (!isEmpty()) { tail = new IntNode(info, null, tail); tail.prev.next = tail; } else { head = tail = new IntNode(info); } } public void insertAfter(int info, int key) { IntNode temp = head; while (temp != null && temp.info != key) { temp = temp.next; } // key if (temp != null) { IntNode newNode = new IntNode(info, temp.next, temp); // new IntNode if (temp.next != null) { temp.next.prev = newNode; } else { tail = newNode; } // prev temp.next = newNode; // next } } // key => new IntNode => next&prev // DELETE METHODS public int deleteFromHead() { if (!isEmpty()) { int deletedInfo = head.info; if (head == tail) { head = tail = null; } else { head = head.next; head.prev = null; } return deletedInfo; } else { System.out.println("List is empty."); return 0; } } public int deleteFromTail() { if (!isEmpty()) { int deletedInfo = tail.info; if (head == tail) { head = tail = null; } else { tail = tail.prev; tail.next = null; } return deletedInfo; } else { System.out.println("List is empty."); return 0; } } public int delete(int key) { IntNode temp = head; while (temp != null && temp.info != key) { temp = temp.next; } // key if (temp != null) { if (temp == head) { return deleteFromHead(); } else if (temp == tail) { return deleteFromTail(); } else { temp.prev.next = temp.next; temp.next.prev = temp.prev; return temp.info; } } return 0; } // FIND METHOD public int find(int key) { IntNode temp = head; while (temp != null && temp.info != key) { temp = temp.next; } // key if (temp == null) { return 0; } else { return temp.info; } } } public class DS { public static void main(String[] args) { DLL list = new DLL(); list.addToHead(0); list.addFromTail(2); list.insertAfter(1, 0); list.printing(); System.out.println("Finding: " + list.find(2)); list.deleteFromHead(); list.deleteFromTail(); list.delete(1); list.printing(); } } ``` #### Output: :::success **0 1 2 Finding: 2 List is empty.** ::: ## 3. 應用實例 - 多項式加減乘除 ```java= import java.io.*; import java.util.*; // 節點類別,用於存儲多項式的每一項 class Node { int coefficient; int exponent; Node prev; Node next; public Node(int coefficient, int exponent) { this.coefficient = coefficient; this.exponent = exponent; this.prev = null; this.next = null; } } // 多項式類別,使用雙向鏈結串列實現 class Polynomial { Node head; Node tail; public Polynomial() { head = null; tail = null; } // 新增項到多項式 public void addTerm(int coefficient, int exponent) { Node newNode = new Node(coefficient, exponent); if (head == null) { head = newNode; tail = newNode; } else { tail.next = newNode; newNode.prev = tail; tail = newNode; } } // 從字串解析多項式 public void parsePolynomial(String input) { String[] terms = input.split("(?=[-+])"); for (String term : terms) { term = term.trim(); int coefficient = 1; int exponent = 0; if (term.contains("x")) { String[] parts = term.split("x"); if (!parts[0].isEmpty() && !parts[0].equals("+") && !parts[0].equals("-")) { coefficient = Integer.parseInt(parts[0]); } else if (parts[0].equals("-")) { coefficient = -1; } if (parts.length > 1 && parts[1].startsWith("^")) { exponent = Integer.parseInt(parts[1].substring(1)); } else { exponent = 1; } } else { coefficient = Integer.parseInt(term); } addTerm(coefficient, exponent); } } // 多項式相加 public Polynomial add(Polynomial other) { Polynomial result = new Polynomial(); Node p = this.head; Node q = other.head; while (p != null && q != null) { if (p.exponent > q.exponent) { result.addTerm(p.coefficient, p.exponent); p = p.next; } else if (p.exponent < q.exponent) { result.addTerm(q.coefficient, q.exponent); q = q.next; } else { int sum = p.coefficient + q.coefficient; if (sum != 0) { result.addTerm(sum, p.exponent); } p = p.next; q = q.next; } } while (p != null) { result.addTerm(p.coefficient, p.exponent); p = p.next; } while (q != null) { result.addTerm(q.coefficient, q.exponent); q = q.next; } return result; } // 多項式相減 public Polynomial subtract(Polynomial other) { Polynomial result = new Polynomial(); Node p = this.head; Node q = other.head; while (p != null && q != null) { if (p.exponent > q.exponent) { result.addTerm(p.coefficient, p.exponent); p = p.next; } else if (p.exponent < q.exponent) { result.addTerm(-q.coefficient, q.exponent); q = q.next; } else { int diff = p.coefficient - q.coefficient; if (diff != 0) { result.addTerm(diff, p.exponent); } p = p.next; q = q.next; } } while (p != null) { result.addTerm(p.coefficient, p.exponent); p = p.next; } while (q != null) { result.addTerm(-q.coefficient, q.exponent); q = q.next; } return result; } // 多項式相乘 public Polynomial multiply(Polynomial other) { Polynomial result = new Polynomial(); Node p = this.head; while (p != null) { Node q = other.head; while (q != null) { int coef = p.coefficient * q.coefficient; int exp = p.exponent + q.exponent; result.addTerm(coef, exp); q = q.next; } p = p.next; } return result.simplify(); } // 簡化多項式(合併同類項) private Polynomial simplify() { Polynomial simplified = new Polynomial(); Map<Integer, Integer> terms = new TreeMap<>(Collections.reverseOrder()); Node current = this.head; while (current != null) { terms.put(current.exponent, terms.getOrDefault(current.exponent, 0) + current.coefficient); current = current.next; } for (Map.Entry<Integer, Integer> entry : terms.entrySet()) { if (entry.getValue() != 0) { simplified.addTerm(entry.getValue(), entry.getKey()); } } return simplified; } // 將多項式轉換為字串 @Override public String toString() { if (head == null) { return "0"; } StringBuilder sb = new StringBuilder(); Node current = head; boolean isFirst = true; while (current != null) { if (current.coefficient != 0) { if (!isFirst && current.coefficient > 0) { sb.append("+"); } if (current.coefficient != 1 || current.exponent == 0) { sb.append(current.coefficient); } if (current.exponent != 0) { sb.append("x"); if (current.exponent != 1) { sb.append("^").append(current.exponent); } } isFirst = false; } current = current.next; } return sb.toString(); } } public class PolynomialOperations { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); Polynomial poly1 = new Polynomial(); Polynomial poly2 = new Polynomial(); while (true) { System.out.println("請選擇輸入方式:"); System.out.println("1. 從檔案讀取"); System.out.println("2. 手動輸入"); System.out.print("請輸入選擇 (1 或 2):"); int choice = scanner.nextInt(); scanner.nextLine(); // 消耗換行符 if (choice == 1) { System.out.print("請輸入檔案名稱:"); String filename = scanner.nextLine(); readFromFile(filename, poly1, poly2); } else if (choice == 2) { System.out.print("請輸入第一個多項式:"); String input1 = scanner.nextLine(); poly1.parsePolynomial(input1); System.out.print("請輸入第二個多項式:"); String input2 = scanner.nextLine(); poly2.parsePolynomial(input2); } else { System.out.println("無效的選擇,請重新輸入。"); continue; } while (true) { System.out.println("\n請選擇操作:"); System.out.println("1. 相加"); System.out.println("2. 相減"); System.out.println("3. 相乘"); System.out.println("4. 輸入新的多項式"); System.out.println("5. 結束程式"); System.out.print("請輸入選擇:"); int operation = scanner.nextInt(); scanner.nextLine(); // 消耗換行符 if (operation == 1) { Polynomial result = poly1.add(poly2); System.out.println("結果:(" + poly1 + ") + (" + poly2 + ") = " + result); } else if (operation == 2) { Polynomial result = poly1.subtract(poly2); System.out.println("結果:(" + poly1 + ") - (" + poly2 + ") = " + result); } else if (operation == 3) { Polynomial result = poly1.multiply(poly2); System.out.println("結果:(" + poly1 + ") * (" + poly2 + ") = " + result); } else if (operation == 4) { break; } else if (operation == 5) { System.out.println("程式結束。"); return; } else { System.out.println("無效的選擇,請重新輸入。"); } } } } // 從檔案讀取多項式 private static void readFromFile(String filename, Polynomial poly1, Polynomial poly2) throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader(filename))) { String line1 = reader.readLine(); String line2 = reader.readLine(); if (line1 != null && line2 != null) { poly1.parsePolynomial(line1); poly2.parsePolynomial(line2); System.out.println("已從檔案讀取多項式:"); System.out.println("多項式1:" + poly1); System.out.println("多項式2:" + poly2); } else { System.out.println("檔案格式錯誤或檔案為空。"); } } catch (FileNotFoundException e) { System.out.println("找不到指定的檔案。"); } } } ```