1. 程式人生 > 實用技巧 >棧和佇列

棧和佇列

在學習棧和佇列前,先補充兩個概念:物理結構和邏輯結構。

1.什麼是資料儲存的物理結構呢?

  舉個易懂的例子,如果把資料結構比作活生生的人,那麼物理結構就是人的血肉和骨骼,看得見,摸得著,實實在在。那麼,陣列和連結串列就是實實在在的儲存結構。

2.什麼是資料儲存的邏輯結構呢?

  就剛剛的例子來說,人體之上,還存在著人的思想和精神,它看不見摸不著。那麼,棧和佇列以及數和圖等資料結構都是屬於邏輯結構。雖然棧和佇列是屬於邏輯結構的,但是他們的物理實現既可以利用陣列,也可以利用連結串列來實現。

3.什麼是棧?

  棧(stack)是一種線性資料結構,它就像一個放羽毛球的圓筒容器,棧中的元素只能先入後出,最早進入的元素存放的位置叫做棧底(bottom),最後進入的元素存放的位置叫做棧頂(top)。

  3.1 棧的基本操作

    3.1.1 入棧

      入棧操作(push)就是把新元素放入棧中,只允許從棧頂一側放入元素,新元素的位置將會稱為新的棧頂。

    3.1.2 出棧

      出棧操作(pop)就是把元素從棧中彈出,只有棧頂元素才允許出棧,出棧元素的前一個元素將會稱為新的棧頂。

      java自己封裝的棧:

 1 package com.algorithm.test;
 2 
 3 import java.util.Stack;
 4 
 5 /**
 6  * @Author Jack丶WeTa
 7  * @Date 2020/7/28 12:06
 8  * @Description 關於棧的簡單操作(入棧和出棧)
9 */ 10 public class StackTest { 11 public static void main(String[] args) { 12 Stack<Integer> stack = new Stack<>(); 13 14 for (int i = 0; i <= 10; i++) { 15 stack.push(i); 16 } 17 18 while (!stack.isEmpty()) { 19 System.out.println("棧:" + stack.toString() + "\t棧大小為:" + stack.size() + "\t出棧元素為:" + stack.pop());
20 } 21 } 22 }

      我們自己也可以用陣列來簡單實現一下:

package com.algorithm.test;


/**
 * @Author Jack丶WeTa
 * @Date 2020/7/28 12:06
 * @Description 關於棧的簡單操作(入棧和出棧)
 */
public class MyStackTest {

    private int[] array;
    private int top; //棧頂

    public MyStackTest(int capacity){
        this.array = new int[capacity];
    }

    public void push(int element){
        if(top >= array.length){
            throw new IndexOutOfBoundsException("滿棧了,無法進行入棧操作!");
        }
        array[top] = element;
        top++;
    }

    public int pop() {
        if(top <= 0){
            throw new RuntimeException("棧為空,無法進行出棧操作!");
        }
        int popElement = array[top-1];
        top--;
        return popElement;
    }

    public void output(){
        for (int i = 0; i < top; i++){
            System.out.print(array[i] + ",");
        }
    }
    public static void main(String[] args) {
        MyStackTest myStackTest = new MyStackTest(6);
        myStackTest.push(5);
        myStackTest.push(2);
        myStackTest.push(4);
        myStackTest.push(8);
        myStackTest.push(0);
        myStackTest.push(9);
        myStackTest.output();
        System.out.println();
        System.out.println("-----開始出棧操作-----");

        for (int i = 0; i < 6; i++){
            System.out.print("出棧第" + (i+1) + "次:");
            myStackTest.pop();
            myStackTest.output();
            System.out.println();
        }
    }
}

    總結下棧操作的時間複雜度。由於入棧和出棧指揮影響到最後一個元素,不涉及到其他元素的整體移動,所以無論是以陣列還是連結串列實現,入棧和出棧的時間複雜度都是O(1)。

4.什麼是佇列呢?

  佇列(queue)是一種線性資料結構,它的特徵和行駛車輛的單行隧道很相似。不同於棧的先入後出,佇列中的元素只能先入先出。佇列的出口端叫做隊頭(front),佇列的入口端叫隊尾(rear)。

  4.1 佇列的基本操作

    4.1.1 入隊

      入隊(enqueue)就是把新元素放入佇列中,只允許在隊尾的位置放置元素,新元素的下一個位置將會成為新的隊尾。

    4.1.2 出隊

      出隊(dequeue)就是把元素移出佇列,只允許在對頭一側移出元素,出隊元素的後一個元素將會成為新的對頭。但是,如果像這樣子不斷出隊的話,對頭左邊的空間將失去作用,那佇列的容量豈不是越來越小了?那麼,我們就需要採取一些措施,用陣列實現的佇列可以採用迴圈佇列的方式來維持佇列容量的恆定。

