Java 內功修煉 之 資料結構與演算法(一)
一、基本認識
1、資料結構與演算法的關係?
(1)資料結構(data structure):
資料結構指的是 資料與資料 之間的結構關係。比如:陣列、佇列、雜湊、樹 等結構。
(2)演算法:
演算法指的是 解決問題的步驟。
(3)兩者關係:
程式 = 資料結構 + 演算法。
解決問題可以有很多種方式,不同的演算法實現 會得到不同的結果。正確的資料結構 是 好演算法的基礎(演算法好壞取決於 如何利用合適的資料結構去 處理資料、解決問題)。
(4)資料結構動態演示地址:
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
2、資料結構分類?
(1)分類:
資料結構 可以分為 兩種:線性資料結構、非線性資料結構。
(2)線性資料結構:
線性資料結構指的是 資料元素之間存在一對一的線性關係。比如:一維陣列、連結串列、佇列、棧。
其又可以分為:
順序儲存結構:指的是 使用一組地址連續的儲存單元 儲存資料元素 的結構,其每個元素節點僅用於 儲存資料元素。比如:一維陣列。
鏈式儲存結構:指的是 可使用一組地址不連續的儲存單元 儲存資料元素 的結構,其每個元素節點 儲存資料元素 以及 相鄰資料元素的地址 資訊。比如:連結串列。
(3)非線性資料結構:
非線性資料結構指的是 資料元素之間存在 一對多、多對多 的關係。比如:二維陣列、多維陣列、樹、圖 等。
3、時間複雜度、空間複雜度
(1)分析多個演算法執行時間:
事前估算時間:程式執行前,通過分析某個演算法的時間複雜度來判斷演算法解決問題是否合適。
事後統計時間:程式執行後,通過計算程式執行時間來判斷(容易被計算機硬體、軟體等影響)。
注:
一般分析演算法都是採用 事前估算時間,即估算分析 演算法的 時間複雜度。
(2)時間頻度、時間複雜度:
時間頻度( T(n) ):
一個演算法中 語句執行的次數 稱為 語句頻度 或者 時間頻度,記為 T(n)。
通常 一個演算法花費的時間 與 演算法中 語句的執行次數 成正比,即 某演算法語句執行次數多,其花費時間就長。
時間複雜度( O(f(n)) ):
(3)通過 時間頻度( T(n) )推算 時間複雜度 ( O(f(n)) ):
對於一個 T(n) 表示式,比如: T(n) = an^2 + bn + c,其推算為 O(n) 需要遵循以下規則:
rule1:使用常數 1 替代表達式中的常數,若表示式存在高階項,則忽略常數項。
即:若 T(n) = 8,則其時間複雜度為 O(1)。若 T(n) = n^2 + 8,則其時間複雜度為 O(n^2)。
rule2:只保留最高階項,忽略所有低次項。
即:T(n) = 3n^2 + n^4 + 3n,其時間複雜度為 O(n^4)。
rule3:去除最高階項的係數。
即:T(n) = 3n^2 + 4n^3,其時間複雜度為 O(n^3)。
注:
T(n) 表示式不同,但是其對應的時間複雜度可能相同。
比如:T(n) = n^2 + n 與 T(n) = 3n^2 + 1 的時間複雜度均為 O(n^2)。
(4)常見時間複雜度
【常見時間複雜度(由小到大排序如下):】 O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(n^k) < O(2^n) 注: 時間複雜度越大,演算法執行效率越低。 【常數階 O(1) :】 演算法複雜度 與 問題規模無關。 比如: int a = 1; int b = 2; int c = a + b; 分析: 程式碼中不存在迴圈、遞迴等結構,其時間複雜度即為 O(1)。 【對數階 O(logn) :】 演算法複雜度 與 問題規模成對數關係。 比如: int i = 1; while(i < n) { i*=2; // 不斷乘 2 } 分析: 上述程式碼中存在迴圈,設迴圈執行次數為 x,則迴圈退出條件為 2^x >= n。 從而推算出 x = logn,此時 log 以 2 為底,即時間複雜度為 O(logn)。 【線性階 O(n) :】 演算法複雜度 與 問題規模成線性關係。 比如: for(int i = 0; i < n; i++) { System.out.println(i); } 分析: 程式碼中存在迴圈,且迴圈次數為 n,即時間頻度為 T(n),從而時間複雜度為 O(n)。 【線性對數階 O(nlogn) :】 演算法複雜度 與 問題規模成線性對數關係(迴圈巢狀)。 比如: for(int j = 0; j < n; j++) { int i = 1; while(i < n) { i*=2; // 不斷乘 2 } } 分析: 程式碼中迴圈巢狀,完成 for 迴圈需要執行 n 次,每次均執行 while 迴圈 logn 次, 即總時間頻度為 T(nlogn), 從而時間複雜度為 O(nlogn)。 【平方階 O(n^2) :】 演算法複雜度 與 問題規模成平方關係(迴圈巢狀)。 比如: for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { System.out.println(i + j); } } 分析: 程式碼中迴圈巢狀,總時間頻度為 T(n*n),即時間複雜度為 O(n^2) 【立方階 O(n^3) 、k 次方階 O(n^k) :】 類似於平方階 O(n^2),只是迴圈巢狀的層數更多了。 O(n^3) 表示三層迴圈。O(n^K) 表示四層迴圈。 【指數階 O(2^n) :】 演算法複雜度 與 問題規模成指數關係(迴圈巢狀)。 這個演算法的執行效率非常糟糕,一般都不考慮。 比如: int n = 3; for (int i = 0; i < Math.pow(2, n); i++) { System.out.println(i); } 分析: 上面迴圈,總時間頻度為 T(2^n),即時間複雜度為 O(2^n)。
(5)空間複雜度
空間複雜度 指的是演算法所需耗費的儲存空間。與時間複雜度類似,但其關注的是演算法執行所需佔用的臨時空間(非語句執行次數)。
一般演算法分析更看重 時間複雜度,即保證程式執行速度快,比如:快取 就是空間換時間。
二、基本資料結構以及程式碼實現
1、稀疏陣列(Sparse Array)
(1)什麼是稀疏陣列?
當陣列中 值為 0 的元素 大於 非 0 元素 且 非 0 元素 分佈無規律時,可以使用 稀疏陣列 來表示該陣列,其將一個大陣列整理、壓縮成一個小陣列,用於節約磁碟空間。
注:
不一定必須為 值為 0 的元素,一般 同一元素在陣列中過多時即可。
使用 稀疏陣列 的目的是為了 壓縮陣列結構、節約磁碟空間(比如:一個二維陣列 a[10][10] 可以儲存 100 個元素,但是其只儲存了 3 個元素後,那麼將會有 97 個空間被閒置,此時可以將 二維陣列 轉為 稀疏陣列 儲存,其最終轉換成 b[4][3] 陣列進行儲存,即從 a[10][10] 的陣列 壓縮到 b[4][3],從而減少空間浪費)。
【舉例:】 定義二維陣列 a[4][5],並存儲 3 個值如下: 0 0 0 0 0 0 1 0 2 0 0 0 0 0 0 0 0 1 0 0 此時,陣列中元素為 0 的個數大於 非 0 元素個數,所以可以作為 稀疏陣列 處理。 換種方式,比如 將 0 替換成 5 如下,也可以視為 稀疏陣列 處理。 5 5 5 5 5 5 1 5 2 5 5 5 5 5 5 5 5 1 5 5
(2)二維陣列轉為稀疏陣列:
【如何處理:】 Step1:先記錄陣列 有幾行幾列,有多少個不同的值。 Step2:將不同的值 的元素 的 行、列、值 記錄在一個 小規模的 陣列中,從而將 大陣列 縮減成 小陣列。 【舉例:】 原二維陣列如下: 0 0 0 0 0 0 1 0 2 0 0 0 0 0 0 0 0 1 0 0 經過處理後變為 稀疏陣列 如下: 行 列 值 4 5 3 // 首先記錄原二維陣列 有 幾行、幾列、幾個不同值 1 1 1 // 表示原二維陣列中 a[1][1] = 1 1 3 2 // 表示原二維陣列中 a[1][3] = 2 3 2 1 // 表示原二維陣列中 a[3][2] = 1 可以看到,原二維陣列 a[4][5] 轉為 稀疏陣列 b[4][3],空間得到利用、壓縮。
(3)二維陣列、稀疏陣列 互相轉換實現
【二維陣列 轉 稀疏陣列:】 Step1:遍歷原始二維陣列,得到 有效資料 個數 num。 Step2:根據有效資料個數建立 稀疏陣列 a[num + 1][3]。 Step3:將原二維陣列中有效資料儲存到 稀疏陣列中。 注: 稀疏陣列有 三列:分別為:行、 列、 值。 稀疏陣列 第一行 儲存的為 原二維陣列的行、列 以及 有效資料個數。其餘行儲存 有效資料所在的 行、列、值。 所以陣列定義為 [num + 1][3] 【稀疏陣列 轉 二維陣列:】 Step1:讀取 稀疏陣列 第一行資料並建立 二維陣列 b[行][列]。 Step2:讀取其餘行,並賦值到新的二維陣列中。 【程式碼實現:】 package com.lyh.array; import java.util.HashMap; import java.util.Map; public class SparseArray { public static void main(String[] args) { // 建立原始 二維陣列,定義為 4 行 10 列,並存儲 兩個 元素。 int[][] arrays = new int[4][10]; arrays[1][5] = 8; arrays[2][3] = 7; // 遍歷輸出原始 二維陣列 System.out.println("原始二維陣列如下:"); showArray(arrays); // 二維陣列 轉 稀疏陣列 System.out.println("\n二維陣列 轉 稀疏陣列如下:"); int[][] sparseArray = arrayToSparseArray(arrays); showArray(sparseArray); // 稀疏陣列 再次 轉為 二維陣列 System.out.println("\n稀疏陣列 轉 二維陣列如下:"); int[][] sparseToArray = sparseToArray(sparseArray); showArray(sparseToArray); } /** * 二維陣列 轉 稀疏陣列 * @param arrays 二維陣列 * @return 稀疏陣列 */ public static int[][] arrayToSparseArray(int[][] arrays) { // count 用於記錄有效資料個數 int count = 0; // HashMap 用於儲存有效資料(把 行,列 用逗號分隔拼接作為 key,值作為 value) Map<String, Integer> map = new HashMap<>(); // 遍歷得到有效資料、以及總個數 for (int i = 0; i < arrays.length; i++) { for (int j = 0; j < arrays[i].length; j++) { if (arrays[i][j] != 0) { count++; map.put(i + "," + j, arrays[i][j]); } } } // 根據有效資料總個數定義 稀疏陣列,並賦值 int[][] result = new int[count + 1][3]; result[0][0] = arrays.length; result[0][1] = arrays[0].length; result[0][2] = count; // 把有效資料從 HashMap 中取出 並放到 稀疏陣列中 for(Map.Entry<String, Integer> entry : map.entrySet()) { String[] temp = entry.getKey().split(","); result[count][0] = Integer.valueOf(temp[0]); result[count][1] = Integer.valueOf(temp[1]); result[count][2] = entry.getValue(); --count; } return result; } /** * 遍歷輸出 二維陣列 * @param arrays 二維陣列 */ public static void showArray(int[][] arrays) { for (int[] a : arrays) { for (int data : a) { System.out.print(data + " "); } System.out.println(); } } /** * 稀疏陣列 轉 二維陣列 * @param arrays 稀疏陣列 * @return 二維陣列 */ public static int[][] sparseToArray(int[][] arrays) { int[][] result = new int[arrays[0][0]][arrays[0][1]]; for (int i = 1; i < arrays.length; i++) { result[arrays[i][0]][arrays[i][1]] = arrays[i][2]; } return result; } } 【輸出結果:】 原始二維陣列如下: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 二維陣列 轉 稀疏陣列如下: 4 10 2 1 5 8 2 3 7 稀疏陣列 轉 二維陣列如下: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2、佇列(Queue)、環形佇列
(1)什麼是佇列?
佇列指的是一種 受限的、線性的資料結構,其僅允許在 一端進行插入操作(隊尾插入,rear),且在另一端進行 刪除操作(隊首刪除,front)。
佇列可以使用 陣列 或者 連結串列 實現(一般採用陣列實現,僅在首尾增刪,效率比連結串列高)。
其遵循 先進先出(First In First Out,FIFO) 原則,即先存入 佇列的值 先取出。
【使用 陣列實現 佇列:】 需要注意三個值: maxSize: 表示佇列最大容量。 front: 表示佇列頭元素下標(指向佇列頭部的第一個元素的前一個位置),初始值為 -1. rear: 表示佇列尾元素下標(指向佇列尾部的最後一個元素),初始值為 -1。 臨界條件: front == rear 時,表示佇列為 空。 rear == maxSize - 1 時,表示佇列已滿。 rear - front, 表示佇列的儲存元素的個數。 資料進入佇列時: front 不動,rear++。 資料出佇列時: rear 不動,front++。
如下圖:
紅色表示入隊操作,rear 加 1。
黃色表示出隊操作,front 加 1。
每次入隊,向當前實際陣列尾部新增元素,每次出隊,從當前實際陣列頭部取出元素,符合 先進先出原則。
可以很明顯的看到,如果按照這種方式實現佇列,黃色區域的空間將不會被再次使用,即此時的佇列是一次性的。
那麼如何重複利用 黃色區域的空間?可以採用 環形佇列實現(看成一個環來實現)。
環形佇列在 上面佇列的基礎上稍作修改,當成環處理(資料首尾相連,可以通過 % 進行取模運算實現),核心是考慮 佇列 什麼時候為空,什麼時候為滿。
一般採用 犧牲一個 陣列空間 作為判斷當前佇列是否已滿的條件。
【使用 陣列 實現環形佇列:(此處僅供參考)】 需要注意三個值: maxSize: 表示佇列最大容量。 front: 表示佇列頭元素下標(指向佇列頭部的第一個元素),初始值為 0。 rear: 表示佇列尾元素下標(指向佇列尾部的最後一個元素的後一個位置),初始值為 0。 臨界條件: front == rear 時,表示佇列為 空。 (rear + 1) % maxSize == front 時,表示佇列已滿。 (rear - front + maxSize) % maxSize, 表示佇列的儲存元素的個數。 資料進入佇列時: front 不動,rear = (rear + 1) % maxSize。 資料出佇列時: rear 不動,front = (front + 1) % maxSize。
(2)使用陣列實現佇列
【程式碼實現:】 package com.lyh.queue; public class ArrayQueue<E> { private int maxSize; // 佇列最大容量 private int front; // 佇列首元素 private int rear; // 佇列尾元素 private Object[] queue; // 儲存佇列 /** * 構造初始佇列 * @param maxSize 佇列最大容量 */ public ArrayQueue(int maxSize) { this.maxSize = maxSize; queue = new Object[maxSize]; front = -1; rear = -1; } /** * 新增資料進入佇列 * @param e 待入資料 */ public void addQueue(E e) { if (isFull()) { System.out.println("佇列已滿"); return; } // 佇列未滿時,新增資料,rear 向後移動一位 queue[++rear] = e; } /** * 從佇列中取出資料 * @return 待取資料 */ public E getQueue() { if (isEmpty()) { System.out.println("佇列已空"); return null; } // 佇列不空時,取出資料,front 向後移動一位 return (E)queue[++front]; } /** * 輸出當前佇列所有元素 */ public void showQueue() { if (isEmpty()) { System.out.println("佇列已空"); return; } System.out.print("當前佇列儲存元素總個數為:" + getSize() + " 當前佇列為:"); for(int i = front + 1; i <= rear; i++) { System.out.print(queue[i] + " "); } System.out.println(); } /** * 獲取當前佇列實際大小 * @return 佇列實際儲存資料數量 */ public int getSize() { return rear - front; } /** * 判斷佇列是否為空 * @return true 為空 */ public boolean isEmpty() { return front == rear; } /** * 判斷佇列是否已滿 * @return true 已滿 */ public boolean isFull() { return rear == maxSize - 1; } public static void main(String[] args) { // 建立佇列 ArrayQueue<Integer> arrayQueue = new ArrayQueue<>(6); // 新增資料 arrayQueue.addQueue(10); arrayQueue.addQueue(8); arrayQueue.addQueue(9); arrayQueue.showQueue(); // 取資料 System.out.println(arrayQueue.getQueue()); System.out.println(arrayQueue.getQueue()); arrayQueue.showQueue(); } } 【輸出結果:】 當前佇列儲存元素總個數為:3 當前佇列為:10 8 9 10 8 當前佇列儲存元素總個數為:1 當前佇列為:9
(3)使用陣列實現環形佇列
【程式碼實現:】 package com.lyh.queue; public class ArrayCircleQueue<E> { private int maxSize; // 佇列最大容量 private int front; // 佇列首元素 private int rear; // 佇列尾元素 private Object[] queue; // 儲存佇列 /** * 構造初始佇列 * @param maxSize 佇列最大容量 */ public ArrayCircleQueue(int maxSize) { this.maxSize = maxSize; queue = new Object[maxSize]; front = 0; rear = 0; } /** * 新增資料進入佇列 * @param e 待入資料 */ public void addQueue(E e) { if (isFull()) { System.out.println("佇列已滿"); return; } // 佇列未滿時,新增資料,rear 向後移動一位 queue[rear] = e; rear = (rear + 1) % maxSize; } /** * 從佇列中取出資料 * @return 待取資料 */ public E getQueue() { if (isEmpty()) { System.out.println("佇列已空"); return null; } // 佇列不空時,取出資料,front 向後移動一位 E result = (E)queue[front]; front = (front + 1) % maxSize; return result; } /** * 輸出當前佇列所有元素 */ public void showQueue() { if (isEmpty()) { System.out.println("佇列已空"); return; } System.out.print("當前佇列儲存元素總個數為:" + getSize() + " 當前佇列為:"); for(int i = front; i < front + getSize(); i++) { System.out.print(queue[i] + " "); } System.out.println(); } /** * 獲取當前佇列實際大小 * @return 佇列實際儲存資料數量 */ public int getSize() { return (rear - front + maxSize) % maxSize; } /** * 判斷佇列是否為空 * @return true 為空 */ public boolean isEmpty() { return front == rear; } /** * 判斷佇列是否已滿 * @return true 已滿 */ public boolean isFull() { return (rear + 1) % maxSize == front; } public static void main(String[] args) { // 建立佇列 ArrayCircleQueue<Integer> arrayQueue = new ArrayCircleQueue<>(3); // 新增資料 arrayQueue.addQueue(10); arrayQueue.addQueue(8); arrayQueue.addQueue(9); arrayQueue.showQueue(); // 取資料 System.out.println(arrayQueue.getQueue()); System.out.println(arrayQueue.getQueue()); arrayQueue.showQueue(); } } 【輸出結果:】 佇列已滿 當前佇列儲存元素總個數為:2 當前佇列為:10 8 10 8 佇列已空
3、連結串列(Linked list)-- 單鏈表 以及 常見筆試題
(1)什麼是連結串列?
連結串列指的是 物理上非連續、非順序,但是 邏輯上 有序 的 線性的資料結構。
連結串列 由 一系列節點 組成,節點之間通過指標相連,每個節點只有一個前驅節點、只有一個後續節點。節點包含兩部分:儲存資料元素的資料域 (data)、儲存下一個節點的指標域 (next)。
可以使用 陣列、指標 實現。比如:Java 中 ArrayList 以及 LinkedList。
(2)單鏈表實現?
單鏈表 指的是 單向連結串列,首節點沒有前驅節點,尾節點沒有後續節點。只能沿著一個方向進行 遍歷、獲取資料的操作(即某個節點無法獲取上一個節點的資料)。
可參考:https://www.cnblogs.com/l-y-h/p/11385295.html
注:
頭節點(非必須):僅用於作為連結串列起點,放在連結串列第一個節點前,無實際意義。
首節點:指連結串列第一個節點,即頭節點後面的第一個節點。
頭節點是非必須的,使用頭節點是方便操作連結串列而設立的。如下程式碼實現採用 頭節點 方式實現。
【模擬 指標形式 實現 單鏈表:】 模擬節點: 節點包括 資料域(儲存資料) 以及 指標域(指向下一個節點)。 class Node<E> { E data; // 資料域,儲存節點資料 Node next; // 指標域,指向下一個節點 public Node(E data) { this.data = data; } public Node(E data, Node<E> next) { this.data = data; this.next = next; } } 【增刪節點:】 直接新增節點 A 到連結串列末尾: 先得遍歷得到最後一個節點 B 所在位置,條件為: B.next == null, 然後將最後一個節點 B 的 next 指向該節點, 即 B.next = A。 向指定位置插入節點: 比如: A->B 中插入 C, 即 A->C->B,此時,先讓 C 指向 B,再讓 A 指向 C。 即 C.next = A.next; // 此時 A.next = B A.next = C; 直接刪除連結串列末尾節點: 先遍歷到倒數第二個節點 C 位置,條件為:C.next.next == null; 然後將其指向的下一個節點置為 null 即可,即 C.next = null。 刪除指定位置的節點: 比如: A->C->B 中刪除 C,此時,直接讓 A 指向 B。 即: A.next = C.next;
【程式碼實現:】 package com.lyh.com.lyh.linkedlist; public class SingleLinkedList<E> { private int size; // 用於儲存連結串列實際長度 private Node<E> header; // 用於儲存連結串列頭節點,僅用作 起點,不儲存資料。 public SingleLinkedList(Node<E> header) { this.header = header; } /** * 在連結串列末尾新增節點 * @param data 節點資料 */ public void addLastNode(E data) { Node<E> newNode = new Node<>(data); // 根據資料建立一個 新節點 Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 遍歷連結串列 while(temp.next != null) { temp = temp.next; } // 在連結串列末尾新增節點,連結串列長度加 1 temp.next = newNode; size++; } /** * 在連結串列末尾新增節點 * @param newNode 節點 */ public void addLastNode(Node<E> newNode) { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 遍歷連結串列 while(temp.next != null) { temp = temp.next; } // 在連結串列末尾新增節點,連結串列長度加 1 temp.next = newNode; size++; } /** * 在連結串列指定位置 插入節點 * @param node 待插入節點 * @param index 指定位置(1 ~ n, 1 表示第一個節點位置) */ public void insert(Node<E> node, int index) { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 節點越界則丟擲異常 if (index < 1 || index > size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } // 若節點為連結串列末尾,則呼叫 末尾新增 節點的方法 if (index == size) { addLastNode(node); return; } // 若節點不是連結串列末尾,則遍歷找到插入位置 while(index != 1) { temp = temp.next; index--; } // A -> B 變為 A -> C -> B, 即 A.next = B 變為 C.next = A.next, A.next = C,即 A 指向 C,C 指向 B。 node.next = temp.next; temp.next = node; size++; } /** * 返回連結串列長度 * @return 連結串列長度 */ public int size() { return size; } /** * 輸出連結串列 */ public void showList() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 if (size == 0) { System.out.println("當前連結串列為空"); return; } // 連結串列不為空時遍歷連結串列 System.out.print("當前連結串列長度為: " + size + " 當前連結串列為: "); while(temp != null) { System.out.print(temp + " ===> "); temp = temp.next; } System.out.println(); } /** * 刪除最後一個節點 */ public void deleteLastNode() { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於遍歷連結串列 if (size == 0) { System.out.println("當前連結串列為空,無需刪除"); return; } while(temp.next.next != null) { temp = temp.next; } temp.next = null; size--; } /** * 刪除指定位置的元素 * @param index 指定位置(1 ~ n, 1 表示第一個節點位置) */ public void delete(int index) { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 節點越界則丟擲異常 if (index < 1 || index > size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } // 若節點為連結串列末尾,則呼叫 末尾刪除 節點的方法 if (index == size) { deleteLastNode(); return; } // 遍歷連結串列,找到刪除位置 while(index != 1) { index--; temp = temp.next; } // A -> C -> B 變為 A -> B,即 A.next = C, C.next = B 變為 A.next = C.next,即 A 直接指向 B temp.next = temp.next.next; size--; } public static void main(String[] args) { // 建立一個單鏈表 SingleLinkedList<String> singleLinkedList = new SingleLinkedList(new Node("Header")); // 輸出,此時連結串列為空 singleLinkedList.showList(); System.out.println("======================================="); // 給連結串列新增資料 singleLinkedList.addLastNode("Java"); singleLinkedList.addLastNode(new Node<>("JavaScript")); singleLinkedList.insert(new Node<>("Phthon"), 1); singleLinkedList.insert(new Node<>("C"), 3); // 輸出連結串列 singleLinkedList.showList(); System.out.println("======================================="); // 刪除連結串列資料 singleLinkedList.deleteLastNode(); singleLinkedList.delete(2); // 輸出連結串列 singleLinkedList.showList(); System.out.println("======================================="); } } class Node<E> { E data; // 資料域,儲存節點資料 Node<E> next; // 指標域,指向下一個節點 public Node(E data) { this.data = data; } public Node(E data, Node<E> next) { this.data = data; this.next = next; } @Override public String toString() { return "Node{ data = " + data + " }"; } } 【輸出結果:】 當前連結串列為空 ======================================= 當前連結串列長度為: 4 當前連結串列為: Node{ data = Phthon } ===> Node{ data = Java } ===> Node{ data = JavaScript } ===> Node{ data = C } ===> ======================================= 當前連結串列長度為: 2 當前連結串列為: Node{ data = Phthon } ===> Node{ data = JavaScript } ===> =======================================
(3)常見的單鏈表筆試題
【筆試題一:】 找到當前連結串列中倒數 第 K 個節點。 【筆試題一解決思路:】 思路一: 連結串列長度 size 可知時,則可以遍歷 size - k 個節點,從而找到倒數第 K 個節點。 當然 size 可以通過遍歷一遍連結串列得到,這會消耗時間。 思路二: 連結串列長度 size 未知時,可使用 快慢指標 解決。 使用兩個指標 A、B 同時遍歷,且指標 B 始終比指標 A 快 K 個節點, 當 指標 B 遍歷到連結串列末尾時,此時 指標 A 指向的下一個節點即為倒數第 K 個節點。 【核心程式碼如下:】 /** * 獲取倒數第 K 個節點。 * 方式一: * size 可知,遍歷 size - k 個節點即可 * @param k K 值,(1 ~ n,1 表示倒數第一個節點) * @return 倒數第 K 個節點 */ public Node<E> getLastKNode(int k) { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助連結串列遍歷 // 判斷節點是否越界 if (k < 1 || k > size) { throw new IndexOutOfBoundsException("Index: " + k + ", Size: " + size); } // 遍歷 size - k 個節點,即可找到倒數第 K 個節點 for (int i = 0; i < size - k; i++) { temp = temp.next; } return temp; } /** * 獲取倒數第 K 個節點。 * 方式二: * size 未知時,使用快慢節點, * 節點 A 比節點 B 始終快 k 個節點,A,B 同時向後遍歷,當 A 遍歷完成後,B 遍歷的位置下一個位置即為倒數第 K 個節點。 * @param k K 值,(1 ~ n,1 表示倒數第一個節點) * @return 倒數第 K 個節點 */ public Node<E> getLastKNode2(int k) { Node<E> tempA = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 Node<E> tempB = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 // 節點越界判斷 if (k < 1) { throw new IndexOutOfBoundsException("Index: " + k); } // A 比 B 快 K 個節點 while(tempA.next != null && k != 0) { tempA = tempA.next; k--; } // 節點越界判斷 if (k != 0) { throw new IndexOutOfBoundsException("K 值大於連結串列長度"); } // 遍歷,當 A 到連結串列末尾時,B 所處位置下一個位置即為倒數第 K 個節點 while(tempA.next != null) { tempA = tempA.next; tempB = tempB.next; } return tempB.next; }
【筆試題二:】 找到當前連結串列的中間節點(連結串列長度未知)。 【筆試題二解決思路:】 連結串列長度未知,可以採用 快慢指標 方式解決。 此處與解決 上題 倒數第 K 個節點類似,只是此時節點 B 比 節點 A 每次都快 1 個節點(即 A 每次遍歷移動一個節點,B 會遍歷移動兩個節點)。 【核心程式碼如下:】 /** * 連結串列長度未知時,獲取連結串列中間節點 * @return 連結串列中間節點 */ public Node<E> getHalfNode() { Node<E> tempA = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 Node<E> tempB = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 // 迴圈遍歷 B 節點,B 節點每次都比 A 節點快一個節點(每次多走一個節點),所以當 B 遍歷完成後,A 節點所處位置即為中間節點。 while(tempB.next != null && tempB.next.next != null) { tempA = tempA.next; tempB = tempB.next.next; } return tempA; }
【筆試題三:】 反轉連結串列。 【筆試題三解決思路:】 思路一: 頭插法,新建一個連結串列,遍歷原始連結串列,將每個節點通過頭插法插入新連結串列。 頭插法,即每次均在第一個節點位置處進行插入操作。 思路二: 直接反轉。 通過三個指標來輔助,beforeNode、currentNode、afterNode,此時 beforeNode -> currentNode -> afterNode。 其中: beforeNode 為當前節點上一個節點。 currentNode 為當前節點。 afterNode 為當前節點下一個節點。 遍歷連結串列,使 currentNode -> beforeNode。 【核心程式碼如下:】 /** * 連結串列反轉。 * 方式一: * 頭插法,新建一個連結串列,遍歷原始連結串列,將每個節點通過頭插法插入新連結串列。 * @return */ public SingleLinkedList<E> reverseList() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷原連結串列 SingleLinkedList singleLinkedList = new SingleLinkedList(new Node("newHeader")); // 新建一個連結串列 // 若原連結串列為空,則直接返回 空的 新連結串列 if (temp == null) { return singleLinkedList; } // 遍歷原連結串列,並呼叫新連結串列的 頭插法新增節點 while(temp != null) { singleLinkedList.addFirstNode(new Node(temp.data)); temp = temp.next; } return singleLinkedList; } /** * 頭插法插入節點,每次均在第一個節點位置處進行插入 * @param node 待插入節點 */ public void addFirstNode(Node<E> node) { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 // 若連結串列為空,則直接賦值即可 if (temp == null) { header.next = node; size++; return; } // 若連結串列不為空,則在第一個節點位置進行插入 node.next = temp; header.next = node; size++; } /** * 連結串列反轉。 * 方式二: * 直接反轉,通過三個指標進行輔助。此方式會直接變化當前連結串列。 */ public void reverseList2() { // 連結串列為空直接返回 if (header.next == null) { System.out.println("當前連結串列為空"); return; } Node<E> beforeNode = null; // 指向當前節點的上個節點 Node<E> currentNode = header.next; // 指向當前節點 Node<E> afterNode = null; // 指向當前節點的下一個節點 // 遍歷節點 while(currentNode != null) { afterNode = currentNode.next; // 獲取當前節點的下一個節點 currentNode.next = beforeNode; // 將當前節點指向上一個節點 beforeNode = currentNode; // 上一個節點後移 currentNode = afterNode; // 當前節點後移,為了下一個遍歷 } header.next = beforeNode; // 遍歷結束後,beforeNode 為最後一個節點,使用 頭節點 指向該節點,即可完成連結串列反轉 }
【筆試題四:】 列印輸出反轉連結串列,不能反轉原連結串列。 【筆試題四解決思路:】 思路一(此處不重複演示,詳見上例程式碼): 由於不能反轉原連結串列,可以與上例頭插法相同, 新建一個連結串列並使用頭插法新增節點,最後遍歷輸出新連結串列。 思路二: 使用棧進行輔助。棧屬於先進後出結構。 可以先遍歷連結串列並存入棧中,然後依次取出棧頂元素即可。 思路三: 使用陣列進行輔助(有序結構儲存一般均可,比如 TreeMap 儲存,根據 key 倒序輸出亦可)。 遍歷連結串列並存入陣列,然後反序輸出陣列即可(注:若是反序存入陣列,可以順序輸出)。 【核心程式碼如下:】 /** * 不改變當前連結串列下,反序輸出連結串列。 * 方式一: * 借用棧結構進行輔助。棧是先進後出結構。 * 先遍歷連結串列並依次存入棧,然後從棧頂挨個取出資料,即可得到反序連結串列。 */ public void printReverseList() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助連結串列遍歷 Stack<Node<E>> stack = new Stack(); // 使用棧儲存節點 // 判斷連結串列是否為空 if (temp == null) { System.out.println("當前連結串列為空"); return; } // 遍歷節點,使用棧儲存連結串列各節點。 while(temp != null) { stack.push(temp); temp = temp.next; } // 遍歷輸出棧 while(stack.size() > 0) { System.out.print(stack.pop() + "==>"); } System.out.println(); } /** * 不改變當前連結串列下,反序輸出連結串列。 * 方式二: * 採用陣列輔助。 * 遍歷連結串列存入陣列,最後反序輸出陣列即可(注:若是反序存入陣列,可以順序輸出)。 */ public void printReverseList2() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助連結串列遍歷 int length = size(); Node<E>[] nodes = new Node[length]; // 使用陣列儲存連結串列節點 // 判斷連結串列是否為空 if(temp == null) { System.out.println("當前連結串列為空"); return; } // 遍歷連結串列,存入陣列,此處反序存入陣列,後面順序輸出即可 while(temp != null) { nodes[--length] = temp; temp = temp.next; } System.out.println(Arrays.toString(nodes)); }
上述所有單鏈表相關程式碼完整版如下(有部分地方還需修改,僅供參考):
【程式碼:】 package com.lyh.com.lyh.linkedlist; import java.util.Arrays; import java.util.Stack; public class SingleLinkedList<E> { private int size; // 用於儲存連結串列實際長度 private Node<E> header; // 用於儲存連結串列頭節點,僅用作 起點,不儲存資料。 public SingleLinkedList(Node<E> header) { this.header = header; } /** * 在連結串列末尾新增節點 * @param data 節點資料 */ public void addLastNode(E data) { Node<E> newNode = new Node<>(data); // 根據資料建立一個 新節點 Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 遍歷連結串列 while(temp.next != null) { temp = temp.next; } // 在連結串列末尾新增節點,連結串列長度加 1 temp.next = newNode; size++; } /** * 在連結串列末尾新增節點 * @param newNode 節點 */ public void addLastNode(Node<E> newNode) { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 遍歷連結串列 while(temp.next != null) { temp = temp.next; } // 在連結串列末尾新增節點,連結串列長度加 1 temp.next = newNode; size++; } /** * 在連結串列指定位置 插入節點 * @param node 待插入節點 * @param index 指定位置(1 ~ n, 1 表示第一個節點位置) */ public void insert(Node<E> node, int index) { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 節點越界則丟擲異常 if (index < 1 || index > size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } // 若節點為連結串列末尾,則呼叫 末尾新增 節點的方法 if (index == size) { addLastNode(node); return; } // 若節點不是連結串列末尾,則遍歷找到插入位置 while (index != 1) { temp = temp.next; index--; } // A -> B 變為 A -> C -> B, 即 A.next = B 變為 C.next = A.next, A.next = C,即 A 指向 C,C 指向 B。 node.next = temp.next; temp.next = node; size++; } /** * 返回連結串列長度 * @return 連結串列長度 */ public int size() { return size; } /** * 輸出連結串列 */ public void showList() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 if (size == 0) { System.out.println("當前連結串列為空"); return; } // 連結串列不為空時遍歷連結串列 System.out.print("當前連結串列長度為: " + size + " 當前連結串列為: "); while(temp != null) { System.out.print(temp + " ===> "); temp = temp.next; } System.out.println(); } /** * 刪除最後一個節點 */ public void deleteLastNode() { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於遍歷連結串列 if (size == 0) { System.out.println("當前連結串列為空,無需刪除"); return; } while(temp.next.next != null) { temp = temp.next; } temp.next = null; size--; } /** * 刪除指定位置的元素 * @param index 指定位置(1 ~ n, 1 表示第一個節點位置) */ public void delete(int index) { Node<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助遍歷連結串列 // 節點越界則丟擲異常 if (index < 1 || index > size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } // 若節點為連結串列末尾,則呼叫 末尾刪除 節點的方法 if (index == size) { deleteLastNode(); return; } // 遍歷連結串列,找到刪除位置 while(index != 1) { index--; temp = temp.next; } // A -> C -> B 變為 A -> B,即 A.next = C, C.next = B 變為 A.next = C.next,即 A 直接指向 B temp.next = temp.next.next; size--; } /** * 獲取倒數第 K 個節點。 * 方式一: * size 可知,遍歷 size - k 個節點即可 * @param k K 值,(1 ~ n,1 表示倒數第一個節點) * @return 倒數第 K 個節點 */ public Node<E> getLastKNode(int k) { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助連結串列遍歷 // 判斷節點是否越界 if (k < 1 || k > size) { throw new IndexOutOfBoundsException("Index: " + k + ", Size: " + size); } // 遍歷 size - k 個節點,即可找到倒數第 K 個節點 for (int i = 0; i < size - k; i++) { temp = temp.next; } return temp; } /** * 獲取倒數第 K 個節點。 * 方式二: * size 未知時,使用快慢節點, * 節點 A 比節點 B 始終快 k 個節點,A,B 同時向後遍歷,當 A 遍歷完成後,B 遍歷的位置下一個位置即為倒數第 K 個節點。 * @param k K 值,(1 ~ n,1 表示倒數第一個節點) * @return 倒數第 K 個節點 */ public Node<E> getLastKNode2(int k) { Node<E> tempA = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 Node<E> tempB = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 // 節點越界判斷 if (k < 1) { throw new IndexOutOfBoundsException("Index: " + k); } // A 比 B 快 K 個節點 while(tempA.next != null && k != 0) { tempA = tempA.next; k--; } // 節點越界判斷 if (k != 0) { throw new IndexOutOfBoundsException("K 值大於連結串列長度"); } // 遍歷,當 A 到連結串列末尾時,B 所處位置下一個位置即為倒數第 K 個節點 while(tempA.next != null) { tempA = tempA.next; tempB = tempB.next; } return tempB.next; } /** * 連結串列長度未知時,獲取連結串列中間節點 * @return 連結串列中間節點 */ public Node<E> getHalfNode() { Node<E> tempA = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 Node<E> tempB = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 // 迴圈遍歷 B 節點,B 節點每次都比 A 節點快一個節點(每次多走一個節點),所以當 B 遍歷完成後,A 節點所處位置即為中間節點。 while(tempB.next != null && tempB.next.next != null) { tempA = tempA.next; tempB = tempB.next.next; } return tempA; } /** * 連結串列反轉。 * 方式一: * 頭插法,新建一個連結串列,遍歷原始連結串列,將每個節點通過頭插法插入新連結串列。 * @return */ public SingleLinkedList<E> reverseList() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷原連結串列 SingleLinkedList singleLinkedList = new SingleLinkedList(new Node("newHeader")); // 新建一個連結串列 // 若原連結串列為空,則直接返回 空的 新連結串列 if (temp == null) { return singleLinkedList; } // 遍歷原連結串列,並呼叫新連結串列的 頭插法新增節點 while(temp != null) { singleLinkedList.addFirstNode(new Node(temp.data)); temp = temp.next; } return singleLinkedList; } /** * 頭插法插入節點,每次均在第一個節點位置處進行插入 * @param node 待插入節點 */ public void addFirstNode(Node<E> node) { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 // 若連結串列為空,則直接賦值即可 if (temp == null) { header.next = node; size++; return; } // 若連結串列不為空,則在第一個節點位置進行插入 node.next = temp; header.next = node; size++; } /** * 連結串列反轉。 * 方式二: * 直接反轉,通過三個指標進行輔助。此方式會直接變化當前連結串列。 */ public void reverseList2() { // 連結串列為空直接返回 if (header.next == null) { System.out.println("當前連結串列為空"); return; } Node<E> beforeNode = null; // 指向當前節點的上個節點 Node<E> currentNode = header.next; // 指向當前節點 Node<E> afterNode = null; // 指向當前節點的下一個節點 // 遍歷節點 while(currentNode != null) { afterNode = currentNode.next; // 獲取當前節點的下一個節點 currentNode.next = beforeNode; // 將當前節點指向上一個節點 beforeNode = currentNode; // 上一個節點後移 currentNode = afterNode; // 當前節點後移,為了下一個遍歷 } header.next = beforeNode; // 遍歷結束後,beforeNode 為最後一個節點,使用 頭節點 指向該節點,即可完成連結串列反轉 } /** * 不改變當前連結串列下,反序輸出連結串列。 * 方式一: * 借用棧結構進行輔助。棧是先進後出結構。 * 先遍歷連結串列並依次存入棧,然後從棧頂挨個取出資料,即可得到反序連結串列。 */ public void printReverseList() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助連結串列遍歷 Stack<Node<E>> stack = new Stack(); // 使用棧儲存節點 // 判斷連結串列是否為空 if (temp == null) { System.out.println("當前連結串列為空"); return; } // 遍歷節點,使用棧儲存連結串列各節點。 while(temp != null) { stack.push(temp); temp = temp.next; } // 遍歷輸出棧 while(stack.size() > 0) { System.out.print(stack.pop() + "==>"); } System.out.println(); } /** * 不改變當前連結串列下,反序輸出連結串列。 * 方式二: * 採用陣列輔助。 * 遍歷連結串列存入陣列,最後反序輸出陣列即可(注:若是反序存入陣列,可以順序輸出)。 */ public void printReverseList2() { Node<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助連結串列遍歷 int length = size(); Node<E>[] nodes = new Node[length]; // 使用陣列儲存連結串列節點 // 判斷連結串列是否為空 if(temp == null) { System.out.println("當前連結串列為空"); return; } // 遍歷連結串列,存入陣列,此處反序存入陣列,後面順序輸出即可 while(temp != null) { nodes[--length] = temp; temp = temp.next; } System.out.println(Arrays.toString(nodes)); } public static void main(String[] args) { // 建立一個單鏈表 SingleLinkedList<String> singleLinkedList = new SingleLinkedList(new Node("Header")); // 輸出,此時連結串列為空 singleLinkedList.showList(); System.out.println("======================================="); // 給連結串列新增資料 singleLinkedList.addLastNode("Java"); singleLinkedList.addLastNode(new Node<>("JavaScript")); singleLinkedList.insert(new Node<>("Phthon"), 1); singleLinkedList.insert(new Node<>("C"), 3); // 輸出連結串列 singleLinkedList.showList(); System.out.println("======================================="); // 刪除連結串列資料 // singleLinkedList.deleteLastNode(); // singleLinkedList.delete(2); // // 輸出連結串列 // singleLinkedList.showList(); // System.out.println("======================================="); // 獲取倒數第 k 個節點 // System.out.println(singleLinkedList.getLastKNode(1)); // System.out.println(singleLinkedList.getLastKNode2(2)); System.out.println("======================================="); // 獲取連結串列中間節點 // System.out.println(singleLinkedList.getHalfNode()); System.out.println("======================================="); // 反轉連結串列(頭插法新建一個新的連結串列) SingleLinkedList singleLinkedList2 = singleLinkedList.reverseList(); singleLinkedList2.showList(); System.out.println("======================================="); // 反轉連結串列(直接反轉) singleLinkedList2.reverseList2(); singleLinkedList2.showList(); System.out.println("======================================="); // 不改變原連結串列下,反序輸出連結串列(藉助棧實現) singleLinkedList2.printReverseList(); System.out.println("======================================="); // 不改變原連結串列下,反序輸出連結串列(藉助陣列實現) singleLinkedList2.printReverseList2(); System.out.println("======================================="); } } class Node<E> { E data; // 資料域,儲存節點資料 Node<E> next; // 指標域,指向下一個節點 public Node(E data) { this.data = data; } public Node(E data, Node<E> next) { this.data = data; this.next = next; } @Override public String toString() { return "Node{ data = " + data + " }"; } } 【輸出結果:】 當前連結串列為空 ======================================= 當前連結串列長度為: 4 當前連結串列為: Node{ data = Phthon } ===> Node{ data = Java } ===> Node{ data = JavaScript } ===> Node{ data = C } ===> ======================================= 當前連結串列長度為: 2 當前連結串列為: Node{ data = Phthon } ===> Node{ data = JavaScript } ===> ======================================= Node{ data = JavaScript } Node{ data = Phthon } ======================================= Node{ data = Phthon } ======================================= 當前連結串列長度為: 2 當前連結串列為: Node{ data = JavaScript } ===> Node{ data = Phthon } ===> ======================================= 當前連結串列長度為: 2 當前連結串列為: Node{ data = Phthon } ===> Node{ data = JavaScript } ===> ======================================= Node{ data = JavaScript }==>Node{ data = Phthon }==> ======================================= [Node{ data = JavaScript }, Node{ data = Phthon }] =======================================View Code
4、連結串列(Linked list)-- 雙向連結串列、環形連結串列(約瑟夫環)
(1)雙向連結串列
通過上面單鏈表相關操作,可以知道 單鏈表的 查詢方向唯一。
而雙向連結串列在 單鏈表的 基礎上在 新增一個指標域(pre),這個指標域用來指向 當前節點的上一個節點,從而實現 連結串列 雙向查詢(某種程度上提高查詢效率)。
【使用指標 模擬實現 雙向連結串列:】 模擬節點: 在單鏈表的基礎上,增加了一個 指向上一個節點的 指標域。 class Node2<E> { Node<E> pre; // 指標域,指向當前節點的上一個節點 Node<E> next; // 指標域,指向當前節點的下一個節點 E data; // 資料域,儲存節點資料 public Node2(E data) { this.data = data; } public Node2(E data, Node<E> pre, Node<E> next) { this.data = data; this.pre = pre; this.next = next; } } 【增刪節點:】 直接新增節點 A 到連結串列末尾: 首先得遍歷到連結串列最後一個節點 B 的位置,條件: B.next = null。 然後將 B 下一個節點指向 A, A 上一個節點指向 B。即 B.next = A; A.pre = B。 指定位置新增節點 C: 比如: A -> B 變為 A -> C -> B。 即 A.next = B; B.pre = A; 變為 C.next = B; C.pre = B.pre; B.pre.next = C; B.pre = C; 直接刪除連結串列末尾節點 A: 遍歷到連結串列最後一個節點 B 的位置,然後將其下一個節點指向 null 即可,即 B.next = null; 刪除指定位置的節點 C: 比如: A -> C -> B 變為 A -> B。 C.pre.next = C.next; C.next.pre = C.pre;
(2)雙向連結串列程式碼實現如下:
【程式碼實現:】 package com.lyh.com.lyh.linkedlist; public class DoubleLinkedList<E> { private int size = 0; // 用於儲存連結串列實際長度 private Node2<E> header; // 用於儲存連結串列頭節點,僅用作 起點,不儲存資料。 public DoubleLinkedList(Node2<E> header) { this.header = header; } /** * 直接在連結串列末尾新增節點 * @param node 待新增節點 */ public void addLastNode(Node2<E> node) { Node2<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 // 遍歷連結串列至連結串列末尾 while(temp.next != null) { temp = temp.next; } // 新增節點 temp.next = node; node.pre = temp; size++; } /** * 直接在連結串列末尾新增節點 * @param data 待新增資料 */ public void addLastNode2(E data) { Node2<E> temp = header; // 使用臨時節點儲存頭節點,用於輔助連結串列遍歷 Node2<E> newNode = new Node2<>(data); // 建立新節點 // 遍歷連結串列至連結串列末尾 while(temp.next != null) { temp = temp.next; } // 新增節點 temp.next = newNode; newNode.pre = temp; size++; } /** * 遍歷輸出連結串列 */ public void showList() { Node2<E> temp = header.next; // 使用臨時變數儲存第一個節點,用於輔助遍歷連結串列 // 判斷連結串列是否為空 if(temp == null) { System.out.println("當前連結串列為空"); return; } // 遍歷輸出連結串列 System.out.print("當前連結串列長度為: " + size() + " == 當前連結串列為: "); while(temp != null) { System.out.print(temp + " ==> "); temp = temp.next; } System.out.println(); } /** * 返回連結串列長度 * @return 連結串列長度 */ public int size() { return this.size; } /** * 在指定位置新增節點 * @param index 1 ~ n(1 表示 第一個節點) */ public void insert(int index, Node2<E> newNode) { Node2<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 // 遍歷找到指定位置 while(index != 0 && temp.next != null) { temp = temp.next; index--; } if (index != 0) { throw new IndexOutOfBoundsException("指定位置有誤: " + index); } newNode.next = temp; newNode.pre = temp.pre; temp.pre.next = newNode; temp.pre = newNode; size++; } /** * 刪除指定位置的節點 * @param index 1 ~ n(1 表示第一個節點) */ public void delete(int index) { Node2<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 // 遍歷找到待刪除節點位置 while(index != 0 && temp.next != null) { index--; temp = temp.next; } // 判斷節點是否存在 if (index != 0) { throw new IndexOutOfBoundsException("指定節點位置不存在"); } temp.pre.next = temp.next; // 若節點為最後一個節點,則無需對下一個節點進行賦值操作 if (temp.next != null) { temp.next.pre = temp.pre; } size--; } /** * 直接刪除連結串列末尾節點 */ public void deleteLastNode() { Node2<E> temp = header; // 使用臨時變數儲存頭節點,用於輔助連結串列遍歷 // 判斷連結串列是否為空 if (temp.next == null) { System.out.println("當前連結串列為空"); return; } // 遍歷連結串列至最後一個節點 while(temp.next != null) { temp = temp.next; } temp.pre.next = null; size--; } public static void main(String[] args) { // 建立雙向連結串列 DoubleLinkedList<String> doubleLinkedList = new DoubleLinkedList<>(new Node2<>("header")); // 輸出連結串列 doubleLinkedList.showList(); System.out.println("=========================="); // 新增節點 doubleLinkedList.addLastNode(new Node2<>("Java")); doubleLinkedList.addLastNode2("JavaScript"); doubleLinkedList.insert(2, new Node2<>("E")); doubleLinkedList.insert(1, new Node2<>("F")); // 輸出連結串列 doubleLinkedList.showList(); System.out.println("=========================="); doubleLinkedList.delete(1); doubleLinkedList.deleteLastNode(); // 輸出連結串列 doubleLinkedList.showList(); System.out.println("=========================="); } } class Node2<E> { Node2<E> pre; // 指標域,指向當前節點的上一個節點 Node2<E> next; // 指標域,指向當前節點的下一個節點 E data; // 資料域,儲存節點資料 public Node2(E data) { this.data = data; } public Node2(E data, Node2<E> pre, Node2<E> next) { this.data = data; this.pre = pre; this.next = next; } @Override public String toString() { return "Node2{ pre= " + (pre != null ? pre.data : null) + ", next= " + (next != null ? next.data : null) + ", data= " + data + '}'; } } 【輸出結果:】 當前連結串列為空 ========================== 當前連結串列長度為: 4 == 當前連結串列為: Node2{ pre= header, next= Java, data= F} ==> Node2{ pre= F, next= E, data= Java} ==> Node2{ pre= Java, next= JavaScript, data= E} ==> Node2{ pre= E, next= null, data= JavaScript} ==> ========================== 當前連結串列長度為: 2 == 當前連結串列為: Node2{ pre= header, nex