Java,  算法和数据结构

如何用队列实现栈?LeetCode第225题

零 题目说明

要求使用2个队列来实现一个后进先出LIFO的栈,该栈支持的方法:入栈push()、出栈pop()、查看栈顶元素top()、栈是否为空empty()。

可以借助队列的方法:入队push()、查看队首元素pop()/peek、查看队列的大小size()、判断队列是否为空isEmpty()。也就是说只能使用单边队列,不能借助DoubleEndedQueue双端队列。

一代码实现思路

定义一个QueueImplementStack<E>类,包含一个成员变量Queue<E> queue,用JDK自带的Queue接口,用链表LinkedList来实例化这个queue;

在构造方法QueueImplementStack()中初始化这个成员变量queue;

入栈push()实现起来比较简单,直接调用LinkedList的add()即可。因为队列的入队操作和栈的入栈操作是一样的,都是添加到后端;

栈是否为空empty()实现也比较简单,直接调用LinkedList的isEmpty()即可。如果链表实现的队列是空的,那么栈也一定为空;

出栈pop()实现起来稍微复杂一点儿,因为栈是LIFO后进先出的,要拿到栈顶元素,其实就是要拿到队尾元素,说白了,就是想要实现从队尾出队就可以了。可是,队列又只能从队首出队,那怎么办呢?如果队列中有n个元素,我们只要想办法先将前面的n-1个元素出队,放另外一个地方暂存,然后队列中只剩下一个元素,就是第n个元素,将其出队,不就实现了出栈嘛。那么,问题又来了,此时队列是空队列了,如果需要继续入栈、出栈操作,岂不是出问题了吗?别急,我们在将队列的前n-1个元素暂存的时候,我们可以新开一个队列assistQueue中,将它们入队到assistQueue,出栈之后,再将原队列的引用指向这个assistQueue不就完美了嘛(#.#)

查看栈顶元素top(),显示一下栈顶元素,并不是真的删除。我们当然可以借助出栈pop()来实现,先调用pop()得到栈顶元素,暂存到临时变量中,然后调用入栈push(),将临时变量重新入栈,最后返回临时变量。这就有点儿像变戏法儿的小把戏一样。好玩儿又有趣。

二 代码实现

import java.util.LinkedList;
import java.util.Queue;
​
public class QueueImplementStack<E>{
    private Queue<E> queue;
    public QueueImplementStack(){
        queue = new LinkedList<>();
    }
​
    //栈是否为空
    public boolean empty(){
        return queue.isEmpty();
    }
​
    //入栈
    public void push(E e){
        queue.add(e);
    }
​
    //出栈
    public E pop(){
        Queue<E> assistQueue = new LinkedList<>();
        while(queue.size()>1){
            assistQueue.add(queue.remove());
        }
        E res = queue.remove();
        queue = assistQueue;
        return res;
    }
​
    //查看栈顶元素
    public E top(){
        E res = pop();
        push(res);
        return res;
    }
​
    //打印
    @Override
    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        Queue<E> assistQueue = new LinkedList<>();
        assistQueue.addAll(queue);
        stringBuilder.append(String.format("QueueImplementStack:size %d\n",assistQueue.size()));
        stringBuilder.append("bottom [");
        for (int i = assistQueue.size(); i >0; i--) {
            stringBuilder.append(assistQueue.remove());
            if (i != 1) {
                stringBuilder.append(" , ");
            }
        }
        stringBuilder.append("] top");
        return stringBuilder.toString();
    }
​
    public static void main(String[] args){
        QueueImplementStack<Integer> queueImplementStack = new QueueImplementStack();
        System.out.println("empty ?" + queueImplementStack.empty());
        for (int i = 0; i < 5; i++) {
            queueImplementStack.push(i);
        }
        System.out.println(queueImplementStack);
        System.out.println("top():"+queueImplementStack.top());
        System.out.println("pop():"+queueImplementStack.pop());
        System.out.println(queueImplementStack);
        System.out.println("empty ?"+queueImplementStack.empty());
    }
}
​
//结果
empty ?true
QueueImplementStack:size 5
bottom [0 , 1 , 2 , 3 , 4] top
top():4
pop():4
QueueImplementStack:size 4
bottom [0 , 1 , 2 , 3] top
empty ?false

三 陷阱小结

在编写toString()方法的过程中,写成了下列代码:

//打印
    @Override
    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        Queue<E> assistQueue = new LinkedList<>();
        assistQueue.addAll(queue);
        stringBuilder.append(String.format("QueueImplementStack:size %d\n",assistQueue.size()));
        stringBuilder.append("bottom[");
        for(int i=0;i<assistQueue.size();i++){
            stringBuilder.append(assistQueue.remove());
            if(i!=assistQueue.size()-1){
                stringBuilder.append(" , ");
            }
        }
        stringBuilder.append("]top");
        return stringBuilder.toString();
    }

打印输出的结果不对,后来经过debug才发现了问题所在。stringBuilder.append(assistQueue.remove());执行之后,队列的长度减小了,不能再次用它作为判断条件了。

的确,这是一个比较有意思的小题目,但是,必须得自己亲自动手敲一遍代码,才能发现更多的问题,才能提升编程能力和算法思维。

四复杂度分析

入栈push():O(1);

出栈pop():O(n);因为每次都要将队列中的n-1个元素搬移到另外一个队列存放,然后再出队第n个元素来实现出栈。

查看栈顶元素top():由于调用了pop(),所以,复杂度也为O(n);

栈判空empty():O(1)

五 改写查看栈顶元素top()为O(1)

如果我们给类添加1个成员变量,让其始终指向栈顶元素的话,那么,top()的复杂度就是O(1)。可是,具体该怎么实现呢?

public class QueueImplementStack<E>{
    private Queue<E> queue;
  //添加1个用于存放栈顶元素的成员变量
    private E topElement;
    public QueueImplementStack(){
        queue = new LinkedList<>();
    }
​
    //栈是否为空
    public boolean empty(){
        return queue.isEmpty();
    }
​
    //入栈
    public void push(E e){
        queue.add(e);
      //每次入栈时,都把栈顶元素赋值给topElement
      topElement = e;
    }
​
    //出栈
    public E pop(){
        Queue<E> assistQueue = new LinkedList<>();
        while(queue.size()>1){
          //每次出栈前,都要把栈顶元素给topElement。因为不这样操作的话,原栈顶元素出栈之后,topElement就不再指向栈顶元素了
          topElement = queue.remove();
          //然后把这个topElement放到assistQueue中
            assistQueue.add(topElement);
        }
      //最后,再拿到仅存的那个元素,使之出栈即可
        E res = queue.remove();
      //将queue重新指向辅助队列
        queue = assistQueue;
        return res;
    }
​
    //查看栈顶元素
    public E top(){
       return topElement;
    }

六 队首“充当”栈顶来实现

前面,我们通过队列来实现的栈,相当于队首是栈底,队尾是栈顶。如果我们可以换一个思路,把队列的队首当作栈顶的话,那么出栈和查看栈顶元素,就变得非常简单了:直接调用队列的出队、查看队首元素即可。因为,对本身就是支持队首出队、查看队首元素的。

那么,问题又来了,入栈操作就变得复杂了:队列是先进先出,栈先进后出,该怎么实现呢?于是,我们想到,针对队列queue而言,我们每次入队元素e时,都把队列queue中的已有的所有元素先逐个逐个出队,并入队到另外一个辅助队列assistQueue中去,然后将元素e入队到当前队列queue,最后再将assistQueue中的所有元素出队,并入队到当前队列queue中去,就解决了这个问题。

//队首充当栈顶的实现方式:出栈、查看栈顶元素变得非常简单
//比较不容易处理的是,入栈操作,
public class QueueImplementStack<E>{
    private Queue<E> queue;
    public QueueImplementStack(){
        queue = new LinkedList<>();
    }
​
    //栈是否为空
    public boolean empty(){
        return queue.isEmpty();
    }
​
    //入栈
    public void push(E e){
      //借助1个辅助队列,先将队列所有元素出队到辅助队列
        Queue<E> assistQueue = new LinkedList<>();
        while (queue.size() > 0) {
            assistQueue.add(queue.remove());
        }
      //元素e入队操作
        queue.add(e);
      //再将辅助队列中的元素全部出队到当前队列中去,
        while (assistQueue.size() > 0) {
            queue.add(assistQueue.remove());
        }
    }
​
    //出栈
    public E pop(){
        return queue.remove();
    }
​
    //查看栈顶元素
    public E top(){
        return queue.peek();
    }
}    

一句话总结:这种思路有点儿移花接木,偷梁换柱的感觉。倒是很有意思。

留言