5.什麼是迴圈佇列呢?

  假設佇列經過反覆的入隊和出隊操作,還剩下2個元素,在“物理”上分佈於陣列的末尾位置,這時又有一個新元素將要入隊。在陣列不做擴容的前提下,如何讓新元素入隊並確定新的隊尾位置呢?我們可以利用已出隊元素留下的空間,讓隊尾指標重新指回陣列的首位。這樣一來,整個佇列的元素就“迴圈”起來了。在物理儲存上,隊尾的位置也可以在對頭之前。當再有元素入隊時,將其放在陣列的首位,隊尾指標繼續後移即可。一直到(隊尾下標+1)%陣列長度 = 對頭下標 時,代表此佇列真的已經滿了。需要注意的是,隊尾指標指向的位置永遠空出1位,所以佇列最大容量比陣列長度小1。

  那麼我們也可以用程式碼自己實現一下所謂的迴圈佇列:

package com.algorithm.test;


/**
 * @Author Jack丶WeTa
 * @Date 2020/7/28 14:57
 * @Description 迴圈佇列的實現demo
 */
public class MyQueueTest {
    private int[] array;
    //隊頭
    private int front;
    //隊尾
    private int rear;

    public MyQueueTest(int capacity) {
        this.array = new int[capacity];
    }

    /**
     * @param element 入隊的元素
     * @throws Exception
     */
    public void enQueue(int element) throws Exception {
        if ((rear + 1) % array.length == front) { //  (隊尾下標+1)% 陣列長度 == 隊頭下標
            //佇列已經滿了
            throw new Exception("佇列已滿!");
        }
        array[rear] = element;
        rear = (rear + 1) % array.length; //入隊後,隊尾向後挪一位
    }

    /**
     * @return 出隊的元素
     * @throws Exception
     */
    public int deQueue() throws Exception {
        if (rear == front) {
            throw new Exception("佇列已空!");
        }
        int deQueueElement = array[front]; //出隊的元素
        front = (front + 1) % array.length; //出隊後,隊頭向後挪1位
        return deQueueElement;
    }

    /**
     * 輸出佇列
     */
    public void output() {
        for (int i = front; i != rear; i = (i+1)%array.length){
            System.out.print(array[i] + ",");
        }
    }

    public static void main(String[] args) throws Exception {
        MyQueueTest myQueueTest = new MyQueueTest(6);
        myQueueTest.enQueue(3);
        myQueueTest.enQueue(5);
        myQueueTest.enQueue(6);
        myQueueTest.enQueue(8);
        myQueueTest.enQueue(1);
        myQueueTest.output();
        System.out.println("--------------------------------------");

        myQueueTest.deQueue();
        myQueueTest.deQueue();
        myQueueTest.deQueue();
        myQueueTest.output();
        System.out.println("--------------------------------------");

        myQueueTest.enQueue(2);
        myQueueTest.enQueue(4);
        myQueueTest.enQueue(9);
        myQueueTest.output();
    }
}

    迴圈佇列不僅充分利用了陣列的空間,還避免了陣列元素整體移動的麻煩,那麼入隊和出隊的時間複雜度也同樣是O(1)。

6.棧和佇列的應用

  6.1 棧的應用

    棧的輸出順序和輸入順序相反,所以棧通常用於對“歷史”的回溯,也就是逆流而上追溯“歷史”。

    例如實現遞迴的邏輯,就可以用棧來代替,因為棧可以回溯方法的呼叫鏈。

    棧還有一個著名的應用場景是麵包屑導航,使使用者在瀏覽頁面時可以輕鬆的回溯到上一級或更上一級的頁面。

  6.2 佇列的應用

    佇列的輸出順序和輸入順序相同,所以佇列通常用於對“歷史”的回放,也就是按照“歷史”的順序把“歷史”重演一遍。

    例如在多執行緒中,爭奪公平鎖的等待佇列,就是按照訪問順序來決定執行緒在佇列中的次序的。

    再如網路爬蟲實現網路抓取時,也是把抓取的網站URL存入佇列中,在按照存入佇列的順序來依次抓取和解析的。

  6.3 雙端佇列

    雙端佇列這種資料結構可以說是綜合了棧和佇列的優點,對雙端佇列來說,從對頭一端可以入隊或出隊,從隊尾一端也可以入隊或出隊。

  6.4 優先佇列

    還有一種佇列,它遵循的不是先入先出,而是誰的優先順序最高,誰先出隊。這種佇列叫做優先佇列。有限佇列已經不屬於線性資料機構的範疇了,他是基於二叉堆來實現的。

  總結一下,本次主要學習棧和簡單的佇列的概念和運用,6.3和6.4的知識後面會進行研究和學習的!