版權聲明:本文出自汪磊的博客,未經作者允許禁止轉載。
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,更要將三者聯系起來比較,提取出核心思想。
本片到此結束,希望對你有用。
青山不改,綠水長流,咱們下篇見!
聲明:文章將會陸續搬遷到個人公眾號,以后文章也會第一時間發布到個人公眾號,及時獲取文章內容請關注公眾號