用有限个栈实现一个队列,保证每次出队和入队(在最坏的情况下)都只需要常数次的栈操作。
本文参考了下面两篇博客,解决这个问题的思路可以阅读这两篇博客,本文不在赘述。在核心思路不变的情况下,本文用Java进行了实现。
这篇思路更清晰 https://www.cnblogs.com/dacc123/p/10574939.html
特别感谢沈星繁的帮助 https://www.cnblogs.com/ikesnowy/p/7157813.html#4287263
前一篇文章用两个栈实现了队列,但是在某些情况下它的时间复杂度是O(N),如下面这种情况,当从st中出队完成后,需要将s中的元素逐一倒入st,这个过程的时间复杂度是O(N),如左图所示。将s中的元素倒入st的原因是需要对翻转该序列,如果队列中有N个元素,翻转序列前后至少需要N步,所以保证每次出队入队都只需要常数次操作的本质爱是将这N步分配到每次的出队和入队中,一理想状况如右图所示。
算法的目的为了让队列中的元素能在每次出队入队操作中有序地在若干个栈中流动起来。实际上六个栈就能完成元素的调度,其中一个为临时栈,用于栈与栈之间的交换(swap),它的作用类似一个指针。
若只考虑进队,则有图(左),元素有序地调度,最终的目的是让元素集中在栈H和栈T中。这种方式不能满足随时出栈的要求,队列需要做好随时出栈的准备。规定元素总是从H出队,则有图(右)。
读取H中的元素并保留的方法是peek,需要在栈Stack类中加入一个游标cursor来实现peek,只读取元素而不弹出,每次peek后游标移动至下一个元素。
Item item = cursor.item; cursor = cursor.next; return item;
各个栈的作用:①H:元素总是从H出队,栈顶为先入队的元素;②T,Tadd:入队的元素先被储存在这两个栈,在需要的时候Tadd中的元素会“复制”到T;③Hflip:栈H的翻转,H中的元素被逐一peek压入栈Hflip;④Hbuffer:T中的元素会逐一pop压入栈Hbuffer,Hflip中的元素也会被逐一pop压入栈Hbuffer,在需要的时候Hbuffer中的元素会“复制”到H。⑤sp:临时栈,可视为一个指针,用于两个栈的复制替换。
入队遵循先入队再整理的基本原则,细则如下:
1.如果Hbuffer非空则入队至栈T,否则入队至栈Tadd。
2.一次入队完成后紧接着对各个栈进行整理。
3.if T非空,则栈顶的元素pop后push入Hbuffer or H中仍有元素未被peek完全
do peek出元素push入Hflip(这种情况出现在出队后继续入队)
if T和Hflip都为空(此时H一定为空)
do 复制Hbuffer并替换H,复制Tadd并替换T
else if T为空
if Hflip不为空 do 将Hflip栈顶元素pop后push入Hbuffer
if T和Hflip都为空(此时H一定为空) do 复制Hbuffer并替换H,复制Tadd并替换T
else if T,Hflip,H都为空 do 复制Hbuffer并替换H,复制Tadd并替换T
4.复制Hbuffer并替换H,这个需要用到临时栈sp。
sp = H; H = Hbuffer; Hbuffer = sp;
每次复制替换后都需要统计H和T中元素的总个数popCnt,这个过程被整合到swap函数。当新的元素入队时进入T,popCnt++。popCnt与出队有关,下文将进行解释。
出队遵循先出队后整理的基本原则,细则如下:
1.总是从H出队,若H为空,则队列为空;出队后马上将T栈顶的元素pop后push入Hbuffer(如果T非空),出队后popCnt–。
2.出队后进行整理。
3.if H为空
if 队列不为空 do swap。
else
if T为空
if 下一个出队的元素就是Hbuffer栈顶的元素 do swap,清空Hflip
else do 将Hflip栈顶的元素pop后push入Hbuffer
4.由于Hflip中的元素是H被peek后push的,所以被从Hflip栈顶pop出的元素可能是已经出队的元素。为了避免这种情况的出现,引入了变量popCnt。swap时,Hbuffer的元素实际上都是来自H和T,是一个阶段内入队的元素。当下面的判断式为真时,说明下一个出队的元素就是Hbuffer栈顶的元素,此时需要swap并清空Hflip。
Hbuffer.size() + H.size() > popCnt && popCnt - Hbuffer.size() <=1
5.清空栈的方法如下,时间复杂度也为常数。
first = new Node(); cursor = first; N = 0;
主要部分代码如下:
public class StackQueue<Item> {
private Stack<Item> H = new Stack<Item>();
private Stack<Item> T = new Stack<Item>();
private Stack<Item> Hbuffer = new Stack<Item>();
private Stack<Item> Hflip = new Stack<Item>();
private Stack<Item> Tadd = new Stack<Item>();
private Stack<Item> sp;
private int popCnt;
private int N = 0;
private int sizeofH = 0; //被peek后H中剩余的元素个数
public boolean isEmpty() {
return H.isEmpty() && T.isEmpty() && Hbuffer.isEmpty()
&& Hflip.isEmpty() && Tadd.isEmpty();
}
public int size() {
return N;
}
private void popPush(Stack<Item> s1, Stack<Item> s2) { //s1弹出一个元素后压入s2
if(!s1.isEmpty())
s2.push(s1.pop());
}
private void peekPush(Stack<Item> s1, Stack<Item> s2) { //H peek一个元素后压入Hflip
if(!s1.isEmpty() && sizeofH > 0) {
s2.push(s1.peek());
sizeofH --;
}
}
private void swap() {
H.clear(); sp = H; H = Hbuffer; Hbuffer = sp;
T.clear(); sp = T; T = Tadd; Tadd = sp;
popCnt = H.size() + T.size();
sizeofH = H.size();
}
public void enQueue(Item item) {
//入队
if(Hbuffer.isEmpty()) {
T.push(item);
popCnt ++;
} else
Tadd.push(item);
//整理
//sizeofH < T.size() 说明H未peek完全,T中仍有元素,需要移动
//sizeofH >= H.size() && T.isEmpty() 说明H中还有元素在被peek前先被pop,需要移动
if(!T.isEmpty() || (sizeofH >= H.size() && T.isEmpty())) {
popPush(T, Hbuffer); peekPush(H, Hflip);
if(T.isEmpty() && Hflip.isEmpty())
swap();
} else if(T.isEmpty()) {
if(!Hflip.isEmpty()) {
popPush(Hflip, Hbuffer);
if(Hflip.isEmpty() && T.isEmpty())
swap();
} else if(Hflip.isEmpty() && T.isEmpty() && H.isEmpty())
swap();
}
N ++;
}
public Item deQueue() {
Item item;
//先出
if(!H.isEmpty()) {
item = H.pop();
popCnt --;
popPush(T, Hbuffer);
}
else
item = null;
//出完后整理
if(H.isEmpty()) { //H为空
if(N > 0 ) { //还有元素
swap(); Hflip.clear();
}
} else { //H不为空
if(T.isEmpty()) { //T为空
if(Hbuffer.size() + H.size() > popCnt && popCnt - Hbuffer.size() <=1) {
swap(); Hflip.clear();
} else if(popCnt < H.size() + Hbuffer.size())
popPush(Hflip, Hbuffer);
}
}
N --;
return item;
}
}