本文參考資料:html
一、《大話數據結構》java
二、http://blog.csdn.net/jzhf2012/article/details/8540543node
三、http://blog.csdn.net/jzhf2012/article/details/8540410算法
四、http://www.cnblogs.com/ITtangtang/p/3948610.html數組
五、http://blog.csdn.net/zw0283/article/details/51132161數據結構
原本在分析完HashSet、HashMap以後,我想緊跟着分析TreeMap以及TreeSet的,可是當我讀過源碼之後,我就放棄了這個想法。並非源碼有多難,而是TreeMap涉及到的數據結構中的樹結構,而我以前一直分析的都是線性結構,並且ArrayList、LinkedList也是線性結構,而且尚未分析。所以,我仍是決定循序漸進的進行,先把線性表所有分析完了,再去分析TreeMap。架構
ArrayList底層源碼基本邏輯結構很簡單,在《JDK學習---深刻理解java中的String》一文中基本已經分析完畢,惟一不一樣的是String的底層數組不可變,而在ArrayList的底層Object[] 數組中,容許數組增、刪、該操做,而且支持數組的動態擴容,這些東西不難,相信讀者能很輕鬆搞明白這些知識,我就再也不說明了。post
本文我將重點的說明一下LinkedList知識點,而LinkedList的底層是一個雙向鏈表結構,所以我會在解析源碼以前,穿插一些雙向鏈表的知識,而後結合代碼進行分析。我不喜歡很空洞的單獨去說數據結構,一是由於本人水平有限說不清楚,二是由於我以爲理論須要結合代碼,這樣分析更加的直觀一些。若是讀者想要仔細的瞭解數據結構的知識,能夠去找一些書籍詳細研讀。性能
雙向鏈表學習
《JDK學習---深刻理解java中的String》一文介紹了數據結構的大致架構,《JDK學習---深刻理解java中的HashMap、HashSet底層實現》介紹了線性表的單鏈表。
本文將繼續介紹數據結構的雙向鏈表。
雙向鏈表:在單鏈表的每一個節點中,再設置一個指向其前前驅節點的指針域 【DP】
既然是雙向鏈表,那麼對於鏈表中的某一個節點(p),它的後繼的前驅,以及前驅的後繼,其實都是這個節點自己:
p->next->prior = p = p->next-prior
雙鏈表的插入操做並不複雜,可是順序很重要,千萬不能寫錯。
假設,咱們如今有一個節點s,它存儲的元素爲e,如今要將節點s插入到節點p和p->next之間,須要嚴格的遵照插入的前後順序,以下圖:
s -> prior = p; //把p賦值給s的前驅,如圖中1 s -> next = p -> next; //把p -> next 賦值給s的後繼,如圖中2 p -> next -> prior = s; //把s 賦值給 p->next的前驅 ,如圖中3 p -> next =s; //把s 賦值給p 的後繼,如圖中4
關鍵在於它們的順序,因爲第二、3步都用到了p->next , 若是第4步先執行,則會使得p->next提早變成了s,使得插入工做完成不了。口訣是:先搞定s的前驅和後繼,再搞定後繼的前驅,最後解決前節點的後繼。
若是插入操做理解了,那麼刪除操做也就簡單了。
p ->prior -> next = p -> next; //把p ->next賦值給p->prior的後繼,如圖中1 p ->next -> prior = p ->prior; //把 p ->prior賦值給p ->next 的前驅,如圖中2 free(p); //釋放節點p
總結:雙向鏈表對於單鏈表而言,增、刪操做要複雜一些,畢竟多了一個prior指針域,因此操做須要格外當心。另外,因爲每一個節點都須要記錄兩份指針,空間相對而言也佔用略多一些。不過,因爲它良好的對稱性,使得對某個節點的增、刪操做帶來了方便。說白了,就是用空間換時間。
LinkedList中的雙向鏈表:
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介紹完了,由於LinkedList的底層源碼確實太簡單了,或者說是太規矩了,規矩到完徹底全的遵照鏈表的插入和刪除操做的思路,一點點變化都沒有,甚至比使用單鏈表+數組實現的HashMap還要簡單。可能我說再多,都不如代碼來的實在,下面進行代碼分析:
add(E e) 方法:
public boolean add(E e) { linkLast(e); return true; }
這個方法沒有邏輯判斷,只是簡單的調用linkLast(e)方法,下面繼續跟進
void linkLast(E e) { final Node<E> l = last; //構造須要插入鏈表的節點元素,由於此方法是固定往鏈表尾部追加節點,所以每一個將要插入的節點都不存後繼節點,或者說後繼節點都爲null; //此處在建立節點的時候,只是制定了當前節點的前驅以及當前節點的值域,由於後繼節點爲null,能夠不指定後繼指針域 final Node<E> newNode = new Node<>(l, e, null); last = newNode; //此處判斷第一個節點是否存在,不存在的話直接將當前節點指定爲頭節點。若是存在,則將當前將要插入的節點指定給前一個節點的後繼。所以是追加,這裏能夠省略當前節點的後繼節點持有當前節點的指針 if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
add(int index, E element)方法:這個方法算可以體現出雙鏈表節點的插入功能。
public void add(int index, E element) { checkPositionIndex(index); if (index == size) //這個地方在上面的方法已經分析過了,比較特殊,就再也不次分析了 linkLast(element); else linkBefore(element, node(index)); }
接下來跟進到linkBefore(element, node(index))方法中:
void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode;
if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
咱們假設這個鏈表很長,咱們正常的在中間插入一個節點,也就是正常的雙向鏈表節點插入功能。還記得口訣嗎?口訣是:先搞定s的前驅和後繼,再搞定後繼的前驅,最後解決前節點的後繼。
下面根據口訣,咱們對linkBefore(E e, Node<E> succ)進行解析: succ節點如今的位置,就是咱們須要插入的位置,也就是說要在succ節點和它的前一個節點succ->prev中間插入當前節點信息E e。
先搞定s的前驅和後繼,此經過final Node<E> newNode = new Node<>(pred, e, succ)生成的newNode不就至關於以前口訣中的s嗎,並且pred和succ不就是s的前驅和後繼嘛。
再搞定後繼的前驅,最後解決前節點的後繼 : 此處不就是經過上面的succ.prev = newNode; 以及 後面的 pred.next = newNode; 來完成的嘛。
再次強調,這樣分析,上面的黃色字體已經進行了假設了,即:這個鏈表很長,咱們正常的在中間插入一個節點,也就是正常的雙向鏈表節點插入功能
鏈表的節點替換set(int index, E element) 方法:這個方法比較簡單
public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
鏈表的節點刪除remove(Object o)方法:
public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { //我想說明這個分支,由於鏈表的節點刪除,確定是要模擬正常的狀況,Object o這個參數正常存在 //在這個地方,我也是有疑問的。若是鏈表長度爲1000,而咱們的o放在第998的位置上,若是是這樣的話,for須要迭代998次,我徹底看不出來它的刪除性能高在哪裏。若是非要說性能高, //那隻能勉強說仍是節省了移動節點的時間吧 for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
看完這個方法,好像並無直接操做鏈表,下面看看unlink(Object)方法:
E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { //關注此處,將前一個節點的後繼直接指定當前節點的後一個節點 prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { //關注此處,將當前節點的後一個節點的前驅,指向當前節點的前一個節點。這兩次徹底按照雙鏈表的節點刪除操做 next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
下面再來看看LinkedList的查詢方法:
get(int index)方法:
public E get(int index) { //此處是判斷給定的index下標是否合法 checkElementIndex(index); return node(index).item; }
跟進node(index)方法:
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; } }
仔細分析一下此方法,不管是if分支,仍是else分支,都涉及到了for循環進行迭代,直至找到知足條件的index位置爲止,這樣的話若是數據量比較大,性能確定會比較低下。而ArrayList則是直接從底層數組中拿,不須要作任何的遍歷,性能明顯高不少。
再看看Iterator()方法:
public Iterator<E> iterator() { return listIterator(); }
跟進:
public ListIterator<E> listIterator() { return listIterator(0); }
再跟進:
public ListIterator<E> listIterator(final int index) { rangeCheckForAdd(index); return new ListItr(index); }
繼續跟進ListItr類:
private class ListItr extends Itr implements ListIterator<E> {
繼續跟進Itr:
public E next() { checkForComodification(); try { int i = cursor; E next = get(i); lastRet = i; cursor = i + 1; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } }
最終咱們發現,若是要是對LinkedList類進行迭代,最終仍是調用的get()方法,而這個方法咱們在上面已經分析過了,性能比ArrayList的get方法要低不少,所以LinkedList的Iterator()方法性能不高。
總結:
一、經過代碼咱們看到,全部的插入方法,替換方法以及刪除方法,都是直接對節點的前驅與後繼進行直接操做,根本沒有涉及到移動節點讓出位置的狀況,這個比線性表的順序存儲結構性能要高;不過須要說明的是,鏈表的性能要體如今數據量上面,好比咱們總共就10個節點元素,那麼使用ArrayList與LinkedList的性能可能根本沒有區別。
二、查詢方面:ArrayList的get方法是直接到底層數組中去拿值,而LinkedList的get方法則每次都須要對鏈表進行遍歷,儘管遍歷的過程當中已經採用了算法進行優化,可是效率依舊仍是很低。
三、ArrayList的iterator底層依舊是本身的get()方法,而LinkedList的iterator方法底層也是本身的get()方法。而ArrayList的get方法性能比LinkedList的get方法性能高,所以,ArrayList的Iterator方法比LinkedList的iterator方法性能要高。總體來講,ArrayList的查詢性能就是比LinkedList的查詢性能高