算法之美:棧和隊列

本文由玉剛說寫做平臺提供寫做贊助java

原做者:像一隻狗node

版權聲明:本文版權歸微信公衆號玉剛說全部,未經許可,不得以任何形式轉載程序員

算法,一門既不容易入門,也不容易精通的學問。面試

對於筆者來講算法算是我程序員生涯很不擅長的技能之一了,自從互聯網界招人進入平靜期後,愈來愈多的大廠在社招的時候不但會考驗面試者的工做所用到的技能,並且會用算法題來考驗面試者的邏輯思惟能力和基本數據結構的掌握能力。這也就讓想要社招進入大廠的部分同窗有了一些望而卻步的心理,畢竟工做中大部分時間在與UI層面的邏輯打交道,數據處理方面及時以前在學校中掌握的還不作,幾年的CV生活,估計也忘的差很少了。算法

可是做爲一條有夢想的鹹魚,咱們仍是要重拾這些知識的。今天筆者將會挑選幾道棧與隊列的題目來回顧下相關算法的基本知識。編程

棧與隊列分別是兩種數據結構,不一樣語言對於棧和隊列有着不一樣的聲明,在 java 中 Stack 類是繼承自 Vector 集合的子類,Queue 則是以接口形式存在,經常使用的其實現類是 LinkedList 這個雙向隊列。在C++的標準模版庫也是有這兩個數據結構定義的具體類的。數組

棧數據結構的特色是 FILO(first in last out) 即先進後出,隊列則是 FIFO(first in first out)即先進先出。相信棧與隊列的數據結構的基本特色你們也是熟記於胸了。下面就帶你們看一道面試題來帶你們看下這二者在面試題中的形式。bash

由兩個棧實現一個隊列 (✭✭✩✩✩)

題目難度兩顆星,主要考察了對於棧和隊列的數據結構特色。微信

前文介紹了,對於一個棧來講遵循 pop 操做時從棧的頂部取一個元素,對於隊列來講 poll 操做時從隊列隊首取一個元素。因此該題翻譯過來就是使用兩個棧定義一種先放入的元素,最早被取出的數據結構。數據結構

此題應考慮到兩種狀況,首先最簡單的一種狀況,假設有 1,2,3,4,5 個元素依次進入自定義的隊列,再依次取出。因爲是進棧操做都進行完了才進行出棧操做,因此咱們只需在元素出隊時,將進棧元素倒入另外一個空棧中便可。示意圖以下:

再一種狀況是,若是 add poll 操做是交替進行的,那麼如何保證數據結構先進先出的定義呢?好比先放入 1,2,3而後要進行一次取出操做取出 1,隨後在進行 add 操做放入4,5,這種狀況下如何操做兩個棧,才能保證以後再取出的時候元素爲 2,3,4,5 順序?實際上咱們只須要保證一下兩點就能夠:

  1. 不管若是 StackA(最開始add元素的那個棧) 要往 StackB 中壓入元素,那麼必須選擇一次性所有壓入。
  2. 不管何時從隊列中取元素,必須保證元素是從 StackB 中 pop 出的,也就是說,當 StackB 不爲空的時候毫不能再次向 StackB 中壓入元素。

爲了方便理解能夠看下邊這幅圖:

明白了須要注意的點後就是該寫代碼的時候了,須要注意的點在圖中已經用紅色字體標出了,也就是在存入元素一直往 StackA 中存,取元素是從 StackB 中取,但要要注意的是取的時候須要保證 StackB 爲空的時候要先將 StackA 中元素一次性壓如 StackB 中,在進行從 StackB 中取的操做。

public static class TwoStackQueue<E>{
        private Stack<E> stackA;
        private Stack<E> stackB;

        public TwoStackQueue() {
            stackA = new Stack<>();
            stackB = new Stack<>();
        }

        /** * 添加元素邏輯 * @param e 要添加的元素 * @return 這裏只是遵循 Queue 的習慣,這裏簡單處理返回 true 便可 */
        public boolean add(E e){
            stackA.push(e);
            return true;
        }

