Arrays and lists · 15 min

Stacks and queues: work at the ends

Last in, first out and first in, first out: why both are O(1) on an array, and how a ring buffer saves a queue from shifting.

Builds onArrays

01Why it matters

Newest first, or oldest first

You press undo, and the last thing you typed disappears, then the thing before it. A printer works the other way round: the job sent first prints first, however many arrive after it.

Those are the two simplest rules for what comes back out of a collection. A stack hands back the newest item and a queue the oldest. Both only ever touch their ends, so both can sit on an array without the shifting that makes an array's front expensive. For the queue that takes one trick: letting the array wrap around into a ring.

02Key parts

The pieces you will see

stack (LIFO)
Last in, first out. Push puts a value on top, pop takes the top one off, and peek reads it without removing it.
queue (FIFO)
First in, first out. Enqueue adds a value at the back and dequeue removes the one at the front.
top, head and tail
Indices that mark the ends. A stack's top points at its newest value. A queue's head points at its front value, and its tail at the free slot where the next value joins.
ring buffer
An array whose last slot is followed by slot 0 again. An index that steps past the end wraps around: next = (i + 1) mod capacity.
deque
A double-ended queue, said "deck": values go in and come out at both the front and the back.

03See it work

Move an index, not the values

This stack holds 7, 23 and 42 in an array of 8 slots. Slot 0 is the bottom, and the index top marks the newest value, so 42 is on top.

Push 99, then pop twice. 99 goes into slot 3, just above 42. The first pop returns 99 and the second 42: the last value in is always the first out.

Push moves top up one and writes one slot; pop reads one slot and moves top down. Nothing below the top is touched, so both are O(1) whether the stack holds 3 values or 3 million. A stack only uses the end of its array, which is the cheap end, so a Python list or a JavaScript array is already a good stack: append and pop work on the last element.

A queue is harder. Values join at the back and leave from the front, and the front is an array's expensive end: removing index 0 shifts every remaining value one cell left, so each dequeue would be O(n). A queue avoids that by moving the front instead of the values. An index, head, marks the front value, and dequeue reads that slot and steps head forward.

Enqueue 99, then dequeue twice. 99 joins at the back in slot 3. Dequeue returns 7 and then 23, the values that have waited longest, and only head moves: the values stay in their slots.

As head steps forward it leaves free slots behind it, at the start of the array, while tail runs on towards the end. When tail steps past the last slot it wraps around to slot 0 and reuses them, so the array behaves like a ring. Both indices step with (i + 1) mod capacity, and the next free slot is always tail = (head + count) mod capacity.

Head is at slot 4 and the values fill slots 4 to 6. Enqueue twice: the first value lands in slot 7 and tail wraps to 0, so the second lands in slot 0. The Ring and Line switch draws the same buffer as a circle or as a row with a wrap arrow.

04Predict

Your turn: follow head and tail

Before each step, predict which value comes out and where head or tail points next. Enqueue until tail wraps, then dequeue a few times and watch head follow it round.

Make 5 predictions: step forward and answer each question before the next step plays.

0 of 5 made

05Try it

Stacks and queues at work

A stack fits whenever the most recent thing must be dealt with first. Matching brackets is the classic case: in {[()]} every closing bracket must match the most recently opened bracket that is still open, and in ([)] the ) comes too early. So push each opening bracket, and at each closing bracket pop one and check that the two match.

def balanced(text):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in text:
        if ch in '([{':
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
    return not stack

print(balanced('{[()]}'))
print(balanced('([)]'))

The same shape turns up everywhere. A program's call stack pushes a frame for each call and pops it on return, so the most recent call always finishes first (the recursion lesson shows it frame by frame). An editor's undo history is a stack, and so is the list of places a depth-first search still has to go back to.

A queue fits whenever work must be handled in the order it arrived: print jobs, requests waiting for a server, messages passed between programs. A breadth-first search keeps the nodes it has found but not yet explored in a queue, and that is what makes it visit a graph level by level: the nodes found first are explored first. The queueing lesson follows requests through the queue in front of a busy server.

The trap is building a queue on a plain list. Appending at the back is cheap, but taking from the front is not.

A deque lets values in and out at both ends. In a ring it is a queue with two more moves: push front steps head back one slot, wrapping from slot 0 to the last slot, and pop back steps tail back one slot.

Push 5 at the front, then pop the back. head steps back from slot 2 to slot 1 for the 5; pop back steps tail back to slot 4 and returns 42. Use one end only and a deque is a stack; push at one end and pop at the other and it is a queue.

The ring raises one more question: how does a queue know it is full? An empty queue has tail = head. Enqueue until all 8 slots are used and tail comes all the way round to head again. The two indices alone can't tell an empty ring from a full one, so the queue also keeps count. (Another common fix is to leave one slot always empty.)

06Check yourself

Check yourself

  1. Question 1 of 7

    You push 3, 1 and 4 onto an empty stack, and enqueue 3, 1 and 4 into an empty queue. Then you pop once and dequeue once. What comes out?

    Your answer comes back in Review in a few days, so it sticks.

  2. Question 2 of 7

    Why does a queue on a ring buffer dequeue faster than one that removes index 0 of a plain array?

  3. Question 3 of 7

    A ring queue of 8 slots has head = 6 and holds 3 values. Which slot does tail point to?

  4. Question 4 of 7

    A ring queue of 8 slots has head = 5, and tail has just wrapped to 0. Can it take another value?

  5. Question 5 of 7

    A breadth-first search keeps its waiting nodes in a Python list, adding them with append and taking them with pop(0). What is wrong with that on a big graph?

  6. Question 6 of 7

    Which of these needs a queue rather than a stack?

  7. Question 7 of 7

    A deque holds 7, 23 and 42 from front to back. You push 5 at the front, then pop the back, then pop the front. What do the two pops return?

07Explain it back

Say it in your own words

How do a stack and a queue differ in what they hand back? And how does a ring buffer let a queue on an array dequeue in O(1) instead of O(n)?

08Recap

What to remember

  • A stack is last in, first out. Push and pop only touch the top, which on an array is an index, so both are O(1).
  • A queue is first in, first out: enqueue at the back, dequeue at the front. Removing index 0 of a plain array shifts everything, so a list with pop(0) is an O(n) queue.
  • A ring buffer moves head and tail instead of the values, wrapping with (i + 1) mod capacity, so enqueue and dequeue are O(1). It is full when count reaches the capacity, not when tail wraps to 0.
  • A deque is O(1) at both ends, so it can be a stack or a queue. Stacks run the call stack, undo and bracket matching; queues run breadth-first search, print jobs and request queues.
  • Next: linked lists, another way to get cheap ends, and breadth-first search, which explores a graph with a queue. Practise with Valid Brackets and Stack with Minimum, or open the stack, queue and deque visualisers to try more operations.
Back to path