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


136

我在算法书(《算法》, Robert Sedgewick和Kevin Wayne 第四版)中遇到了这个问题。

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

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

(哦,这不是家庭作业:))


30
我猜这是河内塔的变体。
Gumbo

14
@Jason:这个问题不是重复的,因为它要求O(1)的摊销时间,而这个问题要求每个操作的O(1)最坏情况。DuoSRX的两层式解决方案每次操作的摊销时间已达到O(1)。
interjay 2011年

15
作者说“警告:高难度”时肯定不会在开玩笑。
BoltClock

9
@Gumbo不幸的是,河内塔的时间复杂度远非恒定时间!
prusswan 2011年

12
注意:文本中的问题已更新为:恒定数量的堆栈 [不是“ 3”] 实现一个队列,以便每个队列操作都采用恒定(最坏情况)数量的堆栈操作。警告:高难度。algs4.cs.princeton.edu/13stacks-第1.3.43节)。似乎Sedgewick教授承认了最初的挑战。
Mark Peters

Answers:


44

摘要

  • O(1)算法已知于6个堆栈
  • O(1)算法已知用于3个堆栈,但使用的是惰性求值,实际上它对应于具有额外的内部数据结构,因此它不构成解决方案
  • Sedgewick附近的人们已经确认他们不知道原始问题的所有限制内的3层式解决方案

细节

该链接背后有两种实现方式:http : //www.eecs.usma.edu/webs/people/okasaki/jfp95/index.html

其中之一是带有三个堆栈的O(1),但是它使用了延迟执行,实际上这会创建额外的中间数据结构(关闭)。

其中另一个是O(1),但使用了SIX堆栈。但是,它无需延迟执行即可工作。

更新:冈崎的论文在这里:http ://www.eecs.usma.edu/webs/people/okasaki/jfp95.ps ,看来他实际上只使用了2个堆栈用于O(1)版本,并且具有惰性评估。问题在于它实际上是基于惰性评估。问题是,是否可以将其转换为3层算法而无需进行延迟评估。

更新:另一种相关算法在Holger Petersen的论文“ Stacks vs Deques”中进行了描述,该论文发表在第七届计算与组合学年会上。您可以从Google图书中找到该文章。检查第225-226页。但是该算法实际上不是实时仿真,而是三个堆栈上的双端队列的线性时间仿真。

gusbro:“正如@Leonel几天前所说,我认为与Sedgewick教授核对是否知道解决方案或存在一些错误是很公平的。所以我确实写信给他。我刚刚收到了答复(尽管不是来自他基本上是说自己不知道使用三个堆栈的算法以及施加的其他限制(例如不使用惰性评估),但他确实知道使用以下算法,例如:普林斯顿大学的一位同事。我们已经知道要在这里找到6个堆栈,因此我想寻找一个算法还是存在一个问题(或者证明找不到一个算法)。”


我只是浏览了您链接中的论文和程序。但是,如果我没看错,他们不使用堆栈,而是使用列表作为基本类型。尤其是 这些列表中的文件头是由标题和其余部分构成的,因此它基本上与我的解决方案相似(我认为这是不对的)。
flolo 2011年

1
嗨,这些实现是在一种功能语言中进行的,只要不共享指针,列表就与堆栈相对应,并且不共享。六栈版本可以使用六个“普通”栈真正实现。两栈/三栈版本的问题在于它使用了隐藏的数据结构(闭包)。
Antti Huima 2011年

您确定六栈解决方案不共享指针吗?在中rotatefront列表似乎同时分配给oldfrontf,然后分别进行修改。
interjay 2011年

14
algs4.cs.princeton.edu/13stacks上的源材料已更改:43.实现一个具有恒定数量的 [not“ 3”]堆栈的队列,以便每个队列操作占用一个恒定(最坏情况)的堆栈数量操作。警告:高难度。 挑战的标题仍然是“带有三个堆栈的队列” :-)。
Mark Peters

3
@AnttiHuima六个堆栈的链接已死,您知道这是否存在吗?
昆汀·普拉德

12

好的,这确实很难,我唯一想出的解决方案,让我想起了柯克斯对Kobayashi Maru测试的解决方案(以某种方式被骗):这个想法是,我们使用堆栈栈(并使用它来对列表建模) )。我将操作称为en / dequeue,然后进行push和pop,然后得到:

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个堆栈的数组并通过索引访问它们,在那里您只需更改index变量的值)。在堆栈操作项中,所有内容都在O(1)中。

是的,我知道它是有争议的,因为我们隐含了3个以上的堆栈,但也许它可以给你们其他人带来好主意。

编辑:解释示例:

 | | | |3| | | |
 | | | |_| | | |
 | | |_____| | |
 | |         | |
 | |   |2|   | |
 | |   |_|   | |
 | |_________| |
 |             |
 |     |1|     |
 |     |_|     |
 |_____________|

我在这里尝试了一些ASCII艺术来显示Stack1。

每个元素都包装在单个元素堆栈中(因此,我们只有类型安全的堆栈堆栈)。

