數(shù)據(jù)結(jié)構(gòu)算法(二) 之 棧和隊列

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

一猴伶、棧

1.棧的定義

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

  • 棧底:另一端

棧是一種后進先出(Last In First Out)的線性表膜蛔,簡稱 LIFO 結(jié)構(gòu)霉咨。既然棧是一種只允許在尾端進行刪除操作的線性表,那么線性表的特性它全部都有拇派。根據(jù)存儲結(jié)構(gòu)的不同荷辕,棧可以分成:順序棧和鏈棧件豌。

2.順序棧

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

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

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

時間復(fù)雜度均為 O(1)险污。

3.鏈棧

棧的鏈式存儲結(jié)構(gòu)痹愚,其實就是單鏈表,只不過它的頭指針不再指向頭結(jié)點蛔糯,而是指向最尾的節(jié)點(棧頂元素)拯腮。

  • 進棧:將新節(jié)點的 next 指針指向 top,然后將 top 指針指向當前的新節(jié)點蚁飒,鏈表數(shù)量加一动壤。

  • 出棧:保存尾節(jié)點,將 top 指針指向 top.next淮逻,釋放剛剛的尾節(jié)點琼懊,鏈表數(shù)量減一。

時間復(fù)雜度均為 O(1)爬早。

4.用途

Java 對棧(Stack)進行了封裝哼丈,可以直接使用,棧一般用作函數(shù)調(diào)用椛秆希或者 Activity 棧醉旦。

對于函數(shù)調(diào)用棧,在前行階段,對于每一層遞歸车胡,函數(shù)的局部變量檬输、參數(shù)值以及返回地址都被壓入棧中。在退回階段匈棘,位于棧頂?shù)木植孔兞可ゴ取?shù)值和返回地址被彈出,用于返回調(diào)用層次中執(zhí)行代碼的其余部分主卫,也就是恢復(fù)了調(diào)用的狀態(tài)逃默。

5.棧的面試題

  • 劍指 Offer 面試題 21:定義棧的數(shù)據(jù)結(jié)構(gòu),請在該類型中實現(xiàn)一個能夠得到棧的最小元素的 min() 函數(shù)队秩。在該棧中笑旺,調(diào)用min(),push() 及 pop() 的時間復(fù)雜度都是 O(1)馍资。

    思路:建立一個數(shù)據(jù)棧和輔助棧,輔助棧的棧頂每次壓入數(shù)據(jù)棧棧頂和輔助棧棧頂兩個元素中的最小值关噪,這樣當彈出一個數(shù)據(jù)棧的時候鸟蟹,對應(yīng)彈出一個輔助棧,因為兩者已經(jīng)關(guān)聯(lián)過大小了使兔,所以當下一次獲取最小值的時候必然跟上次已經(jīng)彈出的元素無關(guān)建钥。如果想要獲取最小的元素,直接彈出輔助棧的棧頂即可虐沥。

show my code

public class MinStack {
    
    //數(shù)據(jù)棧
    private Stack<Integer> data = new Stack<>();
    //輔助棧
    private Stack<Integer> min = new Stack<>();
    
    /**
     * 壓棧
     * @param i
     */
    public void add(Integer item) {
        //數(shù)據(jù)棧直接入棧
        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();
        
        //彈出數(shù)據(jù)棧棧頂
        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("*************************");
    }
    
}

測試過程及結(jié)果

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:題目:輸入兩個整數(shù)序列,第一個序列表示棧的壓入順序欲险,請判斷二個序列是否為該棧的彈出順序镐依。假設(shè)壓入棧的所有數(shù)字均不相等。

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

判斷一個序列是不是棧的彈出序列的規(guī)律:如果下一個彈出的數(shù)字剛好是棧頂數(shù)字喜每,那么直接彈出务唐。如果下一個彈出的數(shù)字不在棧頂,我們把壓棧序列中還沒有入棧的數(shù)字壓入輔助棧带兜,直到把下一個需要彈出的數(shù)字壓入棧頂為止枫笛。如果所有的數(shù)字都壓入棧了仍然沒有找到下一個彈出的數(shù)字,那么該序列不可能是一個彈出序列刚照。

