數據結構算法(二) 之 棧和隊列

棧(stack)是限定僅在表尾進行插入和刪除操作的線性表。
隊列(queue)是一種先進先出(First In First Out)的線性表。

一、棧

1.棧的定義

  • 棧頂:允許插入和刪除的一端為棧頂

  • 棧底:另一端

棧是一種后進先出(Last In First Out)的線性表,簡稱 LIFO 結構。既然棧是一種只允許在尾端進行刪除操作的線性表,那么線性表的特性它全部都有。根據存儲結構的不同,棧可以分成:順序棧和鏈棧。

2.順序棧

順序棧其實就是一個數組,只不過需要在聲明的時候就確定長度。當頭指針 top 指向 -1 的時候,表示空棧。一般將頭指針 top 指向尾端的元素。

  • 進棧:需要注意棧是否已經滿了(top == MAX_SIZE - 1),如果不滿,則壓入棧,同時 top 指針加一。

  • 出棧:需要注意棧是否為空棧(top == - 1),如果不空,則出棧,同時 top 指針減一。

時間復雜度均為 O(1)。

3.鏈棧

棧的鏈式存儲結構,其實就是單鏈表,只不過它的頭指針不再指向頭結點,而是指向最尾的節點(棧頂元素)。

  • 進棧:將新節點的 next 指針指向 top,然后將 top 指針指向當前的新節點,鏈表數量加一。

  • 出棧:保存尾節點,將 top 指針指向 top.next,釋放剛剛的尾節點,鏈表數量減一。

時間復雜度均為 O(1)。

4.用途

Java 對棧(Stack)進行了封裝,可以直接使用,棧一般用作函數調用棧或者 Activity 棧。

對于函數調用棧,在前行階段,對于每一層遞歸,函數的局部變量、參數值以及返回地址都被壓入棧中。在退回階段,位于棧頂的局部變量、參數值和返回地址被彈出,用于返回調用層次中執行代碼的其余部分,也就是恢復了調用的狀態。

5.棧的面試題

  • 劍指 Offer 面試題 21:定義棧的數據結構,請在該類型中實現一個能夠得到棧的最小元素的 min() 函數。在該棧中,調用min(),push() 及 pop() 的時間復雜度都是 O(1)。

    思路:建立一個數據棧和輔助棧,輔助棧的棧頂每次壓入數據棧棧頂和輔助棧棧頂兩個元素中的最小值,這樣當彈出一個數據棧的時候,對應彈出一個輔助棧,因為兩者已經關聯過大小了,所以當下一次獲取最小值的時候必然跟上次已經彈出的元素無關。如果想要獲取最小的元素,直接彈出輔助棧的棧頂即可。

show my code

public class MinStack {
    
    //數據棧
    private Stack<Integer> data = new Stack<>();
    //輔助棧
    private Stack<Integer> min = new Stack<>();
    
    /**
     * 壓棧
     * @param i
     */
    public void add(Integer item) {
        //數據棧直接入棧
        data.push(item);
        
        //輔助棧需要判斷,確保入棧的是最小的元素
        if(min.isEmpty() || item <= min.peek()) {
            min.push(item);
        }else {
            min.push(min.peek());
        }
    }
    
    /**
     * 出棧
     * @return
     */
    public Integer pop() {
        if(data.isEmpty() || min.isEmpty()) {
            return -1;
        }
        
        //彈出輔助棧的棧頂
        min.pop();
        
        //彈出數據棧棧頂
        return data.pop();
    }
    
    /**
     * 獲取棧最小的元素
     * @return
     */
    public Integer min() {
        if(data.isEmpty() || min.isEmpty()) {
            return 0;
        }
        
        //直接彈出輔助棧的棧頂
        return min.peek();
        
    }
    
    /**
     * 打印棧
     */
    public void printStack() {
        System.out.print("棧元素: ");
        for(Integer i:data) {
            System.out.print(i+" ");
        }
        System.out.println("");
        System.out.println("*************************");
    }
    
}

測試過程及結果

