1. 程式人生 > >Java版數據結構與算法(三):基於鏈表的實現LinkedList源碼徹底分析

Java版數據結構與算法(三):基於鏈表的實現LinkedList源碼徹底分析

方法 extends 16px 設置 存在 數組 bounds 自己 數據信息

LinkedList 是一個雙向鏈表。它可以被當作堆棧、隊列或雙端隊列進行操作。LinkedList相對於ArrayList來說,添加,刪除元素效率更高,ArrayList添加刪除元素的話需移動數組元素,甚至還需要考慮到擴容數組長度。

一、LinkedList中成員變量及每個節點信息

源碼如下:

 1     transient int size = 0;
 2 
 3     transient Link<E> voidLink;
 4 
 5     private static final class Link<ET> {
 6         ET data;
7 8 Link<ET> previous, next; 9 10 Link(ET o, Link<ET> p, Link<ET> n) { 11 data = o; 12 previous = p; 13 next = n; 14 } 15 }

1行,size代表當前鏈表中有多少個節點。

3行,voidLink指向鏈表的頭部,稍後具體分析會有更近了解。

5-14行則定義了每個節點所包含的信息。

6行,data存儲每個節點中的數據。

8行,存儲每個節點指向的前一個與後一個節點信息。

10-14行則是節點的構造函數,在初始化的時候需要指定節點的數據,以及當前節點的前一個節點和後一個節點。

數組的每一項只包含數據信息,而在鏈表中每一項不僅包含數據還包含前一項,後一項的信息,在C語言中是通過指針來鏈接起來的,而在java中我們只需要定義一個實體類就可以了,每個節點類似如下結構:

技術分享圖片

二、LinkedList中初始化方式

LinkedList初始化有如下兩種方式:

public LinkedList()

public LinkedList(Collection<? extends E> collection)

接下來,挨個分析。

LinkedList()源碼如下:

1     public LinkedList() {
2         voidLink = new Link<E>(null, null, null);
3         voidLink.previous = voidLink;
4         voidLink.next = voidLink;
5     }

