這一篇是有關LinkedList的學習,那麼閒話不多扯,直接按照上一篇的博文的模式來分析LinkedList的實現和功能。
成員變量
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;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
由定義的成員變量和Node類的結構可以看出LinkedList是以雙向鏈表爲實現形式的List結構。
構造方法
無參構造
public LinkedList() { }
沒有任何其他的操作,僅僅構造一個空的List。
參數爲Collection的構造
public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
很顯然,先構造一個空List,然後執行addAll(c)方法,將Collection c添加入List。關於addAll()方法的介紹請看下文。
成員方法
因爲對LinkedList的操作均是傳遞到對雙向鏈表的操作,所以先來看對鏈表的基礎操作:
操作雙向鏈表方法
添加第一個節點
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++; }
先將f指向原來初始節點對象對應的內存空間,再新建新的初始節點,定義前置爲null,內容爲e,後置爲f(原來的初始節點),同時令first指向新建的初始節點的內存空間。
因此如果之前沒有初始節點,只需要將last指向newNode即可。如果有初始節點,將初始節點f的prev指向newNode即可完成。再將List大小+1,更改次數+1.
作爲最後一個元素節點添加
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++; }
插入過程與第一個很類似。先將 l 指向原來的終節點,再新建一個新的終節點,並將last指向新建的終節點。如果原來沒有終節點,則將first也指向新建節點,此時鏈表中只有一個節點。若原來有終節點,將原來終節點的後置節點指向新建的終節點,完成連接。最後將表大小+1,更改次數+1.
解綁鏈表的第一個節點
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; }
從鏈表角度來看,解綁初始節點,無非是將first節點指向第二個節點,再將第二個節點next的prev設爲null。而java實現中,需要將初始節點f的item和next設爲null,以便對象f沒有對象引用方便GC。當只有鏈表只有一個節點時,需要將last設爲null。
解綁鏈表的最後一個節點
private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; }
同樣從雙向鏈表的角度來看,唯一值得關注的操作就是讓l的item引用和prev引用爲null,方便GC。其他操作是將last指向prev節點。如果prev爲null,則將first設爲null,否則另prev的後置節點爲null。
在一個節點前插入一個元素
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++; }
定義succ之前的前置節點爲pred,新建插入節點。將succ的前置節點設爲新建節點。如果succ沒有前置節點,則將first指向新建節點;若有,則將pred的後置節點指向新建節點newNode。
移除鏈表中一個非空節點
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; }
移除節點x,定義x的前置節點爲prev,後置節點爲next。正常移除時需要將prev的後置引用指向next,next的前置引用指向prev。如果prev或next爲空,則將first指向next或last指向prev。同時清除x殘留的引用。大小-1,更改次數+1.
操作LinkedList方法
常用方法
public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; } public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } public void addFirst(E e) { linkFirst(e); } public void addLast(E e) { linkLast(e); } public int size() { return size; } public boolean add(E e) { linkLast(e); return true; } 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; } public E get(int index) { checkElementIndex(index); return node(index).item; } public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
以上方法常用,且操作簡單,都是對鏈表的簡單操作。因此在這不加多餘的解釋,相信大家也能理解。
addAll
public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index); Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Node<E> pred, succ; if (index == size) { succ = null; pred = last; } else { 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 = newNode; } if (succ == null) { last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true; } //將LinkedList轉化成數組,實際實現是遍歷鏈表,將鏈表中的值存入數組內。 public Object[] toArray() { Object[] result = new Object[size]; int i = 0; for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; return result; } //找到處於index位置的節點,if-else判斷是爲了找出耗時較少的遍歷途徑,返回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; } }
作爲addAll方法,實現的本質還是在鏈表的index位置插入內容爲c的鏈表。定義succ爲當前插入位置的節點,pred爲插入位置的前置節點。插入過程就是遍歷內容數組,新建一個節點,將pred的後置節點指向新建節點newNode。完成後,後移pred指向新建節點。數組循環完成後,建立最後一個新建節點和插入位置節點succ的關聯。至此,操作完成。
而其他的成員方法,都是對鏈表的操作,均可以調用操作鏈表的方法,所以不再一一介紹,如果有興趣可以自己查看源碼。由於LinkedList採用雙向鏈表實現,所以不存在數組擴容的問題。通過以上方法可以看出,LinkedList的增刪操作開銷很小,只需要遍歷到目標位置,進行鏈表節點的增刪,而對於在表起點和終點的增刪,更只是常數時間的操作。但相較於ArrayList的get方法,LinkedList仍需要遍歷鏈表才能找到索引對應值,效率不好。
迭代器的實現
想要在不暴露LinkedList的內部實現的前提下,遍歷訪問List使用迭代器是一種很好的選擇。接下來就是介紹LinkedList中的迭代器實現:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//step 4
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
}
public abstract class AbstractSequentialList<E> extends AbstractList<E> {
//step 1
public Iterator<E> iterator() {
return listIterator();
}
//step 3
public abstract ListIterator<E> listIterator(int index);
}
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public Iterator<E> iterator() {
return new Itr();
}
//step 2
public ListIterator<E> listIterator() {
return listIterator(0);
}
public ListIterator<E> listIterator(final int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
}
按照以上的執行順序可以看到,LinkedList的iterator()的實現最終追溯到內部類ListItr。
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned = null;//可以理解爲當前訪問的節點
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
//判斷是否有下個元素,將下個元素索引值和List大小比較
public boolean hasNext() {
return nextIndex < size;
}
//返回索引指向的元素,並將next指向當前的後置節點,索引值+1。
public E next() {
checkForComodification();//迭代器和初始List一致性校驗
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
//返回當前next節點指向的前置節點,並將next指向當前的前置節點,索引值-1.
//主要應用於定義的另一種倒敘遍歷方式
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
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();
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;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
既然提到迭代遍歷,也可以順帶討論List遍歷時刪除元素的問題:
public static List<String> initialList(){
List<String> list = new LinkedList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
return list;
}
public static void removeByFor(List<String> list){
for (int i = 0; i < list.size(); i++) {
if("b".equals(list.get(i))){
System.out.println(i);
list.remove(i);
}
if("d".equals(list.get(i))){
System.out.println(i);
list.remove(i);
}
}
System.out.println(list);
}
public static void removeByForeach(List<String> list){
for (String string : list) {
if ("b".equals(string)) {
list.remove(string);
}
}
System.out.println(list);
}
public static void removeByIterator(List<String>list){
Iterator<String> e = list.iterator();
while (e.hasNext()) {
String item = e.next();
if ("b".equals(item)) {
e.remove();
}
}
System.out.println(list);
}
public static void main(String []args){
removeByFor(initialList()); //1
removeByForeach(initialList()); //2
removeByIterator(initialList()); //3
}
- 方法1:能正常刪除,但是刪除d時,索引index爲2,和原List中的索引值不同,可能會對其他操作造成影響。且刪除操作經歷了兩次遍歷(外部一次,remove操作一次),時間複雜度增加。
- 方法2:會拋出ConcurrentModificationException異常。因爲foreach內部也是使用iterator進行遍歷,方法2操作只更改了modCount,沒有更改Iterator中的expectedModCount。
- 方法3:最合適的遍歷刪除,只經歷一次遍歷,時間複雜度低。
結語
LinkedList的實現是基於雙向鏈表,因此它的一切性質都是與雙向鏈表相關的:
- get(int index) O(n) 由於雙向鏈表需要依次遍歷到目標index,所以get效率不如ArrayList。
- add(E e) O(1) 添加到鏈表尾部,只需要建立關聯即可。
- add(int index,E e) O(n) 由於需要遍歷到目標index,再添加節點建立關聯。但是效率比ArrayList的移動數組好了不少。
- remove(int index) O(n) 和add方法類似,耗時主要在遍歷到目標index。整體效率比ArrayList的remove()方法好。