Problem overview
A queue returns values in the order they arrived; a stack returns the newest first. The task is to build the struct MyQueue from two stacks, using only stack operations: add on top, look at or remove the top, and check for emptiness. In Rust a Vec<i32> works as a stack, with the top at the end of the vector.
The methods are MyQueue::new(), push(x) to add at the back, peek() for the front value without removing it, pop() to remove and return the front, and empty(). pop and peek are only called on a non-empty queue.
After push(1) and push(2), peek() returns 1, and so does pop(). Then empty() is false, because 2 is still waiting.
The statement asks for O(1) time per operation, amortized.
Approach: reverse once, and only when needed
Pushing is easy: put the value on a stack. The trouble is that the top of that stack is the newest value, while the queue's front is the oldest, at the bottom. Moving all the values onto a second stack, one by one, reverses their order and puts the oldest on top. Doing that move and then moving everything back on every pop works, but it costs O(n) per call.
The better idea is to not move them back. Keep two stacks. New values always go on in_stack. Values leave from out_stack, which holds the older values already in the right order, oldest on top. Only when out_stack is empty and the front is needed, move everything from in_stack to out_stack.
Why wait until out_stack is empty? Everything in it arrived before anything in in_stack, so its top is still the front. Moving earlier would put newer values on top of older ones and break the order.
Here is the second example with the contents of both stacks after each step:
after push(1), push(2): in=[1, 2] out=[]
pop -> 1 in=[] out=[2]
push(3) in=[3] out=[2]
peek -> 2 in=[3] out=[2]
pop -> 2 in=[3] out=[]
pop -> 3 in=[] out=[]
empty() -> true
The first pop moves both values over (2 ends up under 1, then 1 is removed). The 3 waits in in_stack while 2 leaves. Only when out_stack is empty does the next pop move 3 across.
Why it is amortized O(1)
A single pop can move many values, so it can be O(n). But count the work per value instead of per call. Each value is pushed onto in_stack once, moved to out_stack at most once, and popped from out_stack once. Values are never moved back. So a value costs at most three stack operations in its whole life, and n operations cost O(n) in total: O(1) per operation on average.
That matches what a test copy of the code counts: 100,000 pushes followed by 100,000 pops move values between the stacks exactly 100,000 times, once per value.
Step by step
- Declare
in_stack, aVec<i32>that takes every pushed value on top. The end of the vector is the top. - Declare
out_stack, also aVec<i32>, from which values leave in order, oldest on top. - In
new, return the struct with two empty vectors. - In
push, pushxontoin_stack. - In
peek, start anifthat runs only whenout_stackis empty. - Inside it, loop while
in_stackis not empty. - Move one value per round: pop it from
in_stack, unwrap theOption, and push it ontoout_stack. - After the
if, return the top ofout_stackwithout removing it. - In
pop, callself.peek()and keep the result infront. It moves the values if needed and finds the front. - Remove the top of
out_stack. - Return
front. - In
empty, return whether both stacks are empty.
The solution in Rust
struct MyQueue {
in_stack: Vec<i32>,
out_stack: Vec<i32>,
}
impl MyQueue {
fn new() -> MyQueue {
MyQueue { in_stack: Vec::new(), out_stack: Vec::new() }
}
fn push(&mut self, x: i32) {
self.in_stack.push(x);
}
fn peek(&mut self) -> i32 {
if self.out_stack.is_empty() {
while !self.in_stack.is_empty() {
self.out_stack.push(self.in_stack.pop().unwrap());
}
}
*self.out_stack.last().unwrap()
}
fn pop(&mut self) -> i32 {
let front = self.peek();
self.out_stack.pop();
front
}
fn empty(&self) -> bool {
self.in_stack.is_empty() && self.out_stack.is_empty()
}
}
Using it, after push(1) and push(2), the calls peek(), pop() and empty() print 1, 1 and false. For a new queue, empty(), push(7), empty(), pop() and empty() give true, false, 7 and true. Pushing 1 to 3, popping once, pushing 4 and 5 and then popping until the queue is empty returns 1, 2, 3, 4, 5.
The Rust details worth a closer look:
&mut selfonpeek. Looking at the front can move values between the stacks, sopeekchanges the struct even though the queue's contents stay the same.emptyonly reads, so it takes&self.- A
Vecas a stack.push,popandlastall work at the end of the vector inO(1).popreturns anOption<i32>,Nonewhen empty. self.out_stack.push(self.in_stack.pop().unwrap()). The two fields are borrowed separately, so this compiles.unwrap()is safe because the loop only runs whilein_stackhas values.*self.out_stack.last().unwrap().last()returns anOption<&i32>, a reference to the top. The*copies the number out of it. The last line has no semicolon, so it is the return value.self.out_stack.pop();as a statement. It returns the removed value in anOption, and the code throws it away becausefrontalready holds the number.- Calling a method on
self.self.peek()insidepopneeds&mut selftoo, whichpopalready has.
Complexity
Time. push, empty and the final removal in pop are O(1). peek and pop are O(1) amortized: one call may move every value, but each value moves once in its life, so n operations take O(n) in total.
Space. O(n) for the n values in the queue, split across the two vectors.
Edge cases and common mistakes
- Moving while
out_stackstill has values. The newer values would land on top of the older ones, and the queue would return them in the wrong order. Afterpush(1),push(2),pop(),push(3), the nextpop()must return 2, not 3. - Moving everything back after each
pop. It is correct butO(n)per call. - Peeking twice. The second
peekfindsout_stacknon-empty and moves nothing. After pushing 1 to 4, two calls return 1 both times, andout_stackholds[4, 3, 2, 1]with the 1 on top. - Checking only one stack in
empty. After the firstpopin the second example,out_stackholds 2 andin_stackis empty; afterpush(3)the 3 is inin_stack. Either stack alone would give a wrong answer at some point. - Popping an empty queue. The constraints rule it out. In this code,
last().unwrap()would panic if both stacks were empty. - Forgetting that
popmust return the front. Read it withpeekbefore removing it, as the code does.
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.
