Queue Using Two Stacks in DSA Rust - Time & Space Complexity
We want to understand how fast operations run when we build a queue using two stacks.
How does the time to add or remove items change as the queue grows?
Analyze the time complexity of the following code snippet.
struct Queue {
stack_in: Vec<i32>,
stack_out: Vec<i32>,
}
impl Queue {
fn enqueue(&mut self, val: i32) {
self.stack_in.push(val);
}
fn dequeue(&mut self) -> Option<i32> {
if self.stack_out.is_empty() {
while let Some(x) = self.stack_in.pop() {
self.stack_out.push(x);
}
}
self.stack_out.pop()
}
}
This code uses two stacks to add items at the end and remove items from the front, mimicking a queue.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Moving all items from
stack_intostack_outwhenstack_outis empty. - How many times: This move happens only when
stack_outis empty, so not every dequeue triggers it. - Other operations: Pushing to
stack_inand popping fromstack_outhappen once per enqueue or dequeue.
When we add items, each enqueue is quick. Removing items sometimes takes longer when we move all items between stacks.
| Input Size (n) | Approx. Operations for n dequeues |
|---|---|
| 10 | About 20 moves (each item moved twice max) |
| 100 | About 200 moves |
| 1000 | About 2000 moves |
Pattern observation: Each item is moved at most twice, so total work grows linearly with input size.
Time Complexity: O(1) amortized per operation
This means on average, each add or remove takes constant time, even if some removals take longer.
[X] Wrong: "Each dequeue always takes time proportional to the number of items because of moving stacks."
[OK] Correct: The moving happens only when the output stack is empty, so most dequeues are fast. The cost spreads out over many operations.
Understanding this method shows you can combine simple tools (stacks) to build more complex ones (queues) efficiently. It's a great example of thinking about average work, not just worst moments.
"What if we moved items from stack_in to stack_out on every dequeue, not only when stack_out is empty? How would the time complexity change?"
Practice
Queue Using Two Stacks?Solution
Step 1: Understand queue order
A queue follows First-In-First-Out (FIFO) order, meaning the first element added is the first removed.Step 2: Use two stacks to reverse order twice
One stack reverses the order when pushing elements, and the second stack reverses it back when popping, preserving FIFO.Final Answer:
Use two stacks to reverse the order twice to maintain queue order -> Option DQuick Check:
Two stacks reverse order twice = queue order [OK]
- Thinking both stacks store elements in sorted order
- Using stacks only for enqueue or dequeue
- Confusing stack order with queue order
Solution
Step 1: Check correct Vec declaration syntax in Rust
In Rust, to declare a mutable vector of i32, use: let mut stack: Vec<i32> = Vec::new();Step 2: Compare options with correct syntax
let mut stack1: Vec<i32> = Vec::new(); let mut stack2: Vec<i32> = Vec::new(); matches the correct syntax exactly; others have syntax errors or missing mutability.Final Answer:
let mut stack1: Vec<i32> = Vec::new(); let mut stack2: Vec<i32> = Vec::new(); -> Option AQuick Check:
Correct Rust Vec declaration = let mut stack1: Vec<i32> = Vec::new(); let mut stack2: Vec<i32> = Vec::new(); [OK]
- Omitting mut keyword for mutable vectors
- Incorrect generic syntax like Vec<i32> without parentheses
- Using Vec::new<i32>() which is invalid
struct Queue {
stack1: Vec<i32>,
stack2: Vec<i32>,
}
impl Queue {
fn new() -> Self {
Queue { stack1: Vec::new(), stack2: Vec::new() }
}
fn enqueue(&mut self, val: i32) {
self.stack1.push(val);
}
fn dequeue(&mut self) -> Option<i32> {
if self.stack2.is_empty() {
while let Some(x) = self.stack1.pop() {
self.stack2.push(x);
}
}
self.stack2.pop()
}
}
fn main() {
let mut q = Queue::new();
q.enqueue(10);
q.enqueue(20);
q.enqueue(30);
println!("{:?}", q.dequeue());
println!("{:?}", q.dequeue());
q.enqueue(40);
println!("{:?}", q.dequeue());
println!("{:?}", q.dequeue());
}Solution
Step 1: Trace enqueue operations
Enqueue 10, 20, 30 pushes these onto stack1 in order: [10, 20, 30]Step 2: Trace dequeue operations
First dequeue: stack2 empty, pop all from stack1 to stack2: stack2 = [30, 20, 10]. Pop from stack2 returns 10.
Second dequeue: pop from stack2 returns 20.
Enqueue 40: push 40 to stack1.
Third dequeue: pop from stack2 returns 30.
Fourth dequeue: stack2 empty, move 40 from stack1 to stack2, pop returns 40.Final Answer:
Some(10) Some(20) Some(30) Some(40) -> Option AQuick Check:
Queue order preserved by two stacks = Some(10) Some(20) Some(30) Some(40) [OK]
- Assuming stack2 pops in same order as stack1
- Not moving elements from stack1 to stack2 when stack2 is empty
- Confusing order of elements after transfer
fn dequeue(&mut self) -> Option<i32> {
while let Some(x) = self.stack1.pop() {
self.stack2.push(x);
}
self.stack2.pop()
}Solution
Step 1: Analyze the dequeue logic
The code moves all elements from stack1 to stack2 every dequeue call, regardless of stack2's state.Step 2: Identify correct behavior
To maintain queue order efficiently, elements should be moved only if stack2 is empty. Otherwise, stack2's elements should be popped directly.Final Answer:
It moves elements from stack1 to stack2 every time, even if stack2 is not empty -> Option BQuick Check:
Move only if stack2 empty = It moves elements from stack1 to stack2 every time, even if stack2 is not empty [OK]
- Moving elements every dequeue wastes time
- Ignoring stack2's current elements
- Not popping from stack2 after transfer
peek function for the queue using two stacks in Rust that returns the front element without removing it. Which approach correctly achieves this?Solution
Step 1: Understand peek operation in queue using two stacks
Peek should return the front element without removing it. The front is at the bottom of stack1 or top of stack2 after transfer.Step 2: Correct approach for peek
If stack2 is empty, move all elements from stack1 to stack2 to reverse order. Then peek the last element of stack2 (top) without popping.Final Answer:
If stack2 is empty, move all elements from stack1 to stack2, then return the last element of stack2 without popping -> Option CQuick Check:
Peek uses stack2 top after transfer if empty = If stack2 is empty, move all elements from stack1 to stack2, then return the last element of stack2 without popping [OK]
- Peeking stack1 directly ignores queue order
- Popping to peek then pushing back wastes time
- Ignoring stack2 state before peeking
Ready to prove it?
Pass the test and this pattern moves unknown