public static void main(String[] args) {  
        MinStack stack = new MinStack();
        stack.add(10);
        stack.add(999);
        stack.add(23);
        stack.add(654);
       
        stack.printStack();
        
        System.out.println("出棧元素:"+stack.pop());
        stack.printStack();
        
        
        System.out.println("Min 元素:"+stack.min());
        stack.printStack();
    }
MinStack
  • 劍指 Offer 面試題 22:題目:輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。

思路:解決這個問題很直觀的想法就是建立一個輔助棧,把輸入的第一個序列中的數字依次壓入該輔助棧,并按照第二個序列的順序依次從該棧中彈出數字。

判斷一個序列是不是棧的彈出序列的規律:如果下一個彈出的數字剛好是棧頂數字,那么直接彈出。如果下一個彈出的數字不在棧頂,我們把壓棧序列中還沒有入棧的數字壓入輔助棧,直到把下一個需要彈出的數字壓入棧頂為止。如果所有的數字都壓入棧了仍然沒有找到下一個彈出的數字,那么該序列不可能是一個彈出序列。

show my code

/**
     * 檢測一個棧的出棧順序是否存在
     * @param push 數字的入棧順序
     * @param pop  數字的出棧順序
     * @return
     */
    public static boolean verifyStackPopOrder(int push[],int pop[]){
        
        //驗證輸入數據是否合法,壓棧和出棧的數組的長度必須一致
        if(push == null || pop == null || push.length == 0 || pop.length == 0
                || push.length != pop.length){
            System.out.println("輸入數據不合法");
            return false;
        }
        
        //構造輔助棧,作為數據棧
        Stack<Integer> data = new Stack<>();
        //壓棧數組的壓入位置
        int pushIndex = 0;
        //出棧數組的出棧位置
        int popIndex = 0;
        
        //遍歷出棧的數組,假如發現出棧的數據和壓棧的棧頂元素相同,就將壓棧數據的棧頂元素彈出,否則一直壓入,
        //直到壓棧元素和出棧的棧頂元素相等,彈出壓棧的棧頂元素,然后處理下一個出棧的棧頂元素
        
        //未處理完出棧的數組
        while(popIndex < pop.length){
            
            //根據一個出棧的棧頂元素,遍歷入棧的數組,直到找到相等的元素 或者全部已經入棧
            while(pushIndex < push.length && (data.isEmpty() || data.peek() != pop[popIndex])){
                //壓數據進去數據棧
                data.push(push[pushIndex]);
                pushIndex ++;
            }
            
            //出棧棧頂元素找到和入棧的棧頂元素相同的,數據棧棧頂元素出棧,繼續處理下一個出棧元素
            if(data.peek() == pop[popIndex]){
                data.pop();
                popIndex ++;
                //如果出棧順序正確,那么所有數據棧元素都會被出棧,數據棧最后會變為空的棧
            }else {
                //全部已經壓入棧,但是找不到和出棧棧頂元素相等的
                return false;
            }
            
        }
        
        //假如能運行到這里說明,已經全部壓入棧,而且出棧的元素也已經全部彈出,說明順序是正確的,這個肯定是true
        return data.isEmpty();
    }

測試過程及結果

public static void main(String[] args){  
        int[] push = {1, 2, 3, 4, 5};
        int[] pop1 = {4, 5, 3, 2, 1};
        int[] pop2 = {3, 5, 4, 2, 1};
        int[] pop3 = {4, 3, 5, 1, 2};
        int[] pop4 = {3, 5, 4, 1, 2};

        System.out.println("人工計算為true,程序得出的出棧順序為: " + verifyStackPopOrder(push, pop1));
        System.out.println("人工計算為true,程序得出的出棧順序為: " + verifyStackPopOrder(push, pop2));
        System.out.println("人工計算為false,程序得出的出棧順序為: " + verifyStackPopOrder(push, pop3));
        System.out.println("人工計算為false,程序得出的出棧順序為: " + verifyStackPopOrder(push, pop4));
        
        int[] push5 = {1};
        int[] pop5 = {2};
        System.out.println("人工計算為false,程序得出的出棧順序為: " + verifyStackPopOrder(push5, pop5));

        int[] push6 = {1};
        int[] pop6 = {1};
        System.out.println("人工計算為true,程序得出的出棧順序為: " + verifyStackPopOrder(push6, pop6));

        System.out.println("人工計算為false,程序得出的出棧順序為: " + verifyStackPopOrder(null, null));
    }
