Back to the course
Data Structures & Algorithms · Module 05 of 10

Queues

First in, first out. Ring buffers, deques, and the queue behind every breadth-first search.

Build
Ring buffer, two-stack queue
Solve
Josephus, sliding-window max, rate counter
The finish line — what go test shows once every exercise in this module passes
$ go test -v ./05-queues/ | grep '^--- ' --- PASS: TestRingQueueBasics (0.00s) --- PASS: TestRingQueueWrapAndGrow (0.00s) --- PASS: TestStackQueue (0.00s) --- PASS: TestJosephus (0.00s) --- PASS: TestSlidingWindowMax (0.00s) --- PASS: TestRecentCounter (0.00s)
Module 5 · tests 6
WHY

Why it matters.

A queue serves things in arrival order: enqueue at the back, dequeue from the front. Job schedulers, print spoolers, message brokers, network buffers, and rate limiters are all queues. In algorithms, the queue is what makes breadth-first search explore level by level — which you'll lean on for trees (Module 7) and graphs (Module 9).

IDEAS

The core ideas.

FIFO. Enqueue 1, 2, 3 and you dequeue 1, 2, 3.

Ring buffer. Store elements in a fixed array and track where the front is. Both the head and the back "wrap around" with modulo: the element at logical position i lives at buf[(head + i) % len(buf)]. Nothing ever shifts, and freed slots at the front get reused. When it's full, allocate a bigger buffer and copy elements over in order.

Queue from two stacks. Push new items onto an in stack. To dequeue, pop from an out stack; when out is empty, pour all of in into it, which reverses the order. Each element moves at most twice, so it's amortized O(1).

Deque (double-ended queue). Push and pop at both ends. The monotonic deque is the queue version of Module 4's monotonic stack: it keeps candidates in decreasing order so the front is always the current window's maximum.

COST

Cost of operations.

OperationRing bufferSlice q = q[1:]Two stacks
EnqueueO(1) amortizedO(1) amortizedO(1)
DequeueO(1)O(1)O(1) amortized
Reuses freed front slotsYesNoYes
IN GO

Go specifics.

  • The quick slice queue — q = append(q, v) and q = q[1:] — works and is fine for a BFS that runs once. But reslicing from the front never reclaims those slots; a long-lived queue keeps reallocating. A ring buffer doesn't.
  • A buffered channel is a ring buffer, with locking added. It's the right queue between goroutines. Don't use one as a plain queue inside a single goroutine: sending to a full channel blocks forever, and the runtime stops the program with fatal error: all goroutines are asleep - deadlock!
  • container/list gives you a deque, at the cost of an allocation per element and any values.
EXAMPLE

Worked example: BFS on a grid.

