LinkedList

鏈表

LinkedList是基于鏈表結構的一種List,在分析LinkedList源碼前有必要對鏈表結構進行說明。

鏈表的概念

鏈表是由一系列非連續(xù)的節(jié)點組成的存儲結構,簡單分下類的話,鏈表又分為單向鏈表和雙向鏈表,而單向/雙向鏈表又可以分為循環(huán)鏈表和非循環(huán)鏈表,下面簡單就這四種鏈表進行圖解說明。

  1. 單向鏈表
    單向鏈表就是通過每個結點的指針指向下一個結點從而鏈接起來的結構,最后一個節(jié)點的next指向null。
image
  1. 單向循環(huán)鏈表
    單向循環(huán)鏈表和單向列表的不同是,最后一個節(jié)點的next不是指向null,而是指向head節(jié)點,形成一個“環(huán)”。
image
  1. 雙向鏈表
    從名字就可以看出,雙向鏈表是包含兩個指針的,pre指向前一個節(jié)點,next指向后一個節(jié)點,但是第一個節(jié)點head的pre指向null,最后一個節(jié)點的tail指向null。
image
  1. 雙向循環(huán)鏈表
    雙向循環(huán)鏈表和雙向鏈表的不同在于,第一個節(jié)點的pre指向最后一個節(jié)點,最后一個節(jié)點的next指向第一個節(jié)點,也形成一個“環(huán)”。而LinkedList就是基于雙向循環(huán)鏈表設計的。
image

LinkedList簡介

LinkedList定義

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList 是一個繼承于AbstractSequentialList的雙向循環(huán)鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作。
LinkedList 實現(xiàn) List 接口,能對它進行隊列操作。
LinkedList 實現(xiàn) Deque 接口,即能將LinkedList當作雙端隊列使用。
LinkedList 實現(xiàn)了Cloneable接口,即覆蓋了函數(shù)clone(),能克隆。
LinkedList 實現(xiàn)java.io.Serializable接口,這意味著LinkedList支持序列化,能通過序列化去傳輸。
LinkedList 是非同步的。

LinkedList屬性

private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;

LinkedList中提供了兩個屬性,其中size和ArrayList中一樣用來計數(shù),表示list的元素數(shù)量,而header則是鏈表的頭結點,Entry則是鏈表的節(jié)點對象。

