數(shù)據(jù)結(jié)構(gòu)-棧&隊(duì)列&Deque實(shí)現(xiàn)比較

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

  • 后進(jìn)先出(LIFO)。
  • 在表尾進(jìn)行操作,表尾是棧頂;最新進(jìn)棧的元素在棧底。
棧的ADT
Stack_ADT
進(jìn)棧&出棧
棧的存儲(chǔ)結(jié)構(gòu)實(shí)現(xiàn)
  • 順序棧

棧也是線性表,只是對表中元素的插入和刪除位置做了限定,因此我們很容易想到利用一維數(shù)組實(shí)現(xiàn)棧的存儲(chǔ)結(jié)構(gòu)。Java中的Stack類繼承自Vector,就是用數(shù)組實(shí)現(xiàn)。

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;
}
  • 兩棧共享存儲(chǔ)空間

如果我們有兩個(gè)相同類型的棧,我們?yōu)樗麄兏髯蚤_辟了數(shù)組空間,極有可能第一個(gè)棧已經(jīng)滿了,再進(jìn)棧就溢出了,而另一個(gè)棧還有很多存儲(chǔ)空間空閑。這時(shí),我們可以充分利用順序棧的單向延伸的特性,使用一個(gè)數(shù)組來存儲(chǔ)兩個(gè)棧,讓一個(gè)棧的棧底為數(shù)組的始端,另一個(gè)棧的棧底為數(shù)組的末端,每個(gè)棧從各自的端點(diǎn)向中間延伸。

share_stack

ShareStack.java


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

