如何实现三个堆栈的队列?

时间:2011-04-04 12:05:50

标签: algorithm data-structures

我在算法手册(Robert Algorithms, 4th Edition中由Robert Sedgewick和Kevin Wayne)中提到了这个问题。

  

有三个堆栈的队列。实现具有三个堆栈的队列,以便每个队列操作采用恒定(最坏情况)的堆栈操作数。警告:难度很高。

我知道如何使用2个堆栈建立队列,但我找不到3个堆栈的解决方案。任何的想法 ?

(哦,这不是作业:))

5 个答案:

答案 0 :(得分:41)

概要

  • O(1)算法已知6个堆栈
  • O(1)算法已知有3个堆栈,但使用延迟评估,实际上对应于具有额外的内部数据结构,因此它不构成解决方案
  • Sedgewick附近的人已经确认他们在原始问题的所有限制范围内都不知道3-stack解决方案

详情

此链接背后有两个实现: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)
  • 上1个队列操作后的Q(以及A,B和C)状态
  • Q(n) := Q(0)上的n个队列操作后的Q(以及A,B和C)状态

定义

  • |Q(n)| := Q(n)中的元素数量(因此|Q(n)| = n
  • A(n) :=当Q的状态为Q(n)
  • 时,堆栈A的状态
  • |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仍可供参考!

  • s3 =弹出(弹出(s1))--- s3是(3 4)

我们仍然可以看到s1得到1,而在正确的堆栈实现中,元素1从s1消失了!

这是什么意思?

  • s1是对堆栈顶部的引用
  • s2是对堆栈第二个元素的引用
  • s3是第三个参考...

现在创建的其他链接列表每个都作为参考/指针!有限数量的堆栈无法做到这一点。

从我在论文/代码中看到的,算法都利用链接列表的这个属性来保留引用。

编辑:我只是指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)

虽然这可能会成为一个问题,但如果您使用基于数组的堆栈(最快)的命令式语言,那么无论如何您将只能按时间分摊。