資料結構-棧&佇列&Deque實現比較

IAM四十二發表於2017-10-22

棧: 限定僅在表尾進行插入和刪除操作的線性表;

  • 後進先出(LIFO)。
  • 在表尾進行操作,表尾是棧頂;最新進棧的元素在棧底。
棧的ADT

Stack_ADT
Stack_ADT

進棧&出棧

棧

棧的儲存結構實現
  • 順序棧

棧也是線性表,只是對錶中元素的插入和刪除位置做了限定,因此我們很容易想到利用一維陣列實現棧的儲存結構。Java中的Stack類繼承自Vector,就是用陣列實現。

Stack.java

public class Stack<E> extends Vector<E> {

    public Stack() {
    }

    public E push(E item) {
        addElement(item);

        return item;
    }

    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

    public boolean empty() {
        return size() == 0;
    }

    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    private static final long serialVersionUID = 1224463164541339165L;
}複製程式碼
  • 兩棧共享儲存空間

    如果我們有兩個相同型別的棧,我們為他們各自開闢了陣列空間,極有可能第一個棧已經滿了,再進棧就溢位了,而另一個棧還有很多儲存空間空閒。這時,我們可以充分利用順序棧的單向延伸的特性,使用一個陣列來儲存兩個棧,讓一個棧的棧底為陣列的始端,另一個棧的棧底為陣列的末端,每個棧從各自的端點向中間延伸。

share_stack
share_stack

ShareStack.java


/**
 * Created by engineer on 2017/10/22.
 */

public class ShareStack<T> {
    private Object[] element; //存放元素的陣列

    private int stackSize;  // 棧大小

    private int top1; //棧1的棧頂指標

    private int top2; //棧2的棧頂指標


    /**
     * 初始化棧
     * @param size
     */
    public ShareStack(int size){
        element = new Object[size];
        stackSize = size;
        top1 = -1;
        top2 = stackSize;
    }


    /**
     * 壓棧
     * @param i 第幾個棧
     * @param o 入棧元素
     * @return
     */
    public boolean push(int i , Object o){

        if(top1 == top2 - 1)
            throw new RuntimeException("棧滿!");
        else if(i == 1){
            top1++;
            element[top1] = o;
        }else if(i == 2){
            top2--;
            element[top2] = o;
        }else
            throw new RuntimeException("輸入錯誤!");

        return true;
    }

    /**
     * 出棧
     * @param i
     * @return
     */
    @SuppressWarnings("unchecked")
    public T pop(int i){

        if(i == 1){
            if(top1 == -1)
                throw new RuntimeException("棧1為空");
            return (T)element[top1--];
        } else if(i == 2){
            if(top2 == stackSize)
                throw new RuntimeException("棧2為空");
            return (T)element[top2++];
        } else
            throw new RuntimeException("輸入錯誤!");

    }


    /**
     * 獲取棧頂元素
     * @param i
     * @return
     */
    @SuppressWarnings("unchecked")
    public T get(int i){

        if(i == 1){
            if(top1 == -1)
                throw new RuntimeException("棧1為空");
            return (T)element[top1];
        } else if(i == 2){
            if(top2 == stackSize)
                throw new RuntimeException("棧2為空");
            return (T)element[top2];
        } else
            throw new RuntimeException("輸入錯誤!");
    }


    /**
     * 判斷棧是否為空
     * @param i
     * @return
     */
    public boolean isEmpty(int i){

        if(i == 1){
            if(top1 == -1)
                return true;
            else
                return false;
        } else if(i == 2){
            if(top2 == stackSize)
                return true;
            else
                return false;
        } else
            throw new RuntimeException("輸入錯誤!");
    }

}複製程式碼

當然,考慮到陣列需要在初始化的時候限定大小,同時也要考慮擴容的問題。因此棧也可以使用連結串列來實現;這個後面一起討論,這裡就不展開來說了。