您会看到要删除,我们首先弹出第一个元素(此处包含元素1和2的堆栈)。然后弹出下一个元素,并在其中展开。1。然后,我们说第一个弹出的堆栈现在是我们的新Stack1。稍微讲一些功能-这些是通过2个元素的堆栈实现的列表,其中top元素ist cdr和first / below顶部元素是car。另外两个正在帮助筹码。

Esp棘手的问题是插入,因为您不得不以某种方式深入嵌套嵌套堆栈以添加另一个元素。这就是为什么Stack2在那的原因。Stack2始终是最里面的堆栈。然后添加就是将一个元素推入,然后推到一个新的Stack2之上(这就是为什么我们在出队操作中不允许触摸Stack2的原因)。


您是否愿意解释它的工作原理?也许找出推动“ A”,“ B”,“ C”,“ D”然后弹出4次?
MAK

1
@Iceman:没有Stack2是正确的。它们不会丢失,因为Stack始终引用Stack1中最里面的堆栈,因此它们仍隐含在Stack1中。
flolo 2011年

3
我同意这是作弊:-)。那不是3个堆栈,而是3个堆栈引用。但阅读愉快。
马克·彼得斯

1
这是一个聪明的方案,但是如果我理解正确的话,当队列中有n个元素时,它将最终需要n个堆栈。问题要求恰好3个堆栈。
MAK

2
@MAK:我知道,这就是为什么要明确指出其被骗(我什至在赏金上花了声誉,因为我也对真正的解决方案感到好奇)。但是至少可以回答prusswan的评论:堆栈数很重要,因为当您可以使用任意数量的堆栈时,我的解决方案确实是有效的。
flolo 2011年

4

我将尝试证明它无法完成。


假设有一个队列Q,它由3个堆栈A,B和C模拟。

断言

  • ASRT0:=此外,假设Q可以模拟O(1)中的操作{queue,dequeue}。这意味着对于要模拟的每个队列/出队操作,都有特定的堆栈推入/弹出操作序列。

  • 在不失一般性的前提下,假设队列操作是确定性的。

根据排队顺序,将排队到Q中的元素编号为1、2,...,将排队到Q中的第一个元素定义为1,将第二个元素定义为2,依此类推。

定义

  • Q(0) := 当Q中有0个元素(因此A,B和C中有0个元素)时Q的状态
  • Q(1) := 在1个队列上操作后Q的状态(以及A,B和C) Q(0)
  • Q(n) := 在n个队列上进行操作之后Q的状态(以及A,B和C) Q(0)

定义

  • |Q(n)| :=Q(n)(因此|Q(n)| = n)中的元素数
  • A(n) := Q的状态为时的堆栈A的状态 Q(n)
  • |A(n)| := 中的元素数 A(n)

以及堆栈B和C的类似定义。

琐碎地

|Q(n)| = |A(n)| + |B(n)| + |C(n)|

---

|Q(n)| 在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,对于这样的n |A(n)| > K,从中选择K个元素Q(n)。假设这些元素中的1个在A(n + x)all中x >= 0,也就是说,无论完成多少队列操作,该元素始终在堆栈A中。

  • X := 该元素

然后我们可以定义

  • Abv(n) :=堆栈A(n)中大于X 的项目数
  • Blo(n) :=堆栈A(n)中小于X 的元素数

    | A(n)| = Abv(n)+ Blo(n)

ASRT1 :=使X出队所需的爆破声数量Q(n)至少为Abv(n)

必须限制(ASRT0)和(ASRT1)中的内容ASRT2 := Abv(n)

如果Abv(n)是无界的,则如果需要20个出队才能将X从中出队Q(n),则至少需要Abv(n)/20弹出。哪个是无界的。20可以是任何常数。

因此,

ASRT3 := Blo(n) = |A(n)| - Abv(n)

必须是无界的。


WLOG3我们可以从底部的K个元素A(n),其中之一是在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因此,这是矛盾的,因此不可能模拟O(1)具有3个堆栈的队列。


至少1个堆栈必须是无界的。

对于保留在该堆栈中的元素,必须限制其上方的元素数,否则将不限制删除该元素的出队操作。

但是,如果上面的元素数量是有界的,那么它将达到极限。在某个时候,必须在其下方输入一个新元素。

由于我们总是可以从该堆栈的最少几个元素之一中选择旧元素,因此上方可以有无数个元素(基于无限制堆栈的无限制大小)。

要在其下输入一个新元素,由于其上有无数个元素,因此需要无限制数量的pops才能将新元素放在其下。

从而产生矛盾。


有5个WLOG(不失一般性)语句。从某种意义上说,它们可以直观地理解为真实的(但是假设它们是5,则可能需要一些时间)。可以得出没有失去一般性的形式证明,但是这是极其冗长的。他们被省略了。

我确实承认,这样的遗漏可能会使WLOG语句成为问题。对于程序员的错误妄想症,请根据需要验证WLOG语句。

