这一节我们来看两个经常被拿来作为例题或者是面试题的问题。

例 1 :「力扣」第 232 题:用栈实现队列

这道题要求我们用已经实现的栈实现队列。

题意分析:用栈来实现队列,或者用队列实现栈,肯定不是高效的做法。因此,这道题只是让我们实现功能。通过这种「逆来顺受」的方式,来加深对栈和队列的操作理解。

思路分析:思考这个问题,建议采用画图的方式帮助思考,还记得我们在介绍栈的时候说过,栈通常我们把它画成竖直摆放的容器。这里只使用一个栈肯定不能实现队列的功能。因此,我们还需要一个辅助栈,这也是 空间换时间 的思想。

栈队列,队列栈 - 图1

我们知道栈符合「后进先出」的规律,「后进先出」再「后进先出」就是「先进先出」了,这样的思路可以理解为负负得正。因此:入队的时候,始终把数据放入原始栈,出队的时候,始终从辅助栈出队。这其中还有一些细节需要思考。

算法设计流程:我们可以使用两个栈,一个栈(stackPush)用于元素进栈,一个栈(stackPop)用于元素出栈

  • pop() 或者 peek() 的时候:
    • 如果 stackPop 里面有元素,直接从 stackPop 里弹出或者 peek 元素;
    • 如果 stackPop 里面没有元素,一次性 将 stackPush 里面的 所有 元素倒入 stackPop。

在这里要注意一个细节: 一定要保证 stackPop 为空的时候,才能把元素从 stackPush 里拿到 stackPop 中 。要想明白这个细节其实不难:如果 stackPop 里还有元素,从 stackPush 里出栈的那个元素就会成为 stackPop 的新栈顶元素,就打乱了出队的顺序。相信这一点,大家不难想明白。

  1. var MyQueue = function () {
  2. this.pushStack = [];
  3. this.popStack = [];
  4. };
  5. MyQueue.prototype.push = function (x) {
  6. // 在任何时候都可以向 pushStack 推入元素
  7. this.pushStack.push(x);
  8. };
  9. MyQueue.prototype.pop = function () {
  10. this.push2pop(); // 出栈准备一下
  11. return this.popStack.pop();
  12. };
  13. MyQueue.prototype.peek = function () {
  14. this.push2pop(); // 出栈准备
  15. // 这里不出栈,只peek
  16. return this.popStack[this.popStack.length - 1];
  17. };
  18. MyQueue.prototype.empty = function () {
  19. return this.popStack.length === 0 && this.pushStack.length === 0;
  20. };
  21. // 出栈准备 push to pop
  22. MyQueue.prototype.push2pop = function () {
  23. // stackPop 为空的时候,才能把元素从 stackPush 里拿到 stackPop 中
  24. // 否则会打乱出队顺序
  25. if (!this.popStack.length) {
  26. while (this.pushStack.length) {
  27. this.popStack.push(this.pushStack.pop());
  28. }
  29. }
  30. }

复杂度分析:

  • 时间复杂度:O(1)。这里用到的是均摊复杂度分析,每一个元素进 push 栈一次,出 push 栈一次,进 pop 栈一次,出 pop 栈一次,平均操作是直接使用队列的 4 倍,但 4 是一个常数倍,因此,时间复杂度仍为 O(1)
  • 空间复杂度:O(N),这里 N 是输入数据的长度

例 2:「力扣」第 225 题:用队列实现栈

这一题让我们借助已经实现的队列实现栈。有了上一题的经验,我们不难想到,可以 使用两个队列实现栈。但事实上,放入辅助队列的那些元素可以直接放在当前的队列的尾部 ,这样我们就可以用一个队列实现栈的功能。

想通这个问题,依然需要在草稿纸上做一些模拟。

栈队列,队列栈 - 图2

具体的操作是:在 peek() 和 pop() 时,依次将队首出队到队尾。

  • push() 的时候,直接在队列的尾部添加元素即可;
  • 只要涉及到 peek() 或者 pop() 操作,为了满足栈「后进先出」的性质。需要让当前队尾的元素成为队首,而队列只支持队首出队,队尾入队,不难想到需要依次把队尾之前的元素出队。放到哪里呢?直接放在队尾即可。

注意:peek() 的时候,得到队尾元素以后,还得再队首元素移动到队尾一次。

  1. var MyStack = function () {
  2. this.queue = [];
  3. };
  4. MyStack.prototype.push = function (x) {
  5. let curr = 0;
  6. // 每次从队尾进来就重新排, 把当前元素放到第一个
  7. this.queue.push(x);
  8. while (curr < this.queue.length-1) {
  9. this.queue.push(this.queue.shift());
  10. curr++;
  11. };
  12. };
  13. MyStack.prototype.pop = function () {
  14. // push已经排好了,直接shift就行
  15. if (this.queue.length > 0) {
  16. return this.queue.shift();
  17. };
  18. };
  19. MyStack.prototype.top = function () {
  20. return this.queue[0];
  21. };
  22. MyStack.prototype.empty = function () {
  23. return this.queue.length === 0;
  24. };

练习
完成「力扣」第 151 题:最小栈。
这就是这一节的内容。下一节我们看两个设计数据结构相关的问题,分别是队列和双端队列在数组中是如何实现的。