聲明:本文爲作者原創,請勿轉載,如果轉載請註明轉載地址。
文章目錄
LinkedList底層源碼分析
先來看下雙向鏈表的刪除節點、添加節點、遍歷節點的過程:
添加節點
刪除節點:
查詢節點:
1. 數據域
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//鏈表的長度
transient int size = 0;
/**
* Pointer to first node.指向鏈表的頭結點
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.指向鏈表的尾節點
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
//靜態內部類
private static class Node<E> {
E item;
//指向前一個節點的指針
Node<E> next;
//指向後一個節點的指針
Node<E> prev;
//構造函數:prev、元素、next
Node(Node<E> prev, E element, Node<E> next) {
//節點元素
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
2. 構造函數
LinkedList提供了兩個構造器,ArrayList比它多提供了一個通過設置初始化容量來初始化類。LinkedList不提供該方法的原因:因爲LinkedList底層是通過鏈表實現的,每當有新元素添加進來的時候,都是通過鏈接新的節點實現的,也就是說它的容量是隨着元素的個數的變化而動態變化的。而ArrayList底層是通過數組來存儲新添加的元素的,所以我們可以爲ArrayList設置初始容量(實際設置的數組的大小)。
//空參數構造函數
public LinkedList() {
}
//傳入一個集合(Collection)作爲參數初始化LinkedList。
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
3. 在鏈表中添加一個集合
3.1 addAll©
/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the specified
* collection's iterator.
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
3.2 addAll(size, c)
public boolean addAll(int index, Collection<? extends E> c) {
//判斷傳入參數index是否合法(0<=index<=size)
checkPositionIndex(index);
//將傳入的集合轉換爲數組
Object[] a = c.toArray();
//計算數組的長度
int numNew = a.length;
if (numNew == 0)
return false;
//1、下面的作用是定義兩個Node變量pred、succ,並分情況爲這兩個變量初始化
//定義兩個節點指針,一個pred指向待插入位置處的前一個節點,一個succ指向待插入位置
Node<E> pred, succ;
//如果index=size,說明要插入的位置就是鏈表的尾部
if (index == size) {
//初始化succ=null,代表要插入的位置爲null
succ = null;
//初始化pred=last,pred指向last,代表要插入的位置的前一個節點應用爲last
pred = last;
//否則說明要插入的位置在鏈表的中間位置
} else {
//初始化succ=node(index),node(index)作用是返回index位置處的節點,讓succ指向該節點
succ = node(index);
//初始化pred指向succ.prev,pred指向succ指向節點的前一個節點
pred = succ.prev;
}
//2、上面已經完成了節點變量的初始化,下面是遍歷集合中的每個元素,插入到鏈表中
//遍歷數組
for (Object o : a) {
//爲每次遍歷到的元素創建一個節點
@SuppressWarnings("unchecked") E e = (E) o;
//創建一個節點,讓該節點的prev指向pred,讓該節點的next指向null
Node<E> newNode = new Node<>(pred, e, null);
//如果pred爲null,說明鏈表爲null,就讓當前節點作爲頭結點,讓first指向該頭結點
if (pred == null)
first = newNode;
//否則,就讓遍歷到的節點元素插入到pred的後面
else
pred.next = newNode;
//pred指針向後移動
pred = newNode;
}
//3、下面要做的就是將鏈表連接起來,即讓pred.next=succ、succ.prev=pred
//如果要插入的位置爲null,說明要插入的位置爲節點的最後一個位置
if (succ == null) {
//讓last指針指向pred
last = pred;
} else {
//否則讓pred.next指向succ
pred.next = succ;
//讓succ的prev指向pred
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
總結:
(1) 先將傳入的集合轉換爲數組
(2) 定義兩個變量pred、succ並初始化,其中succ指向要插入的節點,pred指向要插入節點的前驅節點;如果要插入的節點爲鏈表的尾部,就讓pred指向last,succ指向null
。如果要插入的節點不在鏈表的尾部,就讓succ指向要插入的節點,pred指向succ.prev
。之所以定義這兩個節點變量一是爲了遍歷鏈表而是爲了防止鏈表斷掉。
(3) 遍歷集合,每次遍歷將集合中的元素創建爲節點,該newCode.prev指向perd
,next指向nul
l。如果原來鏈表爲null,將讓當前節點爲頭結點,讓first指向該節點。如果原來的鏈表不爲null,就讓pred.next指向newCode
。同時讓pred後移。
(4) 現在要做的是把整個鏈表連接上,分兩種情況,如果要插入的節點位於鏈表的尾部,那麼succ=null
,此時讓last指向pred
即可。如果不位於尾部,需要讓pred.next = succ,succ.prev = pred
因爲是集合,因此整體的效果,就是下面圖:
4. 在鏈表的尾部添加元素
4.1 add(E e)
/**
* Appends the specified element to the end of this list.
*/
public boolean add(E e) {
linkLast(e);
return true;
}
4.2 linkLast(E e)
/**
* Links e as last element.
*/
void linkLast(E e) {
//定義一個節點變量l指向last,尾節點不能動,因此定義一個新的節點變量l指向它
final Node<E> l = last;
//定義一個節點newNode,他的prev指向l、他的next指向null
final Node<E> newNode = new Node<>(l, e, null);
//讓指向鏈表尾部的變量last指向newNode
last = newNode;
//如果l爲null,說明原來的鏈表爲null,讓指向鏈表頭部的指針first指向newNode,讓newCode作爲頭結點
//此時頭節點和尾節點都是newCode
if (l == null)
first = newNode;
//否則,讓l指針後移,指向新的節點newNode
else
l.next = newNode;
size++;
modCount++;
}
總結:在鏈表的尾部添加一個元素
(1) 因爲尾節點不能動,定義一個節點變量l
指向尾節點
(2) 將添加的元素創建爲節點對象newCode
,該節點的prev指向l
,該節點的next指向null
,同時讓last指向新添加的節點,如果鏈表爲null,就讓該節點作爲頭結點,同時讓first也指向該節點。否則讓l.next=newCode
由此可見,鏈表在節點尾部添加節點效率是非常高的,因此只需要找到last指向的尾部節點,讓後將待添加的節點放在該節點後面即可。
5. 在鏈表的指定位置添加元素
5.1 add(int index, E element)
/**
* 在指定索引處添加元素
*
* @param index 元素要插入位置的索引
* @param element 要插入的元素
*/
public void add(int index, E element) {
//檢查index是否越界(0 <= index <= size)
checkPositionIndex(index);
//如果要插入的位置爲鏈表的尾部
if (index == size)
//否則,如果要插入的位置爲鏈表的中間或頭部
else
linkBefore(element, node(index));
}
5.2 node(int index)
/**
* 返回在指定索引index處的非空節點
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index < 鏈表長度的一半,就從頭部開始遍歷找到index指向的節點
if (index < (size >> 1)) {
//因爲頭結點不能移動,因此定義一個節點x來遍歷鏈表
//讓x指向頭結點
Node<E> x = first;
//遍歷節點,找到index位置處的節點
for (int i = 0; i < index; i++)
x = x.next;
return x;
//如果index > 鏈表長度的一半,就從尾部開始遍歷找到index指向的節點
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
5.3 linkBefore(E e, Node succ)
/**
* Inserts element e before non-null Node succ.
*
* @param e 要插入的元素
* @param succ 元素想要插入位置處指向的節點
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//找到要插入位置處的節點的前驅節點
final Node<E> pred = succ.prev;
//創建一個節點對象,讓該節點的prev指向pred,該節點的next指向succ
final Node<E> newNode = new Node<>(pred, e, succ);
//讓succ的prev重新指向newCode
succ.prev = newNode;
//如果pred==null,說明要插入的位置爲鏈表的頭部
if (pred == null)
first = newNode;
//否則讓pred後移指向新節點newCode
else
pred.next = newNode;
size++;
modCount++;
}
總結:
(1) 如果待插入的節點位於鏈表的尾部,直接調用linkLast(element)
方法。
(2) 如果待插入的節點位於鏈表的頭部或中間(此時succ一定不爲null,如果爲null就說明是鏈表的尾部),調用linkBefore(E e, Node succ)
方法。這個方法傳入兩個變量:e
代表待插入的節點元素,succ
代表待插入的位置當前指向的節點。
首先找到要插入位置處節點succ的前驅節點pred,然後創建一個節點對象newCode,讓該節點的prev=pred,next=succ
同時讓succ.prev = newCode
;
如果pred=null,說明要插入的位置爲鏈表的頭部,就讓當前節點作爲頭結點,讓first=newCode
,否則pred.next = newNode;
對於鏈表添加元素的效率比較高,因爲只需要遍歷鏈表找到待插入節點的位置,然後將節點插入到該位置即可,node(index)
方法的作用就是查找指定位置處的節點,在查找的過程中,並不是從頭向後直接查找指定位置處的節點,如果index<鏈表長度的一半,就從頭節點開始查,如果index>=鏈表的長度的一半就從尾節點開始查。LinkedList是雙向鏈表。
6. 判斷鏈表中是否包含某一個元素
6.1 contains(Object o)
//判斷鏈表中是否包含某個元素o
public boolean contains(Object o) {
return indexOf(o) != -1;
}
6.2 indexOf(Object o)
/**
* Returns the index of the first occurrence of the specified element in this list
* 返回在鏈表中第一次出現這個元素的位置
* @param o element to search for
* @return the index of the first occurrence of the specified element in
* this list, or -1 if this list does not contain the element
*/
public int indexOf(Object o) {
int index = 0;
//因爲鏈表中的元素可以存放null值,因此要分兩種情況來討論
//如果該元素爲null,遍歷該鏈表
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
//找到第一個爲null的元素
if (x.item == null)
//找到了就返回該元素對應的索引index
return index;
index++;
}
//如果該元素不爲null,遍歷該鏈表
} else {
for (Node<E> x = first; x != null; x = x.next) {
//找到第一個該元素
if (o.equals(x.item))
//找到了就返回該元素對應的索引index
return index;
index++;
}
}
//如果沒有找到該元素,就返回-1
return -1;
}
總結:
因爲鏈表中可以存放null值,因此判斷鏈表中是否包含某個元素時需要分爲兩種情況,待查找的元素是否爲null。返回的是鏈表中第一次出現該元素的位置,沒有找到就返回-1。
7. 從鏈表中刪除指定元素
7.1 remove(Object o)
public boolean remove(Object o) {
//同樣因爲鏈表中可存放null值,因此需要分兩種情況
//如果要刪除的元素位null,遍歷鏈表
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
//找到要刪除的元素
if (x.item == null) {
//刪除該元素,並返回true
unlink(x);
return true;
}
}
//如果要刪除的元素不爲null,遍歷鏈表
} else {
for (Node<E> x = first; x != null; x = x.next) {
//找到要刪除的元素
if (o.equals(x.item)) {
//刪除該元素,並返回true
unlink(x);
return true;
}
}
}
//否則返回false
return false;
}
7.2 unlink(Node x)
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
//要刪除的節點元素
final E element = x.item;
//定義一個變量next指向要刪除的節點的後繼節點
final Node<E> next = x.next;
//定義一個變量prev要刪除的節點的前驅節點
final Node<E> prev = x.prev;
//1、讓指向前驅節點的那根線斷掉
//如前驅節點爲null,說明要刪除的節點爲頭節點,把待刪除節點的後一個節點設置爲頭結點
if (prev == null) {
first = next;
//否則,說明爲鏈表中的某個節點
} else {
//讓prev.next指向next
prev.next = next;
//讓x.prev置爲null
x.prev = null;
}
//2、讓指向後繼節點的那根線斷掉
//如果後繼節點爲null,說明要刪除的節點爲尾節點,把待刪除節點的前一個節點設置爲尾節點
if (next == null) {
last = prev;
//否則
} else {
//讓next.prev指向prev
next.prev = prev;
//讓x.next指向null
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
從鏈表中刪除指定的元素,要分爲兩種情況,就是要刪除的元素爲null值,以及要刪除的元素不爲null值。原理上都是通過遍歷鏈表找到待刪除的元素,然後調用unlink(Node x)
方法刪除該元素:
刪除元素的時候目的是:將該節點與他的前驅節點連接的兩根線斷掉,將該節點與他的後繼節點連接的兩根線斷掉
(1)將該節點與他的前驅節點連接的兩根線斷掉:
如果待刪除的節點爲頭節點,讓first=next
;否則,讓prev.next = next,x.prev = null
(2) 將該節點與他的後繼節點連接的兩根線斷掉:
如果待刪除的節點爲尾節點,讓last=prev
;否則,讓next.prev = prev;x.next = null;
8. 從鏈表中刪除指定索引處的元素
8.1 remove(int index)
public E remove(int index) {
//判斷index索引是否越界
checkElementIndex(index);
return unlink(node(index));
}
9. 查詢index處的節點值
9.1 get(int index)
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
9.2 node(index).item
/**
* 返回在指定索引處的非空節點
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index < 鏈表長度的一半,就從頭部開始遍歷找到index指向的節點
if (index < (size >> 1)) {
//因爲頭結點不能移動,因此定義一個節點x來遍歷鏈表
//讓x指向頭結點
Node<E> x = first;
//遍歷節點,找到index位置處的節點
for (int i = 0; i < index; i++)
x = x.next;
return x;
//如果index > 鏈表長度的一半,就從尾部開始遍歷找到index指向的節點
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
實現隊列:
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E element() {
return getFirst();
}
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E remove() {
return removeFirst();
}
public boolean offer(E e) {
return add(e);
}
繼承了Dequ接口,可實現雙端隊列:
// Deque operations
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}