如何用队列实现栈?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(); } }
一句话总结:这种思路有点儿移花接木,偷梁换柱的感觉。倒是很有意思。