Problem overview
A queue returns values in the order they arrived: the first value pushed is the first to leave. A stack does the opposite, with the last value pushed leaving first. The task is to build a queue out of two stacks, using only what a stack can do: add on top, look at or remove the top, and check whether it is empty.
The class MyQueue has four methods. push(x) adds x at the back. peek() returns the front value without removing it. pop() removes the front value and returns it. empty() returns whether the queue holds no values. The constraints promise that pop and peek are only called when the queue is not empty.
The first example: push 1, push 2, then peek(), pop() and empty(). They return:
1 1 false
1 arrived first, so it is at the front: peek() shows it, pop() removes it. The 2 is still inside, so empty() is false.
Approach: one stack to receive, one stack to give back
In JavaScript an array works as a stack: push and pop both act on its end, the top. A single stack hands back the newest value first, which is the wrong end. The fix uses a property of stacks: moving values one by one from one stack onto another reverses their order.
So the class keeps two arrays. inStack receives every pushed value. outStack is where values leave from. When the front is needed and outStack is empty, every value is popped from inStack and pushed onto outStack. The newest value moves first and ends deepest; the oldest moves last and ends on top. The top of outStack is now the front of the queue.
The important rule: move values only when outStack is empty. While it still holds values, they all arrived before anything in inStack, so its top is still the front. Moving earlier would put newer values under older ones and break the order.
A run with the numbers 1, 2, 3 pushed, then peek(), then 4 pushed, then four pops (the arrays are printed with the top at the end):
after push 1, 2, 3: inStack [1,2,3] outStack []
peek() => 1: inStack [] outStack [3,2,1]
after push 4: inStack [4] outStack [3,2,1]
pop() => 1: inStack [4] outStack [3,2]
pop() => 2: inStack [4] outStack [3]
pop() => 3: inStack [4] outStack []
pop() => 4: empty() is true
Notice that 4 stayed in inStack until the 3 had left, which is exactly the queue order. The last pop found outStack empty, so it moved the 4 over first.
Why it is O(1) amortized
A single peek or pop can be slow, because it may move every value in inStack. But each value is moved from inStack to outStack at most once: after that it only leaves. So a value costs at most three steps in its whole life: one push, one move, one pop. Over n operations the total work is O(n), which makes the average cost per operation constant. That is what "amortized O(1)" means. In a run with 200,000 pushes and 200,000 pops, mixed so that the queue was refilled and emptied many times, the number of values moved was exactly 200,000, one per pushed value.
Moving everything over and back again on every pop would instead cost O(n) per call.
Step by step
- Constructor: set
this.inStackto an empty array. Values go on top of it, with the top at the end of the array. - Set
this.outStackto an empty array, the stack values leave from. Values in it are already in leaving order, the oldest on top. - push: push
xontothis.inStack. A new value joins the back of the queue. - peek: write an
iffor whenthis.outStackis empty (its length is 0). - Inside it, write a
whileloop that runs whilethis.inStackhas values (its length is above 0). - In the loop, pop the top of
this.inStackand push it ontothis.outStack, in one line. Each value is moved at most once. - After the
if, return the top ofthis.outStack: its last element, at indexthis.outStack.length - 1. Reading it removes nothing. - pop: call
this.peek()and store the result infront.peekalready moves values when needed and finds the front, sopopdoes not repeat that. - Pop the top of
this.outStack, which removes the front value of the queue. - Return
front, read before the removal. - empty: return whether both
this.inStack.lengthandthis.outStack.lengthare 0, joined with&&. The queue's values are split across the two stacks, so it is empty only when both are.
The solution in JavaScript
class MyQueue {
constructor() {
this.inStack = [];
this.outStack = [];
}
push(x) {
this.inStack.push(x);
}
peek() {
if (this.outStack.length === 0) {
while (this.inStack.length > 0) {
this.outStack.push(this.inStack.pop());
}
}
return this.outStack[this.outStack.length - 1];
}
pop() {
let front = this.peek();
this.outStack.pop();
return front;
}
empty() {
return this.inStack.length === 0 && this.outStack.length === 0;
}
}
JavaScript details:
- An array as a stack.
pushadds at the end andpopremoves from the end and returns it. Usingshiftorunshiftwould work at the front, which is a different thing and not what a stack does. - Reading the top. The top is
arr[arr.length - 1]. JavaScript does not throw for a missing index but returnsundefined, which is why the lesson's guarantee thatpeekandpopare called on a non-empty queue matters. - Methods calling methods.
popcallsthis.peek(). Without thethis.the call would look for a function calledpeekoutside the class and fail. ===inempty. It compares numbers without converting types, and&&returns a real boolean here because both sides are comparisons.
Complexity
Time: push and empty are O(1). peek and pop are O(1) amortized: a call that moves k values costs O(k), but each value is moved once, so n operations cost O(n) in total.
Space: O(n) for the n values held, split between the two arrays. No value is stored twice at the same time.
Edge cases and common mistakes
- Moving when
outStackis not empty. In the second example, afterpush(1),push(2)andpop(), the 2 sits inoutStack. Thenpush(3)goes toinStack. Moving the 3 over the 2 would make the nextpeek()return 3 instead of 2. The expected output is1 2 2 3 true. - Moving back. Never move values from
outStackback toinStack. The oldest values are already in the right order, and moving them again costs time. - Checking only one stack in
empty. Afterpeek()has moved everything,inStackis empty but the queue is not. Both lengths must be 0. - A new queue.
empty()on a new queue is true. Afterpush(7)it is false, and afterpop()returns 7 it is true again, which givestrue false 7 true. - Duplicating the move logic.
popreusespeek. Copying the loop into both methods invites the two copies to drift apart.
Testing
Outputs of the solution above:
- push 1, push 2, then
peek(),pop(),empty()return1 1 false. - push 1, push 2,
pop(), push 3,peek(),pop(),pop(),empty()return1 2 2 3 true. empty(), push 7,empty(),pop(),empty()returntrue false 7 true.
