Stack & Queue
Quick reference
| Stack push | O(1) amortized (array) / O(1) (DLL) |
| Stack pop / peek | O(1) |
| Queue enqueue/dequeue β naive array (removeFirst shifts) | O(1) enqueue / O(n) dequeue |
| Queue enqueue/dequeue β Doubly Linked List backed | O(1) / O(1) |
| Queue enqueue/dequeue β two stacks | O(1) amortized / O(1) amortized |
A stack is a cafeteria tray pile. Trays go on top, and the only tray you can ever grab is the one on top β you can't pull one from the middle without lifting off everything stacked above it first. That's Last In, First Out (LIFO): whatever was added most recently is the first thing to come off.
A queue is a checkout line. New people join at the back, but the person served next is whoever has been waiting longest β the front of the line. That's First In, First Out (FIFO): whatever was added first is the first thing to leave.
Same two operations β add something, remove something β but the two structures disagree about which end removal happens on. A stack removes from the same end it adds to, so pushing 1, 2, 3 and then popping three times hands them back 3, 2, 1: the order is reversed. A queue removes from the opposite end it adds to, so enqueuing 1, 2, 3 and then dequeuing three times hands them back 1, 2, 3: the order is preserved. That single difference β same-end vs. opposite-end removal β is the entire distinction between the two structures.
Two structures, two disciplines
structure Stack:
// built on an array, OR on a Doubly Linked List's head β either works
items // top of stack is items[length - 1], or list.head
structure Queue:
// built on a Doubly Linked List β head is the front, tail is the back
list // -> DoublyLinkedList from Phase 2, empty when list.head is NULLStack via array β push/pop/peek, O(1) amortized
function push(stack, value):
stack.items.append(value) // grow the array by one slot at the end
function pop(stack):
if stack.items is empty:
error "stack underflow"
return stack.items.removeLast() // shrink the array by one slot at the end
function peek(stack):
if stack.items is empty:
error "stack underflow"
return stack.items[length - 1] // look, don't removeStack via a doubly linked list β push/pop at the head, O(1) worst case
function push(stack, value):
insertAtHead(stack.list, value) // Phase 2's operation, verbatim
function pop(stack):
if stack.list.head is NULL:
error "stack underflow"
value = stack.list.head.value
deleteNode(stack.list, stack.list.head) // O(1) β no walk needed
return valueQueue via a doubly linked list β enqueue at tail, dequeue at head, both O(1)
function enqueue(queue, value):
insertAtTail(queue.list, value) // Phase 2's operation β O(1) thanks to the tail pointer
function dequeue(queue):
if queue.list.head is NULL:
error "queue underflow"
value = queue.list.head.value
deleteNode(queue.list, queue.list.head) // O(1) β no walk needed
return valueQueue via two stacks β O(1) amortized per operation, no linked list needed
structure TwoStackQueue:
inStack // absorbs every enqueue
outStack // supplies every dequeue
function enqueue(queue, value):
push(queue.inStack, value)
function dequeue(queue):
if queue.outStack is empty:
while queue.inStack is not empty: // pour everything across, once
push(queue.outStack, pop(queue.inStack))
if queue.outStack is empty: // still empty after pouring β inStack was empty too
error "queue underflow"
return pop(queue.outStack)Stack push/pop/peek are O(1), full stop β no walk, no search, whichever way you build it. Queue is where the choice of structure matters: a plain array's removeFirst shifts every remaining element down one slot, which is O(n) per dequeue. Swap the array for the Phase 2 doubly linked list β or use the two-stack trick β and dequeue drops to O(1).
| Operation | Time | Space | Why |
|---|---|---|---|
| Stack push | O(1) amortized (array) / O(1) (DLL) | O(1) | Array: one write, occasional resize-and-copy averages out. DLL: rewrite a couple pointers around the new head, no resize ever. |
| Stack pop / peek | O(1) | O(1) | Only the top is ever touched β array's last slot or the list's head, no walk either way. |
| Queue enqueue/dequeue β naive array (removeFirst shifts) | O(1) enqueue / O(n) dequeue | O(1) | Removing the front slot leaves a gap; every remaining element must shift down one index to close it. |
| Queue enqueue/dequeue β Doubly Linked List backed | O(1) / O(1) | O(1) | insertAtTail and deleteNode(head) from Phase 2 β the tail pointer and the node's own prev/next mean neither end ever needs a walk. |
| Queue enqueue/dequeue β two stacks | O(1) amortized / O(1) amortized | O(1) extra (two stacks holding the queue's own elements) | Every value is pushed onto inStack once and poured to outStack at most once β total pouring work across n operations is O(n), so O(1) per operation on average, even though a single dequeue can occasionally cost O(n) mid-pour. |
Underflow β pop or dequeue from empty
Popping or dequeuing without checking for empty first dereferences a value that isn't there β a crash, or worse, silently returning garbage. Every pop/dequeue must check emptiness before it touches anything, the same discipline as checking head is NULL before deleting from an empty linked list.
Fixed-size array stack overflowing, or a resizing one hiding an O(n) cost
A stack backed by a fixed-size array can silently overflow β writing past the allocated end β if push never checks capacity first. A resizing array stack avoids that, but only by occasionally copying every existing element into a bigger array: that single push is O(n), not O(1). It's still O(1) amortized over many pushes, but treating every individual push as cheap is wrong if you're reasoning about worst-case latency for one specific operation, not the long-run average.
Naive array queue degrading to O(n) per operation
It's tempting to reach for the same array that worked fine for a stack, but a queue removes from the opposite end it adds to β and removing index 0 from an array means shifting every other element down by one to close the gap. Do that on every dequeue and a queue that looks O(1) at a glance is actually O(n) per operation, O(nΒ²) over a full run. The fix is the DLL-backed queue or the two-stack trick above, not a plain array with removeFirst.
Two-stack queue implemented wrong β pouring when outStack isn't empty
outStack's order only stays correct β oldest on top β if it's never disturbed by a fresh pour while it still holds older, not-yet-dequeued values. Pour into it while it's non-empty and the older values that were already correctly ordered on top get buried under a fresh reversed batch, corrupting the FIFO order: a later dequeue can return a value that was actually enqueued more recently than one still sitting underneath. The guard "only pour when outStack is empty" is not an optimization β it's what makes the algorithm correct at all.
Sign in to mark problems done β progress syncs across devices.