Problem overview
A queue gives values back in the order they arrived, like a line at a shop. A stack does the opposite: the last value pushed leaves first. The task is to build a queue from 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 is MyQueue:
push(x)addsxat the back of the queue.peek()returns the front value without removing it.pop()removes the front value and returns it.empty()returns whether the queue holds no values.
After push(1) and push(2), the calls peek(), pop() and empty() return 1, 1 and false: 1 arrived first, so it is at the front, and 2 is still inside. In a longer run, push(1) and push(2) are followed by pop(), which returns 1, then push(3). The calls peek(), pop(), pop() and empty() then return 2 2 3 true: value 2 has waited longer than 3, so it leaves first.
Approach: reverse once, and only when needed
Pushing onto a stack puts a value on the top, but the queue needs the oldest value, which is at the bottom. Moving all values from one stack to another reverses their order: the newest goes first and lands deepest, the oldest goes last and lands on top.
The simple way keeps everything in one stack and, on each pop, moves all values to a second stack, takes the bottom one, and moves everything back. That is O(n) per call, where n is the number of values in the queue.
The better idea avoids moving back. Use two stacks with separate jobs:
inStacktakes every pushed value. Its top is the newest value.outStackgives values back. Its top is the oldest value, so it is the front of the queue.
Only when outStack is empty and the front is needed, move everything from inStack to outStack. While outStack still has values, they all arrived before anything in inStack, so its top is still the front. After 1, 2, 3 are pushed, inStack is [1, 2, 3] and outStack is []. After a peek() they are [] and [3, 2, 1], with 1 on top (the end of the array). Pushing 4 then puts it in inStack, giving [4] and [3, 2, 1], and four pop() calls return 1, 2, 3, 4 in that order.
Why it is O(1) amortized
A single pop or peek can move many values, so one call can take O(n). But count the moves per value instead of per call. A value is pushed onto inStack once, moved to outStack at most once, and popped from outStack once. It never goes back. So a value costs at most three stack operations in its life, and n operations cost O(n) in total: O(1) per operation on average, which is what "amortized" means. A run that pushes 1 to 1000 and pops all 1000 values makes exactly 1000 moves onto outStack, one per value, and pops them in the order 1 to 1000 (their sum is 500500).
Step by step
- Declare
inStackas anumber[], set to an empty array. An array'spushandpopwork at its end, so the end is the top of the stack. - Declare
outStackthe same way. push(x): pushxontothis.inStack.peek, the condition: start aniffor whenthis.outStack.length === 0. Moving happens only then.- The loop: inside it,
while (this.inStack.length > 0), since every value ofinStackhas to move for the oldest to reach the top. - The move:
this.outStack.push(this.inStack.pop())in one line. This reverses the order. - Read the front: after the
if, returnthis.outStack[this.outStack.length - 1], the top ofoutStack. Reading removes nothing. pop: callthis.peek()and keep the result infront, becausepeekalready does the moving and finds the front.- Remove it:
this.outStack.pop(). - Return
front. empty: returnthis.inStack.length === 0 && this.outStack.length === 0. The values are split between the two stacks, so the queue is empty only when both are.
The solution in TypeScript
class MyQueue {
inStack: number[] = [];
outStack: number[] = [];
push(x: number): void {
this.inStack.push(x);
}
peek(): number {
if (this.outStack.length === 0) {
while (this.inStack.length > 0) {
this.outStack.push(this.inStack.pop());
}
}
return this.outStack[this.outStack.length - 1];
}
pop(): number {
let front: number = this.peek();
this.outStack.pop();
return front;
}
empty(): boolean {
return this.inStack.length === 0 && this.outStack.length === 0;
}
}
Details:
- A TypeScript array works as a stack with
pushandpopat the end, and reading the top isarr[arr.length - 1]. Arrayshift()andunshift()work at the front and aren't part of what the problem allows. Array.prototype.pop()is typed as returningnumber | undefinedin strict mode; the lesson compiles with strict checks off, sothis.outStack.push(this.inStack.pop())is accepted.- A method calls another method of the same object through
this, aspopdoes withthis.peek(). - The lesson's tests print booleans as
True/False; in the class,empty()returns the realtrueorfalse.
Complexity
Each operation is O(1) amortized, by the counting argument above; the worst single call, a peek or pop that moves all n values, is O(n). Space is O(n) for the n values in the two stacks.
Edge cases and common mistakes
- Moving when
outStackisn't empty. The older values inoutStackwould end up under the newer ones, and the order would break. In the second example, afterpop()returns 1, value 2 is inoutStack; pushing 3 must not touch it, sopeek()still returns 2. - Moving back after each
pop. That works but makes every callO(n). empty()checking one stack. Afterpush(7)alone,outStackis empty but the queue is not.- A new queue.
empty()istrue; afterpush(7)it isfalse, and afterpop()returns 7 it istrueagain. - Peeking an empty queue. The lesson never does this; the class would return
undefined.
