我在算法手册(Robert Algorithms, 4th Edition中由Robert Sedgewick和Kevin Wayne)中提到了这个问题。
有三个堆栈的队列。实现具有三个堆栈的队列,以便每个队列操作采用恒定(最坏情况)的堆栈操作数。警告:难度很高。
我知道如何使用2个堆栈建立队列,但我找不到3个堆栈的解决方案。任何的想法 ?
(哦,这不是作业:))
答案 0 :(得分:41)
概要
详情
此链接背后有两个实现:http://www.eecs.usma.edu/webs/people/okasaki/jfp95/index.html
其中一个是带有三个堆栈的O(1)但它使用延迟执行,这实际上会创建额外的中间数据结构(闭包)。
其中一个是O(1)但使用SIX堆栈。但是,它的工作没有延迟执行。
更新:Okasaki的论文在这里:http://www.eecs.usma.edu/webs/people/okasaki/jfp95.ps并且看起来他实际上只使用了2个堆栈用于具有延迟评估的O(1)版本。问题是它真的基于懒惰的评估。问题是它是否可以在没有延迟评估的情况下转换为3堆栈算法。
更新:Holger Petersen在第7届计算与组合学年会上发表的论文“Stacks vs. Deques”中描述了另一种相关算法。您可以在Google图书中找到该文章。检查第225-226页。但该算法实际上并不是实时仿真,它是三个堆栈上双端队列的线性时间模拟。
gusbro:“正如@Leonel前几天说的那样,我认为与Sedgewick教授核实他是否知道解决方案或有一些错误是公平的。所以我写信给他。我刚收到回复(虽然不是来自他自己,而是来自普林斯顿大学的同事)所以我想与大家分享。他基本上说他知道没有算法使用三个堆栈和强加的其他约束(比如不使用懒惰的评估)。他确实知道一个使用6个堆栈的算法,因为我们已经知道在这里查看答案。所以我想问题仍然是找到一个算法(或证明找不到一个)。“答案 1 :(得分:12)
好的,这真的很难,而且我能想到的唯一解决方案,让我记得Kirks解决了Kobayashi Maru测试(不知何故被骗): 我们的想法是,我们使用堆栈堆栈(并使用它来建模列表)。 我将操作调用en / dequeue并按下并弹出,然后我们得到:
queue.new() : Stack1 = Stack.new(<Stack>);
Stack2 = Stack1;
enqueue(element): Stack3 = Stack.new(<TypeOf(element)>);
Stack3.push(element);
Stack2.push(Stack3);
Stack3 = Stack.new(<Stack>);
Stack2.push(Stack3);
Stack2 = Stack3;
dequeue(): Stack3 = Stack1.pop();
Stack1 = Stack1.pop();
dequeue() = Stack1.pop()
Stack1 = Stack3;
isEmtpy(): Stack1.isEmpty();
(StackX = StackY不是内容的复制,只是一个引用的副本。它只是为了描述它很简单。你也可以使用3个堆栈的数组并通过索引访问它们,你只需改变它的值索引变量)。堆栈操作中的所有内容都在O(1)中。
是的,我知道它是有争议的,因为我们隐含了超过3个堆栈,但也许它给了你们其他好主意。
编辑:解释示例:
| | | |3| | | |
| | | |_| | | |
| | |_____| | |
| | | |
| | |2| | |
| | |_| | |
| |_________| |
| |
| |1| |
| |_| |
|_____________|
我在这里尝试使用一点ASCII艺术来显示Stack1。
每个元素都包含在一个元素堆栈中(因此我们只有堆栈的类型安全堆栈)。
你看到删除我们首先弹出第一个元素(包含元素1和2的堆栈)。然后弹出下一个元素然后打开1.然后我们说第一个poped堆栈现在是我们的新Stack1。更功能一点 - 这些列表是由2个元素的堆栈实现的,其中顶部元素是 cdr ,第一个/下面的顶部元素是 car 。另外两个正在帮助筹码。
Esp棘手的是插入,因为你不得不深入挖掘嵌套堆栈以添加另一个元素。这就是为什么Stack2在那里。 Stack2始终是最里面的堆栈。然后添加只是推入一个元素,然后按下一个新的Stack2(这就是为什么我们不允许在我们的出列操作中触摸Stack2)。
答案 2 :(得分:4)
我打算尝试证明它无法完成。
假设有一个队列Q由3个堆栈A,B和C模拟。
ASRT0:=此外,假设Q可以模拟O(1)中的操作{queue,dequeue}。这意味着每个要模拟的队列/出队操作都存在特定的堆栈推送/弹出序列。
不失一般性,假设队列操作是确定性的。
让排队到Q的元素根据它们的队列顺序编号为1,2,...,排队到Q的第一个元素定义为1,第二个元素定义为2,依此类推。
定义
Q(0) :=
Q中有0个元素时的Q状态(因此A,B和C中有0个元素)Q(1) :=
Q(0)
Q(n) :=
Q(0)
上的n个队列操作后的Q(以及A,B和C)状态定义
|Q(n)| :=
Q(n)
中的元素数量(因此|Q(n)| = n
)A(n) :=
当Q的状态为Q(n)
|A(n)| :=
A(n)
堆栈B和C的类似定义。
中平凡,
|Q(n)| = |A(n)| + |B(n)| + |C(n)|
---
|Q(n)|
显然是无限制的。
因此,|A(n)|
,|B(n)|
或|C(n)|
中至少有一个在n上无界限。
WLOG1
,假设堆栈A是无界的,堆栈B和C是有界的。
定义
* B_u :=
B的上限
* C_u :=
C的上限
* K := B_u + C_u + 1
WLOG2
,对于|A(n)| > K
的n,从Q(n)
中选择K个元素。假设其中1个元素在A(n + x)
中,对于所有x >= 0
,即无论进行了多少队列操作,元素总是在堆栈A中。
X :=
该元素然后我们可以定义
Abv(n) :=
堆栈A(n)
中高于X Blo(n) :=
堆栈A(n)
中低于X
| A(N)| = Abv(n)+ Blo(n)
ASRT1 :=
从Q(n)
出发X所需的流行音乐数量至少为Abv(n)
从(ASRT0
)和(ASRT1
),ASRT2 := Abv(n)
必须有界限。
如果Abv(n)
无限制,那么如果需要20个队列才能使Q(n)
的队列号码出列,则至少需要Abv(n)/20
次点击。这是无限的。 20可以是任何常数。
因此,
ASRT3 := Blo(n) = |A(n)| - Abv(n)
必须是无限的。
WLOG3
,我们可以从A(n)
底部选择K个元素,其中一个元素位于A(n + x)
所有x >= 0
X(n) :=
该元素,对于任何给定的n
ASRT4 := Abv(n) >= |A(n)| - K
每当元素排队到Q(n)
...
WLOG4
,假设B和C已经填充到它们的上限。假设已达到X(n)
以上元素的上限。然后,一个新元素进入A.
WLOG5
,假设结果是新元素必须输入X(n)
以下。
ASRT5 :=
将元素置于X(n) >= Abv(X(n))
从(ASRT4)
起,Abv(n)
在n上无界限。
因此,将元素置于X(n)
以下所需的弹出数量是无限的。
这与ASRT1
相矛盾,因此,无法模拟具有3个堆栈的O(1)
队列。
即
至少有一个堆栈必须无限制。
对于保留在该堆栈中的元素,其上方元素的数量必须有界,或者删除该元素的出列操作将是无限制的。
但是,如果它上面的元素数量有限,那么它将达到极限。在某些时候,必须在其下方输入一个新元素。
因为我们总是可以从该堆栈中最低的几个元素之一中选择旧元素,所以它上面可以有无限数量的元素(基于无界堆栈的无界大小)。
要在它下面输入一个新元素,因为它上面有无限数量的元素,需要一个无限数量的pop来将新元素放在它下面。
因而矛盾。
有5个WLOG(不失一般性)声明。从某种意义上说,它们可以被直观地理解为真实(但鉴于它们是5,可能需要一些时间)。可以推导出没有普遍性的正式证据,但是非常冗长。它们被省略了。
我承认这种遗漏可能会使WLOG的陈述受到质疑。如果程序员对错误抱有偏执,请根据需要验证WLOG语句。
第三堆也无关紧要。重要的是,有一组有界堆栈和一组无界堆栈。示例所需的最小值为2个堆栈。堆栈的数量当然必须是有限的。
最后,如果我说得对,没有证据,那么应该有一个更简单的归纳证明。可能基于每个队列之后发生的事情(记录它如何影响队列中所有元素集的最坏情况)。
答案 3 :(得分:3)
注意:这是对具有单链接列表的实时(恒定时间最坏情况)队列的功能实现的评论。由于声誉,我无法添加评论,但如果有人可以将此更改为antti.huima附加到答案中的评论,那将会很好。再说一遍,评论有点长。
@ antti.huima: 链接列表与堆栈不同。
s1 =(1 2 3 4)---一个包含4个节点的链表,每个节点指向右边的一个节点,并保持值1,2,3和4
s2 =弹出(s1)--- s2现在是(2 3 4)
此时,s2相当于弹出(s1),其行为类似于堆栈。但是,s1仍可供参考!
我们仍然可以看到s1得到1,而在正确的堆栈实现中,元素1从s1消失了!
这是什么意思?
现在创建的其他链接列表每个都作为参考/指针!有限数量的堆栈无法做到这一点。
从我在论文/代码中看到的,算法都利用链接列表的这个属性来保留引用。
编辑:我只是指2和3链接列表算法利用链表的这个属性,因为我先读它们(它们看起来更简单)。这并不意味着它们适用或不适用,只是为了解释链表不一定相同。当我有空的时候,我会读一个带有6的那个。 @Welbog:谢谢你的纠正。
懒惰也可以用类似的方式模拟指针功能。
使用链接列表解决了另一个问题。这个策略可以用来实现Lisp中的实时队列(或者至少Lisps坚持从链表创建所有内容):参考“Pure Lisp中的实时队列操作”(通过antti.huima的链接链接)。这也是使用O(1)操作时间和共享(不可变)结构设计不可变列表的好方法。
答案 4 :(得分:1)
您可以使用两个堆栈以分摊的常量时间执行此操作:
------------- --------------
| |
------------- --------------
添加是O(1)
,如果您要取的边不为空,则删除O(1)
,否则O(n)
(将另一个堆叠分成两部分)。
诀窍是看O(n)
操作只会在O(n)
时间内完成(如果你分开,例如分成两半)。因此,操作的平均时间为O(1)+O(n)/O(n) = O(1)
。
虽然这可能会成为一个问题,但如果您使用基于数组的堆栈(最快)的命令式语言,那么无论如何您将只能按时间分摊。