使用两个队列实现堆栈的意义何在?


34

我有以下作业问题:

使用两个队列实现堆栈方法push(x)和pop()。

我觉得这很奇怪,因为:

  • 堆栈(LIFO)队列
  • 我不明白为什么您需要两个队列来实现它

我到处搜寻:

并找到了一些解决方案。我最终得到的是:

public class Stack<T> {
    LinkedList<T> q1 = new LinkedList<T>();
    LinkedList<T> q2 = new LinkedList<T>();

    public void push(T t) {
        q1.addFirst(t);
    }

    public T pop() {
        if (q1.isEmpty()) {
            throw new RuntimeException(
                "Can't pop from an empty stack!");
        }

        while(q1.size() > 1) {
            q2.addFirst( q1.removeLast() );
        }

        T popped = q1.pop();

        LinkedList<T> tempQ = q1;
        q1 = q2;
        q2 = tempQ;

        return popped;
    }
}

但是我不了解使用单个队列的好处是什么;两个队列版本似乎毫无意义地复杂。

假设我们选择2的效率更高(如我上面所做的那样),push将保持不变,并且pop只需要迭代到最后一个元素并返回它。在两种情况下,“ push将是O(1)”和“ pop将是” O(n);但单队列版本将大大简化。它只需要一个for循环。

我想念什么吗?这里的任何见解将不胜感激。


17
队列通常是指FIFO结构,而堆栈是LIFO结构。Java中LinkedList的接口是双端队列(双端队列)的接口,该接口允许同时访问FIFO和LIFO。尝试将编程更改为Queue接口,而不是LinkedList实现。

12
更常见的问题是使用两个堆栈来实现队列。您可能会发现Chris Okasaki的关于纯函数数据结构的书很有趣。
埃里克·利珀特

2
根据Eric所说的话,有时您会发现自己使用的是基于堆栈的语言(例如dc或具有两个堆栈的下推式自动机(相当于图灵机,因为您可以做更多的事情)),在那里您可能会发现自己多个堆栈,但没有队列。

1
@MichaelT:或者您也可以在基于堆栈的CPU上运行
slebetman 2015年

11
“堆栈是(LIFO)队列” ……嗯,队列是等待线。就像排队使用公共厕所一样。您等待的线路是否一直以LIFO方式表现?停止使用术语“ LIFO队列”,这是荒谬的。
Mehrdad

Answers:


44

没有优势:这纯粹是学术活动。

一个长的时间以前,当我还是一个新生在大学我也有类似的锻炼1。目的是教学生如何使用面向对象的编程来实现算法,而不是使用for带有循环计数器的循环编写迭代解决方案。相反,组合并重用现有数据结构可以实现您的目标。

您将永远不会在Real World TM中使用此代码。您需要从此练习中脱颖而出的是如何“跳出框框思考”和重用代码。


请注意,您应该可以使用java.util.Queue中的代码中的接口,而不是直接使用实现:

Queue<T> q1 = new LinkedList<T>();
Queue<T> q2 = new LinkedList<T>();

Queue如果需要的话,这允许您使用其他实现,以及隐藏2LinkedList可能绕开Queue界面精神的方法。这包括get(int)pop()(在编译代码时,在给定分配约束的情况下,那里存在逻辑错误。将变量声明为Queue而不是LinkedList会显示出来)。相关阅读:了解“对接口编程”,以及为什么接口有用?

1 我仍然记得:这样做的目的是使用Stack接口上的方法来反转Stack,而在java.util.Collections其他“仅静态”实用程序类中不使用任何实用程序方法。正确的解决方案包括使用其他数据结构作为临时保存对象:您必须了解不同的数据结构,它们的属性以及如何将它们组合在一起以实现此目的。困扰我以前从未编程的CS101类的大多数人。

2 这些方法仍然存在,但是如果没有类型强制转换或反射,就无法访​​问它们。因此,使用这些非排队方法并不容易


1
谢谢。我想这很有道理。我还意识到我在上面的代码中使用了“非法”操作(推到FIFO的前面),但是我认为这不会改变任何东西。我撤消了所有操作,但仍可以按预期工作。我要稍等一会儿再接受,因为我不想阻止任何其他人提供意见。不过谢谢你
Carcigenicate

19

没有优势。您已经正确认识到,使用队列实现堆栈会导致可怕的时间复杂性。没有(称职)程序员会在“现实生活”中做这样的事情。

但这是可能的。您可以使用一种抽象来实现另一种抽象,反之亦然。一个堆栈可以用两个队列来实现,同样,您可以用两个堆栈来实现一个队列。此练习的优点是:

  • 您回顾一下堆栈
  • 您回顾队列
  • 您习惯了算法思维
  • 您学习与堆栈有关的算法
  • 您开始考虑算法的权衡
  • 通过了解队列和堆栈的等效性,可以连接课程的各个主题
  • 您将获得实际的编程经验

