Problem overview
A queue gives values back in the order they arrived; a stack gives back the newest first. You write the class MyQueue, a queue built from two Stack<int> objects, using only what a stack offers: Push, Pop, Peek and Count. The methods are Push(x) (add at the back), Peek() (the front value, not removed), Pop() (remove and return the front value) and 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. After it is popped, 2 is still inside, so the queue is not empty. (The lesson's tests print booleans as True and False; the method itself returns the C# values true and false.)
In the second example the order matters. After pushing 1 and 2 and popping 1, the queue pushes 3. A Peek() returns 2, not 3, because 2 has been waiting longer. Then Pop() returns 2, Pop() returns 3, and Empty() is true.
Approach: reverse once, and reuse the reversal
Popping a stack returns the newest value, and a queue needs the oldest. Moving the values from one stack to another one at a time reverses their order: the newest leaves first and lands at the bottom, the oldest leaves last and lands on top.
The naive version moves everything to the second stack for each Pop, takes the top, and moves everything back. That costs O(n) per call for n values in the queue.
The better version keeps the values on the second stack after moving them. The class has two stacks with different jobs:
inStackreceives every pushed value. Its top is the newest value.outStackholds values already in leaving order: its top is the oldest value.
When the front is needed and outStack has values, its top is the answer; nothing moves. Only when outStack is empty are all values of inStack moved over. Values still in outStack arrived before everything in inStack, so they must leave first.
Here is the state after pushing 1, 2 and 3, then calling Peek(). Each stack is listed from its top:
before Peek: inStack 3 2 1 | outStack (empty)
Peek returns 1
after Peek: inStack (empty) | outStack 1 2 3
After Pop() removes the 1 and Push(4) follows, inStack holds 4 and outStack holds 2 and 3 (2 on top). The next two pops return 2 and then 3 without moving anything. The 4 only moves when outStack runs out.
Why it is amortized O(1)
One Peek or Pop can move many values, so a single call can cost O(n). But count the moves per value instead of per call. A value is pushed on inStack once, moved to outStack at most once, and popped from outStack once. That is at most three stack operations per value in its whole life. So n operations on the queue cost O(n) in total, and each costs O(1) on average. This is what amortized O(1) means: a rare expensive call is paid for by the many cheap ones.
Step by step
- Declare
inStackandoutStackasStack<int>fields, each set tonew Stack<int>(). PushputsxoninStack.Peekchecks whetheroutStack.Countis 0. If so, awhileloop pops each value ofinStackand pushes it ontooutStack.- After the
if,PeekreturnsoutStack.Peek(). Whether or not values were just moved, the oldest value is now on top. Popcalls the class's ownPeek()and stores the result infront, becausePeekalready does the moving. It then popsoutStackand returnsfront.Emptyis true when both stacks have aCountof 0, joined with&&: the queue's values are split between them.
The solution in C#
class MyQueue {
Stack<int> inStack = new Stack<int>();
Stack<int> outStack = new Stack<int>();
public void Push(int x) {
inStack.Push(x);
}
public int Peek() {
if (outStack.Count == 0) {
while (inStack.Count > 0) {
outStack.Push(inStack.Pop());
}
}
return outStack.Peek();
}
public int Pop() {
int front = Peek();
outStack.Pop();
return front;
}
public bool Empty() {
return inStack.Count == 0 && outStack.Count == 0;
}
}
C#'s Stack<T> has Push, Pop, Peek and Count, and all of them work at the top. Peek() and Pop() on an empty stack throw InvalidOperationException; the problem promises they are only called on a non-empty queue. Inside a class, Pop calls Peek() by its name alone.
Complexity
- Time:
PushandEmptyare O(1).PeekandPopare amortized O(1): a single call may take O(n), but n operations take O(n) in total. - Space: O(n) for the n values stored in the two stacks.
Edge cases and common mistakes
- Moving when
outStackis not empty. This puts newer values beneath older ones and breaks the order. In the second example, 3 must not move while 2 is still inoutStack. - Moving everything back after each pop. That keeps the queue correct but makes every call O(n).
- Checking only one stack in
Empty. After a move, all the values may be inoutStack; after pushes, all may be ininStack. - Removing before reading.
Popreads the front withPeekfirst, so the value is still available to return.
Testing
The third example starts with an empty queue: Empty() is true, then Push(7) makes it false, Pop() returns 7, and Empty() is true again. The four results are True False 7 True in the tests' output format.