驗證出棧順序

二、隊列

1.隊列的定義

  • 隊頭:允許刪除的一端

  • 隊尾:允許插入的一端

隊列是一種特殊的線性表,所以隊列也是具有順序存儲結構和鏈式存儲結構的。

2.隊列的順序存儲結構(循環隊列)

為了解決用數組來實現隊列的“假溢出”問題,我們一般將順序存儲結構的隊列頭尾相接,這種把隊列的頭尾相接的順序存儲結構稱為循環隊列

int front;  // 頭指針
int rear; // 尾指針
  • 隊列滿的條件:(rear+1) % QueueSize == front

  • 計算隊列長度公式:length = (rear - front + QueueSize)% QueueSize

3.隊列的鏈式存儲結構(鏈隊列)

隊列的鏈式結構其實就是鏈表,只是有頭指針和尾指針。當隊列為空的時候,頭指針和尾指針都指向頭結點。

Node front;  // 頭指針
Node rear; // 尾指針
  • 入隊:在鏈表尾端插入一個節點

  • 出隊:刪除頭結點的后繼節點

4.隊列的面試題

  • 劍指 Offer 面試題 7:用兩個棧實現一個隊列。隊列的聲明如下,請實現它的兩個函數 appendTail()deleteHead(),分別完成在隊列尾部插入結點和在隊列頭部刪除結點的功能。
private Stack<T> stack1;
private Stack<T> stack2; 

思路:我們從一個具體的例子來分析隊列的插入和刪除元素過程,操作兩三次,你就會發現刪除一個元素的步驟:當 stack2 中不為空的時候,在
stack2 中的棧頂元素就是最先進入隊列的元素,可以彈出。如果 stack2 為空時,我們把 stack1 的元素逐個彈出然后壓入 stack2 ,這樣就能保證
stack2 的棧元素順序就是進入隊列的順序。如果要使 stack1 的元素出棧,必須要彈完 stack2 的元素,然后將 stack1 的元素彈出壓入 stack2 ,再彈出 stack2 的元素。入隊的步驟就是將其壓入 stack1 中。

show my code

public class CQueue {

    public static void main(String[] args){  
        
        CQueue cQueue = new CQueue();

        cQueue.appendTail(111);
        cQueue.appendTail(222);
        cQueue.appendTail(333);
        System.out.println("出隊: " + cQueue.deleteHead());
        System.out.println("出隊: " + cQueue.deleteHead());
        
        cQueue.appendTail(444);
        cQueue.appendTail(555);
        System.out.println("出隊: " + cQueue.deleteHead());
        System.out.println("出隊: " + cQueue.deleteHead());
        System.out.println("出隊: " + cQueue.deleteHead());
        
        
    }
    
    //入隊的棧
    private Stack<Integer> stack1 = new Stack<>();
    // 出隊的棧
    private Stack<Integer> stack2 = new Stack<>();
    
    /**
     * 入隊
     * @param item
     */
    public void appendTail(Integer item){
        stack1.push(item);
    }
    
    /**
     * 出隊
     * @return
     */
    public Integer deleteHead(){
        
        //假如 stack2 為空棧,那么當前的隊列的頭結點肯定在 stack1 中,將 stack1 的元素全部彈出,然后入棧 stack2
        if(stack2.isEmpty()){
            while(stack1.size() > 0){
                Integer i = stack1.peek();
                stack1.pop();
                stack2.push(i);
            }
        }
        
        if(stack2.isEmpty()){
            System.out.println("隊列為空,刪除失敗");
            return -1;
        }
        
        Integer head = stack2.peek();
        stack2.pop();
        return head;
    }
}
驗證出隊順序
  • 有個類似的題目:用兩個隊列實現棧。

