1. 程式人生 > >Java 內功修煉 之 資料結構與演算法(一)

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)) ):

  存在某個輔助函式 f(n),當 n 接近無窮大時,若 T(n) / f(n) 的極限值為不等於零的常數,則稱 f(n) 為 T(n) 的同數量級函式,記為 T(n) = O(f(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