# 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("找不到指定的檔案。");
}
}
}
```