思路:假設有兩個隊列Q1和Q2,當二者都為空時,入棧操作可以用入隊操作來模擬,可以隨便選一個空隊列,假設選Q1進行入棧操作,現在假設a,b,c依次入棧了(即依次進入隊列Q1), 這時如果想模擬出棧操作,則需要將c出棧,因為在棧頂,這時候可以考慮用空隊列Q2,將a,b依次從Q1中出隊, 而后進入隊列Q2,將Q1的最后一個元素c出隊即可,此時Q1變為了空隊列,Q2中有兩個元素,隊頭元素為a,隊尾元素為b,接下來如果再執行入棧操作,則需要將元素進入到Q1和Q2中的非空隊列,即進入Q2隊列,出棧的話,就跟前面的一樣,將Q2除最后一個元素外全部出隊,并依次進入隊列Q1,再將Q2的最后一個元素出隊即可。

show my code

public class QueueToStack<T> {
     
    //鏈隊
    Queue<T> queueA = new LinkedList<>();
    //鏈隊
    Queue<T> queueB = new LinkedList<>();
     
    /**
     * 入棧
     * @param value
     */
    public void push(T value) {
        if (queueA.size() == 0 && queueB.size() == 0) {//如果兩個隊列都是空的話,則隨便選擇一個隊列執行入棧操作
            queueA.add(value);
        }else if (queueA.size() == 0 && queueB.size() != 0){///如果不是兩個隊列都是為空的話,則選擇非空的隊列入棧
            queueB.add(value);
        }else if (queueA.size() != 0 && queueB.size() == 0){
            queueA.add(value);
        }
    }
     
    /**
     * 出棧
     * @return
     */
    public T pop() {
        if (queueA.size() == 0 && queueB.size() == 0){
            return null;
        }
        
        T result = null;
        //將非空的隊列的元素按順序出隊,轉移到空的隊列,直到只有一個元素在非空隊列
        if (queueA.size() == 0 && queueB.size() != 0){
             while (queueB.size() > 0){
                 result = queueB.poll();
                 if (queueB.size()!=0){
                     queueA.add(result);
                 }
             }
         }else if (queueA.size() != 0 && queueB.size() == 0){
             while (queueA.size() > 0){
                 result = queueA.poll();
                 if (queueA.size()!=0){
                      queueB.add(result);
                 }
             }
         }
         return result;
    }
     
    public static void main(String[] args) {
        QueueToStack<Integer> stack=new QueueToStack<>();
        int tmp=0;
        stack.push(1);
        stack.push(2);
        stack.push(3);
        tmp=stack.pop();
        System.out.println(tmp);//3
        stack.push(4);
        tmp=stack.pop();
        System.out.println(tmp);//4
        tmp=stack.pop();
        System.out.println(tmp);//2
        stack.push(5);
        stack.push(6);
        tmp=stack.pop();
        System.out.println(tmp);//6
        tmp=stack.pop();
        System.out.println(tmp);//5
        tmp=stack.pop();
        System.out.println(tmp);//1
    }

}
出棧順序

三、詩和遠方

好了,最后兩分鐘,念幾句我在初學棧和隊列時寫的人生感悟的小詩,希望也能引起你們的共鳴。

人生,就像是一個很大的棧演變。 出生時你赤條條地來到人世,慢慢地長大,漸漸地變老,最終還得赤條條地離開世間。

人生,又仿佛是一天一天小小的棧重現。 童年父母每天抱你不斷地進出家門,壯年你每天奔波于家與事業之間,老年你每天獨自路跚于養老院的門里屋前。

人生,更需要有進棧出棧精神的體現。在哪里跌倒,就應該在哪里爬起來。無論陷入何等困境,只要抬頭就能仰望藍天,就有希望,不斷進取,你就可以讓出頭之日重現。困難不會永遠存在,強者才能勇往直前。

人生,其實就是一個大大的隊列演變。無知童年,快樂少年,稚傲青年,成熟中年,安逸晚年。

人生,又是一個又一個小小的隊列重現。 春夏秋冬輪回年年,早中晚夜循環天天。變化的是時間,不變的是你對未來執著的信念。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,702評論 2 380

推薦閱讀更多精彩內容