Implementing a queue with stacks, and back again

The classic exercise, and the amortised argument that makes the answer O(1).

Building one restricted structure out of another is a standard question because the answer hinges on amortised analysis rather than cleverness.

A queue from two stacks

One stack takes arrivals, the other serves departures. Reversing between them converts LIFO into FIFO.

class QueueFromStacks {
  #inbox = [];
  #outbox = [];

  enqueue(value) { this.#inbox.push(value); }

  dequeue() {
    if (!this.#outbox.length) {
      // Pour everything across — the order flips, which is the point.
      while (this.#inbox.length) this.#outbox.push(this.#inbox.pop());
    }

    return this.#outbox.pop();
  }

  peek() {
    if (!this.#outbox.length) {
      while (this.#inbox.length) this.#outbox.push(this.#inbox.pop());
    }

    return this.#outbox.at(-1);
  }

  get size() { return this.#inbox.length + this.#outbox.length; }
}

A single dequeue can cost O(n) when the transfer happens. Yet each element is moved exactly once from inbox to outbox in its lifetime, so across any sequence of n operations the total work is O(n): amortised O(1) per operation.

The mistake to avoid is transferring on every dequeue. Only refill the outbox when it is empty; otherwise you break both the order and the complexity.

A stack from two queues

The mirror problem, and the trade is unavoidable: one of push or pop must be O(n).

class StackFromQueues {
  #main = [];
  #spare = [];

  // Costly push: rotate the new element to the front.
  push(value) {
    this.#spare.push(value);
    while (this.#main.length) this.#spare.push(this.#main.shift());
    [this.#main, this.#spare] = [this.#spare, this.#main];
  }

  pop() { return this.#main.shift(); }    // O(1)
}

Unlike the queue-from-stacks, there is no amortisation to rescue you here — every push pays O(n). Say which operation you chose to make expensive and why; that choice is the answer.

What this exercise is really testing

Three things: whether you know the ADT contracts precisely, whether you can reason about amortised versus worst-case cost, and whether you notice that the two directions are not symmetric. The last point is the one that separates a memorised answer from an understood one.