Design
Implement a queue using two stacks by lazy-transferring: only reverse the input stack into the output stack when the output stack is empty. Implements a stack using two queues by keeping one element in the second queue.
LeetCode 232 presents a classic design challenge: a stack gives you LIFO, last in, first out. A queue needs FIFO, first in, first out. At first glance these feel incompatible, but two stacks together can simulate a queue perfectly, and the key insight is a technique called lazy transfer.
Keep two stacks: an input stack and an output stack. Every push goes onto the input stack. Every pop or peek reads from the output stack. The trick is that the output stack holds elements in reversed order, which is exactly the order a queue wants to serve them.
When you need to pop or peek but the output stack is empty, you drain the entire input stack into the output stack. Because a stack reverses order, pouring input into output un-reverses the elements back to arrival order.
Start with an empty queue. Push 1, 2, 3 in that order.
After push(1): inp=[1] out=[]
After push(2): inp=[1,2] out=[]
After push(3): inp=[1,2,3] out=[]Now call pop(). The output stack is empty, so we transfer: pop everything from inp and push onto out.
Transfer: inp=[] out=[3,2,1]
pop() returns: inp=[] out=[3,2] → 1Push 4, then pop twice more.
After push(4): inp=[4] out=[3,2]
pop() returns: inp=[4] out=[3] → 2
pop() returns: inp=[4] out=[] → 3Now out is empty again. Next pop triggers another transfer: inp drains into out.
Transfer: inp=[] out=[4]
pop() returns: inp=[] out=[] → 4Every element was served in the order 1, 2, 3, 4, exactly FIFO.
class MyQueue:
def __init__(self):
self.inp = [] # new elements land here
self.out = [] # elements leave from here
def push(self, x: int) -> None:
self.inp.append(x)
def pop(self) -> int:
self.peek() # ensure out is populated
return self.out.pop()
def peek(self) -> int:
if not self.out: # lazy transfer only when needed
while self.inp:
self.out.append(self.inp.pop())
return self.out[-1]
def empty(self) -> bool:
return not self.inp and not self.outEach element is moved at most twice: once when it's pushed onto inp, and once when it's transferred to out. After that it just gets popped off out and is gone. Averaged over n operations, the transfer cost is per element. Individual pops may be in the worst case (when a large transfer happens), but the total work over any sequence of operations is , giving amortized per operation.
This is a classic example of amortized analysis: you pay a small cost repeatedly to avoid a large accumulated debt.
With LeetCode 225, going the other direction is more expensive. Queues only expose their front, so to get LIFO behavior you have to rearrange elements on every operation.
The typical approach makes push expensive. When you push a new element, enqueue it into a temporary queue, then drain the main queue into it behind the new element. Now the new element is at the front, which is where you want it for a pop.
from collections import deque
class MyStack:
def __init__(self):
self.q = deque()
def push(self, x: int) -> None:
self.q.append(x)
# rotate so new element is at the front
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self) -> int:
return self.q.popleft()
def top(self) -> int:
return self.q[0]
def empty(self) -> bool:
return not self.qEvery push costs because it rotates the whole queue. There is no way to achieve amortized for both push and pop when using only queues. The asymmetry is fundamental.
LeetCode 155 poses a closely related design question: support push, pop, top, and get_min all in . The standard solution pairs each element with the minimum seen so far.
class MinStack:
def __init__(self):
self.stack = [] # (value, current_min) pairs
def push(self, val: int) -> None:
current_min = min(val, self.stack[-1][1]) if self.stack else val
self.stack.append((val, current_min))
def pop(self) -> None:
self.stack.pop()
def top(self) -> int:
return self.stack[-1][0]
def getMin(self) -> int:
return self.stack[-1][1]By recording the running minimum at every level, each element carries enough information to restore the minimum after it's popped. This is for every operation at the cost of doubling storage.
LeetCode 622 asks you to build a fixed-capacity queue using an array with wrap-around indexing. Instead of shifting elements on every dequeue (which costs ), you maintain a head pointer, a count, and compute the tail position as (head + count) % capacity. Enqueue writes at the tail; dequeue advances the head. Both are .
class MyCircularQueue:
def __init__(self, k: int):
self.data = [0] * k
self.head = 0
self.count = 0
self.capacity = k
def enQueue(self, value: int) -> bool:
if self.count == self.capacity:
return False
tail = (self.head + self.count) % self.capacity
self.data[tail] = value
self.count += 1
return True
def deQueue(self) -> bool:
if self.count == 0:
return False
self.head = (self.head + 1) % self.capacity
self.count -= 1
return True
def Front(self) -> int:
return -1 if self.count == 0 else self.data[self.head]
def Rear(self) -> int:
if self.count == 0:
return -1
tail = (self.head + self.count - 1) % self.capacity
return self.data[tail]
def isEmpty(self) -> bool:
return self.count == 0
def isFull(self) -> bool:
return self.count == self.capacityUsing count rather than separate head/tail pointers avoids the classic ambiguity where head == tail could mean either full or empty. All operations are worst case, and the fixed array means space with no dynamic allocation.
For the two-stack queue: each operation is amortized and worst case; space is . For the stack-from-queues: push is , pop is , space is . For min stack: all operations , space . For circular queue: all operations worst case, space .
You're probably looking at stack/queue emulation when:
Common templates:
in stack for pushes, out stack for pops; transfer lazily when out is empty. Example: Implement Queue using Stacks.Practice Problems (12)