First in, first out. Ring buffers, deques, and the queue behind every breadth-first search.
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).
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.
| Operation | Ring buffer | Slice q = q[1:] | Two stacks |
|---|---|---|---|
| Enqueue | O(1) amortized | O(1) amortized | O(1) |
| Dequeue | O(1) | O(1) | O(1) amortized |
| Reuses freed front slots | Yes | No | Yes |
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.fatal error: all goroutines are asleep - deadlock!container/list gives you a deque, at the cost of an allocation per element and any values.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.
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/
Everything below is also in the course download, in 05-queues/. Work in exercises.go; the tests decide when you’re done.
// 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 }
//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 }
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) } } }
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.
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) }
RingQueue a deque by adding PushFront and PopBack.Three questions. Answer each one in your head — or out loud — before you open the answer.
A ring buffer has len(buf) = 8, head = 6, size = 4. Where is the element at logical position 3 (the back)?
B — (head + i) % len(buf) = (6 + 3) % 8 = 1. The queue occupies slots 6, 7, 0, 1.
In a single goroutine, you use a buffered channel of size 10 as a queue and enqueue an 11th item. What happens?
C — Sends to a full channel wait for a receiver. With no other goroutine, nothing ever receives. Channels are queues between goroutines.
For the two-stack queue, what's the cost of Dequeue?
C — One dequeue may pour the whole in-stack into the out-stack, but each element is moved at most once over its lifetime.
Commit your work before you move on to module 6, so git log shows your own progress through the course.
Last in, first out. Matching, parsing, undo, and the monotonic-stack trick.
6Hash tables, collisions, and load factors — then the map-powered patterns that turn O(n²) into O(n).
⌂All ten modules, the course download, and how to track your progress.