【Java編程的邏輯】列表和隊列

ArrayList

ArrayList中有兩個方法可以返回數據

public Object[] toArray();
public <T> T[] toArray(T[] a);

ArrayList中有一個靜態方法asList可以返回對應的List

Integer[] a = {1, 2, 3};
List<Integer> list = Arrays.asList(a);

這個方法返回的List,內部就是傳入的數組,所以對數據的修改也會反映到List中,對List調用add、remove方法會拋出異常
要使用ArrayList完整的方法,應該新建一個ArrayList

List<Integer> list = new ArrayList<Integer>(Arrays.asList(a));

ArrayList特點總結:
1. 可以隨機訪問,按照索引位置進行訪問效率很高。O(1)
2. 除非數組已排序,否則按照內容查找元素效率比較低。O(N)
3. 插入和刪除元素效率比較低。O(N)
4. 非線程安全的

LinkedList

LinkedList還實現了隊列接口Queue,隊列的特點就是先進先出。

public interface Queue<E> extends Collection<E> {
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
}

Queue主要擴展了Collection接口,主要是三個操作:

  • 在尾部添加元素(add、offer)
  • 查看頭部元素(element、peek),返回頭部元素,但不改變隊列
  • 刪除頭部元素(remove、poll),返回頭部元素,並且從隊列中刪除
    每個操作都對應了兩個方法。它們的區別在於,當隊列爲空的時候,element和remove會拋出異常,而peek和poll會返回null;當隊列爲滿的時候,add會拋出異常,offer返回false

LinkedList其實是直接實現的Duque接口,該接口表示雙端隊列。它可以實現棧相關的操作,棧的特點是先進後出,後進先出。

public interface Deque<E> extends Queue<E> { 
    // 表示入棧,在頭部添加元素,如果棧滿了,會拋出異常
    void push(E e);
    // 出棧,返回頭部元素,並且從棧中刪除,如果棧爲空,拋出異常
    E pop();
    // 查看棧頭部元素,如果棧爲空,返回null
    E peek();
    // 從後往前遍歷
    Iterator<E> descendingIterator();
}

LinkedList和ArrayList用法上類似,只是LinkedList增加了一個接口Deque,可以把它看作隊列、棧、雙端隊列。

LinkedList實現原理

ArrayList內部是數組,元素在內存是連續存放的,但LinkedList不是。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;
    }
}

LinkedList內部組成主要由如下三哥實例變量

int size = 0;       // 表示鏈表長度
Node<E> first;      // 指向頭節點
Node<E> last;       // 指向尾節點

添加元素 add

public boolean add(E e) {
    linkLast(e);
    return true;
}

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++;
}

與ArrayList不同,LinkedList的內存是按需分配的,添加元素也很簡單,直接在尾節點添加鏈接即可

獲取元素 get

public E get(int index) {
    // 檢查index是否超出範圍
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // sieze >> 1相當於size/2
    // 如果index在size的前半部分,則從頭節點開始找,否則從尾節點開始
    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;
    }
}

與ArrayList不同,ArrayList中數組元素連續存放,可以根據索引直接定位,而在LinkedList中,則必須從頭或尾節點順着鏈接查找

插入元素

add是在尾部添加元素,如果在頭部或中間插入元素,可以使用如下的方法

public void add(int index, E element) {
    // 檢查index是否合法
    checkPositionIndex(index);
    // 如果index正好等於size,那麼就直接調用添加方法
    if (index == size)
        linkLast(element);
    else        
        linkBefore(element, node(index));
}
/**
 * @succ 目標位置當前的節點  這裏插入後,就變成後繼了
 */
void linkBefore(E e, Node<E> succ) {
    // 獲取當前位置節點的前驅
    final Node<E> pred = succ.prev;
    // 新建節點,前驅是pred,後繼是之前位置上的節點
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 後驅的前驅重新給值
    succ.prev = newNode;    
    // 前驅的後繼重新給值
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    // 增加長度
    size++;
    modCount++;
}

在中間插入元素,LinkedList只需要按需分配內存,修改前驅和後繼節點的鏈接,而ArrayList可能需要分配很多額外的空間,並移動後續元素

remove 刪除元素

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

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的前驅和後繼鏈接起來。

LinkedList特點

  1. 按需分配空間,不需要預先分配額外的空間
  2. 不可隨機訪問,按照索引位置訪問效率比較低,必須從頭或尾順着鏈接找,效率爲O(N/2)
  3. 不管列表是否已排序,只要是按照內容查找元素,效率都比較低,必須逐個比較,效率爲O(N)
  4. 在兩端添加、刪除元素效率很高,爲O(1)
  5. 在中間插入、刪除元素,要先定位,效率較低,爲O(N),但修改本身的效率很高,爲O(1)

如果列表長度未知,添加、刪除操作比較多,尤其經常從兩端進行操作,而按照索引位置訪問相對比較少,則LinkedList是比較理想的選擇

ArrayDeque

ArrayDeque是基於數組實現的雙端隊列,主要有以下幾個屬性

// 存儲隊列中節點的數組
transient Object[] elements;
// 代表頭指針
transient int head;
// 代表尾指針
transient int tail;
// 代表創建一個隊列的最小容量
private static final int MIN_INITIAL_CAPACITY = 8;

再來看看構造函數

