面試官:你能講講棧和隊列嗎?

以前參加過一次面試,上來就問我棧和隊列。說實話,做爲一個非科班出來的,並無學過《數據結構》這門課,因此也就簡單知道點什麼先進先出,先進後出之類的,當我說完這個以後。html

面試官那表情:就這?出門右轉,簡歷要還給你嗎?java

我:嘿嘿嘿,淦🤡node

我以爲他可能看我太帥了,可是,你就不能先問我String、StringBuffer與StringBuilder的區別嗎?面試

寫在前面

棧和隊列,也屬於線性表,由於它們也都用於存儲邏輯關係爲 "一對一" 的數據。使用棧結構存儲數據,講究先進後出,即最早進棧的數據,最後出棧;使用隊列存儲數據,講究先進先出,即最早進隊列的數據,也最早出隊列。編程

什麼是棧

棧是一種只能從表的一端存取數據且遵循 "先進後出" 原則的線性存儲結構,同順序表鏈表同樣,也是用來存儲邏輯關係爲 "一對一" 數據。棧的應用有不少,好比瀏覽器的跳轉和回退機制等。數組

image-20210711105041523

從上圖能夠看出:瀏覽器

  1. 棧只能從一端存取數據,而另一端是封閉的
  2. 不管是存仍是取,都須要遵循先進後出的原則。如須要取元素1,須要提早將元素4,3,2取出。

一般,棧的開口端被稱爲棧頂;封口端被稱爲棧底。緩存

image-20210711105820105

進棧和出棧

對於棧的操做通常是以下兩種:安全

  1. 進棧:將新元素放到棧頂元素的上面,成爲新的棧頂元素(入棧或壓棧);
  2. 出棧:將棧頂元素刪除掉,使得與其相鄰的元素成爲新的棧頂元素(退棧或彈棧);

image-20210711112016448

棧的存儲結構

  • 順序結構:使用數組實現
  • 鏈式結構:使用鏈表實現

順序棧

image-20210711215111636

一個簡單的隊列,包含如下方法:markdown

方法 說明
push(E e) 入棧
pop() 出棧
peek() 獲取棧頭部元素
isEmpty() 判斷棧是否爲空

代碼測試

public class Stack {

    /**
     * 棧的大小
     */
    private int maxSize;
    /**
     * 數組
     */
    private int[] stackArray;
    /**
     * 棧的top
     */
    private int top;

    /**
     * 初始化棧
     *
     * @param size
     */
    public Stack(int size) {
        this.maxSize = size;
        stackArray = new int[maxSize];
        top = -1;
    }

    /**
     * 入棧
     *
     * @param i
     */
    public void push(int i) {
        stackArray[++top] = i;
    }

    /**
     * 常量出棧
     *
     * @return
     */
    public int pop() {
        return stackArray[top--];
    }

    /**
     * 判空
     *
     * @return
     */
    public boolean isEmpty() {
        return (top == -1);
    }

    /**
     * 是否已滿
     *
     * @return
     */
    public boolean isFull() {
        return (top == maxSize);
    }


    @Override
    public String toString() {
        for (int i : stackArray) {
            System.out.println("top:"+i);
        }
        return super.toString();
    }

    //測試
    public static void main(String[] args) {
        Stack stack = new Stack(5);
        //入棧
        stack.push(1);
        stack.push(2);
        stack.push(3);
        stack.push(4);
        stack.push(5);
        while (!stack.isEmpty()){
            //出棧
            int pop = stack.pop();
            System.out.println("出棧元素:"+pop);
        }
    }
}
複製代碼

測試結果能夠發現顯示的數據和輸入的數據剛好相反。這也就符合**"先進後出"**的原則。

image-20210711224022979

實際運用:字符反轉

public class StackReverse {

    /**
     * 棧的大小
     */
    private int maxSize;
    /**
     * 數組
     */
    private char[] stackArray;
    /**
     * 棧的top
     */
    private int top;

    public StackReverse() {
    }

    /**
     * 初始化棧
     *
     * @param size
     */
    public StackReverse(int size) {
        this.maxSize = size;
        stackArray = new char[maxSize];
        top = -1;
    }

    /**
     * 入棧
     *
     * @param c
     */
    public void push(char c) {
        stackArray[++top] = c;
    }

    /**
     * 常量出棧
     *
     * @return
     */
    public char pop() {
        return stackArray[top--];
    }

    /**
     * 判空
     *
     * @return
     */
    public boolean isEmpty() {
        return (top == -1);
    }

    /**
     * 是否已滿
     *
     * @return
     */
    public boolean isFull() {
        return (top == maxSize);
    }


    public String doReverse(String str) {
        System.out.println("輸入字符爲:" + str);
        String rev = "";
        int stackSize = str.length();
        StackReverse stack = new StackReverse(stackSize);
        for (int i = 0; i < stackSize; i++) {
            //獲取字符
            char c = str.charAt(i);
            //入棧
            stack.push(c);
        }
        while (!stack.isEmpty()) {
            char pop = stack.pop();
            rev = rev + pop;
        }

        return rev;
    }

