1. 程式人生 > 實用技巧 >java演算法篇之三:連結串列

java演算法篇之三:連結串列

1. 簡介

前面已經實現了動態陣列、棧、佇列三種線性資料結構,但其底層都是依託靜態陣列,靠resize解決固定容量問題。

連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。是一種真正的動態資料結構

  • 資料儲存在“節點”(Node)中,一個節點包含至少兩部分,一部分儲存真正的資料,另一部分是指向其他節點的指標。當某個節點的指標指向空節點(NULL)則表明該節點為尾節點

    class Node {
        public E e;
        public Node next;
    }
    

  • 優點:真正的動態,不需要處理固定容量的問題

  • 缺點:喪失了隨機訪問的能力,無法像陣列一樣通過下標或索引直接訪問到資料

2. 新增元素

2.1 分析

新增元素分為三種情況,分別為在連結串列頭新增元素、在連結串列中間新增元素、在連結串列尾新增元素

  • 在連結串列頭新增元素

    • 只需要將新新增元素節點的指標指向頭節點,然後將頭節點設定新新增的元素
  • 在連結串列中間新增元素。假設待新增元素為節點A

    • 向index位置新增元素時,需要先獲取前一個位置index-1節點,記為節點B,然後將節點A的next指標指向節點B的next指標,將節點B的next指標指向節點A。

    eg: 必須先將節點A的next指標指向節點B的next指標,再將節點B的next指標指向節點A。順序不可以反過來,否則節點B的next指標指向節點A後,真正的index位置的節點就丟失了,無法根據節點B獲取到了

  • 在連結串列尾新增元素

    • 跟在連結串列中間新增元素一致。此時尾節點就是節點B節點A的next指標指向節點B的next指標即為NULL,然後節點B的next指標指向新元素節點A

2.2 實現

public class LinkedList<E> {

    private class Node {
        public E e;
        public Node next;

        public Node(E e, Node next) {
            this.e = e;
            this.next = next;
        }

        public Node(E e) {
            this(e, null);
        }

        public Node() {
            this(null, null);
        }

        @Override
        public String toString() {
            return e.toString();
        }
    }

    private Node head;
    private int size;

    public LinkedList() {
        this.head = null;
        this.size = 0;
    }

    // 獲取連結串列中的元素個數
    public int getSize() {
        return this.size;
    }

    // 返回連結串列是否為空
    public boolean isEmpty() {
        return this.size == 0;
    }

    // 在連結串列頭新增新的元素e
    public void addFirst(E e) {
        this.head = new Node(e, this.head);
        this.size++;
    }

    // 在連結串列的index(0-based)位置新增新元素e
    // 在連結串列中不是一個常用的操作,練習用
    public void add(int index, E e) {
        if (index <0 || index > this.size)
            throw new IllegalArgumentException("Add failed. Illegal index.");

        if (index == 0)
            this.addFirst(e);
        else {
            Node prev = head;
            for (int i = 0; i < index - 1; i++) {
                prev = prev.next;
            }
            prev.next = new Node(e, prev.next);
            this.size++;
        }
    }
    
    // 在連結串列末尾新增新的元素e
    public void addLast(E e) {
        this.add(this.size, e);
    }
}

3. 虛擬頭節點

3.1 分析

  • 上面已經實現了在連結串列中新增元素,但在連結串列頭新增元素與其他位置新增元素業務邏輯有差別。此時可以通過虛擬頭節點解決該問題
  • 虛擬頭節點不儲存任何資料
  • 虛擬頭節點的next指標指向的節點才是真正的頭節點

3.2 實現

public class LinkedList<E> {

    private class Node {
        public E e;
        public Node next;

        public Node(E e, Node next) {
            this.e = e;
            this.next = next;
        }

        public Node(E e) {
            this(e, null);
        }

        public Node() {
            this(null, null);
        }

        @Override
        public String toString() {
            return e.toString();
        }
    }

    private Node dummyHead;
    private int size;

    public LinkedList() {
        this.dummyHead = new Node(null, null);
        this.size = 0;
    }

    // 獲取連結串列中的元素個數
    public int getSize() {
        return this.size;
    }

    // 返回連結串列是否為空
    public boolean isEmpty() {
        return this.size == 0;
    }

    // 在連結串列的index(0-based)位置新增新元素e
    // 在連結串列中不是一個常用的操作,練習用
    public void add(int index, E e) {
        if (index <0 || index > this.size)
            throw new IllegalArgumentException("Add failed. Illegal index.");

        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        prev.next = new Node(e, prev.next);
        this.size++;
    }

    // 在連結串列頭新增新的元素e
    public void addFirst(E e) {
        this.add(0, e);
    }
    
    // 在連結串列末尾新增新的元素e
    public void addLast(E e) {
        this.add(this.size, e);
    }
}

4. 連結串列的遍歷、查詢、修改

// 獲得連結串列的第index(0-based)個位置的元素
// 在連結串列中不是一個常用的操作,練習用
public E get(int index) {
    if (index < 0 || index > this.size)
        throw new IllegalArgumentException("Get failed. Illegal index.");

    Node cur = this.dummyHead.next;
    for (int i = 0; i < index; i++)
        cur = cur.next;
    return cur.e;
}

// 獲得連結串列的第一個元素
public E getFirst() {
    return this.get(0);
}

// 獲取連結串列的最後一個元素
public E getLast() {
    return this.get(this.size - 1);
}

// 修改連結串列的第index(0-based)個位置的元素為e
// 在連結串列中不是一個常用的操作,練習用
public void set(int index, E e) {
    if (index < 0 || index > this.size)
        throw new IllegalArgumentException("Set failed. Illegal index.");

    Node cur = this.dummyHead.next;
    for (int i = 0; i < index; i++)
        cur = cur.next;
    cur.e = e;
}

// 查詢連結串列中是否存在元素e
public boolean contains(E e) {
    Node cur = this.dummyHead.next;
    while (cur != null) {
        if (cur.e.equals(e))
            return true;
        cur = cur.next;
    }
    return false;
}

@Override
public String toString() {
    StringBuilder res = new StringBuilder("LinkedList: ");
    Node cur = this.dummyHead.next;
    while (cur != null) {
        res.append(cur + " -> ");
        cur = cur.next;
    }
    res.append("NULL");
    return res.toString();
}

5. 從連結串列中刪除元素

// 從連結串列中刪除index(0-based)位置的元素,返回刪除的元素
// 從連結串列中刪除元素不是一個常用操作,練習用
public E remove(int index) {
    if (index < 0 || index >= this.size)
        throw new IllegalArgumentException("Remove failed. Index is illegal.");

    Node prev = this.dummyHead;
    for (int i = 0; i < index; i++)
        prev = prev.next;

    Node retNode = prev.next;
    prev.next = retNode.next;
    retNode.next = null;
    this.size--;

    return retNode.e;
}

// 從連結串列中刪除第一個元素,返回刪除的元素
public E removeFirst() {
    return this.remove(0);
}

// 從連結串列中刪除最後一個元素,返回刪除的元素
public E removeLast() {
    return this.remove(this.size - 1);
}