public ArrayDeque() {
    elements = new Object[16];
}

public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

private void allocateElements(int numElements) {
    // 如果numElements小於8,那麼數組長度就分配8
    intinitialCapacity = MIN_INITIAL_CAPACITY;
    if (numElements >= initialCapacity) {
        // 如果numElements大於8,分配的長度是嚴格大於numElements並且爲2的整數次冪
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    elements = new Object[initialCapacity];
}

如果沒有指定顯式傳入elements的長度,則默認16。如果顯式傳入一個代表elements的長度的變量,那麼會調用allocateElements做一些簡單的處理,主要的處理都在上面的代碼註釋中。 這裏它計算嚴格大於numElements並且爲2的整數次冪的方式,就是先將numElements二進制形式的所有位置1,然後+1就是了。

從尾部添加 add

public boolean add(E e) {
    addLast(e);
    return true;
}

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    // 將元素添加到尾指針的位置    
    elements[tail] = e;
    // 將tail指向向一個位置,如果滿了,就擴容
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

這裏我們重點看一下判斷當前隊列是否滿了的語句

if ( (tail = (tail + 1) & (elements.length - 1)) == head)

我們之前在構造elements元素的時候,說過它的長度一定是2的指數級,所以對於任意一個2的指數級的值減去1之後它的二進制必然所有位全爲1,例如:8-1之後爲111,16-1之後1111。而對於tail來說,當tail+1小於等於elements.length - 1,兩者與完之後的結果還是tail+1,但是如果tail+1大於elements.length - 1,兩者與完之後就爲0。就等於頭指針的位置了。

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    // 分配一個長度翻倍的數組    
    Object[] a = new Object[newCapacity];
    // 將head右邊的元素賦值到新數組開頭
    System.arraycopy(elements, p, a, 0, r);
    // 將head左邊的元素賦值到新數組中
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    // 重新設置head和tail
    head = 0;
    tail = n;
}

從頭部添加 addFirst

public void addFirst(E e) {
    if(e == null)
        throw new NullPointerException();
    // 讓head指向前一個位置    
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

出棧

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    if (result == null)
        return null;
    elements[h] = null;
    head = (h + 1) & (elements.length - 1);
    return result;
}

該方法很簡單,直接獲取數組頭部元素即可,然後head往後移動一個位置。這是出隊操作,其實刪除操作也是一種出隊,內部還是調用了pollFirst方法。

查看長度

public int size() {
    return (tail - head) & (elements.length - 1);
}

ArrayDeque特點

  1. 在兩端添加、刪除元素的效率很高,動態擴展需要的內存分配以及數組複製開銷可以被平攤,具體來說,添加N個元素的效率爲O(N)
  2. 根據元素內容查找和刪除的效率比較低,爲O(N)
  3. 與ArrayList和LinkedList不同,沒有索引的概念

迭代器

Iterable Iterator

Iterable 表示可迭代的,它有一個方法iterator(),返回Iterator對象。

public interface Iterable<T> {
    Iterator<T> iterator();
}
public interface Iterator<E> {
    // 判斷是否還有元素未訪問
    boolean hasNext();
    // 返回下一個元素
    E next();
    // 刪除最後返回的元素。如果沒有調用過next(),直接調用remove()是會報錯的
    void remove();
}

如果對象實現了Iterable,就可以使用foreach語法。foreach語法,編譯器會轉換爲調用Iterable和Iterator接口的方法。

ListIterator

ArrayList還提供了兩個返回Iterator接口的方法:

// 返回的迭代器從0開始
public ListIterator<E> listIterator();
// 返回的迭代器從指定位置index開始
public ListIterator<E> listIterator(int index);

ListIterator擴展了Iterator接口

public interface ListIterator<E> extends Iterator<E> {  
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void set(E e);
    void add(E e);
}

迭代器常見問題

有一種常見的誤用,就是在迭代的中間調用容器的刪除方法:

public void remove(ArrayList<Integer> list) {
    for(Integer a : list) {
        if(a<=100) {
            list.remove(a);
        }
    }
}

這樣做是會拋出異常的。因爲迭代器內部會維護一些索引位置相關的數據,要求在迭代過程中,容器不能發生結構性變化(添加、刪除元素)
使用迭代器的remove方法可以避免錯誤

public static void remove(ArrayList<Integer> list) {
    Iterator<Integer> it = list.iterator();
    while(it.hasNext()) {
        if(it.next() < 100) {
            it.remove();
        }
    }
}

爲什麼使用迭代器的remove方法就可以呢?

迭代器基本原理

先看看ArrayList中的iterator方法

public Iterator<E> iterator() {
    return new Itr();
}

Itr是ArrayList的成員內部類,

private class Itr implements Iterator<E> {
    // 下一個要返回的元素的位置
    int cursor;
    // 最後一個返回的索引位置,如果沒有爲-1
    int lastRet = -1;
    // 期望的修改次數
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    public E next() {
        // 檢查是否發生了結構變化
        checkForComodification();
        // 更新cursor和lastRet
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        // 返回對應的元素
        return (E) elementData[lastRet = i];
    }
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            // 通過ArrayList的remove方法刪除
            ArrayList.this.remove(lastRet);
            // 更新cursor和lastRet
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
發佈了98 篇原創文章 · 獲贊 97 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章