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 class MyQueue out of two stacks, using only what a stack does: add on top, look at or remove the top, and check whether it is empty. The methods are:
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()returnsTrueif the queue holds nothing.
The tests call peek and pop only on a non-empty queue. Example: after push(1) and push(2), peek() returns 1 and pop() returns 1, because 1 arrived first. Then empty() is False, since 2 is still waiting. The tests print booleans as True and False, which is also how Python spells them.
Approach: reverse once, and only when needed
A stack's top is its newest value, but a queue needs its oldest one. Moving values from one stack to another reverses their order: the newest value leaves first and lands deepest, and the oldest leaves last and ends up on top. So a second stack can turn stack order into queue order.
A simple version does this on every operation: move everything to the second stack, take the top, and move everything back. Each pop then costs O(n).
The better version moves values lazily with two stacks:
inStackreceives every pushed value. Its top is the newest value.outStackgives values back. Its top is the front of the queue.
Values are moved from inStack to outStack only when outStack is empty. While it still holds values, all of them arrived before anything in inStack, so its top is still the front. New values can pile up in inStack and wait.
Why each operation is amortized O(1)
A single peek or pop can move many values, so one call can cost O(n). But each value moves at most once: it is pushed on inStack, moved to outStack once, and popped from there. A value on outStack never goes back. So n operations make at most n moves in total, and the average cost per operation is constant. That is what amortized O(1) means.
A counting version of the class confirms it. After 1,000 rounds of 50 pushes followed by 30 pops (80,000 operations), the total number of moves was 39,800, fewer than the 50,000 values pushed. With 100,000 pushes and then 100,000 pops, the total was exactly 100,000 moves, one per value.
Step by step
- In
__init__, setself.inStackto an empty list. The top of the stack is the end of the list, becauseappendandpopboth work at the end. - Set
self.outStackto an empty list too. The values in it are already in leaving order, oldest on top. - In
push, appendxtoself.inStack. - In
peek, write aniffor whenself.outStackis empty. An empty list counts as false, sonot self.outStackis true then. - Inside it, loop while
self.inStackhas values. - In the loop, pop the top of
self.inStackand append it toself.outStackin one line. This is the move that reverses the order. - Back at the method's level, return
self.outStack[-1]. Reading it removes nothing. - In
pop, callself.peek()and keep the result infront.peekalready moves values if needed. - Pop the top of
self.outStack, which is that front value. - Return
front. - In
empty, the values are split across two stacks, so the queue is empty only when both are:not self.inStack and not self.outStack.
The solution in Python
class MyQueue:
def __init__(self):
self.inStack = []
self.outStack = []
def push(self, x: int) -> None:
self.inStack.append(x)
def peek(self) -> int:
if not self.outStack:
while self.inStack:
self.outStack.append(self.inStack.pop())
return self.outStack[-1]
def pop(self) -> int:
front = self.peek()
self.outStack.pop()
return front
def empty(self) -> bool:
return not self.inStack and not self.outStack
Watching the two stacks as the queue is used:
q = MyQueue()
q.push(1)
q.push(2)
q.push(3)
print(q.inStack, q.outStack)
print(q.peek())
print(q.inStack, q.outStack)
q.push(4)
print(q.inStack, q.outStack)
print(q.pop(), q.pop(), q.pop(), q.pop(), q.empty())
[1, 2, 3] []
1
[] [3, 2, 1]
[4] [3, 2, 1]
1 2 3 4 True
The three pushed values sit in inStack in arrival order. The first peek moves them all, which reverses them: 1 is now on top of outStack. The later push(4) waits in inStack and doesn't disturb the order. Only when outStack runs dry does the 4 move over, and the values come out as 1, 2, 3, 4.
Python details worth knowing:
- A list works as a stack because
appendandpop()both act on the end inO(1).pop(0)would act on the front and costO(n), so the code never uses it. - Inside a method, another method of the same object is called through
self, aspopdoes withself.peek(). - Only
append,pop(),[-1]and emptiness checks are used, which are exactly the stack operations the task allows.
Complexity
Time: push and empty are O(1). peek and pop are O(1) amortized: one call may move all n values, but every value moves at most once, so a series of n operations costs O(n) in total.
Space: O(n), for the n values held across the two stacks.
Edge cases and common mistakes
- Moving when
outStackisn't empty. New values would land on top of older ones, and the order would break. In the example above, moving the 4 whileoutStackstill holds 3, 2 and 1 would put it on top, and it would leave before them. - Moving back and forth. Pushing everything back to
inStackafter each pop works but makes every popO(n). - Checking only one stack in
empty. After apeek, all the values may be inoutStackandinStackis empty, and the reverse is also common. - A fresh queue.
empty()isTrue. Afterpush(7)it isFalse, and afterpop()returns 7 it isTrueagain. - Interleaved operations. Pushing between pops is fine: the stale values in
outStackleave first, and the new ones wait their turn.
Testing
q = MyQueue()
print(q.empty())
q.push(7)
print(q.empty(), q.pop(), q.empty())
True
False 7 True