A queue of coordinates finds the shortest path length in a maze (# is a wall). Notice cells are marked when they're enqueued, so nothing gets queued twice:

func shortestSteps(maze []string, sr, sc, tr, tc int) int {
	type cell struct{ r, c, d int }
	seen := map[[2]int]bool{{sr, sc}: true}
	q := []cell{{sr, sc, 0}}
	for len(q) > 0 {
		cur := q[0]
		q = q[1:]
		if cur.r == tr && cur.c == tc {
			return cur.d
		}
		for _, d := range [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
			r, c := cur.r+d[0], cur.c+d[1]
			if r < 0 || r >= len(maze) || c < 0 || c >= len(maze[r]) ||
				maze[r][c] == '#' || seen[[2]int{r, c}] {
				continue
			}
			seen[[2]int{r, c}] = true
			q = append(q, cell{r, c, cur.d + 1})
		}
	}
	return -1
}

Because BFS explores in order of distance, the first time it reaches the target is the shortest route.

PATTERNS

Patterns to recognize.

  • BFS — shortest path in unweighted graphs and grids, level-order traversal.
  • Simulation — round-robin scheduling, Josephus-style elimination, "hot potato".
  • Monotonic deque — sliding-window max/min in O(n).
  • Time windows — drop events older than the window from the front (rate limiting).
EXERCISES

Your exercises.

Part A is the ring buffer — the test deliberately moves head into the middle of the buffer and then forces growth while wrapped, which is where most bugs hide. Part B builds the two-stack queue. Part C uses queues for Josephus, sliding-window max, and a request counter.

go test ./05-queues/

The files for this module

Everything below is also in the course download, in 05-queues/. Work in exercises.go; the tests decide when you’re done.

▶ types.go the structs and helpers you’re given · 48 lines›
// Package queue is Module 5: queues (FIFO) and deques.
//
// The naive Go queue — append to enqueue, q = q[1:] to dequeue — works but
// never reuses the front of its backing array. A ring buffer fixes that, and
// it's what you'll build first.
package queue

// RingQueue is a FIFO queue on a circular buffer.
//
//	buf:  [ . . C D E . . ]      head = 2, size = 3
//	             ^ head
//
// The element at logical position i lives at buf[(head+i) % len(buf)].
type RingQueue[T any] struct {
	buf  []T
	head int // index of the front element
	size int // number of elements stored
}

// NewRingQueue returns an empty queue with the given starting capacity (min 1).
func NewRingQueue[T any](capacity int) *RingQueue[T] {
	if capacity < 1 {
		capacity = 1
	}
	return &RingQueue[T]{buf: make([]T, capacity)}
}

// Len returns the number of elements.
func (q *RingQueue[T]) Len() int { return q.size }

// Cap returns the current buffer size.
func (q *RingQueue[T]) Cap() int { return len(q.buf) }

// StackQueue is a FIFO queue built from two LIFO stacks (slices).
// Enqueue pushes onto in; Dequeue pops from out, refilling out from in
// only when out is empty.
type StackQueue[T any] struct {
	in  []T
	out []T
}

// Len returns the number of elements.
func (q *StackQueue[T]) Len() int { return len(q.in) + len(q.out) }

// RecentCounter counts requests in a sliding 3000 ms window.
type RecentCounter struct {
	times []int // timestamps still inside the window, oldest first
}
exercises.go — your work: every function is a stub marked TODO79 lines
//go:build !solutions

package queue

// ============================================================
// Part A — ring-buffer queue (see the diagram in types.go)
// ============================================================

// Enqueue adds v at the back. When the buffer is full, allocate one twice as
// large and copy the elements over in order, front first, resetting head to 0.
// Target: amortized O(1).
func (q *RingQueue[T]) Enqueue(v T) {
	// TODO
}

// Dequeue removes and returns the front, or (zero, false) if empty.
// Advance head with wrap-around. Target: O(1).
func (q *RingQueue[T]) Dequeue() (T, bool) {
	// TODO
	var zero T
	return zero, false
}

// Peek returns the front without removing it, or (zero, false) if empty.
func (q *RingQueue[T]) Peek() (T, bool) {
	// TODO
	var zero T
	return zero, false
}

// ============================================================
// Part B — a queue from two stacks
// ============================================================

// Enqueue adds v at the back. Target: O(1).
func (q *StackQueue[T]) Enqueue(v T) {
	// TODO
}

// Dequeue removes and returns the front, or (zero, false) if empty.
// Only move elements from in to out when out is empty — that's what makes
// it amortized O(1).
func (q *StackQueue[T]) Dequeue() (T, bool) {
	// TODO
	var zero T
	return zero, false
}

// ============================================================
// Part C — queue problems
// ============================================================

// Josephus seats n people (numbered 1..n) in a circle. Counting from person
// 1, every k-th person is removed until one remains; return that survivor.
// Josephus(7, 3) -> 4. Simulate it with your RingQueue: rotate k-1 people
// from front to back, then remove the next one. Target: O(n·k).
func Josephus(n, k int) int {
	// TODO
	return 0
}

// SlidingWindowMax returns the max of each window of size k.
// [1 3 -1 -3 5 3 6 7], k=3 -> [3 3 5 5 6 7]. Return nil if k <= 0 or
// k > len(nums).
// Pattern: monotonic deque of indices — drop the front when it leaves the
// window, drop from the back anything smaller than the new value.
// Target: O(n).
func SlidingWindowMax(nums []int, k int) []int {
	// TODO
	return nil
}

// Ping records a request at time t (ms; t strictly increases between calls)
// and returns how many requests happened in [t-3000, t], inclusive.
// Target: amortized O(1).
func (c *RecentCounter) Ping(t int) int {
	// TODO
	return 0
}
▶ queue_test.go the tests — the same file checks yours or the reference · 117 lines›
package queue

import (
	"slices"
	"testing"
)

func TestRingQueueBasics(t *testing.T) {
	q := NewRingQueue[int](4)
	for i := 1; i <= 3; i++ {
		q.Enqueue(i)
	}
	if v, ok := q.Peek(); !ok || v != 1 {
		t.Fatalf("Peek() = (%d, %v), want (1, true)", v, ok)
	}
	for _, want := range []int{1, 2, 3} {
		if v, ok := q.Dequeue(); !ok || v != want {
			t.Fatalf("Dequeue() = (%d, %v), want (%d, true)", v, ok, want)
		}
	}
	if _, ok := q.Dequeue(); ok {
		t.Error("Dequeue() on empty queue should return ok=false")
	}
	if q.Len() != 0 {
		t.Errorf("Len() = %d, want 0", q.Len())
	}
}

func TestRingQueueWrapAndGrow(t *testing.T) {
	q := NewRingQueue[int](4)
	// Move head into the middle of the buffer so the next writes wrap around.
	for i := 0; i < 3; i++ {
		q.Enqueue(-1)
	}
	for i := 0; i < 3; i++ {
		q.Dequeue()
	}
	for i := 1; i <= 4; i++ {
		q.Enqueue(i) // fills the buffer exactly, wrapped
	}
	if q.Cap() != 4 {
		t.Errorf("Cap() = %d, want 4 (no growth needed yet — are you reusing slots?)", q.Cap())
	}
	for i := 5; i <= 20; i++ {
		q.Enqueue(i) // forces growth while wrapped
	}
	if q.Len() != 20 {
		t.Fatalf("Len() = %d, want 20", q.Len())
	}
	for want := 1; want <= 20; want++ {
		if v, ok := q.Dequeue(); !ok || v != want {
			t.Fatalf("Dequeue() = (%d, %v), want (%d, true) — order broken after growth", v, ok, want)
		}
	}
}

func TestStackQueue(t *testing.T) {
	var q StackQueue[string]
	q.Enqueue("a")
	q.Enqueue("b")
	if v, _ := q.Dequeue(); v != "a" {
		t.Fatalf("Dequeue() = %q, want a", v)
	}
	q.Enqueue("c")
	q.Enqueue("d")
	for _, want := range []string{"b", "c", "d"} {
		if v, ok := q.Dequeue(); !ok || v != want {
			t.Fatalf("Dequeue() = (%q, %v), want (%q, true)", v, ok, want)
		}
	}
	if _, ok := q.Dequeue(); ok {
		t.Error("Dequeue() on empty queue should return ok=false")
	}
}

func TestJosephus(t *testing.T) {
	for _, tc := range []struct{ n, k, want int }{
		{7, 3, 4},
		{5, 2, 3},
		{1, 5, 1},
		{6, 1, 6},
		{41, 3, 31},
	} {
		if got := Josephus(tc.n, tc.k); got != tc.want {
			t.Errorf("Josephus(%d, %d) = %d, want %d", tc.n, tc.k, got, tc.want)
		}
	}
}

func TestSlidingWindowMax(t *testing.T) {
	for _, tc := range []struct {
		in   []int
		k    int
		want []int
	}{
		{[]int{1, 3, -1, -3, 5, 3, 6, 7}, 3, []int{3, 3, 5, 5, 6, 7}},
		{[]int{9, 8, 7, 6}, 2, []int{9, 8, 7}},
		{[]int{1, 2, 3}, 1, []int{1, 2, 3}},
		{[]int{4, 2, 12, 3}, 4, []int{12}},
		{[]int{1}, 2, nil},
	} {
		if got := SlidingWindowMax(tc.in, tc.k); !slices.Equal(got, tc.want) {
			t.Errorf("SlidingWindowMax(%v, %d) = %v, want %v", tc.in, tc.k, got, tc.want)
		}
	}
}

func TestRecentCounter(t *testing.T) {
	var c RecentCounter
	for _, step := range []struct{ t, want int }{
		{1, 1}, {100, 2}, {3001, 3}, {3002, 3}, {7000, 1},
	} {
		if got := c.Ping(step.t); got != step.want {
			t.Fatalf("Ping(%d) = %d, want %d", step.t, got, step.want)
		}
	}
}
Before you open the solution

Try not to open solution.go until your tests pass, or until you’ve been stuck for a solid half hour. Then compare: different-but-correct is fine; notice where the reference is simpler. go test -tags solutions ./05-queues/ runs the same tests against it.

▶ solution.go reference solutions — build tag solutions · 101 lines›
//go:build solutions

package queue

func (q *RingQueue[T]) Enqueue(v T) {
	if q.size == len(q.buf) {
		// Unwrap into a buffer twice the size, front element at index 0.
		bigger := make([]T, len(q.buf)*2)
		for i := 0; i < q.size; i++ {
			bigger[i] = q.buf[(q.head+i)%len(q.buf)]
		}
		q.buf, q.head = bigger, 0
	}
	q.buf[(q.head+q.size)%len(q.buf)] = v
	q.size++
}

func (q *RingQueue[T]) Dequeue() (T, bool) {
	var zero T
	if q.size == 0 {
		return zero, false
	}
	v := q.buf[q.head]
	q.buf[q.head] = zero
	q.head = (q.head + 1) % len(q.buf)
	q.size--
	return v, true
}

func (q *RingQueue[T]) Peek() (T, bool) {
	var zero T
	if q.size == 0 {
		return zero, false
	}
	return q.buf[q.head], true
}

func (q *StackQueue[T]) Enqueue(v T) { q.in = append(q.in, v) }

func (q *StackQueue[T]) Dequeue() (T, bool) {
	var zero T
	if len(q.out) == 0 {
		for len(q.in) > 0 {
			last := len(q.in) - 1
			q.out = append(q.out, q.in[last])
			q.in = q.in[:last]
		}
	}
	if len(q.out) == 0 {
		return zero, false
	}
	last := len(q.out) - 1
	v := q.out[last]
	q.out = q.out[:last]
	return v, true
}

func Josephus(n, k int) int {
	q := NewRingQueue[int](n)
	for i := 1; i <= n; i++ {
		q.Enqueue(i)
	}
	for q.Len() > 1 {
		for i := 1; i < k; i++ {
			v, _ := q.Dequeue()
			q.Enqueue(v)
		}
		q.Dequeue()
	}
	v, _ := q.Dequeue()
	return v
}

func SlidingWindowMax(nums []int, k int) []int {
	if k <= 0 || k > len(nums) {
		return nil
	}
	var dq []int // indices; nums[dq[i]] strictly decreasing
	out := make([]int, 0, len(nums)-k+1)
	for i, v := range nums {
		if len(dq) > 0 && dq[0] <= i-k {
			dq = dq[1:] // front fell out of the window
		}
		for len(dq) > 0 && nums[dq[len(dq)-1]] <= v {
			dq = dq[:len(dq)-1] // can never be a max again
		}
		dq = append(dq, i)
		if i >= k-1 {
			out = append(out, nums[dq[0]])
		}
	}
	return out
}

func (c *RecentCounter) Ping(t int) int {
	c.times = append(c.times, t)
	for c.times[0] < t-3000 {
		c.times = c.times[1:]
	}
	return len(c.times)
}
FURTHER

Going further.

  • Make RingQueue a deque by adding PushFront and PopBack.
  • LeetCode practice: Design Circular Queue, Moving Average from Data Stream, Shortest Subarray with Sum at Least K.
QUIZ

Check yourself.

Three questions. Answer each one in your head — or out loud — before you open the answer.

Question 1

A ring buffer has len(buf) = 8, head = 6, size = 4. Where is the element at logical position 3 (the back)?

  • Abuf[9]
  • Bbuf[1]
  • Cbuf[3]
  • Dbuf[7]
Show the answer

B — (head + i) % len(buf) = (6 + 3) % 8 = 1. The queue occupies slots 6, 7, 0, 1.

Question 2

In a single goroutine, you use a buffered channel of size 10 as a queue and enqueue an 11th item. What happens?

  • AThe channel grows automatically
  • BThe oldest item is dropped
  • CThe send blocks forever; the runtime reports a deadlock
  • DIt returns an error
Show the answer

C — Sends to a full channel wait for a receiver. With no other goroutine, nothing ever receives. Channels are queues between goroutines.

Question 3

For the two-stack queue, what's the cost of Dequeue?

  • AO(1) always
  • BO(n) always
  • CO(n) worst case, O(1) amortized
  • DO(log n)
Show the answer

C — One dequeue may pour the whole in-stack into the out-stack, but each element is moved at most once over its lifetime.

NEXT

Where to next.