1. 程式人生 > >【常用演算法思路分析系列】棧和佇列高頻題集(修改版)

【常用演算法思路分析系列】棧和佇列高頻題集(修改版)

本文是【常用演算法思路分析系列】的第三篇,分析棧和佇列相關的高頻題目。本文分析:1、可查詢最值的棧;2、用兩個棧實現佇列的功能;3、反轉棧中元素;4、排序棧中元素;5、滑動視窗問題。
本系列前兩篇導航:
【常用演算法思路分析系列】排序高頻題集
【常用演算法思路分析系列】字串高頻題集

1、可查詢最值的棧

定義棧的資料結構,請在該型別中實現一個能夠得到棧最小元素的min函式。
思路: 定義兩個棧stackData和stackMin,其中stackData用來存放進棧的資料,stackMin用來存放進棧過程中的最小值。 方案一:噹噹前要進棧元素<=stackMin棧頂元素時,將當前要進棧元素同時加入到stackMin中;         噹噹前要進棧元素>stackMin棧頂元素時,stackMin棧不壓入資料; 方案二:噹噹前要進棧元素<=stackMin棧頂元素時,將當前要進棧元素同時加入到stackMin中;         噹噹前要進棧元素>stackMin棧頂元素時,stackMin棧把當前stackMin的棧頂元素再壓入一遍; 上述兩種方案都需要和stackData棧保持同步,只不過因為第一種方案stackMin棧中只儲存較小值,在pop時需要判斷;第二種方案是在pop時要完全同步。 程式碼如下:
public class GetMinStack {
    Stack<Integer> stackData = new Stack<Integer>();
    Stack<Integer> stackMin = new Stack<Integer>();

    public void push(int node) {
        if(stackMin.empty()){
            stackData.push(node);
            stackMin.push(node);
        }else{
            //第一種方案
            if(node > stackMin.peek()){
                stackData.push(node);
            }else{
                stackData.push(node);
                stackMin.push(node);
            }

            /* 第二種方案
            int minTop = stackMin.peek();
            if(node > minTop){
                stackData.push(node);
                stackMin.push(minTop);
            }else{
                stackData.push(node);
                stackMin.push(node);
            }
            */
        }
    }

    public void pop() {
        if(!stackData.empty() && ! stackMin.empty()){
            int dataTop = stackData.peek();
            int minTop = stackMin.peek();
            //第一種方案
            if(dataTop == minTop){//此時兩個棧都需要出棧操作
                stackData.pop();
                stackMin.pop();
            }else if(dataTop > minTop){
                stackData.pop();
            }
            /*
             * 第二種方案
            stackData.pop();
            stackMin.pop();
            */
        }

    }

    public int top() {
        return stackData.peek();
    }

    public int min() {
        return stackMin.peek();
    }
}

2、用兩個棧實現佇列的功能

用兩個棧來實現佇列的入隊、出對功能。定義兩個棧stackPush和stackPoll,stackPush棧用來放進佇列的元素,stackPoll棧用來出佇列。進隊時,元素壓入到stackPush中;出對時,將stackPush棧中的元素全部匯入(pop操作,因為要清空stackPush棧)到stackPoll棧中,stackPoll棧再彈出棧頂元素,然後將stackPoll棧中的元素再全部匯入(同樣是pop操作,因為要清空stackPoll棧)到stackPush棧中(上面的關鍵是,兩個棧互相匯入資料時都要全部匯入,全部匯入了,另一個棧也就清空了,否則不會符合佇列的性質
) 看一個題目:

編寫一個類,只能用兩個棧結構實現佇列,支援佇列的基本操作(push,pop)。

給定一個操作序列ope及它的長度n,其中元素為正數代表push操作,為0代表pop操作,保證操作序列合法且一定含pop操作,請返回pop的結果序列。

