深入Java集合LinkedList

現在由大惡人付有傑來從增刪改查幾個角度輕度解析LinkedList的源碼

1.整體架構

LinkedList 底層數據結構是一個雙向鏈表(),整體結構如下圖所示:
在這裏插入圖片描述
鏈表中的每個節點都可以向前或者向後追溯,我們有幾個概念如下:

  1. 鏈表每個節點我們叫做 Node,Node 有 prev 屬性,代表前一個節點的位置,next 屬性,代表後一個節點的位置;
  2. first 是雙向鏈表的頭節點,它的前一個節點是 null。
  3. last 是雙向鏈表的尾節點,它的後一個節點是 null;
  4. 當鏈表中沒有數據時,first 和 last 是同一個節點,前後指向都是 null;
  5. 因爲是個雙向鏈表,只要機器內存足夠強大,是沒有大小限制的。
    鏈表中的元素叫做 Node,我們看下 Node 的組成部分:
    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;
    }
}

添加

鏈表的添加方式有很多,就說最常用的。add

public boolean add(E e) {
        linkLast(e);
        return true;
    }
// 從尾部開始追加節點
void linkLast(E e) {
    // 把尾節點數據暫存
    final Node<E> l = last;
    // 新建新的節點,初始化入參含義:
    // l 是新節點的前一個節點,當前值是尾節點值
    // e 表示當前新增節點,當前新增節點後一個節點是 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 新建節點追加到尾部
    last = newNode;
    //如果鏈表爲空(l 是尾節點,尾節點爲空,鏈表即空),頭部和尾部是同一個節點,都是新建的節點
    if (l == null)
        first = newNode;
    //否則把前尾節點的下一個節點,指向當前尾節點。
    else
        l.next = newNode;
    //大小和版本更改
    size++;
    modCount++;
}

從源碼上來看,尾部追加節點比較簡單,只需要簡單地把尾巴引用指向位置修改下即可.(會畫圖的人畫圖發博客,不會畫圖的人打字發博客)
當然還有addFirst
此方法用於在頭部添加數據:

 public void addFirst(E e) {
        linkFirst(e);
    }
// 從頭部追加
private void linkFirst(E e) {
    // 頭節點賦值給臨時變量
    final Node<E> f = first;
    // 新建節點,前一個節點指向null,e 是新建節點,f 是新建節點的下一個節點,目前值是頭節點的值
    final Node<E> newNode = new Node<>(null, e, f);
    // 新建節點成爲頭節點
    first = newNode;
    // 頭節點爲空,就是鏈表爲空,頭尾節點是一個節點
    if (f == null)
        last = newNode;
    //上一個頭節點的前一個節點指向當前節點
    else
        f.prev = newNode;
    size++;
    modCount++;
}

頭部追加節點和尾部追加節點非常類似,只是前者是移動頭節點的 prev 指向,後者是移動尾節點的 next 指向。

節點刪除

在看節點刪除之前,我問你,你知道了這個雙鏈表點節點,你會如何刪除它?
我就畫個圖給你看看:
假設要刪除的是第二個節點
在這裏插入圖片描述
**第一步:**修改該節點的前驅節點的next指向自己的next的指向

代碼表示
node2.pre.next = node2.next;

在這裏插入圖片描述
第二步修改自己後面的節點的pre指針指向自己的前一個結點

代碼表示
node2.next.pre = node2.pre;

在這裏插入圖片描述
第三步:將自己的pre,next全部清空

代碼表示
node2.next = null;
node2.pre = null;

在這裏插入圖片描述

節點查詢

因爲鏈表不可以像數組那麼樣按照索引訪問 元素,鏈表查詢某一個節點是比較慢的,需要挨個循環查找才行,我們看看 LinkedList 的源碼是如何尋找節點的:

// 根據鏈表索引位置查詢節點
Node<E> node(int index) {
    // 如果 index 處於隊列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 直到 for 循環到 index 的前一個 node 停止
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {// 如果 index 處於隊列的後半部分,從尾開始找
        Node<E> x = last;
        // 直到 for 循環到 index 的後一個 node 停止
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

從源碼中我們可以發現,LinkedList 並沒有採用從頭循環到尾的做法,而是採取了簡單二分法,首先看看 index 是在鏈表的前半部分,還是後半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使循環的次數至少降低了一半,提高了查找的性能,我只能說,寫的不錯!!!。

其實鏈表就將每個節點通過前後指針連接起來,也就沒有那麼什麼擴容的麻煩啦。如果要按照索引的位置添加元素,就必須遍歷鏈表的同時index++,然後確定元素,直接斷開連接,新增就可以了,不用移動元素那麼麻煩。所以呢鏈表添加刪除的時候還是很簡便的。
我畫個圖吧:
在這裏插入圖片描述
代碼展示就是

//找到符合位置的節點  要添加的節點
add(Node node,Node e){
//獲取前面的節點
	Node pre = node.pre;
	//後面的節點
	Node next = node.next;
	e.pre = node.pre;
	//我在在你的位置 上插入,自然我的下一個節點是你
	e.next = node;
	node.pre.next = e;
	node.pre = null;
	node.next = null;
}

迭代器

因爲 LinkedList 要實現雙向的迭代訪問,所以我們使用 Iterator 接口肯定不行了,因爲 Iterator 只支持從頭到尾的訪問。Java 新增了一個迭代接口,叫做:ListIterator,這個接口提供了向前和向後的迭代方法,如下所示:
在這裏插入圖片描述

// 雙向迭代器
private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;//上一次執行 next() 或者 previos() 方法時的節點位置
    private Node<E> next;//下一個節點
    private int nextIndex;//下一個節點的位置
    //expectedModCount:期望版本號;modCount:目前最新版本號
    private int expectedModCount = modCount;
    …………
}

我們先來看下從頭到尾方向的迭代:
// 判斷還有沒有下一個元素

public boolean hasNext() {
    return nextIndex < size;// 下一個節點的索引小於鏈表的大小,就有
}

// 取下一個元素
public E next() {
    //檢查期望版本號有無發生變化
    checkForComodification();
    if (!hasNext())//再次檢查
        throw new NoSuchElementException();
    // next 是當前節點,在上一次執行 next() 方法時被賦值的。
    // 第一次執行時,是在初始化迭代器的時候,next 被賦值的
    lastReturned = next;
    // next 是下一個節點了,爲下次迭代做準備
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}
// 如果上次節點索引位置大於 0,就還有節點可以迭代
public boolean hasPrevious() {
    return nextIndex > 0;
}
// 取前一個節點
public E previous() {
    checkForComodification();
    if (!hasPrevious())
        throw new NoSuchElementException();
    // next 爲空場景:1:說明是第一次迭代,取尾節點(last);2:上一次操作把尾節點刪除掉了
    // next 不爲空場景:說明已經發生過迭代了,直接取前一個節點即可(next.prev)
    lastReturned = next = (next == null) ? last : next.prev;
    // 索引位置變化
    nextIndex--;
    return lastReturned.item;
}

鏈表就比數組簡單多了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章