        /** * 去除元素的時候須要判斷兩個地方,StackA & StackB 是否都爲空 * StackB 爲空的時候講StackA中的元素所有依次壓入 StackB * @return 返回隊列中的元素 若是隊列爲空返回 null */
        public E poll(){
            //若是隊列中沒有元素則直接返回空,也能夠選擇拋出異常
            if (stackB.isEmpty() && stackA.isEmpty()){
                return null;
            }
            
            if (stackB.isEmpty()){
                while (!stackA.isEmpty()){
                    stackB.add(stackA.pop());
                }
            }
            
            return stackB.pop();
        }

        /** * peek 操做不取出元素,只返回隊列頭部的元素值 * @return 隊列頭部的元素值 */
        public E peek(){
            //若是隊列中沒有元素則直接返回空,也能夠選擇拋出異常
            if (stackB.isEmpty() && stackA.isEmpty()){
                return null;
            }

            if (stackB.isEmpty()){
                while (!stackA.isEmpty()){
                    stackB.add(stackA.pop());
                }
            }

            return stackB.peek();
        }
    }
複製代碼

對應的 C++ 解法:

#include <stdio.h>
#include <stack>
using namespace std;

template <typename T> class TStackQueue {
public:
    void add(T t);
    T poll();
    
private:
    stack<T> stackA;
    stack<T> stackB;
};

template <typename T> void TStackQueue<T>::add(T node) {
    stackA.push(node);
}
template<typename T> T TStackQueue<T>::poll(){
    if (stackB.empty() && stackA.empty()) {
        return NULL;
    }
    
    if (stackB.empty()) {
        while (!stackA.empty()) {
            stackB.push(stackA.top());
            stackA.pop();
        }
    }
    T node = stackB.top();
    stackB.pop();
    return node;
}
複製代碼

兩個隊列實現一個棧 (✭✭✩✩✩)

上道題咱們完成了兩個棧實現一個隊列的題目,那麼兩個隊列實現一個棧又該注意哪些呢?

首先隊列是先進先出,咱們能夠發現隊列不管怎麼倒,咱們不能逆序一個隊列。既然不能套用上題的解法,那麼就得另謀出路,可是能夠預知無非就是兩個隊列進行交替的入隊出隊操做,那麼惟一要作的就是判斷目前出隊的值是不是按照放入元素順序中最後放入的元素。 依舊畫圖舉例

這裏咱們只看首次取出操做,那麼須要注意一點, 如何判斷哪一次取出操做後 QueueA 爲空?

事實上做爲 Queue 做爲容器,咱們能夠經過事先定義好的方法 queue.size() 去判斷一個隊列中元素的個數,有人可能說這是犯規,其實不是的。題目中給出是讓你用隊列去實現,那麼隊列中公共 API 都是你能夠用的。因此能夠想象出下面的僞代碼:

//若是 queueA 的大小不爲 0 則循環取出元素
while(queueA.size() > 0){
    //被取出的元素
    int result = queueA.poll();
    // 這裏注意咱們取出元素後再去判斷一次,隊列是否爲空,若是爲空表明是最後一個元素
    if(queueA.size() != 0){
        queueB.add(result)
    }else{
        return result;
    }
}
複製代碼

上文咱們只是說了一次取出操做,那麼一次取出操做後,再次放入元素應該怎麼放,咱們彷佛又遇到了困難。

與上題不一樣的是,咱們應該先思考若是連續兩次取出應該怎麼操做,上面一次取出後 QueueA 空了,因此咱們若是按照相同的思路將 B 中的元素倒入 A 中,那麼將會獲得 3 ,這看起來沒什麼問題。那麼若是下一步進行的 push 操做,那麼應該放入 QueueA 仍是 QueueB 中才能保證元素先進後出的規則呢,很容易想到是放入 B 中。 那麼總結一下操做要點:

  1. 任什麼時候候兩個隊列總有一個是空的。
  2. 添加元素老是向非空隊列中 add 元素。
  3. 取出元素的時候老是將元素除隊尾最後一個元素外,導入另外一空隊列中,最後一個元素出隊。

接上圖咱們開看第一次取出操做後可能的兩種操做狀況:

思路縷清楚了,那麼時候寫代碼了:

public static class TwoQueueStack<E> {
   private Queue<E> queueA;
   private Queue<E> queueB;

   public TwoQueueStack() {
       queueA = new LinkedList<>();
       queueB = new LinkedList<>();
   }