第三叠也无关紧要。重要的是,有一组有界堆栈和一组无界堆栈。一个示例最少需要2个堆栈。当然,堆栈数必须是有限的。

最后,如果我对没有证据是正确的,那么应该有一个更容易的归纳证明。可能基于每个队列之后发生的情况(在队列中所有元素都存在的情况下,跟踪其如何影响最坏的出队情况)。


2
我认为证明适用于这些假设-但我不确定所有堆栈都必须为空才能使队列为空,或者不确定堆栈大小的总和是否必须等于队列的大小。
Mikeb 2011年

3
“ WLOG1,假设堆栈A是无界的,而堆栈B和C是有界的。” 您不能假设某些堆栈是有界的,因为这会使它们一文不值(它们与O(1)的额外存储空间相同)。
interjay 2011年

3
有时琐碎的事情不是那么琐碎:| Q | = | A | + | B | + | C | 是正确的,如果您假设对Q中的每个条目都向A,B或C中添加了一个精确的元素,但是这可能是某种算法,它总是将元素两次添加到两个堆栈甚至是三个堆栈中。如果以这种方式工作,则您的WLOG1将不再保存(例如,想象C是A的副本(不是有任何意义,但是也许有一个算法,具有不同的顺序或其他内容)……
flolo

@flolo和@mikeb:你们俩都是对的。| Q(n)| 应该定义为| A(n)| + | B(n)| + | C(n)|。然后| Q(n)| > = n。随后,证明将适用于n,并注意只要| Q(n)| 更大,结论适用。
Dingfeng Quek

@interjay:您可以有3个无界堆栈,没有无界堆栈。然后,证明可以仅使用“ 1”代替“ B_u + C_u + 1”。基本上,该表达式表示“有界堆栈的上限的总和+ 1”,因此有界堆栈的数量无关紧要。
Dingfeng Quek

3

注意:这是对具有单链接列表的实时(恒定时间最差情况)队列的功能实现的注释。由于声誉,我无法添加评论,但是如果有人可以将其更改为antti.huima附加在答案后的评论,那将是很好的。再说一次,发表评论的时间有点长。

@ antti.huima:链接列表与堆栈不同。

  • s1 =(1 2 3 4)---包含4个节点的链表,每个节点指向右边的一个节点,并保留值1、2、3和4

  • s2 = popped(s1)--- s2现在是(2 3 4)

此时,s2等效于popped(s1),其行为类似于堆栈。但是,s1仍然可供参考!

  • s3 = popped(popped(s1))-s3是(3 4)

我们仍然可以窥视s1以获得1,而在适当的堆栈实现中,元素1将从s1消失!

这是什么意思?

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

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

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

编辑:我只指的是2和3链表算法,因为我先阅读了链表算法(它们看起来更简单),所以利用了链表的此属性。这并不是要说明它们是否适用,只是为了说明链接列表不一定相同。有空的时候我会读一本6。@Welbog:感谢您的纠正。


懒惰也可以类似的方式模拟指针功能。


使用链表解决了另一个问题。此策略可用于在Lisp中实现实时队列(或至少坚持从链接列表构建所有内容的Lisps):请参阅“ Pure Lisp中的实时队列操作”(通过antti.huima的链接进行链接)。这也是设计具有O(1)操作时间和共享(不可变)结构的不可变列表的好方法。


1
我无法在antti的答案中提及其他算法,但是六栈固定时间解决方案(eecs.usma.edu/webs/people/okasaki/jfp95/queue.hm.sml)没有使用列表的此属性,因为我已经使用java.util.Stack对象在Java中重新实现了它。使用此功能的唯一地方是优化,该优化允许不可变堆栈在恒定时间内“复制”,这是基本Java堆栈无法实现的(但可以用Java实现),因为它们是可变结构。
Welbog 2011年

如果这是不降低计算复杂度的优化,则不应影响结论。很高兴终于有了一个解决方案,现在可以对其进行验证:但是我不喜欢阅读SML。介意分享您的Java代码?(:
Dingfeng Quek

不幸的是,这不是最终解决方案,因为它使用六个堆栈而不是三个。但是,有可能证明六个堆栈是一个最小的解决方案……
Welbog 2011年

@Welbog!您可以共享您的6层实现吗?:)看到它会很酷:)
Antti Huima 2011年

1

您可以通过两个堆栈在摊销的固定时间内完成此操作:

------------- --------------
            | |
------------- --------------

如果要从中O(1)取出的O(1)那面不为空(O(n)否则将其他堆栈一分为二),则添加和删除。

诀窍在于,O(n)仅在每次O(n)操作(如果您拆分,例如分成两半)时才执行该操作。因此,一次手术的平均时间为O(1)+O(n)/O(n) = O(1)

虽然这可能像是一个问题,但是,如果您将命令式语言与基于数组的堆栈一起使用(最快),则无论如何您将只能摊销固定时间。


关于原始问题:拆分堆栈实际上需要一个额外的中间堆栈。这可能就是为什么您的任务包括三个堆栈的原因。
Thomas Ahle
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.