实际上,这是一个很棒的练习。我现在应该自己做:)


3
@JohnKugelman感谢您的编辑,但是我的意思是“可怕的时间复杂性”。对于一个链表-基于堆栈pushpeekpop操作是在O(1)。对于基于可调整大小的基于数组的堆栈,除了push在O(1)中进行摊销(最差情况为O(n))之外,其他均相同。与此相比,基于队列的堆栈在O(n)推送,O(1)pop和peek,或者O(1)推送,O(n)pop和peek的情况下要差得多。
阿蒙2015年

1
“可怕的时间复杂性”和“大大逊色”并不完全正确。对于推送弹出,摊余的复杂度仍然为O(1)。TAOCP(vol1?)中有一个有趣的问题(基本上,您必须证明元素可以从一个堆栈切换到另一个堆栈的次数是恒定的)。一个操作的最坏情况下的性能是不同的,但是随后我很少听到有人谈论过推入ArrayList的O(N)性能- 通常不是有趣的数字。
Voo 2015年

5

从两个堆栈中取出一个队列绝对是一个真正的目的。如果使用功能语言中的不可变数据结构,则可以推入可推项目的堆栈,并从可弹出项目的列表中拉出。当弹出所有项目时,将创建poppable项,并且新的poppable堆栈与pushable堆栈相反,此时pushable堆栈为空。效率很高。

至于由两个队列组成的堆栈?在您有大量可用的大型和快速队列的情况下,这可能很有意义。这种Java练习绝对没有用。但是,如果这些是通道或消息队列,则可能有意义。(即:N条消息入队,并执行O(1)操作将前面的(N-1)个项目移到新队列中。)


嗯..这让我考虑使用移位寄存器作为计算的基础以及皮带/轧机的架构
slebetman 2015年

哇,Mill CPU确实很有趣。肯定是“排队机”。
罗布(Rob)2015年

2

从实际的角度来看,该练习不必要的。关键是要迫使您以一种聪明的方式使用队列的接口来实现堆栈。例如,您的“一个队列”解决方案要求您遍历队列以获取堆栈“ pop”操作的最后一个输入值。但是,队列数据结构不允许迭代这些值,因此只能以先进先出(FIFO)的方式访问它们。


2

正如其他人已经指出的那样:没有现实优势。

无论如何,对于问题第二部分的一个答案(为什么不简单地使用一个队列)超出了Java。

在Java中,即使Queue接口也有一个size()方法,并且该方法的所有标准实现都是O(1)。

对于C / C ++程序员将要实现的幼稚/规范链表,这不一定是正确的,它只会保留指向第一个和最后一个元素的指针,而每个元素保留指向下一个元素的指针。

在这种情况下size()为O(n),应避免循环。或实现是不透明的,仅提供最低的add()remove()

通过这样的实现,您必须先将元素转移到第二个队列中,再将n-1元素转移回第一个队列中,然后返回剩余的元素,以计算元素的数量。

就是说,如果您生活在Java领域,它可能不会构成这样的东西。


关于size()方法的要点。但是,在没有O(1)size()方法的情况下,堆栈跟踪其当前大小本身并不容易。没有什么可以阻止单队列实现的。
cmaster

这完全取决于实现。如果您有一个队列,仅使用前向指针以及指向第一个和最后一个元素的指针来实现,则仍可以编写算法,该算法将删除一个元素,将其保存到局部变量,将先前在该变量中的元素添加到同一队列,直到第一个元素再次出现。仅当您可以唯一地标识一个元素(例如,通过指针)而不仅仅是具有相同值的东西时,此方法才有效。O(n),仅使用add()和remove()。无论如何,除了考虑算法之外,找到一个真正做到这一点的原因更容易优化。
Thraidh 2015年

0

很难想象这种实现的用途,这是事实。但是最重要的是证明它是可以做到的

但是,就这些东西的实际用途而言,我可以想到两个。一种用途是在并非为它设计的受限环境中实现系统:例如,Minecraft的红石块最终代表了图灵完备的系统,人们已经习惯了该系统来实现逻辑电路甚至整个CPU。在脚本驱动游戏的早期,许多第一个游戏机器人也都是通过这种方式实现的。

但是,你也可以在反向运用这一原则,确保东西是不是能够在一个系统中,当你不希望它是。这可能出现在安全性上下文中:例如,功能强大的配置系统可能是资产,但是您仍然宁可不给予用户一定程度的权力。这限制了您可以允许配置语言执行的操作,以免遭到攻击者的破坏,但是在这种情况下,这就是您想要的。

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.