測試樣例:
[1,2,3,0,4,0],6
返回:[1,2]
程式碼如下:
public class TwoStack {
    public int[] twoStack(int[] ope, int n) {
        if(ope == null || n == 0)
            return null;
        Stack<Integer> stackPush = new Stack<Integer>();
        Stack<Integer> stackPoll = new Stack<Integer>();
        int popCount = 0;//出棧次數
        for(int i = 0; i < n; i++){
            if(ope[i] != 0){
                stackPush.push(ope[i]);
            }else{
                popCount++;
            }
        }
        int[] result = new int[popCount];
        //將stackPush棧中的所有資料匯入到stackPoll棧中
        while(!stackPush.empty()){
            stackPoll.push(stackPush.pop());
        }
        for(int i = 0; i < popCount; i++){
            result[i] = stackPoll.pop();
        }
        return result;
    }
}

3、反轉棧中元素

要反轉棧中的元素,我們首先要依次遞迴的拿到棧底元素,拿到棧底元素之後,再一步步把它新增進去。因此分為兩步:拿到棧底元素和反轉新增元素。 程式碼如下:
public class StackReverse {
    //反轉棧
    public static int[] reverseStack(int[] A, int n) {
        if(A == null || n == 0)
            return null;
        Stack<Integer> stack = new Stack<Integer>();
        for(int i = 0; i < n; i++){
            stack.push(A[i]);
        }
        reverse(stack);//開始反轉操作
        for(int i = n-1; i >= 0; i--){
            A[i] = stack.pop();
        }
        return A;
    }

    /**
     * 反轉棧中的元素
     * @param stack
     */
    public static void reverse(Stack<Integer> stack){
        if(stack.isEmpty()){
            return ;
        }
        //下面就是先遞迴拿到棧底元素,然後再把棧底元素入棧,此時棧中元素順序反轉
        int bottom = popBottom(stack);
        reverse(stack);
        stack.push(bottom);
    }

    /**
     * 移除棧底元素,並返回
     * @return
     */
    public static int popBottom(Stack<Integer> stack){
        int result = stack.pop();
        if(stack.isEmpty()){//彈出一個棧頂元素後,棧為空了,表示該元素就是棧底元素
            return result;
        }else{
            int last = popBottom(stack);
            stack.push(result);//注意!!!這裡是把前面拿到的元素壓入,這樣棧底元素才不會再次壓入到棧中
            return last;
        }
    }
   
    public static void main(String[] args) {
        int[] a = {9,8,7,6,5,4,3,2,1};
        reverseStack(a,a.length);
    }
}

4、排序棧中元素

請編寫一個程式,按升序對棧進行排序(即最大元素位於棧頂),要求最多隻能使用一個額外的棧存放臨時資料,但不得將元素複製到別的資料結構中。
思路: 假設棧stack是存放原來資料的,再定義一個輔助棧help,先從stack棧中取出棧頂元素pop,將pop和help中棧頂元素比較,如果pop <= help棧頂元素,將pop壓入到help棧中;如果pop > help棧頂元素,取出help棧頂元素,將其放入到stack棧中,直到help為空或者pop <= help棧頂元素。程式碼如下:
public static ArrayList<Integer> twoStacksSort(int[] numbers) {
        if(numbers == null)
            return null;
        Stack<Integer> stack = new Stack<Integer>();
        for(int i = numbers.length - 1; i >= 0; i--){
            stack.push(numbers[i]);
        }
        Stack<Integer> help = new Stack<Integer>();
        int pop,temp;
        while(!stack.isEmpty()){
            pop = stack.pop();
            if(help.isEmpty()){
                help.push(pop);
            }else{
                if(pop <= help.peek()){
                    help.push(pop);
                }else{
                    while(!help.isEmpty() && pop > help.peek()){//將help中元素放入到stack中
                        temp = help.pop();
                        stack.push(temp);
                    }
                    //help棧為空了或者找到了pop<=help棧頂的元素
                    help.push(pop);
                }
            }
        }
        while(!help.isEmpty()){
            stack.push(help.pop());
        }
        ArrayList<Integer> res = new ArrayList<Integer>();
        while(!stack.isEmpty()){
            res.add(stack.pop());
        }
        return res;
    }
