初識LinkedList底層原理

z1340954953發表於2018-03-22

轉自 五月的倉頡: http://www.cnblogs.com/xrq730/p/5005347.html

LinkedList是通過連結串列實現的,連結串列是一種線性的儲存結構,每個儲存的資料都放在一個儲存單元裡面,每個儲存單元除了存放有待儲存的資料外,還儲存下一個儲存單元的地址(下一個儲存單元的地址是必要的,有些儲存結構還存放前一個儲存單元的地址),每次查詢資料的時候,通過當前儲存單元中儲存的下一個單元的地址,找到下一個單元

LinkedList是一種雙向連結串列,雙向連結串列有兩種定義:

1> 連結串列中的儲存單元能夠向前或向後找到前一個儲存單元或後一個儲存單元

2> 連結串列的頭結點的前一個儲存單元是連結串列的尾節點,連結串列尾節點的後一個儲存單元是連結串列的頭節點

連結串列的每個儲存單元定義

private static class Entry<E> {
	E element;
	Entry<E> next;
	Entry<E> previous;

	Entry(E element, Entry<E> next, Entry<E> previous) {
	    this.element = element;
	    this.next = next;
	    this.previous = previous;
	}
    }
E element 是真正儲存資料的地方,previous是前一個儲存單元的地址,next是後一個儲存單元的地址


LinkedList的關注點

是否允許元素為空允許
是否有序有序
是否允許元素重複允許
是否執行緒安全非執行緒安全
新增元素
public static void main(String[] args)
{
    List<String> list = new LinkedList<String>();
    list.add("111");
    list.add("222");
}

首先建立一個LinkedList例項,看下原始碼

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    private transient Entry<E> header = new Entry<E>(null, null, null);
    private transient int size = 0;

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
        header.next = header.previous = header;
    }

new一個Entry物件,設定prev,next和element都是null,然後將prev,next指向header本身,如果header引用地址的字長為4個位元組,假設是0x000000000,那麼執行List<String> list = new LinkedList<String>()後表示:


接著看add("111")

 public boolean add(E e) {
	addBefore(e, header);
        return true;
    }
private Entry<E> addBefore(E e, Entry<E> entry) {
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
    }

addBefore 第一行翻譯下就是

newEntry.element = e ;

newEntry.next = header;

newEntry.previous = header.previous, 這樣連結串列中兩個儲存單元表示為


後面兩步,將

newEntry.previous.next = newEntry; 就是將header的next地址設定為newEntry地址

newEntry.next.previous = newEntry;將header的previous地址設定為newEntry地址

這樣。連結串列中的兩個儲存單元的結構就變為:


同樣的,如果在add("222")

newEntry.element = e ;

newEntry.next = header;

newEntry.previous = header.previous, 新增的儲存單元表示為


newEntry.previous.next = newEntry; 就是將111的next地址設定為newEntry(222)地址

newEntry.next.previous = newEntry;將header的previous地址設定為newEntry(222)地址

這樣結果就是: 連結串列中的每一個節點都能向前後者向後找到下一個節點,這就是雙向連結串列的特性

新增元素:只要建立一個節點並修改節點的previous和next引用指向的地址,就能新增元素

查詢元素

 public E get(int index) {
        return entry(index).element;
    }
private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

可以看出,按照索引查詢節點是從頭節點向前或者向後一個一個節點遍歷過去,如果index<size/2就是向後遍歷,否則就是向前遍歷.

刪除元素

和ArrayList類似,LinkedList也支援按照索引刪除元素和按照元素刪除元素,按照元素刪除只會刪除第一個匹配到的元素

 public E remove(int index) {
        return remove(entry(index));
    }
public boolean remove(Object o) {
        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;
    }

兩種方法都會遍歷找到這個節點,然後呼叫remove(e)方法

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

        E result = e.element;
	e.previous.next = e.next;
	e.next.previous = e.previous;
        e.next = e.previous = null;
        e.element = null;
	size--;
	modCount++;
        return result;
    }
e.previous.next = e.next; 將刪除節點e的前一個節點的next引用指向e的下一個節點的地址

e.next.previous = e.previous;將刪除節e的後一個節點的previous引用指定e的前一個節點的地址。

最後將e的 element,previous,next全部置null,讓gc回收

插入元素

 public void add(int index, E element) {
        addBefore(element, (index==size ? header : entry(index)));
    }
 private Entry<E> addBefore(E e, Entry<E> entry) {
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
    }

和新增元素類似,只不過將header節點,換為 索引為index的節點,然後建立節點,並將previous和next引用修改

fast-fail

使用迭代器進行迭代的時候,同樣存在fast-fail機制,在多執行緒環境下對LinkedList進行迭代,如果modCount!=exceptedModCount就是丟擲異常,也就是多執行緒環境下操作LinkedList是不安全的。

final void checkForComodification() {
	    if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
	}

ArrayList和LinkedList的比較

1. ArrayList順序新增元素,(在陣列容量足夠的情況下)只需要將陣列某個索引位置指向一個物件,LinkedList順序新增元素則是有建立Entry的開銷,並修改節點的previous和next引用指向的地址,這種情況下, ArrayList比LinkedList更快。

2. ArrayList刪除元素,則是需要根據索引找到元素,然後進行陣列的複製,向前移位的操作。

    LinkedList刪除元素,找到元素需要從header節點開始遍歷,找到這個節點後,需要對前一個和後一個節點進行previous和next維護,最後將這個節點置空. 這裡那個集合效能表現更佳,存在爭議

總結如下:

1> LinkedList做刪除,插入時候,慢在節點的定址,快在前後Entry引用維護

2> ArrayList則是快在元素的定址,慢在陣列的複製

如果待刪除和插入的元素在非常靠前的位置,使用LinkedList效率會遠遠高於ArrayList,此時ArrayList需要運算元組的元素很多,如果是靠後的位置,由於LinkedList是雙向連結串列,靠前和靠後效率都比較高,而ArrayList越是靠後,需要複製的陣列長度越小,效能越高.

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

3. ArrayList查詢元素速度很快,LinkedList則是需要從header節點,向前或向後遍歷,ArrayList使用普通for迴圈遍歷最快,LinkedList建議使用foreach遍歷

ArrayList使用普通for迴圈快的原因

ArrayList的get方法只是從陣列裡面拿一個位置上的元素罷了。我們有結論,ArrayList的get方法的時間複雜度是O(1),O(1)的意思也就是說時間複雜度是一個常數,和陣列的大小並沒有關係,只要給定陣列的位置,直接就能定位到資料。

LinkedList使用普通for迴圈慢的原因

LinkedList在get任何一個位置的資料的時候,都會把前面的資料走一遍。假如我有10個資料,那麼將要查詢1+2+3+4+5+5+4+3+2+1=30次資料,相比ArrayList,卻只需要查詢10次資料就行了,隨著LinkedList的容量越大,差距會越拉越大。其實使用LinkedList到底要查詢多少次資料,大家應該已經很明白了,來算一下:按照前一半算應該是(1 + 0.5N) * 0.5N / 2,後一半算上即乘以2,應該是(1 + 0.5N) * 0.5N = 0.25N2 + 0.5N,忽略低階項和首項係數,得出結論,LinikedList遍歷的時間複雜度為O(N2)N為LinkedList的容量


相關文章