2行,構造一個空節點voidLink,數據,前向指針,後向指針都為null。(java中沒有指針這一概念,為了方便講解,這裏就叫做指針了

3,4行,voidLink前向指針與後向指針都指向自身。

以上方式初始化一個LinkedList後鏈表樣式如下:

技術分享圖片

接下來看下LinkedList(Collection<? extends E> collection)方式如何創建的,源碼如下:

 1     public LinkedList(Collection<? extends E> collection) {
 2         this();
 3         addAll(collection);
 4     }
 5 
 6     @Override
 7     public boolean addAll(Collection<? extends E> collection) {
 8         int adding = collection.size();
 9         if (adding == 0) {
10             return false;
11         }
12         Collection<? extends E> elements = (collection == this) ?
13                 new ArrayList<E>(collection) : collection;
14 
15         Link<E> previous = voidLink.previous;
16         for (E e : elements) {
17             Link<E> newLink = new Link<E>(e, previous, null);
18             previous.next = newLink;
19             previous = newLink;
20         }
21         previous.next = voidLink;
22         voidLink.previous = previous;
23         size += adding;
24         modCount++;
25         return true;
26     }

2行,調用空參數的構造方法,邏輯上面已經講了。this()方法調用完構造了一個空節點如下(上面已經說過):

技術分享圖片

3行,調用addAll(collection)方法,主要邏輯在此方法中。

15行代碼,創建一個新節點previous指向voidLink的前向指針,而此時前向指針指向自身,圖示如下:

技術分享圖片

16-20行,遍歷集合中每個元素加入鏈表中,接下來看看每個元素是怎麽加入鏈表中的。

17行,創建一個新節點,新節點的值就是遍歷出的元素e,前向指針指向previous所指向的節點,後向指針指向null,此時圖示如下:

技術分享圖片

18行,previous的後向指針指向新節點。

19行,previous指向新節點。

18,19行完成後,圖示如下:

技術分享圖片

好了,到此集合中一個元素就加入鏈表中了,不斷遍歷照此邏輯不斷加入鏈表中。

voidLink指向鏈表的頭結點,而previous則指向鏈表的尾節點。

假設集合中只有一個元素那麽經過上述遍歷後鏈表樣式也就如上圖所示了。

接下來看看21,22行邏輯。

21行,將previous的後向指針指向voidLink。

22行,voidLink的前向指針指向previous。

這樣鏈表的首尾也就連接起來了,圖示如下:

技術分享圖片

這樣整個鏈表的初始化完成了,這樣的首尾鏈接的鏈表叫做:雙向循環鏈表。

好了,鏈表的初始化基本就這些玩意,接下來看看其余一些操作。

三、LinkedList中添加數據方式

假設添加之前LinkedList如圖所示:

技術分享圖片

首先我們分析boolean add(E object)添加方法,源碼如下:

 1     @Override
 2     public boolean add(E object) {
 3         return addLastImpl(object);
 4     }
 5 
 6     private boolean addLastImpl(E object) {
 7         Link<E> oldLast = voidLink.previous;
 8         Link<E> newLink = new Link<E>(object, oldLast, voidLink);
 9         voidLink.previous = newLink;
10         oldLast.next = newLink;
11         size++;
12         modCount++;
13         return true;
14     }

其本質調用了addLastImpl(E object)方法。顧名思義,調用這個方法就是將元素放入鏈表的尾部。

7行,將頭部節點voidLink的前向指針指向的節點賦值給oldLast,很簡單,這裏就不畫出圖示了。

8行,創建新節點newLink,值為放入的值object,前向指針指向oldLast,後向指針指向頭指針voidLink,此時圖示如下:

技術分享圖片

咦?怎麽還有兩條線指向voidLink呢?別急啊,還有邏輯沒分析呢。

9行,頭節點voidLink的前向指針指向新節點newLink。

10行,oldLast指向的節點的後向指針指向新節點。

經過9,10行邏輯後,鏈表變為圖示:

技術分享圖片

到此,一個新數據就插入到鏈表尾部了,是不是也沒那麽復雜,整個過程就是對指針的操作。

接下來我們分析add(int location, E object) 可以向指定位置插入元素,源碼如下:

 1     @Override
 2     public void add(int location, E object) {
 3         if (location >= 0 && location <= size) {
 4             Link<E> link = voidLink;
 5             if (location < (size / 2)) {
 6                 for (int i = 0; i <= location; i++) {
 7                     link = link.next;
 8                 }
 9             } else {
10                 for (int i = size; i > location; i--) {
11                     link = link.previous;
12                 }
13             }
14             Link<E> previous = link.previous;
15             Link<E> newLink = new Link<E>(object, previous, link);
16             previous.next = newLink;
17             link.previous = newLink;
18             size++;
19             modCount++;
20         } else {
21             throw new IndexOutOfBoundsException();
22         }
23     }

我們假設插入之前鏈表如下:

技術分享圖片

並且我們要向位置3放入一個數據。

4行,link指向voidLink也就是指向頭部節點。

5-13行,就是查找我們要插入的位置,這裏有個優化,如果我們插入的位置靠前則從頭部向後查找,如果插入的位置靠後,則從後向前查找。

5行,插入的位置location與整個鏈表長度的一半比較,如果小於鏈表長度一半則表明插入位置靠前,否則也就靠後了。

這裏我們向位置3插入數據,明顯位置靠後,所以執行的是9-13行邏輯。

10-12行邏輯執行完link定位到位置3,如圖:

技術分享圖片

****link一開始指向的是頭節點,也就是位置0的節點,這裏經過兩次循環最終定位到位置3處的節點。

接下來就是數據的插入邏輯,還是對各個節點指針的操作,這裏再說一下,後續分析其他方法就不細說了。

14行,定義previous指向link指向節點的前一個節點,如圖:

技術分享圖片

15行,新建一個節點,數據就是我們要放入的數據信息,前向指針指向previous指向的節點,後向指針指向link所指向的節點,如圖:

技術分享圖片

16行,previous指向節點的後向指針指向newLink。

17行,link指向節點的前向指針指向newLink。

16,17行執行完鏈表變為如圖:

技術分享圖片

到此,我們就將一個數據插入到鏈表中指定位置了。

鏈表的插入數據與數組相比不用考慮空間的擴容,以及後面的元素不用移動位置,而只需操作對應位置指針就可以了,可以說性能上提升很多。

四、LinkedList中刪除數據方式

其實上面添加數據邏輯如果你真的理解了,刪除數據的方式也就是大概看一下源碼就明白了,同樣操作相鄰指針就可以了,這裏簡單說一下吧。

刪除數據的方法主要有如下方式:

public boolean remove(Object object)//刪除指定數據

public E remove(int location)//刪除指定位置數據

public E removeFirst()//刪除鏈表第一個數據

public E removeLast()//刪除鏈表最後一個數據

首先我們看下刪除指定位置數據方法remove(int location),源碼如下:

 1     @Override
 2     public E remove(int location) {
 3         if (location >= 0 && location < size) {
 4             Link<E> link = voidLink;
 5             if (location < (size / 2)) {
 6                 for (int i = 0; i <= location; i++) {
 7                     link = link.next;
 8                 }
 9             } else {
10                 for (int i = size; i > location; i--) {
11                     link = link.previous;
12                 }
13             }
14             Link<E> previous = link.previous;
15             Link<E> next = link.next;
16             previous.next = next;
17             next.previous = previous;
18             size--;
19             modCount++;
20             return link.data;
21         }
22         throw new IndexOutOfBoundsException();
23     }

是不是有種似曾相識的感覺,沒錯大體邏輯和向指定位置添加數據一樣一樣的。

4-13行,找出待刪除位置的節點,優化的地方是判斷一下刪除的位置靠鏈表前半部分還是後半部分。

14-17行,就是操作指針刪除對應位置節點,這裏就不細說了,講述添加方法邏輯的時候如果你真的理解了那麽這裏很easy。

至於其余刪除方法也很簡單,真的沒什麽特意要說的,就是對指針的操作。

鏈表的刪除數據與數組相比沒有後續數據的前移操作,同樣只是對指定數據所在節點的指針進行操作就可以了,性能上也有所提升。

五、LinkedList中更改數據方式

LinkedList中更改數據方法為:public E set(int location, E object) ,源碼如下:

 1     @Override
 2     public E set(int location, E object) {
 3         if (location >= 0 && location < size) {
 4             Link<E> link = voidLink;
 5             if (location < (size / 2)) {
 6                 for (int i = 0; i <= location; i++) {
 7                     link = link.next;
 8                 }
 9             } else {
10                 for (int i = size; i > location; i--) {
11                     link = link.previous;
12                 }
13             }
14             E result = link.data;
15             link.data = object;
16             return result;
17         }
18         throw new IndexOutOfBoundsException();
19     }

大體邏輯也很簡單了,4-13行同樣查找指定位置元素,然後15行,就是將指定位置節點中的數據設置為我們設定的數據object就可以了,整個過程沒有指針的操作,不看源碼是不是還以為又是新建一個節點然後操作指針替換呢?其實不必那麽麻煩,找到指定節點替換數據就可以了。

看完set源碼,想必獲取指定位置上數據也不難理解了,找到指定位置節點,然後返回節點數據就可以了,源碼都不用看了。

六、LinkedList中查找是否包含某一數據

判斷是否包含某一數據方法為public boolean contains(Object object),源碼如下:

 1     @Override
 2     public boolean contains(Object object) {
 3         Link<E> link = voidLink.next;
 4         if (object != null) {
 5             while (link != voidLink) {
 6                 if (object.equals(link.data)) {
 7                     return true;
 8                 }
 9                 link = link.next;
10             }
11         } else {
12             while (link != voidLink) {
13                 if (link.data == null) {
14                     return true;
15                 }
16                 link = link.next;
17             }
18         }
19         return false;
20     }

3行,link指向voidLink.next,這裏需要註意一下:如果是空鏈表,也就是只有voidLink自己一個節點,那麽voidLink.next指向的依然是voidLink節點,這裏不明白看一下上面講的初始化邏輯。如果鏈表中有其余數據,那麽next指向的就是鏈表出去頭結點的第一個節點了。

object不為null則執行4-10行邏輯,為null則執行11-18行邏輯。

5行,這裏為什麽判斷link不等於voidLink呢才繼續執行查找邏輯呢?兩種情況,1:空鏈表,也就是只有voidLink自己,那麽就沒必要查找了。2:不是空鏈表,看下9行每次循環後link都會指向下一個節點,也就是挨個遍歷鏈表的每一個節點,但是鏈表是循環鏈表,當遍歷到link等於voidLink也就是已經把鏈表遍歷一整遍了。

6-8行也就是挨個比較了,相等則找到了,說明鏈表中存在我們要查找的數據,直接返回true。

至於11-18行邏輯,就不用我多少了吧。

可以看到鏈表的查找與數組一樣,需要挨個遍歷鏈表中的每個數據項,如果數據量很大,那麽效率是很低下的,怎麽優化呢?答案是哈希表思想,這裏不細說,下一篇分析hashmap的時候會體現這種思想。

七、LinkedList的隊列與棧性質

這裏簡單提一下。

隊列:一種數據結構,最明顯的特性是只允許隊頭刪除,隊尾插入。

棧:同樣是一種數據結構,特性是插入刪除都在棧的頂部。

LinkedList提供了pop()與push(E e)方法使其有棧的特性。

LinkedList提供了addLast(E object)E removeFirst()方法使其有隊列的特性。

所以,我們要向實現棧與隊列只需要新建一個類封裝LinkedList就可以了。

八、總結

好了,到此LinkedList我想說的就基本就講完了,只要理解了指針的操作,基本沒什麽難度,還有,不要單獨看LinkedList,要與ArrayList比較來看,本質就是鏈表與數組的比較,下一篇講到hashmap,更要將三者聯系起來比較,提取出核心思想。

本片到此結束,希望對你有用。

青山不改,綠水長流,咱們下篇見!

Java版數據結構與算法(三):基於鏈表的實現LinkedList源碼徹底分析