📈 Algorithms & Complexity
Stacks & queues: LIFO meets FIFO
▶ Open the interactive lesson — free, no signupCtrl+Z, your browser's back button, and the printer's job line all run on the same two tiny structures — about thirty lines of C each.
You'll build both from scratch, then use one to do what your code editor does every time you type a closing bracket: instantly check that every ( and [ has its matching partner.
Arrays let you touch any element any time. Sometimes that freedom is exactly wrong — you want discipline. A stack only lets you touch the newest item (that's the "last in, first out" — LIFO — of the title); a queue only the oldest ("first in, first out" — FIFO). These two constraints turn out to run half of computing: your function calls, your undo history, your print jobs, the part of the operating system that decides which program runs next.
The stack: Last In, First Out
Picture a stack of plates. You can push a plate on top, pop the top one off, or peek at it. The one you get back is always the most recently added — LIFO. An array plus one integer is all it takes:
#include <stdio.h>
#include <stdbool.h>
#define CAP 16
typedef struct {
int data[CAP];
int top; /* count; index of next free slot */
} Stack;
void st_init(Stack *s) { s->top = 0; }
bool st_push(Stack *s, int v) {
if (s->top == CAP) return false; /* full: overflow */
s->data[s->top++] = v;
return true;
}
bool st_pop(Stack *s, int *out) {
if (s->top == 0) return false; /* empty: underflow */
*out = s->data[--s->top];
return true;
}
bool st_peek(const Stack *s, int *out) {
if (s->top == 0) return false;
*out = s->data[s->top - 1]; /* look, don't take */
return true;
}
int main(void) {
Stack s; st_init(&s);
st_push(&s, 10); st_push(&s, 20); st_push(&s, 30);
int v;
while (st_pop(&s, &v)) printf("popped %d\n", v);
return 0;
}$ gcc stack.c -o stack && ./stack popped 30 popped 20 popped 10 # pushed 10,20,30 — got them back REVERSED. That's LIFO.
Note the elegant symmetry: push is data[top++] (store, then step up), pop is data[--top] (step down, then read). top always equals the number of elements, and every operation is a single index tweak — O(1).
🧠 Checkpoint: You push 1, push 2, pop, push 3, pop, pop. In what order did the pops come out?
- 1, 2, 3
- 2, 3, 1
- 3, 2, 1
- 2, 1, 3
Show answer
2, 3, 1 — Pop #1 takes the newest (2). Push 3, then pop #2 takes 3. Pop #3 finally reaches 1. Always the most recent survivor first.
Stacks are everywhere (you've been using one all along)
- The call stack — last function called is first to return. Recursion IS a stack; last lesson's trace was push/pop in disguise.
- Undo (Ctrl+Z) — the most recent change reverts first.
- The back button — most recent page first.
- Matched brackets — the classic interview problem, worked below.
Why does a stack solve bracket matching? Because a closing bracket must match the most recently opened, not-yet-closed one — "most recent first" is literally the stack's job description. Step through checking "([])":
▶ This spot has an interactive trace widget — open the interactive lesson to play with it.
Two easy-to-forget failure modes: a closer arriving when the stack is empty (like ")(" — popping nothing is an underflow bug!), and leftovers at the end (like "(((" — balanced means the stack finishes empty). The trace above skips the empty-check for brevity; your exercise version must not.
🧠 Checkpoint: Using the stack method, why is "([)]" UNbalanced?
- It has an odd number of brackets
- When ')' arrives, the top of the stack is '[', not '('
- The stack overflows
- It ends with ']'
Show answer
When ')' arrives, the top of the stack is '[', not '(' — After pushing '(' then '[', the ')' arrives — but the most recently opened bracket is '[', and a closer must match the TOP of the stack. Mismatched pop → reject. (Bracket count is even; that alone proves nothing.)
The queue: First In, First Out
A queue is the line at the bakery: enqueue at the tail, dequeue from the head, and the first one in is the first one out — FIFO. Naive array version: dequeue from index 0 and shift everything left — O(n), yuck. The classy fix is the circular buffer: let head and tail chase each other around a fixed array, wrapping with modulo:
#include <stdio.h>
#include <stdbool.h>
#define CAP 8
typedef struct {
int data[CAP];
int head; /* index of the oldest element */
int tail; /* index where the next enqueue lands */
int count;
} Queue;
void q_init(Queue *q) { q->head = q->tail = q->count = 0; }
bool enqueue(Queue *q, int v) {
if (q->count == CAP) return false; /* full */
q->data[q->tail] = v;
q->tail = (q->tail + 1) % CAP; /* wrap around! */
q->count++;
return true;
}
bool dequeue(Queue *q, int *out) {
if (q->count == 0) return false; /* empty */
*out = q->data[q->head];
q->head = (q->head + 1) % CAP; /* wrap around! */
q->count--;
return true;
}
int main(void) {
Queue q; q_init(&q);
for (int i = 1; i <= 5; i++) enqueue(&q, i * 10);
int v;
dequeue(&q, &v); printf("first out: %d\n", v);
dequeue(&q, &v); printf("then : %d\n", v);
enqueue(&q, 60); enqueue(&q, 70); enqueue(&q, 80);
enqueue(&q, 90); /* tail wraps: 7 -> 0 ! */
while (dequeue(&q, &v)) printf("%d ", v);
printf("\n");
return 0;
}$ gcc queue.c -o queue && ./queue first out: 10 then : 20 30 40 50 60 70 80 90 # in: 10..90, out: 10..90 — same order. That's FIFO.
Here's the buffer from that program at the moment right after enqueue(90) — the tail ran off the end of the array and wrapped around to slot 0:
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
🧠 Checkpoint: Why bother with the circular buffer instead of dequeuing from index 0 and shifting everything left?
- Shifting would reverse the order
- It saves memory
- Shifting makes dequeue O(n); the ring keeps every operation O(1)
- Plain arrays can’t be dequeued
Show answer
Shifting makes dequeue O(n); the ring keeps every operation O(1) — Shifting n−1 elements on every dequeue is Θ(n) each time. Moving the head index instead is one increment and one modulo — O(1), no matter how big the queue. Same data, smarter bookkeeping.
Scoreboard & variants
| operation | stack (array) | queue (circular) |
|---|---|---|
| insert (push / enqueue) | O(1) | O(1) |
| remove (pop / dequeue) | O(1) | O(1) |
| peek | O(1) | O(1) |
| search for a value | O(n) — and it's rude | O(n) — ditto |
Both can also be built on linked lists (push/pop at the list head; enqueue at a tail pointer): still O(1) per operation and never "full", at the cost of a heap allocation per element and worse cache behavior. Array-backed wins for a known size bound; linked wins for unbounded growth. Circular buffers in particular are the backbone of real systems — keyboard input, audio streaming, network packet rings.
Party trick: recursion and stacks are interchangeable. Any recursive algorithm can be rewritten iteratively with an explicit stack — that's literally what the compiler was doing for you with the call stack. Iterative quicksort? Push the (lo, hi) ranges onto your own stack instead of recursing.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Stacks and queues organize data by when it arrived — but to find things by what they are, in O(1), you need one more trick: hashing.
▶ Practice this lesson interactively (with live gcc)