當然,也可以使用陣列作為棧來使用,將陣列下標為0處作為棧頂,程式碼如下:
/**
     * 陣列作為棧,0的位置為棧頂
     * @param numbers
     * @return
     */
    public static ArrayList<Integer> twoStacksSort2(int[] numbers) {
        if(numbers == null)
            return null;
        int[] help = new int[numbers.length];
        int i = 0;//指向numbers棧頂元素
        int j = -1;//指向help棧頂元素
        int pop;
        while(i >= 0 && i != numbers.length){
            pop = numbers[i];
            if(j < 0){
                help[++j] = pop;
            }else{
                if(pop <= help[j]){
                    help[++j] = pop;
                }else{
                    while(j >= 0 && pop > help[j]){
                        numbers[i--] = help[j--];
                    }
                    help[++j] = pop;
                }
            }
            i++;
        }
        ArrayList<Integer> res = new ArrayList<Integer>();
        for (int k = 0; k < help.length; k++) {
            res.add(help[k]);
            System.out.println(help[k]);
        }
        return res;
    }

5、滑動視窗問題

有一個整型陣列 arr 和一個大小為 w 的視窗從陣列的最左邊滑到最右邊,視窗每次向右邊滑一個位置。 返回一個長度為n-w+1的陣列res,res[i]表示每一種視窗狀態下的最大值。 以陣列為[4,3,5,4,3,3,6,7],w=3為例。因為第一個視窗[4,3,5]的最大值為5,第二個視窗[3,5,4]的最大值為5,第三個視窗[5,4,3]的最大值為5。第四個視窗[4,3,3]的最大值為4。第五個視窗[3,3,6]的最大值為6。第六個視窗[3,6,7]的最大值為7。所以最終返回[5,5,5,4,6,7]。

給定整形陣列arr及它的大小n,同時給定w,請返回res陣列。保證w小於等於n,同時保證陣列大小小於等於500。

測試樣例:
[4,3,5,4,3,3,6,7],8,3
返回:[5,5,5,4,6,7]
思路: 核心是定義一個雙端佇列qmax,這個佇列維護一個w個數據的視窗,佇列中儲存的是陣列的下標,在隊頭和隊尾分別進行彈出和插入操作,使得以隊頭元素為下標所指的陣列元素,在這個視窗中值最大。 對於陣列arr,當遍歷到陣列中第i個元素時, 在隊尾執行插入規則:     佇列為空肯定直接插入;     佇列不空,如果隊尾元素為下標所指的陣列元素arr[qmax.peekLast] > 當前遍歷元素arr[i],直接將下標i插入到隊尾;(因為雖然當前元素arr[i]較小,但是當隊頭元素過期之後,它可能成為另一個視窗的最大值,因此需要加入);     如果隊尾元素為下標所指的陣列元素arr[qmax.peekLast] <= 當前遍歷元素arr[i],說明當前隊尾元素下標不可能成為後面視窗的最大值了,因此直接將隊尾元素彈出,再繼續比較新的隊尾元素所指陣列元素和當前元素arr[i],根據上面規則加入; 在隊頭執行彈出規則:     如果隊頭元素 == i- w,表示隊頭元素已過期,超出了w個視窗的範圍了,直接將隊頭元素彈出; 過程如下圖:
如果佇列中滿足維護w個元素(當然,不一定佇列中有w個元素值,因為佇列在隊頭維持了最大值),則可以直接拿到隊尾元素所指的陣列元素值,這個值就是當前視窗的最大值。 程式碼如下:
public static int[] slide(int[] arr, int n, int w) {
        if(arr == null || w < 1 || n < w){
            return null;
        }
        int[] res = new int[n - w + 1];
        //一個維護w個視窗的雙端佇列,保持下標為對頭元素的值最大
        LinkedList<Integer> qmax = new LinkedList<Integer>();
        int index = 0;
        for(int i = 0; i < n; i++){
            //執行隊尾進入規則
            while(!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[i]){
                qmax.pollLast();
            }
            qmax.addLast(i);//將下標加入到隊尾
            //執行對頭彈出規則
            if(qmax.peekFirst() == i - w){
                qmax.pollFirst();
            }
            if(i >= w - 1){//如果雙端佇列裡面至少維持了w個數據,則每次可以從對頭中拿到最大值
                res[index++] = arr[qmax.peekFirst()];
            }
        }

        return res;
    }
陣列下標值每次最多進qmax一次,出qmax一次,因此整個陣列元素進出佇列的時間複雜度為O(N),整個演算法複雜度也就為O(N)。

本系列下一篇將是與連結串列相關演算法題。