棧這種資料結構,非常實用;Android中Activity的回退棧就是最好的例子,正常模式下,我們通過startActivity就是將一個Activity壓入了回退棧,finish()方法就是從回退棧裡彈出最頂部的Activity;當然,實際流程有很多別的操作,這裡也只是大體流程;遞迴思想也是利用了棧這種結構。

佇列

佇列: 只允許在一端進行插入操作、而在另一端進行刪除操作的線性表。

  • 先進先出(FIFO)
  • 在隊尾進行插入,從隊頭進行刪除
佇列的ADT

Queue_ADT
Queue_ADT

入佇列&出佇列

Deque
Deque

棧的儲存結構實現
  • 順序儲存結構

使用陣列實現佇列的儲存結構時,為了避免每次從隊頭刪除元素時,移動後面的每個元素,加入了front和rear兩個指標,分別指向隊頭和隊尾;這樣每次從隊頭刪除元素時,移動front指標即可,而不必移動大量的元素,但是這樣勢必會造成假溢位的問題,儲存空間得不到充分的利用,因此需要採用迴圈佇列的方式實現了佇列的順序儲存結構。

  • 迴圈佇列

假定在迴圈佇列中,QueueSize為迴圈佇列大小,即陣列長度,則有以下結論:

  1. 迴圈佇列空的條件:front==rear;
  2. 迴圈佇列滿的條件:(rear+1)%QueueSize=front;
  3. 迴圈佇列長度:(rear-front*QueueSize)%QueueSize;

總的來說,採用順序儲存結構,還是需要考慮容量的問題。因此,在我們無法預估佇列長度的情況下,需要關注鏈式儲存結構。

  • 鏈式儲存結構

上文中我們已經說過,LinkList實現了Deque介面,因此它就是用連結串列實現的佇列。這裡簡單分析一下入隊push和出隊pop操作的實現。

LinkedList-add 佇列入隊

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        //建立新的結點,其前驅指向last,後繼為null
        final Node<E> newNode = new Node<>(l, e, null);
        //last 指標指向新的結點
        last = newNode;
        if (l == null)
            first = newNode;  //如果連結串列為空,frist指標指向新的結點
        else
            l.next = newNode; //連結串列不為空,新的結點連線到原來最後一個結點之後
        size++; //連結串列長度+1
        modCount++;
    }複製程式碼

LinkList是一個雙向連結串列,這裡first是執行第一個結點的指標,last是指向最後一個結點指標。

LinkList-pop 佇列出隊

    public E pop() {
        return removeFirst();
    }
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        //獲取要刪除結點的值
        final E element = f.item;
        //得到f的下一個結點,也就是第二個結點
        final Node<E> next = f.next;
        // f 釋放
        f.item = null;
        f.next = null; // help GC
        // first 指標指向f的下個結點,
        first = next;
        // f 後面已經沒有結點了
        if (next == null)
            last = null; 
        else
            next.prev = null; // 第二個結點(也就是現在的第一個結點)前驅為null,因為LinkList 是雙端連結串列,非迴圈。
        size--;
        modCount++;
        return element;
    }複製程式碼

這裡就是一個典型的單連結串列刪除頭結點的實現。至此,我們已經掌握了棧和佇列這兩種資料結構各自的特點;下面再來看看Java官方提供的關於棧和佇列的實現。

Deque

這裡主要說一下Deque這個類。