show my code

/**
     * 檢測一個棧的出棧順序是否存在
     * @param push 數(shù)字的入棧順序
     * @param pop  數(shù)字的出棧順序
     * @return
     */
    public static boolean verifyStackPopOrder(int push[],int pop[]){
        
        //驗證輸入數(shù)據(jù)是否合法刑巧,壓棧和出棧的數(shù)組的長度必須一致
        if(push == null || pop == null || push.length == 0 || pop.length == 0
                || push.length != pop.length){
            System.out.println("輸入數(shù)據(jù)不合法");
            return false;
        }
        
        //構(gòu)造輔助棧,作為數(shù)據(jù)棧
        Stack<Integer> data = new Stack<>();
        //壓棧數(shù)組的壓入位置
        int pushIndex = 0;
        //出棧數(shù)組的出棧位置
        int popIndex = 0;
        
        //遍歷出棧的數(shù)組,假如發(fā)現(xiàn)出棧的數(shù)據(jù)和壓棧的棧頂元素相同海诲,就將壓棧數(shù)據(jù)的棧頂元素彈出繁莹,否則一直壓入,
        //直到壓棧元素和出棧的棧頂元素相等特幔,彈出壓棧的棧頂元素咨演,然后處理下一個出棧的棧頂元素
        
        //未處理完出棧的數(shù)組
        while(popIndex < pop.length){
            
            //根據(jù)一個出棧的棧頂元素,遍歷入棧的數(shù)組蚯斯,直到找到相等的元素 或者全部已經(jīng)入棧
            while(pushIndex < push.length && (data.isEmpty() || data.peek() != pop[popIndex])){
                //壓數(shù)據(jù)進去數(shù)據(jù)棧
                data.push(push[pushIndex]);
                pushIndex ++;
            }
            
            //出棧棧頂元素找到和入棧的棧頂元素相同的薄风,數(shù)據(jù)棧棧頂元素出棧,繼續(xù)處理下一個出棧元素
            if(data.peek() == pop[popIndex]){
                data.pop();
                popIndex ++;
                //如果出棧順序正確拍嵌,那么所有數(shù)據(jù)棧元素都會被出棧遭赂,數(shù)據(jù)棧最后會變?yōu)榭盏臈?            }else {
                //全部已經(jīng)壓入棧,但是找不到和出棧棧頂元素相等的
                return false;
            }
            
        }
        
        //假如能運行到這里說明横辆,已經(jīng)全部壓入棧撇他,而且出棧的元素也已經(jīng)全部彈出,說明順序是正確的,這個肯定是true
        return data.isEmpty();
    }

測試過程及結(jié)果

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.隊列的定義

  • 隊頭:允許刪除的一端

  • 隊尾:允許插入的一端

隊列是一種特殊的線性表,所以隊列也是具有順序存儲結(jié)構(gòu)和鏈式存儲結(jié)構(gòu)的幻捏。

2.隊列的順序存儲結(jié)構(gòu)(循環(huán)隊列)

為了解決用數(shù)組來實現(xiàn)隊列的“假溢出”問題盆犁,我們一般將順序存儲結(jié)構(gòu)的隊列頭尾相接,這種把隊列的頭尾相接的順序存儲結(jié)構(gòu)稱為循環(huán)隊列粘咖。

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

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

3.隊列的鏈式存儲結(jié)構(gòu)(鏈隊列)

隊列的鏈式結(jié)構(gòu)其實就是鏈表,只是有頭指針和尾指針瓮下。當隊列為空的時候翰铡,頭指針和尾指針都指向頭結(jié)點。

