Java-基礎-LinkedList

張鐵牛發表於2021-10-29

1. 簡介

LinkedList 同時實現了ListDeque介面,也就是說它既可以看作是一個順序容器,又可以看作是雙向佇列。

既然是雙向列表,那麼它的每個資料節點都一定有兩個指標,分別指向它的前驅和後繼。所以,從LinkedList 連結串列中的任意一個節點開始,都可以很方便的訪問它的前驅和後繼節點。

1.1 節點

程式碼實現:

Node 為 LinkedList的靜態內部類

// LinkedList.Node
private static class Node<E> {
    // 當前節點元素
    E item;
    // 前驅指標
    Node<E> next;
    // 後繼指標
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

多個節點相連:

每個Node都有指標指向前驅和後繼節點,“null”並非Node節點,只不過是firstNode prev 為null,並且 lastNode next 為null。

我們再來看下LinkedList 的幾個核心的變數:

// 連結串列長度
transient int size = 0;

 /**
  * Pointer to first node. 指向第一個節點
  * Invariant: (first == null && last == null) ||         
  *            (first.prev == null && first.item != null) 
  * first == null && last == null) :剛初始化還未賦值的狀態
  * 因為是佇列第一個元素,所以 前驅指標為null,item不為null      
  */
 transient Node<E> first;

 /**
  * Pointer to last node.
  * Invariant: (first == null && last == null) ||
  *            (last.next == null && last.item != null)
  * 因為是最後一個元素,所以 後繼指標為null,item不為null
  */
 transient Node<E> last;

2. 初始化

首先我們建立一個LinkedList物件:

// Test::main() 構造一個List例項
List<User> list1 = new LinkedList<>();

LinkedList 構造方法如下:

public LinkedList() {
}

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param  c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

納尼? 啥都沒幹。只是開闢了個堆記憶體空間而已。。。

如圖所示:

3. 新增元素

原始碼走起:

// 將指定的元素附加到此列表的末尾。
public boolean add(E e) {
    linkLast(e);
    return true;
}

// 尾部追加
void linkLast(E e) {
    // 第一次新增,這裡last為null,所以l也為null
    final Node<E> l = last; 
    // 建立一個後繼指標為null的node例項
    final Node<E> newNode = new Node<>(l, e, null);
    // 賦值給 last 屬性
    last = newNode;
    if (l == null)
      // l為null,將建立出來的node再賦值給first
      first = newNode;
    else
      // 如果不是第一次新增,將隊尾的node 的後繼指標指向 新建立的node
      l.next = newNode;
    size++;
    modCount++;
}

那麼我們給list1例項新增一個元素後記憶體地址會如何變化呢?

User user = new User("張三", 1);
LinkedList<User> list1 = new LinkedList<>();
list1.add(user);

如圖所示:

此時我們再新增一個元素呢?

User user = new User("張三", 1);
User user1 = new User("李四", 1);
LinkedList<User> list1 = new LinkedList<>();
list1.add(user);
list1.add(user1);

如圖所示:

再新增一個王五物件:

那如果我們是插入元素,不是尾部追加,會是什麼情況?

public void add(int index, E element) {
    // 檢查索引下標   index >= 0 && index < size
    checkPositionIndex(index);
    if (index == size)
        // 如果index == size 那麼尾部追加
        linkLast(element);
    else
        // 插入元素
        linkBefore(element, node(index));
}

/**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // 獲取之前index所在位置node的前驅
    final Node<E> pred = succ.prev;
    // 建立一個node。前驅 == 之前index所在位置node的前驅,後繼 == 之前index所在位置的node
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 之前index所在位置node的前驅指向 新建立的node
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

// 查詢指定索引位置的node。4.0有講,這裡不再贅述
Node<E> node(int index) {
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

其原理如圖所示:

4. 獲取元素

因為LinkedList本身就是個雙端佇列,所以LinkedList支援從雙端獲取元素,即:firstNode 和 lastNode。

/**
 * Returns the first element in this list.
 *
 * @return the first element in this list
 * @throws NoSuchElementException if this list is empty
 */
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

/**
 * Returns the last element in this list.
 *
 * @return the last element in this list
 * @throws NoSuchElementException if this list is empty
 */
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

我們再來看下get()方法:

public E get(int index) {
    // 檢查索引下標   index >= 0 && index < size
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // 如果索引 < size / 2 , 右移一位相當於除以2
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 從連結串列的最左端一直 遍歷到 index為止
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        // 從連結串列的最右端 遍歷到 index為止
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

啊哈,所以說為什麼LinkedList查詢元素慢了,原來是從離 index 最近的一端 一直遍歷到 index 位置為止。

5. 刪除元素

/**
 * Removes the element at the specified position in this list.  Shifts any
 * subsequent elements to the left (subtracts one from their indices).
 * Returns the element that was removed from the list.
 * 移除此列表中指定位置的元素。將任何後續元素向左移動(從它們的索引中減去一個)。返回從列表中刪除的元素
 * @param index the index of the element to be removed
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        // 將刪除node前驅的後繼指標指向刪除node的後繼
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        // 將刪除node後繼的前驅指標指向刪除node的前驅
        next.prev = prev;
        x.next = null;
    }
    // 設定為null 為了讓GC清除被刪除的node
    x.item = null;
    size--;
    modCount++;
    return element;
}

參考:
https://zhuanlan.zhihu.com/p/28101975

相關文章