/**
 * A linear collection that supports element insertion and removal at
 * both ends.  The name <i>deque</i> is short for "double ended queue"
 * and is usually pronounced "deck".  Most {@code Deque}
 * implementations place no fixed limits on the number of elements
 * they may contain, but this interface supports capacity-restricted
 * deques as well as those with no fixed size limit.
 * /
public interface Deque<E> extends Queue<E> {
    void addFirst(E var1);

    void addLast(E var1);

    boolean offerFirst(E var1);

    boolean offerLast(E var1);

    E removeFirst();

    E removeLast();

    E pollFirst();

    E pollLast();

    E getFirst();

    E getLast();

    E peekFirst();

    E peekLast();

    boolean add(E var1);

    boolean offer(E var1);

    E remove();

    E poll();

    E element();

    E peek();

    void push(E var1);

    E pop();

    ........
}複製程式碼

Deque介面是“double ended queue”的縮寫(通常讀作“deck”),即雙端佇列,支援線上性表的兩端插入和刪除元素,繼承Queue介面。大多數的實現對元素的數量沒有限制,但這個介面既支援有容量限制的deque,也支援沒有固定大小限制的。

我們知道Queue介面定義了佇列的操作集合,而Deque介面又在其基礎上擴充套件,定義了在雙端進行插入刪除的操作。因此,我們很可以認為,Deque介面既可以當做佇列,也可以當做棧。

因此,回過頭來,我們可以發現LinkList以連結串列結構,同時實現了佇列和棧。前面已經分析了LinkList作為一個佇列的操作。下面我們可以看看,他又是如何實現鏈式結構實現佇列的。

入棧

    public void addLast(E e) {
        linkLast(e);
    }複製程式碼

可以看到,對於入棧操作和佇列樣,都是在連結串列最後插入元素,和佇列一樣使用了linkLast()方法。

出棧

    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }複製程式碼

出棧同樣是用了unlinkLast 方法,只不過出棧的元素是last。而不是佇列中的first。

Deque的順序儲存實現 ArrayDeque

ArrayDeque 用一個動態陣列實現了棧和佇列所需的所有操作。

新增元素

    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }

    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

    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];
        System.arraycopy(elements, p, a, 0, r);
        System.arraycopy(elements, 0, a, r, p);
        elements = a;
        head = 0;
        tail = n;
    }複製程式碼

這裡可以看到,無論是頭部還是尾部新增新元素,當需要擴容時,會直接變化為原來的2倍。同時需要複製並移動大量的元素。

刪除元素

public E pollFirst() {
        final Object[] elements = this.elements;
        final int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result != null) {
            elements[h] = null; // Must null out slot
            head = (h + 1) & (elements.length - 1);
        }
        return result;
    }

    public E pollLast() {
        final Object[] elements = this.elements;
        final int t = (tail - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        E result = (E) elements[t];
        if (result != null) {
            elements[t] = null;
            tail = t;
        }
        return result;
    }複製程式碼

從頭部和尾部刪除(獲取)元素,就比較方便了,修改head和tail位置即可。head是當前陣列中第一個元素的位置,tail是陣列中第一個空的位置。

BlockingDeque

/**
 * A {@link Deque} that additionally supports blocking operations that wait
 * for the deque to become non-empty when retrieving an element, and wait for
 * space to become available in the deque when storing an element.
 * /

public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {
}複製程式碼

關於Deque最後一點,BlockingDeque 在Deque 基礎上又實現了阻塞的功能,當棧或佇列為空時,不允許出棧或出佇列,會保持阻塞,直到有可出棧元素出現;同理,佇列滿時,不允許入隊,除非有元素出棧騰出了空間。常用的具體實現類是LinkedBlockingDeque,使用鏈式結構實現了他的阻塞功能。Android中大家非常熟悉的AsyncTask 內部的執行緒池佇列,就是使用LinkedBlockingDeque實現,長度為128,保證了AsyncTask的序列執行。

這裡比較一下可以發現,對於棧和佇列這兩種特殊的資料結構,由於獲取(查詢)元素的位置已經被限定,因此採用順序儲存結構並沒有非常大的優勢,反而是在新增元素由於陣列容量的問題還會帶來額外的消耗;因此,在無法預先知道資料容量的情況下,使用鏈式結構實現棧和佇列應該是更好的選擇。


好了,棧和佇列就先到這裡了。

相關文章