private static class Entry<E> {
    E element;  // 當前存儲元素
    Entry<E> next;  // 下一個元素節(jié)點
    Entry<E> previous;  // 上一個元素節(jié)點
    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

Entry為LinkedList 的內(nèi)部類,其中定義了當前存儲的元素,以及該元素的上一個元素和下一個元素。

LinkedList構造函數(shù)

/**
* 構造一個空的LinkedList .
*/
public LinkedList() {
    //將header節(jié)點的前一節(jié)點和后一節(jié)點都設置為自身
    header.next = header. previous = header ;
}

/**
* 構造一個包含指定 collection 中的元素的列表,這些元素按其 collection 的迭代器返回的順序排列
*/
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

需要注意的是空的LinkedList構造方法,它將header節(jié)點的前一節(jié)點和后一節(jié)點都設置為自身,這里便說明LinkedList 是一個雙向循環(huán)鏈表,如果只是單純的雙向鏈表而不是循環(huán)鏈表,他的實現(xiàn)應該是這樣的:

public LinkedList() {
    header.next = null;
    header. previous = null;
}

非循環(huán)鏈表的情況應該是header節(jié)點的前一節(jié)點和后一節(jié)點均為null(參見鏈表圖解)。

LinkedList源碼解析(基于JDK1.6.0_45)

增加

/**
 * 將一個元素添加至list尾部
 */
public boolean add(E e) {
   // 在header前添加元素e,header前就是最后一個結點啦,就是在最后一個結點的后面添加元素e
   addBefore(e, header);
    return true;
}
/**
 * 在指定位置添加元素
 */
public void add(int index, E element) {
    // 如果index等于list元素個數(shù),則在隊尾添加元素(header之前),否則在index節(jié)點前添加元素
    addBefore(element, (index== size ? header : entry(index)));
}

private Entry<E> addBefore(E e, Entry<E> entry) {
    // 用entry創(chuàng)建一個要添加的新節(jié)點,next為entry,previous為entry.previous,意思就是新節(jié)點插入entry前面,確定自身的前后引用,
    Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
     // 下面修改newEntry的前后節(jié)點的引用,確保其鏈表的引用關系是正確的
    // 將上一個節(jié)點的next指向自己
    newEntry. previous.next = newEntry;
    // 將下一個節(jié)點的previous指向自己
    newEntry. next.previous = newEntry;
    // 計數(shù)+1
     size++;
     modCount++;
     return newEntry;
}

到這里可以發(fā)現(xiàn)一點疑慮,header作為雙向循環(huán)鏈表的頭結點是不保存數(shù)據(jù)的,也就是說hedaer中的element永遠等于null

/**
 * 添加一個集合元素到list中
 */
public boolean addAll(Collection<? extends E> c) {
        // 將集合元素添加到list最后的尾部
    return addAll(size , c);
}

/**
 * 在指定位置添加一個集合元素到list中
 */
public boolean addAll(int index, Collection<? extends E> c) {
    // 越界檢查
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException( "Index: "+index+
                                            ", Size: "+size );
    Object[] a = c.toArray();
    // 要插入元素的個數(shù)
    int numNew = a.length ;
    if (numNew==0)
        return false;
    modCount++;

    // 找出要插入元素的前后節(jié)點
    // 獲取要插入index位置的下一個節(jié)點,如果index正好是lsit尾部的位置那么下一個節(jié)點就是header,否則需要查找index位置的節(jié)點
    Entry<E> successor = (index== size ? header : entry(index));
    // 獲取要插入index位置的上一個節(jié)點,因為是插入,所以上一個點擊就是未插入前下一個節(jié)點的上一個
    Entry<E> predecessor = successor. previous;
    // 循環(huán)插入
    for (int i=0; i<numNew; i++) {
        // 構造一個節(jié)點,確認自身的前后引用
        Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
        // 將插入位置上一個節(jié)點的下一個元素引用指向當前元素(這里不修改下一個節(jié)點的上一個元素引用,是因為下一個節(jié)點隨著循環(huán)一直在變)
        predecessor. next = e;
        // 最后修改插入位置的上一個節(jié)點為自身,這里主要是為了下次遍歷后續(xù)元素插入在當前節(jié)點的后面,確保這些元素本身的順序
        predecessor = e;
    }
    // 遍歷完所有元素,最后修改下一個節(jié)點的上一個元素引用為遍歷的最后一個元素
    successor. previous = predecessor;

    // 修改計數(shù)器
    size += numNew;
    return true;
}

image

增加方法的代碼理解起來可能有些困難,但是只要理解了雙向鏈表的存儲結構,掌握增加的核心邏輯就可以了,這里總結一下往鏈表中增加元素的核心邏輯:1.將元素轉(zhuǎn)換為鏈表節(jié)點,2.增加該節(jié)點的前后引用(即pre和next分別指向哪一個節(jié)點),3.前后節(jié)點對該節(jié)點的引用(前節(jié)點的next指向該節(jié)點,后節(jié)點的pre指向該節(jié)點)。現(xiàn)在再看下就這么簡單么,就是改變前后的互相指向關系(看圖增加元素前后的變化)。

刪除也是一樣的,下面看看刪除方法的實現(xiàn)。

刪除

/**
 * 刪除第一個匹配的指定元素
 */
public boolean remove(Object o) {
     // 遍歷鏈表找到要被刪除的節(jié)點
    if (o==null) {
        for (Entry<E> e = header .next; e != header; e = e.next ) {
            if (e.element ==null) {
                remove(e);
                return true;
            }
        }
    } else {
        for (Entry<E> e = header .next; e != header; e = e.next ) {
            if (o.equals(e.element )) {
                remove(e);
                return true;
            }
        }
    }
    return false;
}

private E remove(Entry<E> e) {
    if (e == header )
       throw new NoSuchElementException();

   // 被刪除的元素,供返回
    E result = e. element;
   // 下面修正前后對該節(jié)點的引用
   // 將該節(jié)點的上一個節(jié)點的next指向該節(jié)點的下一個節(jié)點
   e. previous.next = e.next;
   // 將該節(jié)點的下一個節(jié)點的previous指向該節(jié)點的上一個節(jié)點
   e. next.previous = e.previous;
   // 修正該節(jié)點自身的前后引用
    e. next = e.previous = null;
   // 將自身置空,讓gc可以盡快回收
    e. element = null;
   // 計數(shù)器減一
    size--;
    modCount++;
    return result;
}

由于節(jié)點被刪除,該節(jié)點的上一個節(jié)點和下一個節(jié)點互相拉一下小手就可以了,注意的是“互相”,不能一廂情愿。

修改

/**
 * 修改指定位置索引位置的元素
 */
public E set( int index, E element) {
    // 查找index位置的節(jié)點
    Entry<E> e = entry(index);
    // 取出該節(jié)點的元素,供返回使用
    E oldVal = e. element;
    // 用新元素替換舊元素
    e. element = element;
    // 返回舊元素
    return oldVal;
}    

set方法看起來簡單了很多,只要修改該節(jié)點上的元素就好了,但是不要忽略了這里的entry()方法,重點就是它。

查詢

終于到查詢了,終于發(fā)現(xiàn)了上面經(jīng)常出現(xiàn)的那個方法entry()根據(jù)index查詢節(jié)點,我們知道數(shù)組是有下標的,通過下標操作天然的支持根據(jù)index查詢元素,而鏈表中是沒有index概念呢,那么怎么樣才能通過index查詢到對應的元素呢,下面就來看看LinkedList是怎么實現(xiàn)的。

/**
 * 查找指定索引位置的元素
 */
public E get( int index) {
    return entry(index).element ;
}

/**
 * 返回指定索引位置的節(jié)點
 */
private Entry<E> entry( int index) {
    // 越界檢查
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException( "Index: "+index+
                                            ", Size: "+size );
    // 取出頭結點
    Entry<E> e = header;
    // size>>1右移一位代表除以2,這里使用簡單的二分方法,判斷index與list的中間位置的距離
    if (index < (size >> 1)) {
        // 如果index距離list中間位置較近,則從頭部向后遍歷(next)
        for (int i = 0; i <= index; i++)
            e = e. next;
    } else {
        // 如果index距離list中間位置較遠,則從頭部向前遍歷(previous)
        for (int i = size; i > index; i--)
            e = e. previous;
    }
    return e;
}

LinkedList是通過從header開始index計為0,然后一直往下遍歷(next),直到到index位置。為了優(yōu)化查詢效率,LinkedList采用了二分查找(這里說的二分只是簡單的一次二分),判斷index與size中間位置的距離,采取從header向后還是向前查找。
到這里我們明白,基于雙向循環(huán)鏈表實現(xiàn)的LinkedList,通過索引Index的操作是低效的,index所對應的元素越靠近中間所費時間越長。而向鏈表兩端插入和刪除元素則是非常高效的(如果不是兩端的話,都需要對鏈表進行遍歷查找)。

是否包含

// 判斷LinkedList是否包含元素(o)
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

// 從前向后查找,返回“值為對象(o)的節(jié)點對應的索引”
// 不存在就返回-1
public int indexOf(Object o) {
    int index = 0;
    if (o==null) {
        for (Entry e = header .next; e != header; e = e.next ) {
            if (e.element ==null)
                return index;
            index++;
        }
    } else {
        for (Entry e = header .next; e != header; e = e.next ) {
            if (o.equals(e.element ))
                return index;
            index++;
        }
    }
    return -1;
}

// 從后向前查找,返回“值為對象(o)的節(jié)點對應的索引”
// 不存在就返回-1
public int lastIndexOf(Object o) {
    int index = size ;
    if (o==null) {
        for (Entry e = header .previous; e != header; e = e.previous ) {
            index--;
            if (e.element ==null)
                return index;
        }
    } else {
        for (Entry e = header .previous; e != header; e = e.previous ) {
            index--;
            if (o.equals(e.element ))
                return index;
        }
    }
    return -1;
}

public boolean remove(Object o) 一樣,indexOf查詢元素位于容器的索引位置,都是需要對鏈表進行遍歷操作,當然也就是低效了啦。

判斷容量

/**
 * Returns the number of elements in this list.
 *
 * @return the number of elements in this list
 */
public int size() {
    return size ;
}

/**
 * {@inheritDoc}
 *
 * <p>This implementation returns <tt>size() == 0 </tt>.
 */
public boolean isEmpty() {
    return size() == 0;
}

和ArrayList一樣,基于計數(shù)器size操作,容量判斷很方便。

LinkedList實現(xiàn)的Deque雙端隊列

/**
 * Adds the specified element as the tail (last element) of this list.
 *
 * @param e the element to add
 * @return <tt> true</tt> (as specified by {@link Queue#offer})
 * @since 1.5
 */
public boolean offer(E e) {
    return add(e);
}

/**
 * Retrieves and removes the head (first element) of this list
 * @return the head of this list, or <tt>null </tt> if this list is empty
 * @since 1.5
 */
public E poll() {
    if (size ==0)
        return null;
    return removeFirst();
}

/**
 * Removes and returns the first element from this list.
 *
 * @return the first element from this list
 * @throws NoSuchElementException if this list is empty
 */
public E removeFirst() {
    return remove(header .next);
}

/**
 * Retrieves, but does not remove, the head (first element) of this list.
 * @return the head of this list, or <tt>null </tt> if this list is empty
 * @since 1.5
 */
public E peek() {
    if (size ==0)
        return null;
    return getFirst();
}

/**
 * Returns the first element in this list.
 *
 * @return the first element in this list
 * @throws NoSuchElementException if this list is empty
 */
public E getFirst() {
    if (size ==0)
       throw new NoSuchElementException();

    return header .next. element;
}

/**
 * Pushes an element onto the stack represented by this list.  In other
 * words, inserts the element at the front of this list.
 *
 * <p>This method is equivalent to {@link #addFirst}.
 *
 * @param e the element to push
 * @since 1.6
 */
public void push(E e) {
    addFirst(e);
}

/**
 * Inserts the specified element at the beginning of this list.
 *
 * @param e the element to add
 */
public void addFirst(E e) {
   addBefore(e, header.next );
}

看看Deque 的實現(xiàn)是不是很簡單,邏輯都是基于上面講的鏈表操作的。


總結
(01) LinkedList 實際上是通過雙向鏈表去實現(xiàn)的。
它包含一個非常重要的內(nèi)部類:Entry。Entry是雙向鏈表節(jié)點所對應的數(shù)據(jù)結構,它包括的屬性有:當前節(jié)點所包含的值上一個節(jié)點下一個節(jié)點
(02) 從LinkedList的實現(xiàn)方式中可以發(fā)現(xiàn),它不存在LinkedList容量不足的問題。
(03) LinkedList的克隆函數(shù),即是將全部元素克隆到一個新的LinkedList對象中。
(04) LinkedList實現(xiàn)java.io.Serializable。當寫入到輸出流時,先寫入“容量”,再依次寫入“每一個節(jié)點保護的值”;當讀出輸入流時,先讀取“容量”,再依次讀取“每一個元素”。
(05) 由于LinkedList實現(xiàn)了Deque,而Deque接口定義了在雙端隊列兩端訪問元素的方法。提供插入、移除和檢查元素的方法。每種方法都存在兩種形式:一種形式在操作失敗時拋出異常,另一種形式返回一個特殊值(null 或 false,具體取決于操作)。

對LinkedList以及ArrayList的迭代效率比較

結論:ArrayList使用最普通的for循環(huán)遍歷比較快,LinkedList使用foreach循環(huán)比較快。

看一下兩個List的定義:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

注意到ArrayList是實現(xiàn)了RandomAccess接口而LinkedList則沒有實現(xiàn)這個接口,關于RandomAccess這個接口的作用,看一下JDK API上的說法:

image

總結

ArrayList和LinkedList的比較

1、順序插入速度ArrayList會比較快,因為ArrayList是基于數(shù)組實現(xiàn)的,數(shù)組是事先new好的,只要往指定位置塞一個數(shù)據(jù)就好了;LinkedList則不同,每次順序插入的時候LinkedList將new一個對象出來,如果對象比較大,那么new的時間勢必會長一點,再加上一些引用賦值的操作,所以順序插入LinkedList必然慢于ArrayList

2、基于上一點,因為LinkedList里面不僅維護了待插入的元素,還維護了Entry的前置Entry和后繼Entry,如果一個LinkedList中的Entry非常多,那么LinkedList將比ArrayList更耗費一些內(nèi)存

3、數(shù)據(jù)遍歷的速度,結論是:使用各自遍歷效率最高的方式,ArrayList的遍歷效率會比LinkedList的遍歷效率高一些

4、有些說法認為LinkedList做插入和刪除更快,這種說法其實是不準確的:

(1)LinkedList做插入、刪除的時候,慢在尋址,快在只需要改變前后Entry的引用地址

(2)ArrayList做插入、刪除的時候,慢在數(shù)組元素的批量copy,快在尋址

所以,如果待插入、刪除的元素是在數(shù)據(jù)結構的前半段尤其是非常靠前的位置的時候,LinkedList的效率將大大快過ArrayList,因為ArrayList將批量copy大量的元素;越往后,對于LinkedList來說,因為它是雙向鏈表,所以在第2個元素后面插入一個數(shù)據(jù)和在倒數(shù)第2個元素后面插入一個元素在效率上基本沒有差別,但是ArrayList由于要批量copy的元素越來越少,操作速度必然追上乃至超過LinkedList

從這個分析看出,如果你十分確定你插入、刪除的元素是在前半段,那么就使用LinkedList;如果你十分確定你刪除、刪除的元素在比較靠后的位置,那么可以考慮使用ArrayList。如果你不能確定你要做的插入、刪除是在哪兒呢?那還是建議你使用LinkedList吧,因為一來LinkedList整體插入、刪除的執(zhí)行效率比較穩(wěn)定,沒有ArrayList這種越往后越快的情況;二來插入元素的時候,弄得不好ArrayList就要進行一次擴容,記住,ArrayList底層數(shù)組擴容是一個既消耗時間又消耗空間的操作

參考

該文為本人學習的筆記,讀取了網(wǎng)上好幾篇大牛的心得,其中主要是摘取了http://www.lxweimin.com/p/d5ec2ff72b33當中的內(nèi)容,請勿怪,謹當復習使用。


最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374