Problem overview
A queue gives values back in the order they arrived, like a line at a shop. A stack gives back the newest value first. The task is to build the queue class MyQueue from two stacks, using only what a stack offers: add on top, look at or remove the top, and check whether it is empty.
The class 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, and empty() says whether the queue holds nothing. pop and peek are only called on a non-empty queue.
In the second example the calls are push(1), push(2), pop(), push(3), peek(), pop(), pop(), empty(). They return 1 2 2 3 true. The value 2 has been waiting longer than 3, so it leaves first.
The statement asks for O(1) time per operation, amortized: one call may be slow, but a long run of calls is fast on average.
Approach: one stack to receive, one to give back
Pushing is easy. The problem is the front: the oldest value sits at the bottom of the stack that received the pushes, and a stack only gives access to its top.
Moving every value from one stack to another reverses their order. Values pushed 1, 2, 3 sit in the first stack with 3 on top. Move them one by one onto a second stack and 1 is on top, ready to leave first. That reversal turns stack order into queue order.
The simple version does this move on every pop and moves all the values back afterwards. A pop then costs O(n): with 1,000 pushes followed by 1,000 pops, that design makes 1,000,000 moves.
The better idea is to not move the values back. Keep two stacks:
inStackreceives every pushed value, with the newest on top;outStackholds values in leaving order, with the oldest on top.
Values move from inStack to outStack only when outStack is empty and the front is needed. While outStack still has values, all of them arrived before anything in inStack, so its top is still the front of the queue.
Why this is amortized O(1)
Count the moves per value. Each value is pushed once onto inStack, moved at most once to outStack, and popped once from outStack. It never moves back. So a value costs at most three constant-time operations in its whole life. For n operations, the total work is O(n), and one slow pop that moves many values is paid for by the many cheap calls before it. After 100,000 pushes followed by 100,000 pops, the design moves exactly 100,000 values, one move per value.
Here is the second example's state after each step (the top of a stack is its last value):
push(1), push(2): inStack [1 2] outStack []
peek() = 1: inStack [] outStack [2 1]
pop() = 1: inStack [] outStack [2]
push(3): inStack [3] outStack [2]
peek() = 2: inStack [3] outStack [2]
pop() = 2, then pop() = 3: both stacks empty
The second peek moves nothing: outStack still has the 2, which is older than the 3. Only the last pop moves the 3 across.
Step by step
- Declare
inStackas astack<int>. Every pushed value goes on top of it. - Declare
outStackas a secondstack<int>. Values leave from it, oldest on top. - In
push, pushxontoinStack. The new value joins the back of the queue. - In
peek, check whetheroutStackis empty. Only then is a move needed. - Loop while
inStackis not empty. Every value in it must move, so that the oldest one ends up on top. - Move one value: push
inStack.top()ontooutStack, then popinStack. In C++,pop()removes the top but returns nothing, so the value is read withtop()first. - Return
outStack.top(). Whether values were just moved or not, the oldest value is on top. Reading it removes nothing. - In
pop, storepeek()infront.peekalready moves values when needed and finds the front. - Pop
outStack. Afterpeek, its top is the front, so this removes the front. - Return
front, which was read before the removal. - In
empty, return whether both stacks are empty, joined with&&. The queue's values are split between them.
The solution in C++
class MyQueue {
public:
stack<int> inStack;
stack<int> outStack;
void push(int x) {
inStack.push(x);
}
int peek() {
if (outStack.empty()) {
while (!inStack.empty()) {
outStack.push(inStack.top());
inStack.pop();
}
}
return outStack.top();
}
int pop() {
int front = peek();
outStack.pop();
return front;
}
bool empty() {
return inStack.empty() && outStack.empty();
}
};
The C++ details worth a closer look:
std::stack.stack<int>has exactly the operations the task allows:push,top,pop,empty(andsize). It has no indexing and no iterators.pop()returnsvoid. In some languages a stack's pop returns the removed value. In C++ you read withtop()and then callpop(). That is why the move loop has two lines and whyMyQueue::popsavespeek()before removing.top()andpop()on an empty stack are undefined behaviour. The constraints promise thatpopandpeekare called on a non-empty queue, so after the moveoutStackis never empty.- A method calling a method. Inside the class,
peek()is called by its name alone, on the same object. - The names. The class's own
pushandpopshare their names with the stack's, butinStack.push(x)andMyQueue::push(x)are different functions, chosen by the object before the dot.
A run on the third example gives true false 7 true for empty(), push(7), empty(), pop(), empty().
Complexity
Time: O(1) amortized per operation. push and empty are O(1). A single peek or pop can cost O(n) when it moves all n waiting values. But each value moves from inStack to outStack at most once in its life, so any sequence of m operations makes at most m moves, which is O(m) in total.
Space: O(n). The two stacks together hold exactly the n values in the queue, with each value in one stack at a time.
Edge cases and common mistakes
- Moving while
outStackis not empty. If values frominStackwere put on top of the old ones, the order would break. In the second example, 3 would land above 2 and leave first. - Moving back after each
pop. It keeps the order right but makes everypopcostO(n): 1,000 pushes followed by 1,000 pops make 1,000,000 moves. emptychecking only one stack. Afterpush(1)andpeek(),inStackis empty but the queue is not. Afterpush(1)alone,outStackis empty and the queue is not either.- Calling
pop()on a stack and expecting a value. In C++ it returns nothing. The codeint front = outStack.pop();does not compile. - Peeking without moving. If
peekreturnedinStack.top(), it would give the newest value, not the oldest. - Interleaved pushes and pops. A push between two pops goes to
inStackand waits. It crosses over only afteroutStackhas run dry.
Testing
push(1),push(2),peek(),pop(),empty()gives1 1 false.push(1),push(2),pop(),push(3),peek(),pop(),pop(),empty()gives1 2 2 3 true.empty(),push(7),empty(),pop(),empty()givestrue false 7 true.