public class ShareStack<T> {
    private Object[] element; //存放元素的數(shù)組

    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 第幾個(gè)棧
     * @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("輸入錯(cuò)誤!");

        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("輸入錯(cuò)誤!");

    }


    /**
     * 獲取棧頂元素
     * @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("輸入錯(cuò)誤!");
    }


    /**
     * 判斷棧是否為空
     * @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("輸入錯(cuò)誤!");
    }

}

當(dāng)然,考慮到數(shù)組需要在初始化的時(shí)候限定大小,同時(shí)也要考慮擴(kuò)容的問題。因此棧也可以使用鏈表來實(shí)現(xiàn);這個(gè)后面一起討論,這里就不展開來說了。

棧這種數(shù)據(jù)結(jié)構(gòu),非常實(shí)用;Android中Activity的回退棧就是最好的例子,正常模式下,我們通過startActivity就是將一個(gè)Activity壓入了回退棧,finish()方法就是從回退棧里彈出最頂部的Activity;當(dāng)然,實(shí)際流程有很多別的操作,這里也只是大體流程;遞歸思想也是利用了棧這種結(jié)構(gòu)。

隊(duì)列

隊(duì)列: 只允許在一端進(jìn)行插入操作、而在另一端進(jìn)行刪除操作的線性表。

  • 先進(jìn)先出(FIFO)
  • 在隊(duì)尾進(jìn)行插入,從隊(duì)頭進(jìn)行刪除
隊(duì)列的ADT
Queue_ADT
入隊(duì)列&出隊(duì)列
Deque
隊(duì)列的存儲(chǔ)結(jié)構(gòu)實(shí)現(xiàn)
  • 順序存儲(chǔ)結(jié)構(gòu)

使用數(shù)組實(shí)現(xiàn)隊(duì)列的存儲(chǔ)結(jié)構(gòu)時(shí),為了避免每次從隊(duì)頭刪除元素時(shí),移動(dòng)后面的每個(gè)元素,加入了front和rear兩個(gè)指針,分別指向隊(duì)頭和隊(duì)尾;這樣每次從隊(duì)頭刪除元素時(shí),移動(dòng)front指針即可,而不必移動(dòng)大量的元素,但是這樣勢必會(huì)造成假溢出的問題,存儲(chǔ)空間得不到充分的利用,因此需要采用循環(huán)隊(duì)列的方式實(shí)現(xiàn)了隊(duì)列的順序存儲(chǔ)結(jié)構(gòu)。

  • 循環(huán)隊(duì)列

假定在循環(huán)隊(duì)列中,QueueSize為循環(huán)隊(duì)列大小,即數(shù)組長度,則有以下結(jié)論:

  1. 循環(huán)隊(duì)列空的條件:front==rear;
  2. 循環(huán)隊(duì)列滿的條件:(rear+1)%QueueSize=front;
  3. 循環(huán)隊(duì)列長度:(rear-front*QueueSize)%QueueSize;

總的來說,采用順序存儲(chǔ)結(jié)構(gòu),還是需要考慮容量的問題。因此,在我們無法預(yù)估隊(duì)列長度的情況下,需要關(guān)注鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)。

  • 鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)

上文中我們已經(jīng)說過,LinkList實(shí)現(xiàn)了Deque接口,因此它就是用鏈表實(shí)現(xiàn)的隊(duì)列。這里簡單分析一下入隊(duì)push和出隊(duì)pop操作的實(shí)現(xiàn)。

LinkedList-add 隊(duì)列入隊(duì)

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        //創(chuàng)建新的結(jié)點(diǎn),其前驅(qū)指向last,后繼為null
        final Node<E> newNode = new Node<>(l, e, null);
        //last 指針指向新的結(jié)點(diǎn)
        last = newNode;
        if (l == null)
            first = newNode;  //如果鏈表為空,frist指針指向新的結(jié)點(diǎn)
        else
            l.next = newNode; //鏈表不為空,新的結(jié)點(diǎn)連接到原來最后一個(gè)結(jié)點(diǎn)之后
        size++; //鏈表長度+1
        modCount++;
    }

LinkList是一個(gè)雙向鏈表,這里first是執(zhí)行第一個(gè)結(jié)點(diǎn)的指針,last是指向最后一個(gè)結(jié)點(diǎn)指針。

LinkList-pop 隊(duì)列出隊(duì)

    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;
        //獲取要?jiǎng)h除結(jié)點(diǎn)的值
        final E element = f.item;
        //得到f的下一個(gè)結(jié)點(diǎn),也就是第二個(gè)結(jié)點(diǎn)
        final Node<E> next = f.next;
        // f 釋放
        f.item = null;
        f.next = null; // help GC
        // first 指針指向f的下個(gè)結(jié)點(diǎn),
        first = next;
        // f 后面已經(jīng)沒有結(jié)點(diǎn)了
        if (next == null)
            last = null; 
        else
            next.prev = null; // 第二個(gè)結(jié)點(diǎn)(也就是現(xiàn)在的第一個(gè)結(jié)點(diǎn))前驅(qū)為null,因?yàn)長inkList 是雙端鏈表,非循環(huán)。
        size--;
        modCount++;
        return element;
    }

這里就是一個(gè)典型的單鏈表刪除頭結(jié)點(diǎn)的實(shí)現(xiàn)。至此,我們已經(jīng)掌握了棧和隊(duì)列這兩種數(shù)據(jù)結(jié)構(gòu)各自的特點(diǎn);下面再來看看Java官方提供的關(guān)于棧和隊(duì)列的實(shí)現(xiàn)。

Deque

這里主要說一下Deque這個(gè)類。

/**
 * 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”),即雙端隊(duì)列,支持在線性表的兩端插入和刪除元素,繼承Queue接口。大多數(shù)的實(shí)現(xiàn)對元素的數(shù)量沒有限制,但這個(gè)接口既支持有容量限制的deque,也支持沒有固定大小限制的。

我們知道Queue接口定義了隊(duì)列的操作集合,而Deque接口又在其基礎(chǔ)上擴(kuò)展,定義了在雙端進(jìn)行插入刪除的操作。因此,我們很可以認(rèn)為,Deque接口既可以當(dāng)做隊(duì)列,也可以當(dāng)做棧。

Deque的鏈?zhǔn)酱鎯?chǔ)實(shí)現(xiàn)LinkList

因此,回過頭來,我們可以發(fā)現(xiàn)LinkList以鏈表結(jié)構(gòu),同時(shí)實(shí)現(xiàn)了隊(duì)列和棧。前面已經(jīng)分析了LinkList作為一個(gè)隊(duì)列的操作。下面我們可以看看,他又是如何實(shí)現(xiàn)鏈?zhǔn)浇Y(jié)構(gòu)實(shí)現(xiàn)隊(duì)列的。

入棧

    public void addLast(E e) {
        linkLast(e);
    }

可以看到,對于入棧操作和隊(duì)列樣,都是在鏈表最后插入元素,和隊(duì)列一樣使用了linkLast()方法。

出棧

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

出棧同樣是用了unlinkLast 方法,只不過出棧的元素是last。而不是隊(duì)列中的first。

Deque的順序存儲(chǔ)實(shí)現(xiàn) ArrayDeque

ArrayDeque 用一個(gè)動(dòng)態(tài)數(shù)組實(shí)現(xiàn)了棧和隊(duì)列所需的所有操作。

添加元素

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

這里可以看到,無論是頭部還是尾部添加新元素,當(dāng)需要擴(kuò)容時(shí),會(huì)直接變化為原來的2倍。同時(shí)需要復(fù)制并移動(dòng)大量的元素。

刪除元素

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是當(dāng)前數(shù)組中第一個(gè)元素的位置,tail是數(shù)組中第一個(gè)空的位置。

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> {
}

關(guān)于Deque最后一點(diǎn),BlockingDeque 在Deque 基礎(chǔ)上又實(shí)現(xiàn)了阻塞的功能,當(dāng)棧或隊(duì)列為空時(shí),不允許出棧或出隊(duì)列,會(huì)保持阻塞,直到有可出棧元素出現(xiàn);同理,隊(duì)列滿時(shí),不允許入隊(duì),除非有元素出棧騰出了空間。常用的具體實(shí)現(xiàn)類是LinkedBlockingDeque,使用鏈?zhǔn)浇Y(jié)構(gòu)實(shí)現(xiàn)了他的阻塞功能。Android中大家非常熟悉的AsyncTask 內(nèi)部的線程池隊(duì)列,就是使用LinkedBlockingDeque實(shí)現(xiàn),長度為128,保證了AsyncTask的串行執(zhí)行。

這里比較一下可以發(fā)現(xiàn),對于棧和隊(duì)列這兩種特殊的數(shù)據(jù)結(jié)構(gòu),由于獲取(查找)元素的位置已經(jīng)被限定,因此采用順序存儲(chǔ)結(jié)構(gòu)并沒有非常大的優(yōu)勢,反而是在添加元素由于數(shù)組容量的問題還會(huì)帶來額外的消耗;因此,在無法預(yù)先知道數(shù)據(jù)容量的情況下,使用鏈?zhǔn)浇Y(jié)構(gòu)實(shí)現(xiàn)棧和隊(duì)列應(yīng)該是更好的選擇。


好了,棧和隊(duì)列就先到這里了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,928評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內(nèi)容