JDK學習---深刻理解java中的LinkedList

本文參考資料: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的查詢性能高

相關文章
相關標籤/搜索