   /** * 選一個非空的隊列入隊 * * @param e * @return */
   public E push(E e) {
       if (queueA.size() != 0) {
           System.out.println("從 queueA 入隊 " + e);
           queueA.add(e);
       } else if (queueB.size() != 0) {
           System.out.println("從 queueB 入隊 " + e);
           queueB.add(e);
       } else {
           System.out.println("從 queueA 入隊 " + e);
           queueA.add(e);
       }
       return e;
   }

   public E pop() {
       if (queueA.size() == 0 && queueB.size() == 0) {
           return null;
       }

       E result = null;
       if (queueA.size() != 0) {
           while (queueA.size() > 0) {
               result = queueA.poll();
               if (queueA.size() != 0) {
                   System.out.println("從 queueA 出隊 並 queueB 入隊 " + result);
                   queueB.add(result);
               }
           }
           System.out.println("從 queueA 出隊 " + result);

       } else {
           while (queueB.size() > 0) {
               result = queueB.poll();
               if (queueB.size() != 0) {
                   System.out.println("從 queueB 出隊 並 queueA 入隊 " + result);
                   queueA.add(result);
               }
           }
           System.out.println("從 queueB 出隊" + result);
       }
       return result;
   }
}
複製代碼

爲了方便你們理解我將文章進行下測試:

public static void main(String[] args) {
        TwoQueueStack<Integer> queueStack = new TwoQueueStack<>();
        queueStack.push(1);
        queueStack.push(2);
        queueStack.push(3);
        queueStack.push(4);
        queueStack.pop();
        queueStack.pop();
        queueStack.push(5);
        queueStack.pop();
    }
複製代碼

結果爲下面所示,看上去咱們的代碼是對的

從 queueA 入隊 1
從 queueA 入隊 2
從 queueA 入隊 3
從 queueA 入隊 4
從 queueA 出隊 並 queueB 入隊 1
從 queueA 出隊 並 queueB 入隊 2
從 queueA 出隊 並 queueB 入隊 3
從 queueA 出隊 4
從 queueB 出隊 並 queueA 入隊 1
從 queueB 出隊 並 queueA 入隊 2
從 queueB 出隊3
從 queueA 入隊 5
從 queueA 出隊 並 queueB 入隊 1
從 queueA 出隊 並 queueB 入隊 2
從 queueA 出隊 5
複製代碼

附C++ 代碼實現:

#include <stdio.h>
#include<queue>
#include<exception>

using namespace std;

template <typename T> class TQueueStack {
public:
    void push(const T& node);
    T pop();
    
private:
    queue<T> queueA;
    queue<T> queueB;
};

// 插入元素
template<typename T> void TQueueStack<T>::push(const T& node)
{
    
    //插入到非空隊列,若是均爲空則插入到queueB中
    if (queueA.size() == 0)
    {
        queueB.push(node);
    }
    else
    {
        queueA.push(node);
    }
}

template<typename T> T TQueueStack<T>::pop()
{
    if (queueA.size() == 0 && queueB.size() == 0)
    {
        return NULL;
    }
    T head;
    if (queueA.size() > 0)
    {
        while (queueA.size()>1)
        {
            //queueA中的元素依次刪除,並插入到queueB中,其中queueA刪除最後一個元素
            //至關於從棧中彈出隊尾元素
            T& data = queueA.front();
            queueA.pop();
            queueB.push(data);
        }
        head = queueA.front();
        queueA.pop();
    }
    else
    {
        while (queueB.size()>1)
        {
            //queueB 中的元素依次刪除,並插入到 queueA 中,其中 queueB 刪除最後一個元素
            //至關於從棧中彈出隊尾元素
            
            T& data = queueB.front();
            queueB.pop();
            queueA.push(data);
        }
        head = queueB.front();
        queueB.pop();
    }
    return head;
}
複製代碼

判斷出棧順序是否符合要求(✭✭✭✩✩)

經歷了上兩道題,你們是否是感受對棧和隊列更反感,哦不對是更瞭解了呢。(額~ 一不當心把實話說出來了)。下面咱們來看第二道題這是一個有關於出棧順序的判斷的題目:

題目: 輸入兩個整數數組,第一個表示一個棧的壓入序列,請寫一個函數,判斷第二個數組是否爲該棧的出棧序列,假設數組中的全部數字均不相等。例如序列 1,2,3,4,5 是某棧的壓入順序,序列 4,5,3,2,1 是該壓棧序列對應的一個彈出序列,但 4,3,5,1,2 就不多是該壓棧序列的彈出序列。

看到這道題咱們首先應該去理解題目中的怎麼去判斷是否符合出棧順序,其實題目想要表達的意思是若是以數組 A 的方式進棧但並非一次所有進棧,好比咱們先進棧1,2,3,4 而後出棧 4,而後進棧 5,而後在出棧 5,3,2,1。 那麼什麼狀況下是不可能知足的出棧順序呢?好比 1,確定是比 2 先進棧的,因此 2確定比 1先出棧。因此解題的關鍵就在於,如何判斷數組2 中的元素,是按數組1 中某種進棧順序操做的出棧序列。

思路是若是咱們在進棧的同時維護一個出棧角標,若是棧頂元素等於 popA[popIndex] 的時候,將角標加一,並出棧該元素,並繼續判斷下一個棧頂元素,若是棧頂元素不等於 popA[popIndex] 的時候繼續入棧元素,直到全部元素入棧完畢若是,棧不爲空則表示 popA 不是一個出棧序列。經過下圖能夠更好的理解題目要考察的內容:

因此在編程的只須要注意一下三點:

  1. 執行放入操做後,若是棧頂的元素等於對應角標在 popA 數組中的元素值,那麼就須要出棧該元素,同事角標加1
  2. 若是棧頂的元素不等於對應角標在 popA 數組中的元素值,那麼就執行放入操做
  3. 待全部的元素都被放入棧中,此時若是棧爲空,那麼 popA 就是一個出棧序列,反之則不是。

下面看代碼實現:

public static class Solution {

   public boolean IsPopOrder(int[] pushA, int[] popA) {
       int len = pushA.length;

       Stack<Integer> stack = new Stack<>();
       for (int pushIndex = 0, popIndex = 0; pushIndex < len; pushIndex++) {
           stack.push(pushA[pushIndex]);
           //若是棧頂元素等於 popA[popIndex] 則一直出棧且 popIndex++
           while (popIndex < popA.length && popA[popIndex] == stack.peek()) {
               stack.pop();
               popIndex++;
           }
       }
       return stack.isEmpty();
   }
}
複製代碼

C++實現以下

class Solution {
public:
    bool IsPopOrder(vector<int> pushA, vector<int> popA) {
        if(pushA() == 0) return false;
        vector<int> stack;
        for(int i = 0,j = 0 ;i < pushA.size();){
            stack.push_back(pushA[i++]);
            while(j < popA.size() && stack.back() == popA[j]){
                stack.pop_back();
                j++;
            }       
        }
        return stack.empty();
    }
};
複製代碼

測試結果以下:

public static void main(String[] args) {

   Solution solution = new Solution();
   int[] pushA = new int[]{1, 2, 3, 4, 5};
   int[] popA1 = new int[]{4, 3, 5, 1, 2};
   int[] popA2 = new int[]{4, 5, 3, 2, 1};

   System.out.println("popA1 是不是出棧隊列 " + solution.IsPopOrder(pushA, popA1));
   System.out.println("popA2 是不是出棧隊列 " + solution.IsPopOrder(pushA, popA2));
}
// 結果
//popA1 是不是出棧隊列 false
//popA2 是不是出棧隊列 true
複製代碼

總結

本文列舉了棧和隊列的一些面試題目,經過這些面試題目咱們能夠了解到一些面試中算法的考點,對於運算相關題目,咱們仍是須要多加練習,可是不要懼怕本身某些地方不會限制瞭解題思路,經過多加練習,記住見過的解題中的規律,相信通過一段時間練習後,也會感覺到自個人提升。

最後歡迎你們關注個人掘金專欄,不定時分享一些本身的學習工做總結。

像一隻狗的掘金專欄

參考: 《劍指 offer 第二版》 《程序員代碼面試指南 - 左程雲》

歡迎關注個人微信公衆號,接收第一手技術乾貨
相關文章
相關標籤/搜索