    public static void main(String[] args) {
        String rev = new StackReverse().doReverse("reverse");
        System.out.println("字符反轉後:" + rev);
    }
}
複製代碼

測試結果:

image-20210711230420120

以前我面試碰到過,我當時是這樣回答的:直接調用reverse方法就好了啊。如今想一想無地自容啊,由於面試官問的是實現方式,固然還有不少方法去實現,好比遞歸等等,這裏只是舉個例子。

鏈棧

image-20210711220838402

public class StackByLink<E> {

    private Node<E> top;

    /**
     * 返回棧頂元素
     *
     * @return
     */
    public E peek() {
        return top.getData();
    }

    /**
     * 出棧
     *
     * @return
     */
    public E pop() {
        E data = null;
        if (top != null) {
            data = top.getData();
            //把棧頂元素彈出,並把原棧頂的下一個元素設爲棧頂元素
            top = top.getNext();
        }
        return data;
    }

    /**
     * 入棧
     * 不太清楚的能夠看上面的圖
     * 把an設爲新棧頂元素node  an-1爲原來的棧頂元素 an-1的前一個元素就是an即node
     *
     * @param data
     */
    public void push(E data) {
        Node<E> node = new Node<E>();
        node.setData(data);
        if (top == null) {
            top = node;
        } else {
            //原來的元素設爲新棧頂的下一個元素
            node.setNext(top);
            //
            top.setPre(node);
            top = node;
        }
    }

    /**
     * 判空
     *
     * @return
     */
    public boolean isEmpty() {
        return (top == null);
    }


    class Node<E> {
        E data;
        //前驅節點
        Node pre;
        //後繼節點
        Node next;
		...省略get set
    }

	//測試
    public static void main(String[] args) {
        StackByLink<String> stack = new StackByLink<>();
        stack.push("我");
        stack.push("是");
        stack.push("鏈");
        stack.push("表");
        System.out.println("peek:" + stack.peek());
        stack.push("實");
        stack.push("現");
        stack.push("的");
        stack.push("棧");
        System.out.println("pop:" + stack.pop());
        System.out.println("peek:" + stack.peek());
    }
}
複製代碼

Java中的棧

java中的棧是經過繼承Vector來實現的,以下:

image-20210711231251027

Vector做爲List的另一個典型實現類,支持List的所有功能,Vector類也封裝了一個動態的,容許在分配的Object[]數組,Vector是一個比較古老的集合,JDK1.0就已經存在,建議儘可能不要使用這個集合,Vector與ArrayList的主要是區別是,Vector是線程安全的,可是性能比ArrayList要低,主要緣由是每一個方法都是經過synchronized來保證線程同步的。

因此問題就來了,既然只是爲了實現棧,爲何不用鏈表來單獨實現,只是爲了複用簡單的方法而迫使它繼承Vector(Stack和Vector原本是毫無關係的)。這使得Stack在基於數組實現上效率受影響,另外由於繼承Vector類,Stack能夠複用Vector大量方法,這使得Stack在設計上不嚴謹,當咱們看到Vector中:

public void add(int index, E element) {
    insertElementAt(element, index);
}
複製代碼

該方法能夠在指定位置添加元素,這與Stack的設計理念相沖突(棧只能在棧頂添加或刪除元素),我估計當時的開發者確定是偷懶了。

什麼是隊列

隊列是一種要求數據只能從一端進,從另外一端出且遵循 "先進先出" 原則的線性存儲結構

一般,稱進數據的一端爲 "隊尾",出數據的一端爲 "隊頭",數據元素進隊列的過程稱爲 "入隊",出隊列的過程稱爲 "出隊"。生活中常見的排隊買票和醫院掛號都是隊列。

image-20210712102144430

隊列的存儲結構

  • 順序結構:使用數組實現
  • 鏈式結構:使用鏈表實現

一個簡單的隊列,包含如下方法:

方法 說明
add(E e) 入隊
remove() 刪除此隊列的頭部
peek() 獲取隊列頭部元素
isEmpty() 判斷隊列是否爲空

順序隊列

public class Queue {

    private int maxSize;
    private int[] queueArray;
    private int front;
    private int rear;
    private int count;

    /**
     * 初始化
     * @param s
     */
    public Queue(int s) {
        this.maxSize = s;
        queueArray = new int[s];
        front = 0;
        rear = -1;
        count = 0;
    }

    /**
     * 入隊
     * @param i
     */
    public void add(int i) {
        if (rear == maxSize - 1) {
            rear = -1;
        }
        queueArray[++rear] = i;
        count++;
    }

    /**
     * 出隊
     * @return
     */
    public int remove() {
        int temp = queueArray[front++];
        if (front == maxSize) {
            front = 0;
        }
        count--;
        return temp;
    }

