LinkedList是也是很是常見的集合類,LinkedList是基於鏈表實現的集合。它擁有List集合的特色:java
- 存取有序
- 帶索引
- 容許重複元素
還擁有Deque集合的特色:node
- 先入先出
- 雙端操做
它自己的特色是:數組
- 對元素進行插入或者刪除,只須要更改一些數據,不須要元素進行移動。
依然是經過源碼來看看LinkedList如何實現本身的特性的。數據結構
Doubly-linked list implementation of the {@code List} and {@code Deque} interfaces. Implements all optional list operations,and permits all elements (including {@code null}).性能
對於List接口和Deque接口的雙鏈表實現。實現了全部List接口的操做而且能存儲全部的元素。測試
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
能夠看到LinkedList實現了一個Deque接口,實際上是說,LinkedList除了有List的特性,還有Deque的特性,那麼Deque是什麼呢?優化
public interface Deque<E> extends Queue<E> public interface Queue<E> extends Collection<E>
原來是繼承了Collection集合的另外一個接口。this
Queue就是咱們常說的隊列,它的特性是FIFO( First In First Out )先進先出,它的操做只有兩個:spa
- 把元素存進隊列尾部
- 從頭部取出元素
就像排隊辦事同樣的。3d
而它的子接口Deque除了這兩操做之外,還能比Queue隊列有更多的功能
- 既能夠添加元素到隊尾,也能夠添加元素到隊頭
- 既能夠從隊尾取元素,也能夠從隊頭取元素
如此看來就像兩邊均可以當隊頭和隊尾同樣,因此Deque又叫雙端隊列 。
理所應當的,LinkedLisk也實現了這些特性,而且有Doubly-linked(雙鏈表的特性)。
那麼什麼又是鏈表呢?
其實鏈表是一種線性的存儲結構,意思是將要存儲的數據存在一個存儲單元裏面,這個存儲單元裏面除了存放有待存儲的數據之外,還存儲有其下一個存儲單元的地址。
雙鏈表顧名思義,存儲單元除了存儲其下一個存儲單元的地址,還存儲了上一個存儲單元的地址。每次查找數據的時候,就經過存儲單元裏存儲的地址信息進行查找。
成員變量:
transient int size = 0; transient Node<E> first; transient Node<E> last;
只有三個,size表明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; } }
它是LinkedList內部的數據結構Node,做爲LinkedList的基本存儲單元,也最能體現LinkedList雙鏈表的特性。
像這樣的。
其中prev存儲上一個節點的引用(地址),next存儲下一個單元的引用,item就是具體要存的數據。
First和Last用來標明隊頭跟隊尾。
添加數據:
public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
默認是調用添加到尾部的方法。前面說過,LinkedList的基本存儲單元是Node,因此添加進來的數據會被封裝進Node的item屬性裏,並且這個新Node的prev會指向前一個Node,前一個Node的next會指向這個新Node。
相似這樣,可是注意畫線只是一種形象的表達方法,就如上面所說,在Node裏的prev屬性和next屬性是用來存儲引用的,經過這個引用就能找到前一個Node或者後一個Node。
public void addFirst(E e) { linkFirst(e); } private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } public void addLast(E e) { linkLast(e); } public boolean offerLast(E e) { addLast(e); return true; }
其實LinkedList不少不一樣名的方法,可是實現方式都是相似的,這是由於咱們有可能用LinkedList表達不一樣的數據結構,雖然都是添加元素到隊首/隊尾,可是清晰的描述對代碼的可讀性是有好處的。像若是要用LinkedList表示Stack(棧)數據結構時候用push()/pop()/peek()等方法來描述,用LinkedList表示Queue(隊列)數據結構時候用add()/offer()等方法來描述。(固然,更好的實現方式是多態。)
刪除數據:
//刪除頭Node public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } //刪除操做 private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } //刪除尾Node public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } //刪除操做 private E unlinkLast(Node<E> l) { // assert l == last && l != null; //拿到最後一個元素存放的數據 final E element = l.item; //拿到最後一個元素的prev前元素的引用 final Node<E> prev = l.prev; //將它們賦值爲null l.item = null; l.prev = null; // help GC //如今前元素是list(最後一個Node) last = prev; //若是前元素已是null說明沒有Node了 if (prev == null) first = null; else //說明前面還有元素,那麼前元素的next就存放null prev.next = null; size--; modCount++; return element; }
先看看簡單的刪除, 這裏是指定刪除最前跟最後的元素,因此只要判斷刪除後Node的prev或者next是否還有值,有就說明還有Node,沒有就說明LinkedList已經爲空了。
怎樣纔算刪除了頭/尾Node,只要它的next/prev爲空,說明沒有引用指向它了,那麼咱們就認爲它從LinkedList裏刪除了,由於咱們沒法經過存儲單元的引用找到這個Node,因此GC很快也會來回收掉這個Node。
這只是刪除頭尾Node,那要是刪除中間的Node呢?這要跟下面的查找和插入一塊兒看。
查找元素:
public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { // assert isElementIndex(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; } }
數組默認是有下標的,能夠一次就取出所在位置的元素,可是LinkedList底層可沒有維護這麼一個數組,那怎麼知道第幾個元素是什麼呢?
笨方法,我有size個元素,我不知道你指定的index在哪,那我一個一個找過去不就完事了?畢竟個人存儲單元Node記得它旁邊的單元的引用(地址)。
若是你的index比我size的一半還大,那我就從後面找,由於我是雙端隊列,有Last的引用(地址),因此能夠調換兩頭。
因此,在LinkedList裏面找元素可不容易,最多可能要找size/2次才能找到。
只要找到了想要的位置,那麼插入和刪除指定的這個Node就很簡單了。
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } E unlink(Node<E> x) { // assert x != null; //拿到所要刪除的Node的item final E element = x.item; //後一個Node final Node<E> next = x.next; //前一個Node final Node<E> prev = x.prev; //若是前一個Node爲null(說明是第一個Node) if (prev == null) { //那麼後一個Node做爲first first = next; } else {//不然說明前面有Node //那前一個Node的下一個Node引用變爲後一個Node prev.next = next; //當前的前引用變成null x.prev = null; } //若是後一個Node爲null(說明是最後一個Node) if (next == null) { //那麼前一個Node做爲last last = prev; } else {//不然說明後面還有Node //那後一個Node的下一個Node引用變爲前一個Node next.prev = prev; //當前的後引用變爲null x.next = null; } //保存的元素也設爲null x.item = null; //元素-1 size--; //修改次數+1 modCount++; return element; } public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } void linkBefore(E e, Node<E> succ) { // assert succ != null; //要插入位置的前一個Node final Node<E> pred = succ.prev; //新Node,前引用是前一個Node,後引用是當前位置的Node final Node<E> newNode = new Node<>(pred, e, succ); //後一個Node的前引用變爲這個新Node succ.prev = newNode; //若是沒有前一個Node if (pred == null) //說明添加的就是第一個Node了 first = newNode; else//說明前面還有Node //將前一個Node的後引用變爲這個新的Node pred.next = newNode; //元素+1 size++; modCount++; }
只是改變了存儲單元Node裏的prev和next,咱們就能夠認爲這個Node被插入/刪除了。
代碼的註釋配合着下圖看,就會方便理解不少,其中注意區分源代碼中的命名,最好拿筆記一下容易區分一些。
若是是插入元素,倒着看就能夠了。
關於遍歷:
咱們能夠了解到,LinkedList最大的性能消耗就在node(index)這步,這會須要去查找大量的元素。可是隻要找到了這個元素所在的Node,插入跟刪除就很是的方便了。
因此對於get(index)這個方法,咱們須要很是當心的去使用,若是隻想看一看這個位置的元素,能夠用這個方法,可是若是是遍歷LinkedList,千萬不能夠這樣寫:
for (int i = 0; i < linkedList.size(); i++) { linkedList.get(i).equals(Obj); }
這樣對於每次循環,get總會從前或者從後走i次,不考慮get方法中>>1的優化的話,這是一種O(n^2)時間複雜度的作法,效率十分低下。
因此LinkedList提供了內部的Iterator迭代器供咱們使用:
private class ListItr implements ListIterator<E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } public boolean hasNext() { return nextIndex < size; } public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; }
其實就是經過不斷調用next()方法取得Node,而後再對Node作操做,這樣時間複雜度就是O(n)了,不會有大量重複無用的遍歷。
總結:其實LinkedList的特色插入、刪除快,只是針對此次的操做而言的。
LinkedList作插入、刪除的時候,慢在要找到具體的位置,快在只須要改變先後Node的引用地址
ArrayList作插入、刪除的時候,慢在數組元素的批量賦值(前文裏的System.arraycopy),快在搜索
因此,若是待插入、刪除的元素是在數據結構的前半段尤爲是很是靠前的位置的時候,LinkedList的效率將大大快過ArrayList,由於ArrayList將批量copy大量的元素;越日後,對於LinkedList來講,由於它是雙向鏈表,因此在第2個元素後面插入一個數據和在倒數第2個元素後面插入一個元素在效率上基本沒有差異,可是ArrayList因爲要批量copy的元素愈來愈少,操做速度必然追上乃至超過LinkedList。
不論怎麼說,須要根據具體狀況來選擇對應的集合,最好作一下性能測試,這樣纔能有更高的效率。