Node front;  // 頭指針
Node rear讽坏; // 尾指針
  • 入隊:在鏈表尾端插入一個節(jié)點

  • 出隊:刪除頭結(jié)點的后繼節(jié)點

4.隊列的面試題

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

思路:我們從一個具體的例子來分析隊列的插入和刪除元素過程织咧,操作兩三次,你就會發(fā)現(xiàn)刪除一個元素的步驟:當 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 為空棧,那么當前的隊列的頭結(jié)點肯定在 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;
    }
}
驗證出隊順序
  • 有個類似的題目:用兩個隊列實現(xiàn)棧爵赵。

思路:假設(shè)有兩個隊列Q1和Q2档插,當二者都為空時,入棧操作可以用入隊操作來模擬亚再,可以隨便選一個空隊列,假設(shè)選Q1進行入棧操作晨抡,現(xiàn)在假設(shè)a,b,c依次入棧了(即依次進入隊列Q1)氛悬, 這時如果想模擬出棧操作,則需要將c出棧耘柱,因為在棧頂如捅,這時候可以考慮用空隊列Q2,將a调煎,b依次從Q1中出隊镜遣, 而后進入隊列Q2,將Q1的最后一個元素c出隊即可士袄,此時Q1變?yōu)榱丝贞犃斜兀琎2中有兩個元素,隊頭元素為a娄柳,隊尾元素為b寓辱,接下來如果再執(zhí)行入棧操作,則需要將元素進入到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) {//如果兩個隊列都是空的話,則隨便選擇一個隊列執(zhí)行入棧操作
            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;
        //將非空的隊列的元素按順序出隊崔涂,轉(zhuǎn)移到空的隊列阳掐,直到只有一個元素在非空隊列
        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
    }

}
出棧順序

三、詩和遠方

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

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

人生,又仿佛是一天一天小小的棧重現(xiàn)帝雇。 童年父母每天抱你不斷地進出家門,壯年你每天奔波于家與事業(yè)之間,老年你每天獨自路跚于養(yǎng)老院的門里屋前涮俄。

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

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

人生,又是一個又一個小小的隊列重現(xiàn)。 春夏秋冬輪回年年,早中晚夜循環(huán)天天宦芦。變化的是時間宙址,不變的是你對未來執(zhí)著的信念。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末调卑,一起剝皮案震驚了整個濱河市抡砂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恬涧,老刑警劉巖注益,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溯捆,居然都是意外死亡丑搔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門提揍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來啤月,“玉大人,你說我怎么就攤上這事碳锈⊥缫保” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵售碳,是天一觀的道長强重。 經(jīng)常有香客問我绞呈,道長,這世上最難降的妖魔是什么间景? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任佃声,我火速辦了婚禮,結(jié)果婚禮上倘要,老公的妹妹穿的比我還像新娘圾亏。我一直安慰自己,他們只是感情好封拧,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布志鹃。 她就那樣靜靜地躺著,像睡著了一般泽西。 火紅的嫁衣襯著肌膚如雪曹铃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天捧杉,我揣著相機與錄音陕见,去河邊找鬼。 笑死味抖,一個胖子當著我的面吹牛评甜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仔涩,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼忍坷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了熔脂?” 一聲冷哼從身側(cè)響起承匣,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锤悄,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘉抒,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡零聚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年晚唇,在試婚紗的時候發(fā)現(xiàn)自己被綠了勋乾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡呐粘,死狀恐怖岗宣,靈堂內(nèi)的尸體忽然破棺而出蚂会,到底是詐尸還是另有隱情,我是刑警寧澤耗式,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布胁住,位于F島的核電站趁猴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏彪见。R本人自食惡果不足惜儡司,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望余指。 院中可真熱鬧捕犬,春花似錦、人聲如沸酵镜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淮韭。三九已至垢粮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缸濒,已是汗流浹背足丢。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留庇配,地道東北人斩跌。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像捞慌,于是被迫代替她去往敵國和親耀鸦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

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