我在算法书(《算法》, Robert Sedgewick和Kevin Wayne 第四版)中遇到了这个问题。
与三个堆栈队列。实现一个具有三个堆栈的队列,以便每个队列操作都采用恒定数量(最坏情况)的堆栈操作。警告:高难度。
我知道如何使2个堆栈的队列,但我找不到3个堆栈的解决方案。任何想法 ?
(哦,这不是家庭作业:))
我在算法书(《算法》, Robert Sedgewick和Kevin Wayne 第四版)中遇到了这个问题。
与三个堆栈队列。实现一个具有三个堆栈的队列,以便每个队列操作都采用恒定数量(最坏情况)的堆栈操作。警告:高难度。
我知道如何使2个堆栈的队列,但我找不到3个堆栈的解决方案。任何想法 ?
(哦,这不是家庭作业:))
Answers:
摘要
细节
该链接背后有两种实现方式: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个堆栈,因此我想寻找一个算法还是存在一个问题(或者证明找不到一个算法)。”
rotate
,front
列表似乎同时分配给oldfront
和f
,然后分别进行修改。
好的,这确实很难,我唯一想出的解决方案,让我想起了柯克斯对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的原因)。
我将尝试证明它无法完成。
假设有一个队列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个堆栈。当然,堆栈数必须是有限的。
最后,如果我对没有证据是正确的,那么应该有一个更容易的归纳证明。可能基于每个队列之后发生的情况(在队列中所有元素都存在的情况下,跟踪其如何影响最坏的出队情况)。
注意:这是对具有单链接列表的实时(恒定时间最差情况)队列的功能实现的注释。由于声誉,我无法添加评论,但是如果有人可以将其更改为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仍然可供参考!
我们仍然可以窥视s1以获得1,而在适当的堆栈实现中,元素1将从s1消失!
这是什么意思?
现在创建的其他链接列表每个都用作参考/指针!有限数量的堆栈无法做到这一点。
从我在论文/代码中看到的,算法都利用链接列表的此属性来保留引用。
编辑:我只指的是2和3链表算法,因为我先阅读了链表算法(它们看起来更简单),所以利用了链表的此属性。这并不是要说明它们是否适用,只是为了说明链接列表不一定相同。有空的时候我会读一本6。@Welbog:感谢您的纠正。
懒惰也可以类似的方式模拟指针功能。
使用链表解决了另一个问题。此策略可用于在Lisp中实现实时队列(或至少坚持从链接列表构建所有内容的Lisps):请参阅“ Pure Lisp中的实时队列操作”(通过antti.huima的链接进行链接)。这也是设计具有O(1)操作时间和共享(不可变)结构的不可变列表的好方法。
java.util.Stack
对象在Java中重新实现了它。使用此功能的唯一地方是优化,该优化允许不可变堆栈在恒定时间内“复制”,这是基本Java堆栈无法实现的(但可以用Java实现),因为它们是可变结构。
您可以通过两个堆栈在摊销的固定时间内完成此操作:
------------- --------------
| |
------------- --------------
如果要从中O(1)
取出的O(1)
那面不为空(O(n)
否则将其他堆栈一分为二),则添加和删除。
诀窍在于,O(n)
仅在每次O(n)
操作(如果您拆分,例如分成两半)时才执行该操作。因此,一次手术的平均时间为O(1)+O(n)/O(n) = O(1)
。
虽然这可能像是一个问题,但是,如果您将命令式语言与基于数组的堆栈一起使用(最快),则无论如何您将只能摊销固定时间。