線性鏈表 LinkedList 學習,比起 HashMap 真是簡單多了。
@[toc]
LinkedList
特點
- 有序,但內存空間中可能比較分散;
- 存儲相對較快、獲取相對較慢;
- 存儲的數據可以為 null;
- 線程不安全。
LinkedList 繼承/實現
- 繼承
AbstractSequentialList
:
線性序列結構的抽象,繼承于 AbstractList;
抽象方法listIterator()
,子類必須實現此方法用于創建迭代器;
內部封裝使用該迭代器實現的add()
、set()
、remove()
等方法。
public abstract ListIterator<E> listIterator(int index);
- 實現
List
接口:具有線性表的特性。 - 實現
Deque
接口:該接口繼承于Queue
接口,具有隊列特性。
參數以及構造函數
- 參數
// 結點數量
transient int size = 0;
// 頭結點
transient Node<E> first;
// 尾結點
transient Node<E> last;
- 構造函數
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
傳入 Collection c
參數的構造函數的實現就是遍歷將結點存入 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;
}
}
添加元素 add(E e)
/ offer(E e)
添加單個元素
public boolean offer(E e) {
return add(e);
}
public boolean add(E e) {
linkLast(e);
return true;
}
重要函數: linkLast()
向隊尾添加元素,多個添加元素的功能都依靠該方法實現。
void linkLast(E e) {
// 先找到尾結點
final Node<E> l = last;
// 創建新結點,新結點的前一結點指向尾結點,后一結點為 null
final Node<E> newNode = new Node<>(l, e, null);
// 新加入的結點要作為最后一個結點記錄
last = newNode;
if (l == null) // 如果尾結點為 null,說明是空的,將首結點也置為新結點
first = newNode;
else // 尾結點存在,新結點放到尾結點后面
l.next = newNode;
size++;
modCount++;
}
- 新結點創建后,把頭結點指向隊列的尾結點,尾結點指向 null。注意此時只是新結點指向了前一結點指向了尾結點;
- 尾結點為 null 說明隊列是空的,需要把頭結點也指向新結點;
- 如果隊列不為空,需要把尾結點的下一結點指向新結點。雙向鏈表的一種體現,新結點的前方指向尾結點、尾結點的后方指向新結點。
指定位置添加元素
LinkedList 是有序的,所以是可以往中間插入數據的:
public void add(int index, E element) {
// 1. 檢查下標
checkPositionIndex(index);
// 2. 尾部插入數據
if (index == size)
linkLast(element);
else // 3. 指定位置插入數據
linkBefore(element, node(index));
}
- 檢查下標,大于等于零且小于等于結點數量 size;
- 如果要插入的數據位置等于 size,說明要添加到鏈表尾部,調用上面的 linkLast 方法插入即可;
- 指定位置添加的話先找到這個位置的數據:
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;
}
}
- 查找時二分了一下,后面還是遍歷。
- 有意思的是前半截查找是正著遍歷,后半截查找是倒著遍歷。
- 正序遍歷只需找到所查元素的前一個元素即可,結果是前一結點的 next;
- 倒序遍歷同理,找到所查元素后一個元素,結果是 prev 結點。
找到 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++;
}
添加一堆元素
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); // 判斷下標是否越界,需要 >=0 && <=size
// 轉換為數組
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
// 如果是在尾部添加,當前結點是 null,前一結點是最后結點
if (index == size) {
succ = null;
pred = last;
} else {
// 否則找到當前下標的結點,pred 記錄前一結點
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 創建新結點,設置前結點和數據
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null) // 熟悉的設置頭結點
first = newNode;
else // 如果前一結點存在,往后添加
pred.next = newNode;
// pred 指向新結點,下次遍歷接著往后添加
pred = newNode;
}
// 如果是在鏈表尾部添加的,last 結點標記為最后添加的結點
if (succ == null) {
last = pred;
} else {
// 如果不是在鏈表尾部添加,last 結點不用動
// 并且把最后遍歷的結點和插入位置的結點連起來,放在了前面
pred.next = succ;
succ.prev = pred;
}
// 標記次數
size += numNew;
modCount++;
return true;
}
添加元素到頭尾
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
// 找到頭結點
final Node<E> f = first;
// 創建新結點,頭結點作為后結點
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
// 如果頭結點也不存在,說明是空的,尾結點也是新結點
if (f == null)
last = newNode;
else // 頭結點的前結點是新結點,鏈接起來
f.prev = newNode;
size++;
modCount++;
}
public void addLast(E e) {
linkLast(e);
}
// 上文有寫到該方法,往鏈表后添加大多調用次方法
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
清空元素 clear()
public void clear() {
// 遍歷置空,從頭開始
for (Node<E> x = first; x != null; ) {
// 先找到 x 的下一個元素,遍歷完賦值給 x
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
// 歸零
first = last = null;
size = 0;
modCount++;
}
獲取元素
獲取下標 indexOf(Object o)
& 是否包含 contains(Object o)
獲取下標 indexOf() 方法,找到返回下標,找不到返回 -1:
// 主要就是遍歷,注意區分查詢 null 值的情況
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
contains 調用的是 indexOf()
方法,返回 -1 說明沒查到,反之返回 true。
public boolean contains(Object o) {
return indexOf(o) != -1;
}
獲取某元素 get()
首先查詢下標是否越界,越界拋異常。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
接著還是調用 node()
方法傳入下標,返回獲取到的數據即可。
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;
}
}
獲取頭元素 element() peek()
element()
是從 Queue 接口實現來的方法,功能是返回頭元素,特點是查不到拋出NoSuchElementException 異常。
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
peek()
方法元素返回頭元素,查不到返回 null,不會拋出異常:
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
刪除元素 remove() poll()
remove()
方法默認刪除的是頭元素,如果是 null 的話會拋 NoSuchElementException異常:
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
會調用 unlinkFirst()
方法移除首元素:
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
poll()
方法不會拋出異常:
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
刪除某位置的元素 remove(int index)
涉及下標的依舊先確定下標是否越界,然后找到改位置的元素:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
調用 node(index)
方法查找元素,之前多次見過,就是那個把數據分成兩半去查詢的。
然后再調用 unlink()
方法把該元素從鏈中移除,要注意頭尾結點的處理:
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;
}
// 置空、元素數量 -1
x.item = null;
size--;
modCount++;
return element;
}
刪除某元素 remove(Object o)
刪除某元素就是根據值遍歷找到該元素,執行 unlink()
斷鏈刪除,注意刪除 null 值的情況。
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 {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
設置某位置的值 set()
檢查下標、找到該位置的數據、更新值即可。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
迭代器
線性迭代器 ListItr
常用的獲取迭代器:
LinkedList linkedList = new LinkedList();
linkedList.iterator();// 會創建 LinkedList 的線性迭代器,
該方法調用 LinkedList
的 listIterator()
方法:
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
ListItr 源碼:
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// 默認傳的 0,next 值為頭結點
// 倒序迭代器會傳元素數量 size,next 為 null
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
// 判斷遍歷過程中數據是否被修改,出現問題拋異常
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
// 記錄最后返回的值,然后指針后移
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
// 是否包含前一個元素,下標 >=0 說明存在前一元素
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// 倒序遍歷用,記錄的前一數據為 null 則返回 LinkedList 的最后一個數據
// 下次遍歷,next 就不為 null 了,返回 前一個元素
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
// 先記錄要移除的下一元素 next 結點
Node<E> lastNext = lastReturned.next;
// 把下一結點移除
unlink(lastReturned);
// 如果指針結點就是要移除的結點,后移一下
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
// 默認設置最后返回數據的值
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
// next 為 null 說明后面沒數據了,添加到隊尾
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
// 檢測是否被修改(強一致性特點)
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
倒序迭代器
private class DescendingIterator implements Iterator<E> {
// 傳入 size 創建倒序迭代器
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
- 因為創建時傳遞的是 size,迭代器創建時指針結點 next 為 null,調用
previous()
方法會返回最后一個元素。下次就會返回最后一個元素的前一元素。
同步性
- 不同步,遍歷時如果數據修改會拋異常;
- 多線程下可能造成數據覆蓋、丟失。
解決方法:
List list = Collections.synchronizedList(new LinkedList(...));
總結及對比
ArrayList
- 基于數組,查詢速度較快,時間復雜度 O(1);
- 添加刪除需要復制數據,影響效率;
- 容量不足時需要擴容。
LinkedList
- 基于鏈表,元素必須挨個查詢。下標查詢或許可以通過二分提高效率;
- 頭尾添加刪除數據較快,往固定位置添加數據還是需要遍歷查找位置;
- 無需擴容,只有內存夠,使勁添加。