【常用算法思路分析系列】栈和队列高频题集(修改版)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shakespeare001/article/details/51469268

本文是【常用算法思路分析系列】的第三篇,分析栈和队列相关的高频题目。本文分析: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)。

本系列下一篇将是与链表相关算法题。




猜你喜欢

转载自blog.csdn.net/shakespeare001/article/details/51469268