    /**
     * 獲取隊列頭部元素
     * @return
     */
    public int peek() {
        return queueArray[front];
    }

    public boolean isEmpty() {
        return (count == 0);
    }

    @Override
    public String toString() {
        return "入隊元素:" + Arrays.toString(queueArray);
    }

    public static void main(String[] args) {
        Queue queue = new Queue(5);
        queue.add(1);
        queue.add(2);
        queue.add(3);
        queue.add(4);
        queue.add(5);
        System.out.println(queue);
        while (!queue.isEmpty()){
            int remove = queue.remove();
            System.out.println("出隊元素:"+remove);
        }
    }
}
複製代碼

測試結果

image-20210712134637944

鏈式隊列

隊列的鏈式存儲結構,本質就是線性表的單鏈表,但它只能尾進頭出,可參考前面的單鏈表,這裏再也不復述。

image-20210712145144005

注: 如下隊列只作介紹,並附上詳細講解鏈接,感興趣的能夠自行查看

雙端隊列

image-20210712152355746

雙端隊列Deque是一種具備隊列和棧的性質的數據結構。雙端隊列中的元素能夠從兩端彈出,其限定插入和刪除操做在表的兩端進行。

ArrayDequeDeque接口的一個實現,使用了可變數組,因此沒有容量上的限制。 同時,ArrayDeque是線程不安全的,在沒有外部同步的狀況下,不能再多線程環境下使用。

ArrayDequeDeque的實現類,能夠做爲棧來使用,效率高於Stack

也能夠做爲隊列來使用,效率高於LinkedList

須要注意的是,ArrayDeque不支持null值。

可參考:ArrayDeque類的使用詳解

阻塞隊列

當隊列是空的時候,從隊列獲取元素的操做將會被阻塞,當隊列是滿的時候,從隊列插入元素的操做將會被阻塞。後面會在併發編程的JUC中講到。

Java中阻塞隊列BlockingQueue是一個接口,他的實現類主要包括如下幾種:

類名 說明
ArrayBlockingQueue 基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,以便緩存隊列中的數據對象,這是一個經常使用的阻塞隊列,除了一個定長數組外,ArrayBlockingQueue內部還保存着兩個整形變量,分別標識着隊列的頭部和尾部在數組中的位置。
LinkedBlockingQueue 基於鏈表的阻塞隊列,同ArrayListBlockingQueue相似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成),當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,並緩存在隊列內部,而生產者當即返回;只有當隊列緩衝區達到最大值緩存容量時(LinkedBlockingQueue能夠經過構造函數指定該值),纔會阻塞生產者隊列,直到消費者從隊列中消費掉一份數據,生產者線程會被喚醒,反之對於消費者這端的處理也基於一樣的原理。而LinkedBlockingQueue之因此可以高效的處理併發數據,還由於其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的狀況下生產者和消費者能夠並行地操做隊列中的數據,以此來提升整個隊列的併發性能。
DelayQueue DelayQueue中的元素只有當其指定的延遲時間到了,纔可以從隊列中獲取到該元素。DelayQueue是一個沒有大小限制的隊列,所以往隊列中插入數據的操做(生產者)永遠不會被阻塞,而只有獲取數據的操做(消費者)纔會被阻塞。
PriorityBlockingQueue 基於優先級的阻塞隊列(優先級的判斷經過構造函數傳入的Compator對象來決定),但須要注意的是PriorityBlockingQueue並不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。所以使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,不然時間一長,會最終耗盡全部的可用堆內存空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖。
SynchronousQueue 一種無緩衝的等待隊列,相似於無中介的直接交易,有點像原始社會中的生產者和消費者,生產者拿着產品去集市銷售給產品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產者,若是一方沒有找到合適的目標,那麼對不起,你們都在集市等待。和其餘隊列不一樣的還有,SynchronousQueue直接使用CAS實現線程的安全訪問。主要使用場景是在線程池中,後續會講到。

可參考:BlockingQueue(阻塞隊列)詳解

優先級隊列

優先級隊列和一般的棧和隊列同樣,只不過裏面的每個元素都有一個優先級,在處理的時候,首先處理優先級最高的。若是兩個元素具備相同的優先級,則按照他們插入到隊列中的前後順序處理。

可參考:Java Queue系列之PriorityQueue

總結

棧和隊列涉及的方面很廣,好比JVM的虛擬機棧,消息中間件MQ和隊列的關係,本文主要強調對棧和隊列概念的理解,後續會逐步分享。

參考

拓展延伸

  • Java中的Stack是經過Vector來實現的,這種設計被認爲是不良的設計,說說你的見解?
  • LIFO和FIFO各表明什麼含義?
  • 棧的入棧和出棧操做與隊列的插入和移除的時間複雜度是否相同?

都是本身人,點贊關注我就收下了🤞(白piao無罪🥺)

相關文章
相關標籤/搜索