01 Variables & Types

What it is

Named storage locations bound to a specific type. Go is statically typed — the compiler knows every variable's type at compile time. Zero values are guaranteed; there is no uninitialised memory.

Real-world analogy

Labelled boxes in a warehouse. The label (type) dictates what shape of item fits inside. You can relabel a box only by explicitly converting its contents — never silently.

How it works internally

Small, short-lived variables live on the goroutine's stack. Variables whose address escapes (returned pointer, captured by closure) are moved to the heap by the compiler's escape analysis. Zero values are written by the runtime before any code runs.

When to use

Use := inside functions for brevity. Use var at package level or when the zero value is the intent. Use const for compile-time fixed values and iota for enum-style sequences.

Complexity
Read/Write O(1)Stack alloc O(1)Heap alloc O(1) amortised
Declaration Styles
variables.go
package main

import "fmt"

func main() {
    // var declaration — explicit type
    var x int = 10
    var s string = "hello"

    // Short declaration — type inferred (ONLY inside functions)
    y := 42
    name := "gopher"

    // Multiple assignment
    a, b := 1, 2
    a, b = b, a // swap — no temp variable needed

    // Zero values (default when declared without init)
    var i int     // 0
    var f float64 // 0.0
    var bl bool   // false
    var str string // ""
    var p *int    // nil

    // Constants
    const Pi = 3.14159
    const (
        KB = 1024
        MB = KB * 1024
        GB = MB * 1024
    )

    // iota — auto-incrementing constant
    const (
        Sunday = iota // 0
        Monday        // 1
        Tuesday       // 2
    )

    fmt.Println(x, s, y, name, a, b, i, f, bl, str, p, Pi, KB, Sunday, Monday, Tuesday)
}
Type System Quick Reference
TypeZeroNotes
int0Platform size (64-bit on 64-bit systems). Use int64 explicitly for interviews.
int8/16/32/640Prefer int64 to avoid overflow bugs
float32/640.0Use float64 by default
boolfalse
string""Immutable byte slice; len(s) = bytes not runes
byte0Alias for uint8
rune0Alias for int32, Unicode code point
*TnilPointer to T
Type Conversion
conversion.go
// Go is STRICT — no implicit conversion
var i int = 42
var f float64 = float64(i)   // explicit cast
var u uint = uint(f)

// string ↔ int via strconv (NOT string(65) = "A")
import "strconv"
n, err := strconv.Atoi("123")  // string → int
s := strconv.Itoa(n)           // int → string

// string ↔ []byte
bs := []byte("hello")   // string → []byte (copy)
str := string(bs)       // []byte → string (copy)

// string ↔ []rune (for Unicode)
runes := []rune("日本語") // len = 3
bytes := []byte("日本語") // len = 9
⚠ Trap string(65) gives "A", not "65". Always use strconv.Itoa for int→string.

02 Strings & Bytes

What it is

An immutable sequence of bytes. Go strings are UTF-8 encoded, but the language exposes them at the byte level by default. A rune (int32) represents a single Unicode code point.

Real-world analogy

A sealed printed letter — you can read it and make photocopies, but you can't erase and rewrite it in place. To edit, you open the envelope ([]byte conversion), make changes, then re-seal it into a new string.

How it works internally

A string header is just (pointer, length) — two words. Slicing creates a new header sharing the same backing bytes (no copy). range decodes UTF-8 and yields (byteIndex, rune) pairs. Direct index s[i] yields a raw byte.

When to use

Use []byte for mutation and I/O. Use []rune only when you need character-level indexing on non-ASCII input. Use strings.Builder to concatenate in loops — never s += piece which is O(n²).

Complexity
len(s) O(1)Index s[i] O(1)String→[]byte copy O(n)Concat loop O(n²) — use Builder
strings_bytes.go
package main

import "fmt"

func main() {
    s := "hello, 世界"

    // len = bytes, not runes
    fmt.Println(len(s))          // 13 (not 9)
    fmt.Println(len([]rune(s)))  // 9

    // Iterate bytes (WRONG for Unicode)
    for i := 0; i < len(s); i++ {
        fmt.Printf("%d: %c\n", i, s[i]) // s[i] is byte
    }

    // Iterate runes (CORRECT)
    for i, r := range s {
        fmt.Printf("%d: %c (%d)\n", i, r, r)
    }

    // Slicing is byte-based → can corrupt multi-byte runes
    fmt.Println(s[:5])   // "hello" — safe, all ASCII
    // s[:8] would be unsafe if cutting mid-rune

    // String is immutable; convert to []byte to modify
    b := []byte(s)
    b[0] = 'H'
    modified := string(b)
    fmt.Println(modified) // "Hello, 世界"

    // strings.Builder for efficient concatenation (O(n) not O(n²))
    // See stdlib section
}
⚠ Trap for i, v := range si is byte index (can jump by 2-4 for non-ASCII), v is rune. Never assume i++ on rune iteration.

03 Control Flow

What it is

Mechanisms to branch execution (if, switch) and repeat it (for — the only loop keyword in Go). Go's control flow is intentionally minimal but covers every use case.

Real-world analogy

Traffic infrastructure: if/switch are junctions and traffic lights that route you down one road. for is a roundabout — you keep circling until a condition lets you exit.

How it works internally

switch compiles to a jump table or binary search for large cases — no fall-through by default. for range on a slice copies the value into the loop variable each iteration; the index is the only way to modify the original. Map iteration order is randomised by the runtime intentionally.

When to use

Prefer switch over long if/else if chains. Use for range over slices/maps/channels/strings. Use the if err := f(); err != nil init-statement pattern to keep error variables scoped.

Complexity
if/switch O(1) branchfor range O(n)Map range order: non-deterministic
control.go
package main

import "fmt"

func main() {
    // ── if / else if / else ─────────────────────────────
    x := 10
    if x > 5 {
        fmt.Println("big")
    } else if x == 5 {
        fmt.Println("five")
    } else {
        fmt.Println("small")
    }

    // if with init statement (common in Go)
    if n, err := someFunc(); err == nil {
        fmt.Println(n)
    }

    // ── switch ──────────────────────────────────────────
    switch x {
    case 1, 2:
        fmt.Println("one or two")
    case 10:
        fmt.Println("ten")       // NO fallthrough by default
    default:
        fmt.Println("other")
    }

    // switch without expression = if/else chain
    switch {
    case x < 0:
        fmt.Println("negative")
    case x == 0:
        fmt.Println("zero")
    default:
        fmt.Println("positive")
    }

    // ── for (the ONLY loop in Go) ───────────────────────
    // C-style
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }

    // while-style
    n := 0
    for n < 5 {
        n++
    }

    // infinite loop
    for {
        break // or continue
    }

    // range over slice
    nums := []int{1, 2, 3}
    for i, v := range nums {
        fmt.Println(i, v)
    }

    // range over map (ORDER NOT GUARANTEED)
    m := map[string]int{"a": 1, "b": 2}
    for k, v := range m {
        fmt.Println(k, v)
    }

    // range over string gives runes
    for i, r := range "hello" {
        fmt.Println(i, r)
    }

    // range over channel
    ch := make(chan int, 3)
    ch <- 1; ch <- 2; ch <- 3; close(ch)
    for v := range ch {  // exits when channel closed
        fmt.Println(v)
    }
}

func someFunc() (int, error) { return 42, nil }
⚠ Trap Map iteration order is random by design. Never rely on it. Sort keys first if you need determinism.

04 Slices

What it is

A dynamic, resizable view over an underlying fixed array. The fundamental Go sequence type — arrays are rarely used directly. A slice is a 3-field struct: pointer to array, length, capacity.

Real-world analogy

A camera viewfinder over a landscape. Multiple viewfinders can look at overlapping parts of the same scene (shared backing array). Zooming in/out changes what you see but not the landscape itself — until you actually edit it.

How it works internally

append returns a new slice header. If len < cap, it extends in-place (still shares array!). If len == cap, a new larger array is allocated and data is copied. Growth: 2× below 1024 elements, then ~1.25× above.

When to use

Default choice for ordered collections. Pre-allocate with make([]T, 0, n) when size is known — avoids repeated reallocation. Use copy() explicitly whenever you need an independent clone of a sub-slice.

Complexity
Index O(1)Append amortised O(1)Copy O(n)Insert/Delete at middle O(n)

Slices are the most important data structure in Go. A slice is a 3-field header: pointer to underlying array, length, capacity.

slices.go
package main

import "fmt"

func main() {
    // Creation
    var s []int              // nil slice (len=0, cap=0)
    s1 := []int{1, 2, 3}    // literal
    s2 := make([]int, 5)    // len=5, cap=5, all zeros
    s3 := make([]int, 3, 10) // len=3, cap=10

    _ = s; _ = s2; _ = s3

    // append
    s1 = append(s1, 4, 5)   // append multiple
    s1 = append(s1, s2...)  // append slice

    // Slicing [low:high] — shares underlying array!
    a := []int{0, 1, 2, 3, 4, 5}
    b := a[1:4]    // [1 2 3], cap=5
    b[0] = 99      // modifies a[1] too!
    fmt.Println(a) // [0 99 2 3 4 5]

    // copy to avoid aliasing
    c := make([]int, len(b))
    copy(c, b)
    c[0] = 0       // does NOT affect a

    // 2D slice
    matrix := make([][]int, 3)
    for i := range matrix {
        matrix[i] = make([]int, 3)
    }

    // Delete element at index i (preserve order)
    i := 2
    a = append(a[:i], a[i+1:]...)

    // Delete element (fast — swap with last)
    a[i] = a[len(a)-1]
    a = a[:len(a)-1]

    // Stack operations
    stack := []int{}
    stack = append(stack, 1)          // push
    top := stack[len(stack)-1]        // peek
    stack = stack[:len(stack)-1]      // pop
    _ = top

    fmt.Println(len(s1), cap(s1))
}
⚠ Trap — Slice Aliasing Slicing (b := a[1:4]) shares the underlying array. Modifying b modifies a. Use copy() when you need independence. Also: append may or may not allocate — never assume it creates a new slice.
Growth Strategy Go doubles capacity below 1024, then grows by ~1.25x. Pre-allocate with make([]T, 0, n) when you know the size.

05 Maps

What it is

An unordered hash table mapping comparable keys to values. Go's built-in map provides O(1) average-case get, set, and delete. Keys must be comparable (support ==) — slices, maps, and functions cannot be keys.

Real-world analogy

A coat-check room with numbered tickets. You hand in your coat (value) and get a ticket (key). Retrieval is instant — the attendant goes directly to the hanger, regardless of how full the room is.

How it works internally

Go maps use a hash table with 8-element buckets. On overflow, overflow buckets are chained. Load factor triggers rehashing. Iteration order is randomised every program run to prevent reliance on ordering. A nil map reads safely (returns zero value) but panics on write.

When to use

Frequency counting (freq[x]++), membership testing (map[T]struct{} set), grouping, caching/memoisation, and anagram/pattern detection. Always initialise with make or a literal before writing.

Complexity
Get/Set/Delete O(1) avgO(n) worst (hash collision)Iteration O(n)Nil map write = PANIC
maps.go
package main

import "fmt"

func main() {
    // Creation — always use make or literal
    m := make(map[string]int)
    m2 := map[string]int{"a": 1, "b": 2}
    _ = m2

    // CRUD
    m["key"] = 42         // set
    v := m["key"]         // get (returns zero value if missing)
    delete(m, "key")      // delete (no-op if key absent)

    // Comma-ok idiom — check existence
    val, ok := m["key"]
    if !ok {
        fmt.Println("not found")
    }
    _ = val

    // Common patterns
    // Frequency counter
    words := []string{"a", "b", "a", "c", "b", "a"}
    freq := make(map[string]int)
    for _, w := range words {
        freq[w]++  // zero value of int is 0, so safe
    }

    // Set (map[T]struct{} uses less memory than map[T]bool)
    seen := make(map[string]struct{})
    seen["x"] = struct{}{}
    _, exists := seen["x"]
    _ = exists

    // Map of slices
    groups := make(map[int][]string)
    groups[1] = append(groups[1], "a")
    groups[1] = append(groups[1], "b")

    fmt.Println(freq, groups)
}
⚠ Danger — Nil Map Write Writing to a nil map panics: var m map[string]int; m["k"] = 1 // PANIC. Always initialize with make or a literal. Reading from a nil map is safe (returns zero value).
⚠ Trap — Concurrent Access Maps are NOT safe for concurrent read/write. Use sync.RWMutex or sync.Map. This is a common Staff+ follow-up.

06 Functions

What it is

Named, reusable blocks of code that accept inputs and return outputs. Functions are first-class values in Go — they can be stored in variables, passed as arguments, and returned from other functions.

Real-world analogy

A recipe card. It defines steps (body), accepts ingredients (params), and produces a dish — plus a note if something went wrong (multiple return values, idiomatic error as last return). You can hand the card to anyone or pin it to the fridge (first-class/closure).

How it works internally

Arguments are passed by value (copies). Closures capture outer variables by reference to their memory location — the captured variable lives on the heap if it outlives the enclosing scope. defer pushes calls onto a LIFO stack within the current goroutine stack frame, executed on any return path.

When to use

Closures for stateful callbacks, memoisation wrappers, and the var dfs func(...) recursive pattern. Variadic (...T) for flexible APIs. Named returns only when defer needs to modify the return value — avoid otherwise (reduces clarity).

Complexity
Call overhead O(1)Closure capture O(1) per varDefer: LIFO, args evaluated at defer site
functions.go
package main

import (
    "errors"
    "fmt"
)

// ── Basic: multiple return values ────────────────────────
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// ── Named returns (use sparingly, good for defer) ────────
func minMax(nums []int) (min, max int) {
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return  // naked return — returns named vars
}

// ── Variadic ─────────────────────────────────────────────
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// ── First-class functions & closures ─────────────────────
func makeCounter() func() int {
    count := 0
    return func() int {  // closes over count
        count++
        return count
    }
}

// ── Higher-order functions ────────────────────────────────
func filter(nums []int, predicate func(int) bool) []int {
    result := make([]int, 0)
    for _, n := range nums {
        if predicate(n) {
            result = append(result, n)
        }
    }
    return result
}

// ── Defer ─────────────────────────────────────────────────
func deferDemo() {
    // Defers execute LIFO when function returns
    defer fmt.Println("third")   // printed last
    defer fmt.Println("second")
    defer fmt.Println("first")   // printed first
    fmt.Println("body")
}

// ── Defer with named return (advanced) ───────────────────
func readFile() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r) // modifies named return
        }
    }()
    panic("something went wrong")
}

func main() {
    r, err := divide(10, 3)
    fmt.Printf("%.2f %v\n", r, err)

    mn, mx := minMax([]int{3, 1, 4, 1, 5, 9})
    fmt.Println(mn, mx)

    fmt.Println(sum(1, 2, 3))
    s := []int{1, 2, 3}
    fmt.Println(sum(s...)) // unpack slice into variadic

    counter := makeCounter()
    fmt.Println(counter(), counter(), counter()) // 1 2 3

    evens := filter([]int{1,2,3,4,5,6}, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4 6]

    deferDemo()
    fmt.Println(readFile())
}
⚠ Trap — Defer Argument Evaluation Defer arguments are evaluated immediately, but the call is deferred. defer fmt.Println(x) captures x's value now. Use a closure defer func() { fmt.Println(x) }() to capture by reference.
⚠ Trap — Defer in Loop for { defer f() } — all defers stack up, run when function exits (not each iteration). Wrap in helper function or IIFE: func() { defer f(); body() }()

07 Pointers

What it is

A variable that holds the memory address of another variable. Dereferencing a pointer gives you the value at that address. Go pointers are type-safe — a *int can only point to an int. No pointer arithmetic.

Real-world analogy

A postal address. Instead of mailing someone a copy of your entire house (pass by value), you give them the address. They can visit and rearrange the furniture (mutate). nil is an address with no house — ring the bell and crash.

How it works internally

&x takes the address of x, returning *T. *p dereferences to get/set the value. If a pointer escapes its declaring scope (returned or stored), the compiler moves the pointee to the heap via escape analysis. Struct pointer fields are auto-dereferenced: p.Field is sugar for (*p).Field.

When to use

Pointer receiver methods when the method mutates the receiver or the struct is large (avoid copying). Pass pointers to functions that need to modify a value. Use *T as an optional type (nil = absent). Avoid pointer-heavy code in hot paths — cache misses are expensive.

Complexity
Dereference O(1)Address-of O(1)nil deref = PANICHeap alloc → GC pressure
pointers.go
package main

import "fmt"

func increment(n *int) {
    *n++ // dereference and modify
}

func newInt(n int) *int {
    return &n // safe: Go's GC keeps n alive (escapes to heap)
}

func main() {
    x := 10
    p := &x          // p is *int, &x takes address
    fmt.Println(*p)  // 10 — dereference
    *p = 20          // modify via pointer
    fmt.Println(x)   // 20

    increment(&x)
    fmt.Println(x)   // 21

    // new() allocates and returns pointer to zero value
    q := new(int)    // *int pointing to 0
    *q = 5

    // Pointer to struct — auto-dereference
    type Point struct{ X, Y int }
    pt := &Point{1, 2}
    pt.X = 10        // equivalent to (*pt).X = 10

    // nil pointer
    var nilP *int
    fmt.Println(nilP)  // 
    // *nilP = 1  ← PANIC: nil pointer dereference
}
Value vs Pointer Receiver Rule Use pointer receiver when: (1) method modifies the receiver, (2) struct is large (avoid copy), (3) need consistency — if any method is pointer receiver, all should be.

08 Structs, Methods & Interfaces

What it is

Struct: a composite type grouping named fields of different types — Go's primary data-modelling tool. Methods: functions with a receiver, associating behaviour with a type. Interface: an implicit contract of method signatures — satisfied automatically, not declared.

Real-world analogy

Struct is a blueprint; a value is a house built from it. An interface is a job description — anyone who can perform the listed duties qualifies, regardless of background. Embedding is like hiring a specialist (Animal) inside a broader role (Dog) — their skills are promoted to the surface.

How it works internally

Interfaces store a (type pointer, value pointer) pair — an interface is never truly nil unless both are nil. Struct fields are laid out sequentially in memory with alignment padding. Embedding copies the embedded type's method set into the outer type's method set at compile time.

When to use

Define interfaces at the consumer site, not the producer (Go proverb). Use pointer receivers consistently across a type. Use interface{} / any sparingly — prefer generics (Go 1.18+) for type-safe polymorphism. Type switches for runtime dispatch on interface values.

Complexity
Field access O(1)Method dispatch O(1)Interface conversion O(1), but indirect callTyped nil in interface ≠ nil
structs_interfaces.go
package main

import (
    "fmt"
    "math"
)

// ── Struct ────────────────────────────────────────────────
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

type Rectangle struct {
    Width, Height float64
}

// ── Methods ───────────────────────────────────────────────
// Pointer receiver — can mutate, avoids copy
func (c *Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c *Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (c *Circle) Scale(f float64)    { c.Radius *= f }

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }

// ── Embedding (Go's inheritance alternative) ─────────────
type Animal struct{ Name string }
func (a Animal) Speak() string { return a.Name + " speaks" }

type Dog struct {
    Animal        // embedded — Dog promotes Animal's methods
    Breed string
}

// ── Interface satisfaction is implicit ───────────────────
func printShape(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

// ── Empty interface & type assertion ─────────────────────
func describe(i interface{}) {
    // Type assertion
    if s, ok := i.(string); ok {
        fmt.Println("string:", s)
        return
    }
    // Type switch
    switch v := i.(type) {
    case int:     fmt.Println("int:", v)
    case float64: fmt.Println("float64:", v)
    case []int:   fmt.Println("[]int, len:", len(v))
    default:      fmt.Printf("unknown: %T\n", v)
    }
}

func main() {
    c := &Circle{Radius: 5}
    r := Rectangle{Width: 4, Height: 6}

    printShape(c) // *Circle satisfies Shape
    printShape(r) // Rectangle satisfies Shape (value receiver)
    // printShape(&r) also works

    c.Scale(2)
    fmt.Println(c.Radius) // 10

    d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Lab"}
    fmt.Println(d.Speak()) // promoted method: "Rex speaks"
    fmt.Println(d.Name)    // promoted field

    describe(42)
    describe("hello")
    describe(3.14)
    describe([]int{1,2,3})

    // Struct literal
    _ = Circle{Radius: 1}   // named
    _ = Circle{1}           // positional (avoid in production)
}
⚠ Trap — Interface Nil A nil pointer stored in an interface is NOT nil. var c *Circle = nil; var s Shape = c; s == nil // FALSE! The interface holds (type=*Circle, value=nil). Check with reflect.ValueOf(s).IsNil() or restructure to return untyped nil.
★ Staff+ — Generics (Go 1.18+) func Map[T, U any](s []T, f func(T) U) []U — use type parameters for reusable algorithms. Common constraints: comparable, constraints.Ordered from golang.org/x/exp/constraints.

09 Goroutines

What it is

A lightweight, concurrently executing function managed by the Go runtime scheduler — not a direct OS thread. The go keyword spawns one. Goroutines are Go's core concurrency primitive.

Real-world analogy

Waitstaff in a busy restaurant. Many servers (goroutines) work simultaneously, each handling a table (task). A small manager team (OS threads, controlled by GOMAXPROCS) executes the actual work. When a server waits for the kitchen (I/O block), the manager serves another table.

How it works internally

Each goroutine starts with a 2KB stack that grows and shrinks dynamically. The runtime uses M:N scheduling — M goroutines multiplexed onto N OS threads. A goroutine blocked on I/O or channel ops yields the OS thread to another runnable goroutine. GOMAXPROCS (default = CPU count) controls parallelism.

When to use

Any concurrent work: I/O fan-out, background processing, parallel computation. Always answer "who waits for this goroutine?" — use sync.WaitGroup or a done channel. Never fire-and-forget if the goroutine touches shared state or can panic.

Complexity
Spawn O(1)~2–8KB initial stackContext switch ~100ns vs ~1µs OS threadLeaked goroutine = memory leak

Goroutines are lightweight (2KB initial stack, grows as needed). The runtime multiplexes them onto OS threads (GOMAXPROCS). Always think: who waits for this goroutine to finish?

goroutines.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // signal completion even on panic
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(10 * time.Millisecond) // simulate work
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait() // blocks until all Done() calls
    fmt.Println("All workers done")
}
⚠ Trap — Loop Variable Closure Capture for i := 0; i < 5; i++ { go func() { fmt.Println(i) }() } — all goroutines print 5 (the final value). Fix: go func(i int) { fmt.Println(i) }(i) or i := i shadowing inside loop. (Go 1.22+ fixes this by default.)
⚠ Goroutine Leak A goroutine blocked forever (no reader on channel, no done signal) is a leak. Always use context.WithCancel or a done channel for long-running goroutines.

10 Channels

What it is

A typed conduit for communicating between goroutines, combining synchronisation and data transfer. Go's concurrency philosophy: "Do not communicate by sharing memory; share memory by communicating."

Real-world analogy

An unbuffered channel is a direct handoff — like handing a baton in a relay race. Both runners must be ready simultaneously. A buffered channel is a conveyor belt with limited capacity — the sender places items, the receiver picks them up, but the belt can hold only N items before blocking the sender.

How it works internally

Channels are implemented as a ring buffer (buffered) or a direct goroutine wakeup (unbuffered). Blocked goroutines are parked on the channel's send/recv queue and rescheduled by the runtime when space/data is available. close() marks the channel done; buffered data is still drainable. Sending to a closed channel panics.

When to use

Ownership transfer of data between goroutines, pipeline stages, done/cancellation signalling (close(done) broadcasts to all receivers), fan-in merging. Use directional types (chan<-, <-chan) in function signatures to enforce intent.

Complexity
Send/Receive O(1)Unbuffered: blocks until both sides readySend on closed = PANICNil channel blocks forever
channels.go
package main

import "fmt"

func main() {
    // Unbuffered — sender blocks until receiver ready (synchronous)
    ch := make(chan int)
    go func() { ch <- 42 }() // goroutine blocks until we receive
    v := <-ch
    fmt.Println(v) // 42

    // Buffered — sender blocks only when buffer full
    bch := make(chan string, 3)
    bch <- "a"; bch <- "b"; bch <- "c"
    // bch <- "d" would block here (buffer full)
    fmt.Println(<-bch) // "a" (FIFO)

    // close signals "no more values"
    // range over channel reads until closed
    jobs := make(chan int, 5)
    for i := 0; i < 5; i++ { jobs <- i }
    close(jobs)
    for j := range jobs { // exits when closed AND empty
        fmt.Println(j)
    }

    // Check if closed (comma-ok)
    ch2 := make(chan int, 1)
    ch2 <- 1; close(ch2)
    v1, ok := <-ch2 // ok=true, v1=1
    v2, ok := <-ch2 // ok=false, v2=0 (zero value after close)
    fmt.Println(v1, ok, v2)

    // Directional types (enforce in function signatures)
    ping := make(chan string, 1)
    pong := make(chan string, 1)
    go pingFunc(ping, pong)  // send-only ping, recv-only pong
    ping <- "passed message"
    result := <-pong
    fmt.Println(result)
}

// chan<- = send only, <-chan = receive only
func pingFunc(in <-chan string, out chan<- string) {
    msg := <-in
    out <- msg + " pong"
}
⚠ Channel Panics & Blocks Sending to closed channel = panic. Sending/receiving on nil channel = blocks forever. Receiving from closed, empty channel = zero value + ok=false.
OperationNil chOpen emptyOpen with dataClosed emptyClosed with data
ReceiveBlockBlockValueZero, falseValue, true
SendBlockBlock (unbuf)Send or blockPANICPANIC
ClosePANICCloses okCloses okPANICPANIC

11 Select

What it is

A control structure that waits on multiple channel operations simultaneously, executing whichever case becomes ready first. Go's mechanism for multiplexing concurrent communication.

Real-world analogy

A call centre agent with multiple phone lines. They pick up whichever line rings first. If no lines are ringing and there's a default case, they do admin work instead of waiting. If two lines ring at once, they pick one at random.

How it works internally

The runtime evaluates all case channel expressions, then atomically checks each for readiness. If multiple are ready, one is selected uniformly at random. If none are ready and there is no default, the goroutine parks. default makes select non-blocking.

When to use

Timeout: case <-time.After(d). Cancellation: case <-ctx.Done(). Non-blocking probe: default. Fan-in: receive from N channels. Always include a ctx.Done() case in long-running selects to prevent goroutine leaks.

Complexity
O(n) where n = casesMultiple ready: random selectionNo ready + no default: blocks forever
select.go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)
    ch1 <- "one"
    ch2 <- "two"

    // select picks ready channel randomly if multiple ready
    select {
    case msg := <-ch1:
        fmt.Println("ch1:", msg)
    case msg := <-ch2:
        fmt.Println("ch2:", msg)
    }

    // Non-blocking receive (default case)
    select {
    case v := <-ch1:
        fmt.Println("received:", v)
    default:
        fmt.Println("no value ready")
    }

    // Timeout pattern
    resultCh := make(chan int, 1)
    go func() {
        time.Sleep(50 * time.Millisecond)
        resultCh <- 42
    }()

    select {
    case res := <-resultCh:
        fmt.Println("got:", res)
    case <-time.After(100 * time.Millisecond):
        fmt.Println("timeout!")
    }

    // Context cancellation pattern
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel()

    select {
    case res := <-resultCh:
        fmt.Println("result:", res)
    case <-ctx.Done():
        fmt.Println("context:", ctx.Err())
    }
}

12 sync Package

What it is

Package providing low-level shared-memory synchronisation primitives: Mutex, RWMutex, WaitGroup, Once, Map, Cond, and the atomic sub-package for lock-free integer ops.

Real-world analogy

Mutex: a single toilet key passed between co-workers — only one at a time. RWMutex: a library — many people can read simultaneously, but writing requires clearing the room. WaitGroup: a project manager waiting for all contractors to sign off before closing a job.

How it works internally

Mutex uses a futex — fast userspace spin, only a syscall when contended. WaitGroup uses an atomic counter; Wait() parks until counter hits zero. Once combines atomic flag with a mutex for the one-time execution guarantee. atomic uses CPU-level compare-and-swap instructions — no lock needed.

When to use

Mutex: protecting a struct with mixed reads/writes. RWMutex: read-heavy workloads (8:1+ ratio). WaitGroup: wait for N goroutines to finish. Once: lazy singleton init. atomic: single integer counter shared across goroutines. sync.Map: stable key sets with high concurrent read throughput.

Complexity
Lock/Unlock O(1) uncontendedatomic ops O(1) lock-freeRLock: concurrent readers allowedCopying a Mutex after first use = data race
sync_demo.go
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

// ── Mutex ──────────────────────────────────────────────────
type SafeCounter struct {
    mu sync.Mutex
    count int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()  // always defer to avoid forgetting unlock
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// ── RWMutex (read-heavy workloads) ────────────────────────
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()        // multiple concurrent readers allowed
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

func (c *Cache) Set(key, val string) {
    c.mu.Lock()         // exclusive write lock
    defer c.mu.Unlock()
    c.data[key] = val
}

// ── Once (singleton / one-time init) ─────────────────────
var (
    once     sync.Once
    instance *SafeCounter
)

func getInstance() *SafeCounter {
    once.Do(func() {
        instance = &SafeCounter{}
    })
    return instance
}

// ── sync.Map (concurrent map, read-heavy) ────────────────
func syncMapDemo() {
    var sm sync.Map
    sm.Store("key", "value")
    v, ok := sm.Load("key")
    fmt.Println(v, ok)
    sm.Delete("key")
    sm.Range(func(k, v any) bool {
        fmt.Println(k, v)
        return true // continue iteration
    })
}

// ── atomic (lock-free integer ops) ───────────────────────
func atomicDemo() {
    var counter int64
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)
        }()
    }
    wg.Wait()
    fmt.Println(atomic.LoadInt64(&counter)) // 1000
}

func main() {
    c := &SafeCounter{}
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() { defer wg.Done(); c.Inc() }()
    }
    wg.Wait()
    fmt.Println(c.Value()) // 100

    cache := &Cache{data: make(map[string]string)}
    cache.Set("go", "awesome")
    v, _ := cache.Get("go")
    fmt.Println(v)

    fmt.Println(getInstance() == getInstance()) // true — same instance

    syncMapDemo()
    atomicDemo()
}
★ Staff+ — When to use what sync.Mutex: general mutual exclusion. sync.RWMutex: read-heavy (8:1+ reads). sync.Map: stable key sets with high concurrent reads. atomic: single-variable counters (lock-free). Channels: coordination and ownership transfer.

13 Context

What it is

An immutable value that carries deadlines, cancellation signals, and request-scoped data across API and goroutine boundaries. The standard Go mechanism for graceful shutdown and propagating cancellation through a call tree.

Real-world analogy

A project brief handed down through a chain of contractors. If the client cancels the project, every sub-contractor checks their copy of the brief and stops work. The brief also carries a deadline: if it passes, everyone stops regardless of client instructions.

How it works internally

Contexts form an immutable tree. Cancelling a parent atomically closes its Done() channel and propagates cancellation to all children. Each WithCancel/WithTimeout/WithDeadline creates a child node. Done() returns a channel; Err() returns context.Canceled or context.DeadlineExceeded.

When to use

Pass as first argument to any function doing I/O, DB queries, RPCs, or long computation. Always defer cancel() to release resources. Use WithValue only for request-scoped metadata (trace IDs, auth tokens) — never for optional function parameters.

Complexity
WithCancel O(1)Cancellation propagation O(children)Forgetting defer cancel() = goroutine/timer leak
context_demo.go
package main

import (
    "context"
    "fmt"
    "time"
)

func doWork(ctx context.Context, id int) error {
    select {
    case <-time.After(100 * time.Millisecond): // simulate work
        fmt.Printf("worker %d done\n", id)
        return nil
    case <-ctx.Done():
        return ctx.Err() // context.DeadlineExceeded or context.Canceled
    }
}

func main() {
    // WithCancel — manual cancellation
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(50 * time.Millisecond)
        cancel() // trigger cancellation
    }()
    fmt.Println(doWork(ctx, 1)) // context canceled

    // WithTimeout
    ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel2() // ALWAYS defer cancel to free resources
    fmt.Println(doWork(ctx2, 2)) // done (200ms > 100ms)

    // WithDeadline
    deadline := time.Now().Add(30 * time.Millisecond)
    ctx3, cancel3 := context.WithDeadline(context.Background(), deadline)
    defer cancel3()
    fmt.Println(doWork(ctx3, 3)) // deadline exceeded

    // WithValue — pass request-scoped values (NOT for optional params!)
    type ctxKey string
    ctx4 := context.WithValue(context.Background(), ctxKey("requestID"), "abc-123")
    reqID := ctx4.Value(ctxKey("requestID")).(string)
    fmt.Println("requestID:", reqID)
}
Context Rules (1) Always pass ctx as first parameter. (2) Never store ctx in structs. (3) Always defer cancel() to avoid resource leaks. (4) Use typed keys for WithValue to avoid collisions.

14 Concurrency Patterns

What it is

Reusable structural blueprints for orchestrating goroutines. The three foundational patterns are Worker Pool (bounded parallelism), Pipeline (streaming stages), and Fan-Out/Fan-In (parallel work + result merge).

Real-world analogy

Factory floor layouts: a worker pool is shift workers sharing machines — N tasks, M workers, M ≤ N. A pipeline is an assembly line — each stage does one job and passes to the next. Fan-out/fan-in is splitting a shipment among multiple warehouses then consolidating at one distribution centre.

How it works internally

Worker pool: buffered job channel consumed by N goroutines + WaitGroup. Pipeline: each stage owns an input and output channel, closed when done. Fan-out: N goroutines reading from one channel. Fan-in: merge goroutine reads from N channels into one, closes output after WaitGroup finishes.

When to use

Worker pool for rate-limiting API calls, CPU-bound tasks, database writes. Pipeline for streaming ETL, log processing, image transforms. Fan-out/fan-in when N independent tasks produce results that must be merged — avoids O(N) sequential waiting.

Complexity
Worker pool: O(work/N) parallel timePipeline: O(longest stage) throughputCoordination overhead: O(N goroutines)

★ Staff+ Worker Pool

Use when: bounded parallelism, rate-limiting external calls, CPU-bound tasks

worker_pool.go
package main

import (
    "fmt"
    "sync"
)

func workerPool(numWorkers int, jobs []int) []int {
    jobCh := make(chan int, len(jobs))
    resultCh := make(chan int, len(jobs))

    // Start workers
    var wg sync.WaitGroup
    for w := 0; w < numWorkers; w++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobCh { // exit when channel closed
                resultCh <- job * job // simulate work
            }
        }()
    }

    // Send jobs
    for _, j := range jobs { jobCh <- j }
    close(jobCh) // signal no more jobs

    // Close results when all workers done
    go func() { wg.Wait(); close(resultCh) }()

    // Collect results
    var results []int
    for r := range resultCh { results = append(results, r) }
    return results
}

func main() {
    results := workerPool(4, []int{1, 2, 3, 4, 5, 6, 7, 8})
    fmt.Println(results)
}

★ Staff+ Pipeline

Use when: streaming multi-stage data transformations

pipeline.go
package main

import "fmt"

func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums { out <- n }
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in { out <- n * n }
    }()
    return out
}

func double(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in { out <- n * 2 }
    }()
    return out
}

func main() {
    // Chain stages: generate → square → double
    out := double(square(generate(1, 2, 3, 4, 5)))
    for v := range out {
        fmt.Println(v) // 2, 8, 18, 32, 50
    }
}

★ Staff+ Fan-Out / Fan-In

fanout_fanin.go
package main

import (
    "fmt"
    "sync"
)

// Fan-out: distribute work across N goroutines
// Fan-in: merge N channels into one

func merge(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    merged := make(chan int, 10)

    output := func(c <-chan int) {
        defer wg.Done()
        for v := range c { merged <- v }
    }

    wg.Add(len(channels))
    for _, c := range channels { go output(c) }

    go func() { wg.Wait(); close(merged) }()
    return merged
}

func fanOut(in <-chan int, n int) []<-chan int {
    channels := make([]<-chan int, n)
    for i := 0; i < n; i++ {
        ch := make(chan int, 5)
        channels[i] = ch
        go func(out chan<- int) {
            defer close(out)
            for v := range in { out <- v * v }
        }(ch)
    }
    return channels
}

func main() {
    in := make(chan int, 10)
    for i := 1; i <= 10; i++ { in <- i }
    close(in)

    workers := fanOut(in, 3)
    for v := range merge(workers...) {
        fmt.Println(v)
    }
}

15 Two Pointers

What it is

Two index variables scanning a sorted array or string — either from opposite ends (converging) or in the same direction (fast/slow) — to find pairs, subarrays, or positions that satisfy a condition in O(n) instead of O(n²).

Real-world analogy

Two people walking toward each other on a number line. If their combined position overshoots the target, the right person steps left. If it undershoots, the left person steps right. They meet exactly at the answer.

How it works internally

Relies on the sorted invariant: moving the left pointer right increases the sum; moving the right pointer left decreases it. One pass suffices because each pointer moves monotonically — no element is visited twice, giving O(n).

When to use

Sorted array + target sum/difference. Palindrome check. Remove duplicates in-place. Container with most water. Three-sum (sort first, then two-pointer inside). Any "find pair" or "shrink from both ends" problem.

Complexity
Time O(n)Space O(1)Prerequisite: sorted input (or sort first O(n log n))
Concept·Two Pointers
What it is
Two index variables that move through a data structure — usually toward each other from both ends, or in the same direction at different speeds. Eliminates the inner loop of an O(n²) brute force by leveraging sorted order or a monotonic constraint.
Real-world analogy
🔍Two people walking toward each other along a hallway to meet in the middle — far faster than one person checking every spot from the start.
How it works
Place l=0, r=len-1. Evaluate the pair. If condition is met → record and move both. If sum/value too small → advance l. If too large → retreat r. Loop while l < r.
Complexity
Time O(n) Space O(1)
Requires sorted input (or sortable in O(n log n)). One pass after sort.
When to use — recognition cues
sorted array find pair with target sum palindrome check remove duplicates in-place container with most water 3-sum / k-sum partition by condition

Use when: sorted array, find pair with sum, remove duplicates, palindrome check.

two_pointers.go
package main

import "fmt"

// Two Sum (sorted array)
func twoSumSorted(nums []int, target int) (int, int) {
    l, r := 0, len(nums)-1
    for l < r {
        sum := nums[l] + nums[r]
        switch {
        case sum == target: return l, r
        case sum < target:  l++
        default:            r--
        }
    }
    return -1, -1
}

// Valid palindrome
func isPalindrome(s string) bool {
    l, r := 0, len(s)-1
    for l < r {
        if s[l] != s[r] { return false }
        l++; r--
    }
    return true
}

// Container with most water
func maxWater(height []int) int {
    l, r, maxArea := 0, len(height)-1, 0
    for l < r {
        h := min(height[l], height[r])
        if area := h * (r - l); area > maxArea { maxArea = area }
        if height[l] < height[r] { l++ } else { r-- }
    }
    return maxArea
}

// Three sum (sorted)
func threeSum(nums []int) [][3]int {
    // Sort first (not shown — use sort.Ints)
    var result [][3]int
    for i := 0; i < len(nums)-2; i++ {
        if i > 0 && nums[i] == nums[i-1] { continue } // skip dup
        l, r := i+1, len(nums)-1
        for l < r {
            sum := nums[i] + nums[l] + nums[r]
            switch {
            case sum == 0:
                result = append(result, [3]int{nums[i], nums[l], nums[r]})
                for l < r && nums[l] == nums[l+1] { l++ }
                for l < r && nums[r] == nums[r-1] { r-- }
                l++; r--
            case sum < 0: l++
            default:      r--
            }
        }
    }
    return result
}

func min(a, b int) int { if a < b { return a }; return b }

func main() {
    fmt.Println(twoSumSorted([]int{1,2,3,4,6}, 6)) // 1 3
    fmt.Println(isPalindrome("racecar"))             // true
    fmt.Println(maxWater([]int{1,8,6,2,5,4,8,3,7})) // 49
}

16 Sliding Window

What it is

A contiguous subarray or substring whose left and right boundaries slide rightward, maintaining a window invariant. The window expands by advancing the right pointer and shrinks by advancing the left pointer when the constraint is violated.

Real-world analogy

A physical window frame sliding along a wall of tiles. Fixed window: the frame is a fixed size, showing exactly k tiles. Variable window: the frame stretches to include as many tiles as possible while a rule is met, then contracts from the left when the rule breaks.

How it works internally

Maintain window state (sum, frequency map, distinct count) incrementally: add nums[right] on expand, subtract nums[left] on shrink. Each element is added once and removed at most once → O(n). The invariant check is O(1) per step if state is tracked correctly.

When to use

Keywords: "longest/shortest subarray/substring", "contiguous subarray with property", "max sum of size k". Fixed window: exactly k elements. Variable window: "at most k distinct", "sum equals target", "all chars appear".

Complexity
Time O(n)Space O(1) fixed / O(k) variableEach element added and removed at most once
Concept·Sliding Window
What it is
A contiguous subarray or substring "window" that expands right and shrinks left to maintain a constraint. Avoids recomputing the entire window from scratch by keeping a running state — turning O(n²) or O(nk) into O(n).
Real-world analogy
🪟A train window: as the train moves, new scenery enters on the right and old scenery exits on the left — you never recount everything you've seen from the beginning.
How it works
Fixed: advance both pointers together, remove nums[i-k], add nums[i].
Variable: expand right always; shrink left when constraint violated. Answer = right - left + 1 when valid.
Complexity
Time O(n) Space O(k) or O(1)
Each element enters and leaves the window exactly once → O(n) total moves.
When to use — recognition cues
subarray / substring contiguous elements max/min of window size k longest substring with constraint minimum window containing all chars at most K distinct
Time: O(n) Space: O(k) or O(1)

Fixed window — window size is constant k

fixed_window.go
// Max sum of subarray of size k
func maxSumK(nums []int, k int) int {
    if len(nums) < k { return -1 }
    windowSum := 0
    for i := 0; i < k; i++ {
        windowSum += nums[i]
    }
    maxSum := windowSum
    for i := k; i < len(nums); i++ {
        windowSum += nums[i] - nums[i-k]
        if windowSum > maxSum { maxSum = windowSum }
    }
    return maxSum
}

Variable window — shrink/grow based on constraint

variable_window.go
// Longest substring without repeating chars
func lengthOfLongestSubstring(s string) int {
    charIdx := make(map[byte]int)
    maxLen, left := 0, 0
    for right := 0; right < len(s); right++ {
        if idx, ok := charIdx[s[right]]; ok && idx >= left {
            left = idx + 1 // shrink window
        }
        charIdx[s[right]] = right
        if l := right - left + 1; l > maxLen { maxLen = l }
    }
    return maxLen
}
window_advanced.go — min window substring
// Minimum window in s that contains all chars of t
func minWindow(s, t string) string {
    need := make(map[byte]int)
    for i := 0; i < len(t); i++ { need[t[i]]++ }

    have, total := 0, len(need)
    window := make(map[byte]int)
    resL, resLen := 0, len(s)+1
    l := 0

    for r := 0; r < len(s); r++ {
        c := s[r]
        window[c]++
        if need[c] > 0 && window[c] == need[c] { have++ }

        for have == total { // valid window, shrink left
            if r-l+1 < resLen { resL, resLen = l, r-l+1 }
            window[s[l]]--
            if need[s[l]] > 0 && window[s[l]] < need[s[l]] { have-- }
            l++
        }
    }
    if resLen == len(s)+1 { return "" }
    return s[resL : resL+resLen]
}

16c Hashing

Concept·Hashing
What it is
A technique that maps keys to array indices via a hash function, enabling O(1) average insert, lookup, and delete. In Go, map[K]V is the built-in hash map. A map[T]struct{} is a hash set. Hashing converts O(n) linear scans into O(1) lookups — the single most impactful tool for reducing time complexity.
Real-world analogy
📇 A library card catalogue — instead of scanning every shelf to find a book, you look up the call number (hash) and go directly to the right shelf. The catalogue is the hash map; the call number is the key; the shelf location is the value.
How it works
A hash function converts a key into an integer index. Collisions (two keys hash to same index) are resolved by chaining (linked lists at each bucket) or open addressing. Go's runtime handles this automatically. Keys must be comparable — slices and maps cannot be keys.
Complexity
Lookup O(1) avg Insert O(1) avg O(n) worst (collisions)
Space O(n). Go maps are not ordered — never assume iteration order. Zero value for missing key is returned (not an error).
When to use — recognition cues
check existence in O(1) count frequencies group elements by property find duplicate / missing anagram / pangram two-sum pattern track visited nodes prefix sum + map combo
① Existence Checking — O(n) → O(1)

Convert "is X in the collection?" from O(n) linear scan into O(1) by loading elements into a map/set first.

hash_existence.go
package main

import "fmt"

// ── Set pattern: map[T]struct{} ────────────────────────────
// struct{} uses 0 bytes vs bool which uses 1 byte per entry
func contains(slice []int, target int) bool {
    set := make(map[int]struct{})
    for _, v := range slice { set[v] = struct{}{} }
    _, ok := set[target]
    return ok
}

// ── Check if sentence is pangram ──────────────────────────
// Every letter a-z appears at least once
func checkIfPangram(sentence string) bool {
    seen := make(map[rune]bool)
    for _, c := range sentence { seen[c] = true }
    return len(seen) >= 26
}

// Bit manipulation variant — O(1) space
func checkIfPangramBits(sentence string) bool {
    var mask int
    for _, c := range sentence {
        if c >= 'a' && c <= 'z' {
            mask |= 1 << (c - 'a')
        }
    }
    return mask == (1<<26)-1
}

// ── Missing number: find what's absent from [0..n] ────────
func missingNumber(nums []int) int {
    set := make(map[int]bool)
    for _, n := range nums { set[n] = true }
    for i := 0; i <= len(nums); i++ {
        if !set[i] { return i }
    }
    return -1
}

// Math trick — O(1) space: expected sum minus actual sum
func missingNumberMath(nums []int) int {
    n := len(nums)
    expected := n * (n + 1) / 2
    actual := 0
    for _, v := range nums { actual += v }
    return expected - actual
}

// ── Counting Elements: how many x where x+1 also exists ───
func countElements(arr []int) int {
    set := make(map[int]bool)
    for _, v := range arr { set[v] = true }
    count := 0
    for _, v := range arr {
        if set[v+1] { count++ }
    }
    return count
}

// ── Two Sum — classic hash pattern ────────────────────────
// For each num, check if (target - num) already seen
func twoSum(nums []int, target int) (int, int) {
    seen := make(map[int]int) // value → index
    for i, n := range nums {
        if j, ok := seen[target-n]; ok {
            return j, i
        }
        seen[n] = i
    }
    return -1, -1
}

// ── Two Sum II: count pairs summing to target ─────────────
func twoSumCount(nums []int, target int) int {
    freq := make(map[int]int)
    for _, n := range nums { freq[n]++ }
    count := 0
    for n, c := range freq {
        complement := target - n
        if complement == n {
            count += c * (c - 1) / 2 // C(c, 2) pairs
        } else if freq[complement] > 0 && complement > n {
            count += c * freq[complement]
        }
    }
    return count
}

func main() {
    fmt.Println(checkIfPangram("thequickbrownfoxjumpsoverthelazydog")) // true
    fmt.Println(checkIfPangramBits("abcdefghijklmnopqrstuvwxyz"))       // true
    fmt.Println(missingNumberMath([]int{3, 0, 1}))                      // 2
    fmt.Println(countElements([]int{1, 2, 3, 3, 5, 0}))                 // 3
    i, j := twoSum([]int{2, 7, 11, 15}, 9)
    fmt.Println(i, j) // 0 1
}

② Frequency Counting

Build a frequency map to answer "how many times does X appear?" or "do two collections have the same composition?"

hash_frequency.go
package main

import (
    "fmt"
    "sort"
)

// ── Anagram check ─────────────────────────────────────────
func isAnagram(s, t string) bool {
    if len(s) != len(t) { return false }
    freq := make(map[rune]int)
    for _, c := range s { freq[c]++ }
    for _, c := range t {
        freq[c]--
        if freq[c] < 0 { return false }
    }
    return true
}

// Fixed-size array variant for lowercase only — faster
func isAnagramArray(s, t string) bool {
    if len(s) != len(t) { return false }
    var freq [26]int
    for i := 0; i < len(s); i++ {
        freq[s[i]-'a']++
        freq[t[i]-'a']--
    }
    for _, v := range freq { if v != 0 { return false } }
    return true
}

// ── Group anagrams together ───────────────────────────────
// Key insight: anagrams have the same sorted string
func groupAnagrams(words []string) [][]string {
    groups := make(map[string][]string)
    for _, w := range words {
        key := sortedKey(w)
        groups[key] = append(groups[key], w)
    }
    result := make([][]string, 0, len(groups))
    for _, g := range groups { result = append(result, g) }
    return result
}

func sortedKey(s string) string {
    b := []byte(s)
    sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
    return string(b)
}

// ── Find players with 0 or 1 losses ──────────────────────
// losses[player] = loss count; find: 0 losses and exactly 1 loss
func findWinners(matches [][2]int) ([]int, []int) {
    losses := make(map[int]int)
    played := make(map[int]bool)
    for _, m := range matches {
        played[m[0]] = true
        played[m[1]] = true
        losses[m[1]]++ // only loser gets a loss
    }
    var noLoss, oneLoss []int
    for p := range played {
        switch losses[p] {
        case 0: noLoss  = append(noLoss, p)
        case 1: oneLoss = append(oneLoss, p)
        }
    }
    sort.Ints(noLoss); sort.Ints(oneLoss)
    return noLoss, oneLoss
}

// ── Largest unique number ──────────────────────────────────
// Largest integer that appears exactly once
func largestUniqueNumber(nums []int) int {
    freq := make(map[int]int)
    for _, n := range nums { freq[n]++ }
    result := -1
    for n, c := range freq {
        if c == 1 && n > result { result = n }
    }
    return result
}

// ── Maximum number of balloons ─────────────────────────────
// How many times can you form the word "balloon"?
func maxNumberOfBalloons(text string) int {
    freq := make(map[rune]int)
    for _, c := range text { freq[c]++ }
    // "balloon" needs: b×1, a×1, l×2, o×2, n×1
    return min(freq['b'], freq['a'], freq['l']/2, freq['o']/2, freq['n'])
}

func min(vals ...int) int {
    m := vals[0]
    for _, v := range vals[1:] { if v < m { m = v } }
    return m
}

func main() {
    fmt.Println(isAnagram("anagram", "nagaram")) // true
    fmt.Println(isAnagram("rat", "car"))          // false
    fmt.Println(groupAnagrams([]string{"eat","tea","tan","ate","nat","bat"}))
    fmt.Println(largestUniqueNumber([]int{5, 7, 3, 9, 4, 9, 8, 3, 1})) // 8
    fmt.Println(maxNumberOfBalloons("loonbalxballoon")) // 2
}

③ Grouping & Aggregation

Use a map to bucket elements by a computed key. Replaces nested loops with a single O(n) pass.

hash_grouping.go
package main

import (
    "fmt"
    "sort"
)

// ── Top K frequent elements ────────────────────────────────
// Bucket sort: frequency → []elements (O(n), no heap needed)
func topKFrequent(nums []int, k int) []int {
    freq := make(map[int]int)
    for _, n := range nums { freq[n]++ }

    // Bucket: index = frequency, value = list of nums with that freq
    buckets := make([][]int, len(nums)+1)
    for n, f := range freq { buckets[f] = append(buckets[f], n) }

    result := make([]int, 0, k)
    for i := len(buckets) - 1; i >= 0 && len(result) < k; i-- {
        result = append(result, buckets[i]...)
    }
    return result[:k]
}

// ── Contiguous array — equal 0s and 1s ────────────────────
// Replace 0→-1, find longest subarray with sum 0
// prefix sum + map: first time we see a prefix sum
func findMaxLength(nums []int) int {
    firstSeen := map[int]int{0: -1} // prefixSum → first index
    maxLen, sum := 0, 0
    for i, n := range nums {
        if n == 0 { sum-- } else { sum++ }
        if j, ok := firstSeen[sum]; ok {
            if l := i - j; l > maxLen { maxLen = l }
        } else {
            firstSeen[sum] = i
        }
    }
    return maxLen
}

// ── Subarray sum equals K (prefix sum + map) ──────────────
func subarraySum(nums []int, k int) int {
    count, sum := 0, 0
    freq := map[int]int{0: 1}
    for _, n := range nums {
        sum += n
        count += freq[sum-k]
        freq[sum]++
    }
    return count
}

// ── Longest substring with at most K distinct chars ───────
// Sliding window + frequency map
func lengthOfLongestSubstringKDistinct(s string, k int) int {
    freq := make(map[byte]int)
    l, maxLen := 0, 0
    for r := 0; r < len(s); r++ {
        freq[s[r]]++
        for len(freq) > k { // shrink until valid
            freq[s[l]]--
            if freq[s[l]] == 0 { delete(freq, s[l]) }
            l++
        }
        if w := r - l + 1; w > maxLen { maxLen = w }
    }
    return maxLen
}

// ── Ransom note ───────────────────────────────────────────
// Can ransomNote be built from magazine letters?
func canConstruct(ransomNote, magazine string) bool {
    freq := make(map[rune]int)
    for _, c := range magazine { freq[c]++ }
    for _, c := range ransomNote {
        freq[c]--
        if freq[c] < 0 { return false }
    }
    return true
}

// ── 4Sum count ────────────────────────────────────────────
// Count tuples (i,j,k,l) where A[i]+B[j]+C[k]+D[l]==0
// Split into two halves: hash all A+B sums, count matches in C+D
func fourSumCount(A, B, C, D []int) int {
    abSum := make(map[int]int)
    for _, a := range A {
        for _, b := range B {
            abSum[a+b]++
        }
    }
    count := 0
    for _, c := range C {
        for _, d := range D {
            count += abSum[-(c + d)]
        }
    }
    return count
}

// ── Largest number of K-sum pairs ─────────────────────────
func maxOperations(nums []int, k int) int {
    freq := make(map[int]int)
    for _, n := range nums { freq[n]++ }
    ops := 0
    for n, c := range freq {
        comp := k - n
        if comp == n {
            ops += c / 2
        } else if freq[comp] > 0 {
            pairs := c
            if freq[comp] < pairs { pairs = freq[comp] }
            ops += pairs
            freq[comp] = 0
            freq[n]  = 0
        }
    }
    return ops
}

// ── Jewels and stones ─────────────────────────────────────
func numJewelsInStones(jewels, stones string) int {
    set := make(map[rune]bool)
    for _, j := range jewels { set[j] = true }
    count := 0
    for _, s := range stones { if set[s] { count++ } }
    return count
}

func main() {
    fmt.Println(topKFrequent([]int{1,1,1,2,2,3}, 2))  // [1 2]
    fmt.Println(findMaxLength([]int{0,1,0,0,1,1,0}))   // 6
    fmt.Println(subarraySum([]int{1,1,1}, 2))           // 2
    fmt.Println(canConstruct("aa", "aab"))              // true
    fmt.Println(numJewelsInStones("aA", "aAAbbbb"))     // 3
    fmt.Println(fourSumCount(
        []int{1,2}, []int{-2,-1}, []int{-1,2}, []int{0,2})) // 2

    words := []string{"eat","tea","tan","ate","nat","bat"}
    groups := groupAnagramsFromAbove(words)
    for _, g := range groups { sort.Strings(g); fmt.Println(g) }
}

// placeholder — already defined above in grouping example
func groupAnagramsFromAbove(words []string) [][]string {
    groups := make(map[string][]string)
    for _, w := range words {
        b := []byte(w); sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
        key := string(b)
        groups[key] = append(groups[key], w)
    }
    result := make([][]string, 0, len(groups))
    for _, g := range groups { result = append(result, g) }
    return result
}

④ Prefix Sum + HashMap — The Power Combo
Time O(n) Space O(n)

Prefix sum alone answers range-sum queries. Combined with a hash map it answers "how many subarrays satisfy condition X" in a single pass. The key insight: sum[l..r] = prefix[r] - prefix[l-1]. If we want sum == k, we need prefix[r] - k to have appeared before.

prefix_hash_combo.go
package main

import "fmt"

// ── Template: count subarrays where f(sum) is true ────────
//
//  freq := map[int]int{identity: 1}  // seed for subarrays starting at 0
//  sum, count := 0, 0
//  for _, n := range nums {
//      sum = transform(sum, n)        // update running aggregate
//      count += freq[sum - target]    // how many earlier prefixes match
//      freq[sum]++                    // record this prefix
//  }

// ── Subarray sum equals K ──────────────────────────────────
func subarraySumK(nums []int, k int) int {
    freq := map[int]int{0: 1}
    sum, count := 0, 0
    for _, n := range nums {
        sum += n
        count += freq[sum-k]
        freq[sum]++
    }
    return count
}

// ── Subarray sum divisible by K ───────────────────────────
// Two subarrays have same (sum % k) → their difference is divisible by k
func subarraysDivByK(nums []int, k int) int {
    freq := map[int]int{0: 1}
    sum, count := 0, 0
    for _, n := range nums {
        sum += n
        mod := ((sum % k) + k) % k // handle negative modulo
        count += freq[mod]
        freq[mod]++
    }
    return count
}

// ── Longest subarray with sum 0 ───────────────────────────
func longestSubarraySumZero(nums []int) int {
    firstSeen := map[int]int{0: -1}
    sum, maxLen := 0, 0
    for i, n := range nums {
        sum += n
        if j, ok := firstSeen[sum]; ok {
            if l := i - j; l > maxLen { maxLen = l }
        } else {
            firstSeen[sum] = i // only store FIRST occurrence
        }
    }
    return maxLen
}

// ── Count subarrays with odd sum ─────────────────────────
// Odd sum = prefix sums have different parity at endpoints
func numOfSubarrays(nums []int) int {
    const mod = 1_000_000_007
    odd, even, sum, count := 0, 1, 0, 0 // even starts at 1 (empty prefix)
    for _, n := range nums {
        sum += n
        if sum%2 == 0 {
            count = (count + odd) % mod
            even++
        } else {
            count = (count + even) % mod
            odd++
        }
    }
    return count
}

// ── Binary array: longest subarray with equal 0s and 1s ───
// Map 0→-1, find longest subarray with sum 0
func findMaxLengthBinary(nums []int) int {
    firstSeen := map[int]int{0: -1}
    sum, maxLen := 0, 0
    for i, n := range nums {
        if n == 0 { sum-- } else { sum++ }
        if j, ok := firstSeen[sum]; ok {
            if l := i - j; l > maxLen { maxLen = l }
        } else {
            firstSeen[sum] = i
        }
    }
    return maxLen
}

// ── Path sum equals target (tree, prefix sum + map) ───────
// Extends the 1D pattern to binary trees via DFS
type TreeNode struct{ Val int; Left, Right *TreeNode }

func pathSum(root *TreeNode, target int) int {
    freq := map[int]int{0: 1}
    var dfs func(node *TreeNode, curr int) int
    dfs = func(node *TreeNode, curr int) int {
        if node == nil { return 0 }
        curr += node.Val
        count := freq[curr-target]   // subarrays ending here with sum=target
        freq[curr]++
        count += dfs(node.Left, curr) + dfs(node.Right, curr)
        freq[curr]--                 // backtrack
        return count
    }
    return dfs(root, 0)
}

func main() {
    fmt.Println(subarraySumK([]int{1, 2, 3}, 3))              // 2
    fmt.Println(subarraySumK([]int{1, 1, 1}, 2))              // 2
    fmt.Println(subarraysDivByK([]int{4,5,0,-2,-3,1}, 5))     // 7
    fmt.Println(longestSubarraySumZero([]int{1,-1,3,-3,2}))    // 4
    fmt.Println(findMaxLengthBinary([]int{0,1,0,0,1,1,0}))     // 6
}

⑤ Hash Set as Visited — Cycle & Path Detection
hash_visited.go
package main

import "fmt"

// ── Happy number ──────────────────────────────────────────
// Repeatedly sum squares of digits — happy if reaches 1, else cycles
func isHappy(n int) bool {
    seen := make(map[int]bool)
    for n != 1 {
        if seen[n] { return false }
        seen[n] = true
        n = sumOfSquares(n)
    }
    return true
}

func sumOfSquares(n int) int {
    sum := 0
    for n > 0 { d := n % 10; sum += d * d; n /= 10 }
    return sum
}

// Floyd's cycle variant — O(1) space (no set needed)
func isHappyFloyd(n int) bool {
    slow, fast := n, sumOfSquares(n)
    for fast != 1 && slow != fast {
        slow = sumOfSquares(slow)
        fast = sumOfSquares(sumOfSquares(fast))
    }
    return fast == 1
}

// ── Longest consecutive sequence ─────────────────────────
// O(n) using set: only start counting from sequence beginnings
func longestConsecutive(nums []int) int {
    set := make(map[int]bool)
    for _, n := range nums { set[n] = true }
    maxLen := 0
    for n := range set {
        if !set[n-1] { // n is start of a sequence
            length := 1
            for set[n+length] { length++ }
            if length > maxLen { maxLen = length }
        }
    }
    return maxLen
}

// ── Destination city ──────────────────────────────────────
// Find the city that is never a source (only a destination)
func destCity(paths [][]string) string {
    sources := make(map[string]bool)
    for _, p := range paths { sources[p[0]] = true }
    for _, p := range paths {
        if !sources[p[1]] { return p[1] }
    }
    return ""
}

// ── Unique number of occurrences ──────────────────────────
// True if all occurrence counts are distinct
func uniqueOccurrences(arr []int) bool {
    freq := make(map[int]int)
    for _, n := range arr { freq[n]++ }
    seen := make(map[int]bool)
    for _, c := range freq {
        if seen[c] { return false }
        seen[c] = true
    }
    return true
}

// ── Intersection of two arrays ────────────────────────────
func intersection(a, b []int) []int {
    setA := make(map[int]bool)
    for _, v := range a { setA[v] = true }
    seen := make(map[int]bool)
    var result []int
    for _, v := range b {
        if setA[v] && !seen[v] {
            result = append(result, v)
            seen[v] = true
        }
    }
    return result
}

// ── Find duplicate in array of N+1 integers [1..N] ────────
// Without modifying the array, O(1) space: Floyd on array-as-graph
func findDuplicate(nums []int) int {
    slow, fast := nums[0], nums[nums[0]]
    for slow != fast {
        slow = nums[slow]
        fast = nums[nums[fast]]
    }
    slow = 0
    for slow != fast {
        slow = nums[slow]
        fast = nums[fast]
    }
    return slow
}

func main() {
    fmt.Println(isHappy(19))                                     // true
    fmt.Println(isHappyFloyd(19))                                // true
    fmt.Println(longestConsecutive([]int{100,4,200,1,3,2}))     // 4 (1-2-3-4)
    fmt.Println(longestConsecutive([]int{0,3,7,2,5,8,4,6,0,1})) // 9
    fmt.Println(destCity([][]string{{"B","C"},{"D","B"},{"A","D"}})) // "C"
    fmt.Println(uniqueOccurrences([]int{1, 2, 2, 1, 1, 3}))     // true
    fmt.Println(intersection([]int{1,2,2,1}, []int{2,2}))        // [2]
    fmt.Println(findDuplicate([]int{1, 3, 4, 2, 2}))             // 2
}
⚠ Trap — Map Iteration Order Go maps have randomized iteration order by design. Never rely on for k, v := range m giving the same order twice. If you need sorted output, collect the keys into a slice and sort.Ints(keys) first.
⚠ Trap — Zero Value Reads Reading a missing key returns the zero value (0 for int, false for bool) — no panic, no error. This is useful for frequency maps (freq[x]++ works even on first access) but dangerous when zero is a valid value. Always use the comma-ok idiom: v, ok := m[k] when you need to distinguish "missing" from "zero".
★ Staff+ — When Hash Map Beats Sorting Sorting costs O(n log n). A hash map gives O(n). Whenever you catch yourself thinking "sort first, then binary search / two pointers", ask: can a map give me O(1) lookup instead? Anagram check, two-sum, frequency problems, and existence queries all fall into this bucket.

16b Array / Slice Patterns

What it is

A collection of algorithmic building blocks specifically for slice manipulation: prefix sums, Kadane's max subarray, Dutch flag partition, interval merging, product-except-self, rain water trapping, jump games, matrix operations, and in-place index tricks.

Real-world analogy

A craftsperson's toolkit. Prefix sums are a measuring tape — cut any span instantly. Kadane's is a treasure hunter deciding at each step "do I keep going or start fresh?" Dutch flag is sorting coloured beads by hand in one pass. Each tool solves a specific class of problem elegantly.

How it works internally

Each pattern exploits a specific slice property. Prefix sums trade O(n) space for O(1) query time. Kadane's uses the "extend or reset" DP decision in a single pass. Interval merging relies on sorting by start/end to reduce N² comparisons to N. Cyclic sort exploits value-in-range-[1..n] to use the array as its own index.

When to use

Match the problem keywords: "subarray sum" → prefix sum + hashmap. "max contiguous" → Kadane's. "sort 0/1/2" or "partition" → Dutch flag. "merge/insert intervals" → sort by start. "product except self" → left/right pass. "missing/duplicate in [1..n]" → cyclic sort.

Complexity
Prefix sum build O(n), query O(1)Kadane's O(n) time O(1) spaceDutch flag O(n) one-passInterval merge O(n log n) due to sort
Concept·Array / Slice Patterns
What it is
A collection of algorithmic techniques that operate on contiguous indexed sequences. Arrays are the most fundamental data structure — virtually every problem reduces to one of a small set of array manipulation patterns once you recognise the shape.
Real-world analogy
🗂A spreadsheet column — cells are numbered, access is instant by index, but insertion/deletion in the middle is expensive. All optimisation tricks exploit that index access is O(1).
How it works (internals)
Go slices are a 3-word header: pointer to backing array, length, capacity. Append is amortised O(1) — doubles capacity on growth. Slicing shares the backing array (aliasing risk).
Pattern → Complexity map
Prefix sum: build O(n), query O(1) · Kadane: O(n) time O(1) space · Dutch flag: O(n) one-pass · Intervals: O(n log n) sort + O(n) sweep · Cyclic sort: O(n) for [1..n] ranges
When to use — recognition cues
range sum queries max contiguous subarray sort 0/1/2 in one pass overlapping intervals product except self values in [1..n] missing / duplicate number rotate array

These patterns come up in the majority of slice-based interview questions. Master the template first, then the variations will fall into place.

① Prefix Sum
Build: O(n)Query: O(1)Space: O(n)

Precompute cumulative sums so any subarray sum [l..r] is answered in O(1). Foundation for many DP and range-query problems.

prefix_sum.go
package main

import "fmt"

// ── 1D Prefix Sum ─────────────────────────────────────────
func buildPrefix(nums []int) []int {
    prefix := make([]int, len(nums)+1) // prefix[0] = 0 sentinel
    for i, v := range nums {
        prefix[i+1] = prefix[i] + v
    }
    return prefix
}

// Range sum [l, r] inclusive — O(1)
func rangeSum(prefix []int, l, r int) int {
    return prefix[r+1] - prefix[l]
}

// ── Subarray Sum Equals K ─────────────────────────────────
// Count subarrays whose sum == k  →  O(n) with prefix+hashmap
func subarraySum(nums []int, k int) int {
    count := 0
    sum := 0
    freq := map[int]int{0: 1} // sum→count; seed 0 for subarrays from index 0
    for _, n := range nums {
        sum += n
        count += freq[sum-k] // how many times (sum-k) appeared before
        freq[sum]++
    }
    return count
}

// ── Subarray Sum Divisible by K ───────────────────────────
func subarraySumDivK(nums []int, k int) int {
    count, sum := 0, 0
    rem := map[int]int{0: 1}
    for _, n := range nums {
        sum += n
        mod := ((sum % k) + k) % k // handle negative
        count += rem[mod]
        rem[mod]++
    }
    return count
}

// ── 2D Prefix Sum (matrix range queries) ─────────────────
func build2DPrefix(matrix [][]int) [][]int {
    m, n := len(matrix), len(matrix[0])
    p := make([][]int, m+1)
    for i := range p { p[i] = make([]int, n+1) }
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            p[i][j] = matrix[i-1][j-1] + p[i-1][j] + p[i][j-1] - p[i-1][j-1]
        }
    }
    return p
}

// Sum of submatrix (r1,c1) to (r2,c2) inclusive
func matrixRangeSum(p [][]int, r1, c1, r2, c2 int) int {
    return p[r2+1][c2+1] - p[r1][c2+1] - p[r2+1][c1] + p[r1][c1]
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    pre := buildPrefix(nums)
    fmt.Println(rangeSum(pre, 1, 3)) // 2+3+4 = 9
    fmt.Println(rangeSum(pre, 0, 4)) // 15

    fmt.Println(subarraySum([]int{1, 1, 1}, 2))              // 2
    fmt.Println(subarraySum([]int{1, 2, 3, -1, 1, 2}, 3))    // 3

    fmt.Println(subarraySumDivK([]int{4, 5, 0, -2, -3, 1}, 5)) // 7

    matrix := [][]int{{3,0,1,4,2},{5,6,3,2,1},{1,2,0,1,5},{4,1,0,1,7},{1,0,3,0,5}}
    p2 := build2DPrefix(matrix)
    fmt.Println(matrixRangeSum(p2, 2, 1, 4, 3)) // 8
}
Key Insight freq[sum-k] asks: "how many prefixes ended with sum sum-k?" The subarray between that prefix and now sums to exactly k. The seed {0: 1} handles subarrays starting at index 0.

② Kadane's Algorithm — Max Subarray
Time: O(n)Space: O(1)
kadane.go
package main

import "fmt"

// Max subarray sum
func maxSubArray(nums []int) int {
    maxSum, curr := nums[0], nums[0]
    for _, n := range nums[1:] {
        if curr < 0 { curr = 0 } // reset: starting fresh is better
        curr += n
        if curr > maxSum { maxSum = curr }
    }
    return maxSum
}

// Max subarray sum — with indices (track start/end)
func maxSubArrayIdx(nums []int) (int, int, int) {
    maxSum, curr := nums[0], nums[0]
    start, end, tempStart := 0, 0, 0
    for i := 1; i < len(nums); i++ {
        if curr < 0 { curr = 0; tempStart = i }
        curr += nums[i]
        if curr > maxSum {
            maxSum = curr
            start, end = tempStart, i
        }
    }
    return maxSum, start, end
}

// Max product subarray (handle negatives with two passes)
func maxProduct(nums []int) int {
    maxP, minP, result := nums[0], nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < 0 { maxP, minP = minP, maxP } // swap: negative flips max/min
        if n > maxP*1 { maxP = n } else { maxP = maxP * n } // simplified:
        maxP = max(n, maxP*n) // max of: start fresh OR extend
        minP = min(n, minP*n) // track min for future negative flip
        if maxP > result { result = maxP }
    }
    return result
}

// Cleaner max product
func maxProductClean(nums []int) int {
    maxP, minP, res := nums[0], nums[0], nums[0]
    for _, n := range nums[1:] {
        maxP, minP = max(n, max(maxP*n, minP*n)), min(n, min(maxP*n, minP*n))
        if maxP > res { res = maxP }
    }
    return res
}

// Circular subarray max (Kadane + total-minSubarray)
func maxSubarrayCircular(nums []int) int {
    // Case 1: normal Kadane
    maxSum, minSum := nums[0], nums[0]
    currMax, currMin, total := nums[0], nums[0], nums[0]
    for _, n := range nums[1:] {
        total += n
        currMax = max(n, currMax+n)
        currMin = min(n, currMin+n)
        if currMax > maxSum { maxSum = currMax }
        if currMin < minSum { minSum = currMin }
    }
    // Case 2: wrap-around = total - minSubarray
    // Edge: if all negative, wrap would give 0 (empty), return maxSum
    if maxSum < 0 { return maxSum }
    if wrap := total - minSum; wrap > maxSum { return wrap }
    return maxSum
}

func max(a, b int) int { if a > b { return a }; return b }
func min(a, b int) int { if a < b { return a }; return b }

func main() {
    fmt.Println(maxSubArray([]int{-2, 1, -3, 4, -1, 2, 1, -5, 4})) // 6
    sum, s, e := maxSubArrayIdx([]int{-2, 1, -3, 4, -1, 2, 1, -5, 4})
    fmt.Printf("sum=%d idx=[%d,%d]\n", sum, s, e) // sum=6 idx=[3,6]
    fmt.Println(maxProductClean([]int{2, 3, -2, 4})) // 6
    fmt.Println(maxSubarrayCircular([]int{1, -2, 3, -2})) // 3
    fmt.Println(maxSubarrayCircular([]int{5, -3, 5}))     // 10 (wrap)
}
⚠ Trap — All Negative Input Return the least-negative element, not 0. The standard template handles this if you initialize maxSum = nums[0] (not 0) and start iterating from index 1.

③ Dutch National Flag — 3-Way Partition
Time: O(n)Space: O(1)One pass

Partition a slice into three regions: <pivot | ==pivot | >pivot. Classic: sort 0/1/2 in one pass.

dutch_flag.go
package main

import "fmt"

// Sort Colors — LeetCode 75
// Invariants: nums[0..lo-1]=0, nums[lo..mid-1]=1, nums[hi+1..n-1]=2
func sortColors(nums []int) {
    lo, mid, hi := 0, 0, len(nums)-1
    for mid <= hi {
        switch nums[mid] {
        case 0:
            nums[lo], nums[mid] = nums[mid], nums[lo]
            lo++; mid++
        case 1:
            mid++ // already in place
        case 2:
            nums[mid], nums[hi] = nums[hi], nums[mid]
            hi-- // don't mid++ — swapped element not yet examined
        }
    }
}

// General 3-way partition around pivot value
func threeWayPartition(nums []int, pivot int) []int {
    lo, mid, hi := 0, 0, len(nums)-1
    for mid <= hi {
        switch {
        case nums[mid] < pivot:
            nums[lo], nums[mid] = nums[mid], nums[lo]
            lo++; mid++
        case nums[mid] == pivot:
            mid++
        default:
            nums[mid], nums[hi] = nums[hi], nums[mid]
            hi--
        }
    }
    return nums
}

// Move Zeros to end (2-way partition, stable)
func moveZeroes(nums []int) {
    writeIdx := 0
    for _, n := range nums {
        if n != 0 { nums[writeIdx] = n; writeIdx++ }
    }
    for writeIdx < len(nums) { nums[writeIdx] = 0; writeIdx++ }
}

// Partition array: negatives left, non-negatives right (2-way)
func partition(nums []int) []int {
    l, r := 0, len(nums)-1
    for l < r {
        for l < r && nums[l] < 0  { l++ }
        for l < r && nums[r] >= 0 { r-- }
        if l < r { nums[l], nums[r] = nums[r], nums[l] }
    }
    return nums
}

func main() {
    c := []int{2, 0, 2, 1, 1, 0}
    sortColors(c)
    fmt.Println(c) // [0 0 1 1 2 2]

    nums := []int{3, 1, 4, 1, 5, 9, 2, 6, 5}
    fmt.Println(threeWayPartition(nums, 4)) // all <4 | 4s | all >4

    z := []int{0, 1, 0, 3, 12}
    moveZeroes(z)
    fmt.Println(z) // [1 3 12 0 0]
}

④ Merge Intervals
Time: O(n log n)Space: O(n)
intervals.go
package main

import (
    "fmt"
    "sort"
)

type Interval [2]int

// Merge overlapping intervals
func merge(intervals []Interval) []Interval {
    sort.Slice(intervals, func(i, j int) bool {
        return intervals[i][0] < intervals[j][0]
    })
    result := []Interval{intervals[0]}
    for _, curr := range intervals[1:] {
        last := &result[len(result)-1]
        if curr[0] <= last[1] { // overlap
            if curr[1] > last[1] { last[1] = curr[1] } // extend
        } else {
            result = append(result, curr)
        }
    }
    return result
}

// Insert interval into sorted non-overlapping list
func insert(intervals []Interval, newInterval Interval) []Interval {
    result := []Interval{}
    i := 0
    // Add all intervals that end before new one starts
    for i < len(intervals) && intervals[i][1] < newInterval[0] {
        result = append(result, intervals[i]); i++
    }
    // Merge all overlapping intervals
    for i < len(intervals) && intervals[i][0] <= newInterval[1] {
        if intervals[i][0] < newInterval[0] { newInterval[0] = intervals[i][0] }
        if intervals[i][1] > newInterval[1] { newInterval[1] = intervals[i][1] }
        i++
    }
    result = append(result, newInterval)
    // Add remaining
    result = append(result, intervals[i:]...)
    return result
}

// Meeting rooms — can attend all? (no overlap)
func canAttendMeetings(intervals []Interval) bool {
    sort.Slice(intervals, func(i, j int) bool { return intervals[i][0] < intervals[j][0] })
    for i := 1; i < len(intervals); i++ {
        if intervals[i][0] < intervals[i-1][1] { return false }
    }
    return true
}

// Meeting rooms II — min rooms needed (sweep line / min-heap)
func minMeetingRooms(intervals []Interval) int {
    if len(intervals) == 0 { return 0 }
    starts := make([]int, len(intervals))
    ends   := make([]int, len(intervals))
    for i, iv := range intervals { starts[i], ends[i] = iv[0], iv[1] }
    sort.Ints(starts); sort.Ints(ends)

    rooms, endPtr := 0, 0
    for i := range starts {
        if starts[i] < ends[endPtr] {
            rooms++ // new room needed
        } else {
            endPtr++ // reuse a room
        }
    }
    return rooms
}

func main() {
    ivs := []Interval{{1,3},{2,6},{8,10},{15,18}}
    fmt.Println(merge(ivs)) // [[1 6] [8 10] [15 18]]

    ivs2 := []Interval{{1,2},{3,5},{6,7},{8,10},{12,16}}
    fmt.Println(insert(ivs2, Interval{4, 8})) // [[1 2] [3 10] [12 16]]

    meetings := []Interval{{0,30},{5,10},{15,20}}
    fmt.Println(canAttendMeetings(meetings))  // false
    fmt.Println(minMeetingRooms(meetings))    // 2
}

⑤ Product of Array Except Self
Time: O(n)Space: O(1) output
product_except_self.go
package main

import "fmt"

// No division, O(1) extra space (output array doesn't count)
// Left pass: result[i] = product of all elements left of i
// Right pass: multiply in product of all elements right of i
func productExceptSelf(nums []int) []int {
    n := len(nums)
    result := make([]int, n)

    // Left products
    result[0] = 1
    for i := 1; i < n; i++ {
        result[i] = result[i-1] * nums[i-1]
    }

    // Right products — running right multiplier
    right := 1
    for i := n - 1; i >= 0; i-- {
        result[i] *= right
        right *= nums[i]
    }
    return result
}

// Variant: handle zeros — count zeros, then build answer
func productExceptSelfWithZeros(nums []int) []int {
    total, zeros := 1, 0
    for _, n := range nums {
        if n == 0 { zeros++ } else { total *= n }
    }
    result := make([]int, len(nums))
    for i, n := range nums {
        switch zeros {
        case 0: result[i] = total / n
        case 1:
            if n == 0 { result[i] = total }
        // zeros >= 2: all stay 0
        }
    }
    return result
}

func main() {
    fmt.Println(productExceptSelf([]int{1, 2, 3, 4}))      // [24 12 8 6]
    fmt.Println(productExceptSelf([]int{-1, 1, 0, -3, 3})) // [0 0 9 0 0]
}

⑥ Trapping Rain Water
Time: O(n)Space: O(1)
trapping_rain.go
package main

import "fmt"

// Two-pointer approach: O(n) time, O(1) space
// Water at position i = min(maxLeft, maxRight) - height[i]
// Move pointer from the side with smaller max height
func trap(height []int) int {
    l, r := 0, len(height)-1
    maxL, maxR := 0, 0
    water := 0
    for l < r {
        if height[l] < height[r] {
            if height[l] >= maxL { maxL = height[l] } else { water += maxL - height[l] }
            l++
        } else {
            if height[r] >= maxR { maxR = height[r] } else { water += maxR - height[r] }
            r--
        }
    }
    return water
}

// Prefix max approach (easier to understand, O(n) space)
func trapWithArrays(height []int) int {
    n := len(height)
    maxL, maxR := make([]int, n), make([]int, n)
    maxL[0] = height[0]
    for i := 1; i < n; i++ {
        if height[i] > maxL[i-1] { maxL[i] = height[i] } else { maxL[i] = maxL[i-1] }
    }
    maxR[n-1] = height[n-1]
    for i := n - 2; i >= 0; i-- {
        if height[i] > maxR[i+1] { maxR[i] = height[i] } else { maxR[i] = maxR[i+1] }
    }
    water := 0
    for i := 0; i < n; i++ {
        waterLevel := maxL[i]; if maxR[i] < waterLevel { waterLevel = maxR[i] }
        if diff := waterLevel - height[i]; diff > 0 { water += diff }
    }
    return water
}

func main() {
    fmt.Println(trap([]int{0,1,0,2,1,0,1,3,2,1,2,1})) // 6
    fmt.Println(trap([]int{4,2,0,3,2,5}))               // 9
}

⑦ Jump Game I & II
Time: O(n)Space: O(1)
jump_game.go
package main

import "fmt"

// Jump Game I — can reach last index?
// Greedy: track the furthest reachable index
func canJump(nums []int) bool {
    maxReach := 0
    for i, n := range nums {
        if i > maxReach { return false } // can't reach index i
        if i+n > maxReach { maxReach = i + n }
    }
    return true
}

// Jump Game II — minimum jumps to reach last index
// Greedy: at each "jump boundary", take the jump that reaches farthest
func jump(nums []int) int {
    jumps, currEnd, farthest := 0, 0, 0
    for i := 0; i < len(nums)-1; i++ {
        if i+nums[i] > farthest { farthest = i + nums[i] }
        if i == currEnd { // must jump here
            jumps++
            currEnd = farthest
        }
    }
    return jumps
}

// Jump Game III — can reach index with value 0?
// BFS from start, jump ±nums[i]
func canReachZero(arr []int, start int) bool {
    visited := make([]bool, len(arr))
    queue := []int{start}
    for len(queue) > 0 {
        i := queue[0]; queue = queue[1:]
        if arr[i] == 0 { return true }
        if visited[i] { continue }
        visited[i] = true
        if l := i - arr[i]; l >= 0 && !visited[l] { queue = append(queue, l) }
        if r := i + arr[i]; r < len(arr) && !visited[r] { queue = append(queue, r) }
    }
    return false
}

func main() {
    fmt.Println(canJump([]int{2, 3, 1, 1, 4})) // true
    fmt.Println(canJump([]int{3, 2, 1, 0, 4})) // false
    fmt.Println(jump([]int{2, 3, 1, 1, 4}))    // 2
    fmt.Println(jump([]int{2, 3, 0, 1, 4}))    // 2
    fmt.Println(canReachZero([]int{4,2,3,0,3,1,2}, 5)) // true
}

⑧ Rotate Matrix & Spiral Order
Time: O(n²)Space: O(1) for rotate
matrix.go
package main

import "fmt"

// Rotate NxN matrix 90° clockwise in-place
// Step 1: Transpose (swap [i][j] with [j][i])
// Step 2: Reverse each row
func rotate(matrix [][]int) {
    n := len(matrix)
    // Transpose
    for i := 0; i < n; i++ {
        for j := i + 1; j < n; j++ {
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
        }
    }
    // Reverse each row
    for i := 0; i < n; i++ {
        for l, r := 0, n-1; l < r; l, r = l+1, r-1 {
            matrix[i][l], matrix[i][r] = matrix[i][r], matrix[i][l]
        }
    }
}

// 90° counter-clockwise: reverse each row first, then transpose

// Set matrix zeroes — if cell is 0, set its row+col to 0
// O(1) space: use first row/col as flags
func setZeroes(matrix [][]int) {
    m, n := len(matrix), len(matrix[0])
    firstRowZero, firstColZero := false, false
    for j := 0; j < n; j++ { if matrix[0][j] == 0 { firstRowZero = true } }
    for i := 0; i < m; i++ { if matrix[i][0] == 0 { firstColZero = true } }
    // Use first row/col to mark
    for i := 1; i < m; i++ {
        for j := 1; j < n; j++ {
            if matrix[i][j] == 0 { matrix[i][0] = 0; matrix[0][j] = 0 }
        }
    }
    for i := 1; i < m; i++ { if matrix[i][0] == 0 { for j := 1; j < n; j++ { matrix[i][j] = 0 } } }
    for j := 1; j < n; j++ { if matrix[0][j] == 0 { for i := 1; i < m; i++ { matrix[i][j] = 0 } } }
    if firstRowZero { for j := 0; j < n; j++ { matrix[0][j] = 0 } }
    if firstColZero { for i := 0; i < m; i++ { matrix[i][0] = 0 } }
}

// Spiral order traversal
func spiralOrder(matrix [][]int) []int {
    result := []int{}
    top, bottom, left, right := 0, len(matrix)-1, 0, len(matrix[0])-1
    for top <= bottom && left <= right {
        for j := left; j <= right; j++  { result = append(result, matrix[top][j]) };    top++
        for i := top; i <= bottom; i++  { result = append(result, matrix[i][right]) };  right--
        if top <= bottom {
            for j := right; j >= left; j-- { result = append(result, matrix[bottom][j]) }; bottom--
        }
        if left <= right {
            for i := bottom; i >= top; i-- { result = append(result, matrix[i][left]) };  left++
        }
    }
    return result
}

// Search in sorted matrix (each row sorted, first of row > last of prev)
// Treat as flat 1D sorted array — binary search
func searchMatrix(matrix [][]int, target int) bool {
    m, n := len(matrix), len(matrix[0])
    lo, hi := 0, m*n-1
    for lo <= hi {
        mid := lo + (hi-lo)/2
        val := matrix[mid/n][mid%n]
        switch {
        case val == target: return true
        case val < target:  lo = mid + 1
        default:            hi = mid - 1
        }
    }
    return false
}

func main() {
    m := [][]int{{1,2,3},{4,5,6},{7,8,9}}
    rotate(m)
    fmt.Println(m) // [[7 4 1] [8 5 2] [9 6 3]]

    m2 := [][]int{{1,2,3},{4,5,6},{7,8,9}}
    fmt.Println(spiralOrder(m2)) // [1 2 3 6 9 8 7 4 5]

    m3 := [][]int{{1,3,5,7},{10,11,16,20},{23,30,34,60}}
    fmt.Println(searchMatrix(m3, 3))  // true
    fmt.Println(searchMatrix(m3, 13)) // false
}
Rotate Pattern Clockwise 90° = Transpose + Reverse rows. Counter-clockwise 90° = Reverse rows + Transpose. 180° = Reverse rows + Reverse each row (or double-transpose). These come up in game-board and image-processing problems.

⑨ Next Permutation & Cyclic Sort
Time: O(n)Space: O(1)
permutation_cyclic.go
package main

import "fmt"

// Next Permutation (lexicographically next)
// Algorithm: find rightmost ascent, swap with next-larger, reverse suffix
func nextPermutation(nums []int) {
    n := len(nums)
    // Step 1: find rightmost i where nums[i] < nums[i+1]
    i := n - 2
    for i >= 0 && nums[i] >= nums[i+1] { i-- }

    if i >= 0 {
        // Step 2: find rightmost j > i where nums[j] > nums[i]
        j := n - 1
        for nums[j] <= nums[i] { j-- }
        nums[i], nums[j] = nums[j], nums[i]
    }
    // Step 3: reverse suffix after i
    for l, r := i+1, n-1; l < r; l, r = l+1, r-1 {
        nums[l], nums[r] = nums[r], nums[l]
    }
}

// Cyclic Sort — for arrays with values in range [1..n] or [0..n-1]
// Place each number at its correct index in O(n) time
func cyclicSort(nums []int) {
    i := 0
    for i < len(nums) {
        correct := nums[i] - 1 // expected index for value nums[i] (1-indexed)
        if nums[i] != nums[correct] {
            nums[i], nums[correct] = nums[correct], nums[i]
        } else {
            i++
        }
    }
}

// Find missing number using cyclic sort (values 1..n, one missing)
func findMissingNumber(nums []int) int {
    cyclicSort(nums)
    for i, n := range nums {
        if n != i+1 { return i + 1 }
    }
    return len(nums) + 1
}

// Find all duplicates (values 1..n, each appears once or twice)
func findDuplicates(nums []int) []int {
    cyclicSort(nums)
    var result []int
    for i, n := range nums {
        if n != i+1 { result = append(result, n) }
    }
    return result
}

// All permutations (backtracking)
func permutations(nums []int) [][]int {
    var result [][]int
    var backtrack func(start int)
    backtrack = func(start int) {
        if start == len(nums) {
            tmp := make([]int, len(nums))
            copy(tmp, nums)
            result = append(result, tmp)
            return
        }
        for i := start; i < len(nums); i++ {
            nums[start], nums[i] = nums[i], nums[start]
            backtrack(start + 1)
            nums[start], nums[i] = nums[i], nums[start]
        }
    }
    backtrack(0)
    return result
}

func main() {
    n := []int{1, 2, 3}
    nextPermutation(n); fmt.Println(n) // [1 3 2]
    nextPermutation(n); fmt.Println(n) // [2 1 3]

    s := []int{3, 2, 1}
    nextPermutation(s); fmt.Println(s) // [1 2 3] — wraps around

    nums := []int{3, 1, 4, 2, 5}
    cyclicSort(nums)
    fmt.Println(nums) // [1 2 3 4 5]

    fmt.Println(findMissingNumber([]int{3, 0, 1}))        // 2  (0-indexed variant)
    fmt.Println(findDuplicates([]int{4, 3, 2, 7, 8, 2, 3, 1})) // [2 3]

    fmt.Println(permutations([]int{1, 2, 3})) // all 6 permutations
}

⑩ Miscellaneous — Majority Element, Missing/Duplicate, Rotate Array
array_misc.go
package main

import "fmt"

// Boyer-Moore Voting — majority element (appears > n/2 times)
func majorityElement(nums []int) int {
    candidate, count := nums[0], 1
    for _, n := range nums[1:] {
        if count == 0 { candidate = n }
        if n == candidate { count++ } else { count-- }
    }
    return candidate // guaranteed to exist per problem constraint
}

// Majority Element II — all elements > n/3 (at most 2 candidates)
func majorityElementII(nums []int) []int {
    c1, c2, cnt1, cnt2 := 0, 1, 0, 0 // c2=1 ensures c1 != c2 initially
    for _, n := range nums {
        switch {
        case n == c1: cnt1++
        case n == c2: cnt2++
        case cnt1 == 0: c1, cnt1 = n, 1
        case cnt2 == 0: c2, cnt2 = n, 1
        default: cnt1--; cnt2--
        }
    }
    var result []int
    cnt1, cnt2 = 0, 0
    for _, n := range nums { if n == c1 { cnt1++ } else if n == c2 { cnt2++ } }
    if cnt1 > len(nums)/3 { result = append(result, c1) }
    if cnt2 > len(nums)/3 { result = append(result, c2) }
    return result
}

// Rotate array right by k positions — reversal trick
func rotateRight(nums []int, k int) {
    n := len(nums)
    k %= n
    reverse := func(l, r int) {
        for l < r { nums[l], nums[r] = nums[r], nums[l]; l++; r-- }
    }
    reverse(0, n-1)   // reverse all
    reverse(0, k-1)   // reverse first k
    reverse(k, n-1)   // reverse rest
}

// Find duplicate in array [1..n] — Floyd's cycle detection on array
// Treat array as linked list: index → value → index → ...
func findDuplicate(nums []int) int {
    slow, fast := nums[0], nums[nums[0]]
    for slow != fast {
        slow = nums[slow]
        fast = nums[nums[fast]]
    }
    slow = 0
    for slow != fast {
        slow = nums[slow]
        fast = nums[fast]
    }
    return slow
}

// H-Index — max h such that h papers have >= h citations
func hIndex(citations []int) int {
    n := len(citations)
    bucket := make([]int, n+1)
    for _, c := range citations {
        if c >= n { bucket[n]++ } else { bucket[c]++ }
    }
    total := 0
    for h := n; h >= 0; h-- {
        total += bucket[h]
        if total >= h { return h }
    }
    return 0
}

// Smallest missing positive integer — O(n) time, O(1) space
// Key: answer is in [1, n+1]; use the array itself as a hash
func firstMissingPositive(nums []int) int {
    n := len(nums)
    // Place each nums[i] at index nums[i]-1 if in range [1..n]
    for i := 0; i < n; {
        j := nums[i] - 1
        if j >= 0 && j < n && nums[j] != nums[i] {
            nums[i], nums[j] = nums[j], nums[i]
        } else {
            i++
        }
    }
    for i, v := range nums {
        if v != i+1 { return i + 1 }
    }
    return n + 1
}

func main() {
    fmt.Println(majorityElement([]int{3,2,3}))             // 3
    fmt.Println(majorityElement([]int{2,2,1,1,1,2,2}))     // 2
    fmt.Println(majorityElementII([]int{3,2,3}))            // [3]
    fmt.Println(majorityElementII([]int{1,1,1,3,3,2,2,2})) // [1 2]

    arr := []int{1, 2, 3, 4, 5, 6, 7}
    rotateRight(arr, 3)
    fmt.Println(arr) // [5 6 7 1 2 3 4]

    fmt.Println(findDuplicate([]int{1, 3, 4, 2, 2})) // 2
    fmt.Println(findDuplicate([]int{3, 1, 3, 4, 2})) // 3

    fmt.Println(hIndex([]int{3, 0, 6, 1, 5})) // 3

    fmt.Println(firstMissingPositive([]int{3, 4, -1, 1})) // 2
    fmt.Println(firstMissingPositive([]int{1, 2, 0}))     // 3
    fmt.Println(firstMissingPositive([]int{7, 8, 9, 11})) // 1
}
★ Staff+ — When Interviewer Asks "Can You Do Better?" For majority element: from O(n) space hashmap → Boyer-Moore O(1) space. For missing positive: from O(n) extra array → in-place index-as-hash trick. For duplicate: from sort → Floyd's cycle detection (treats array as implicit linked list). These upgrades show you know multiple approaches and understand the space/time tradeoff.

18 Stack & Queue

What it is

Stack: LIFO (Last In, First Out) — access only the top. Queue: FIFO (First In, First Out) — add at back, remove from front. Go has no built-in implementations — both are built from slices. A monotonic stack maintains elements in sorted order, enabling O(1) next-greater/smaller queries.

Real-world analogy

Stack: a stack of plates — you can only add or remove from the top. Undo history in an editor. Queue: a supermarket checkout line — first in, first served. Print queue. Monotonic stack: a bouncer at a club who keeps the line sorted by height — shorter arrivals displace previous guests.

How it works internally

Stack: append to push, reslice [:n-1] to pop — O(1) amortised. Queue: naive reslice is O(n) per dequeue; use a head pointer for O(1) amortised (compact when head passes halfway). Monotonic stack: pop all elements violating the monotone invariant before pushing — each element pushed/popped at most once, so O(n) total.

When to use

Stack: DFS, parentheses matching, expression evaluation, undo/redo, call stack simulation. Queue: BFS, task scheduling, sliding window max (deque). Monotonic stack: next greater/smaller element, largest rectangle in histogram, daily temperatures.

Complexity
Push/Pop O(1) amortisedEnqueue/Dequeue O(1) amortisedMonotonic stack: O(n) totalPeek on empty = index out of range panic
Concept·Stack & Queue
What it is
Stack (LIFO) — last in, first out. Only access the top. Queue (FIFO) — first in, first out. Add to back, remove from front. Neither exists as a stdlib type in Go — both are slices. Monotonic stack is a stack that maintains elements in sorted order by discarding violating elements.
Real-world analogy
📚Stack: a pile of plates — you only take from the top. 🎟Queue: a ticket line — first person in line is first served.
How it works (Go)
push: s = append(s, v)
pop: v = s[len(s)-1]; s = s[:len(s)-1]
peek: s[len(s)-1]
Queue enqueue: append. Dequeue: track head index to avoid O(n) shifts.
Complexity
Push/Pop O(1) amortised
Monotonic stack: O(n) total — each element pushed and popped at most once across the entire array traversal.
When to use — recognition cues
matching brackets / parentheses undo / redo operations next greater / smaller element evaluate expression DFS iteratively BFS level traversal largest rectangle in histogram daily temperatures
stack_queue.go
package main

import "fmt"

// Stack — LIFO using slice
type Stack[T any] struct{ data []T }
func (s *Stack[T]) Push(v T)    { s.data = append(s.data, v) }
func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.data) == 0 { return zero, false }
    v := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return v, true
}
func (s *Stack[T]) Peek() (T, bool) {
    var zero T
    if len(s.data) == 0 { return zero, false }
    return s.data[len(s.data)-1], true
}
func (s *Stack[T]) Len() int { return len(s.data) }

// Queue — FIFO using slice (amortized O(1) with head tracking)
type Queue[T any] struct{ data []T; head int }
func (q *Queue[T]) Enqueue(v T) { q.data = append(q.data, v) }
func (q *Queue[T]) Dequeue() (T, bool) {
    var zero T
    if q.head >= len(q.data) { return zero, false }
    v := q.data[q.head]; q.head++
    if q.head > len(q.data)/2 { // compact to avoid leak
        q.data = q.data[q.head:]; q.head = 0
    }
    return v, true
}
func (q *Queue[T]) Len() int { return len(q.data) - q.head }

// Monotonic stack — next greater element
func nextGreaterElement(nums []int) []int {
    n := len(nums)
    result := make([]int, n)
    for i := range result { result[i] = -1 }
    stack := []int{} // stores indices

    for i := 0; i < n; i++ {
        for len(stack) > 0 && nums[i] > nums[stack[len(stack)-1]] {
            idx := stack[len(stack)-1]
            stack = stack[:len(stack)-1]
            result[idx] = nums[i]
        }
        stack = append(stack, i)
    }
    return result
}

// Valid parentheses
func isValid(s string) bool {
    stack := []rune{}
    pairs := map[rune]rune{')': '(', ']': '[', '}': '{'}
    for _, c := range s {
        if c == '(' || c == '[' || c == '{' {
            stack = append(stack, c)
        } else {
            if len(stack) == 0 || stack[len(stack)-1] != pairs[c] { return false }
            stack = stack[:len(stack)-1]
        }
    }
    return len(stack) == 0
}

func main() {
    s := &Stack[int]{}
    s.Push(1); s.Push(2); s.Push(3)
    v, _ := s.Pop(); fmt.Println(v) // 3

    q := &Queue[string]{}
    q.Enqueue("a"); q.Enqueue("b")
    val, _ := q.Dequeue(); fmt.Println(val) // "a"

    fmt.Println(nextGreaterElement([]int{2, 1, 2, 4, 3})) // [4 2 4 -1 -1]
    fmt.Println(isValid("()[]{}")  ) // true
    fmt.Println(isValid("([)]")   ) // false
}

19 Linked List

What it is

A chain of nodes, each holding a value and a pointer to the next node. Unlike slices, there is no random access — traversal is sequential from the head. Insertions and deletions are O(1) given a pointer to the location — no shifting needed.

Real-world analogy

A treasure hunt. Each clue (node) tells you the prize at this location and where to find the next clue. There's no shortcut to the 10th clue — you must follow all 9 preceding clues. Inserting a new clue 5 only requires rewriting clue 4 and clue 5's pointers.

How it works internally

Each node is a heap-allocated struct with a Next *ListNode field. The dummy head sentinel node eliminates nil-head edge cases — return dummy.Next at the end. Floyd's cycle detection uses a slow (1-step) and fast (2-step) pointer; they meet iff a cycle exists.

When to use

Interview problems: reverse, find middle, detect cycle, merge sorted lists, remove nth from end. Fast/slow pointer (Floyd's) for cycle detection and finding the middle. Dummy head for any insert/delete/merge operation to avoid nil-handling edge cases at the head.

Complexity
Insert/Delete with pointer O(1)Access by index O(n)Search O(n)No random access — must traverse
Concept·Linked List
What it is
A sequence of nodes where each node holds a value and a pointer to the next node. Unlike slices, nodes are scattered in memory — no index access. The trade-off: O(1) insert/delete at a known pointer vs O(n) for arrays. In Go, built manually with a struct{ Val int; Next *Node }.
Real-world analogy
🚂A train — each car only knows which car comes after it. To reach car 7, you must walk through cars 1→2→3→…→7. But adding a new car between two existing ones is instant — just re-link the connections.
How it works
Traversal: for node != nil { node = node.Next }. Modification always requires a pointer to the previous node. Use a dummy head sentinel to unify edge cases (empty list, single node, head deletion). Fast + slow pointer detects cycles and finds midpoints.
Complexity
Access O(n) Insert/Delete O(1)*
*Given a pointer to the node. Search is still O(n). No random access.
When to use — recognition cues
reverse a list detect cycle (Floyd's) find middle node merge two sorted lists remove nth from end reorder list LRU cache (doubly linked + hashmap)
linked_list.go
package main

import "fmt"

type ListNode struct {
    Val  int
    Next *ListNode
}

// Helper: build list from slice
func buildList(vals []int) *ListNode {
    dummy := &ListNode{}
    curr := dummy
    for _, v := range vals { curr.Next = &ListNode{Val: v}; curr = curr.Next }
    return dummy.Next
}

// Helper: print list
func printList(head *ListNode) {
    for head != nil { fmt.Printf("%d ", head.Val); head = head.Next }
    fmt.Println()
}

// Reverse linked list — iterative
func reverse(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head
    for curr != nil {
        next := curr.Next
        curr.Next = prev
        prev = curr
        curr = next
    }
    return prev
}

// Floyd's cycle detection
func hasCycle(head *ListNode) bool {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast { return true }
    }
    return false
}

// Find middle (slow/fast pointer)
func middleNode(head *ListNode) *ListNode {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
    }
    return slow // for even length: second middle
}

// Merge two sorted lists
func mergeSorted(l1, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    curr := dummy
    for l1 != nil && l2 != nil {
        if l1.Val <= l2.Val { curr.Next = l1; l1 = l1.Next } else { curr.Next = l2; l2 = l2.Next }
        curr = curr.Next
    }
    if l1 != nil { curr.Next = l1 } else { curr.Next = l2 }
    return dummy.Next
}

// Remove nth from end (one pass)
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    dummy := &ListNode{Next: head}
    fast, slow := dummy, dummy
    for i := 0; i <= n; i++ { fast = fast.Next }
    for fast != nil { fast = fast.Next; slow = slow.Next }
    slow.Next = slow.Next.Next
    return dummy.Next
}

func main() {
    list := buildList([]int{1, 2, 3, 4, 5})
    printList(list)
    printList(reverse(list))
    list2 := buildList([]int{1, 2, 3, 4, 5})
    fmt.Println(middleNode(list2).Val) // 3
    l1 := buildList([]int{1, 3, 5})
    l2 := buildList([]int{2, 4, 6})
    printList(mergeSorted(l1, l2))
    list3 := buildList([]int{1,2,3,4,5})
    printList(removeNthFromEnd(list3, 2)) // 1 2 3 5
}
Dummy Node Pattern Use a dummy := &ListNode{} sentinel node to avoid nil-head edge cases in insert, delete, and merge operations. Return dummy.Next.

19b Doubly Linked List & LRU Cache

Concept·Doubly Linked List
What it is
A linked list where each node holds a pointer to both its next and prev node. This makes removal of a known node O(1) — no need to traverse to find the predecessor. The key upgrade over singly linked list.
Real-world analogy
A browser history — you can go forward and backward. Each page knows both the page you came from and the page you can go to next. Removing a page from history is instant because each page knows its neighbours.
How it works
Use two sentinel nodes: head and tail (dummy). Real nodes live between them. Insert after head: wire 4 pointers. Remove any node: wire its prev and next to each other. No nil checks needed because sentinels always exist.
Complexity
Insert O(1) Delete O(1)* Search O(n)
*Given a pointer to the node. Combined with a hashmap for O(1) lookup → LRU Cache pattern.
When to use — recognition cues
LRU / LFU cache move-to-front on access O(1) remove from middle undo/redo history browser forward/back deque implementation
Doubly Linked List — Core Implementation
doubly_linked_list.go
package main

import "fmt"

// ── Node ──────────────────────────────────────────────────
type DNode struct {
    Val        int
    Prev, Next *DNode
}

// ── Doubly Linked List with sentinel head/tail ────────────
// head ↔ [node1] ↔ [node2] ↔ ... ↔ tail
// Sentinels eliminate all nil-pointer edge cases
type DoublyLinkedList struct {
    head, tail *DNode
    size       int
}

func NewDLL() *DoublyLinkedList {
    head := &DNode{}
    tail := &DNode{}
    head.Next = tail
    tail.Prev = head
    return &DoublyLinkedList{head: head, tail: tail}
}

// insertAfter inserts node n immediately after node at
func (dl *DoublyLinkedList) insertAfter(at, n *DNode) {
    n.Prev = at
    n.Next = at.Next
    at.Next.Prev = n
    at.Next = n
    dl.size++
}

// remove detaches node n from the list
func (dl *DoublyLinkedList) remove(n *DNode) {
    n.Prev.Next = n.Next
    n.Next.Prev = n.Prev
    n.Prev = nil // help GC
    n.Next = nil
    dl.size--
}

// AddFront inserts a new node at the front (after head sentinel)
func (dl *DoublyLinkedList) AddFront(val int) *DNode {
    n := &DNode{Val: val}
    dl.insertAfter(dl.head, n)
    return n
}

// AddBack inserts a new node at the back (before tail sentinel)
func (dl *DoublyLinkedList) AddBack(val int) *DNode {
    n := &DNode{Val: val}
    dl.insertAfter(dl.tail.Prev, n)
    return n
}

// RemoveFront removes and returns the front node (nil if empty)
func (dl *DoublyLinkedList) RemoveFront() *DNode {
    if dl.size == 0 { return nil }
    n := dl.head.Next
    dl.remove(n)
    return n
}

// RemoveBack removes and returns the back node (nil if empty)
func (dl *DoublyLinkedList) RemoveBack() *DNode {
    if dl.size == 0 { return nil }
    n := dl.tail.Prev
    dl.remove(n)
    return n
}

// MoveToFront moves an existing node to the front
func (dl *DoublyLinkedList) MoveToFront(n *DNode) {
    dl.remove(n)
    dl.size++ // remove decremented, insertAfter will increment
    dl.insertAfter(dl.head, n)
}

// Front / Back peek
func (dl *DoublyLinkedList) Front() *DNode {
    if dl.size == 0 { return nil }
    return dl.head.Next
}

func (dl *DoublyLinkedList) Back() *DNode {
    if dl.size == 0 { return nil }
    return dl.tail.Prev
}

func (dl *DoublyLinkedList) Len() int { return dl.size }

// Print traverses forward
func (dl *DoublyLinkedList) Print() {
    for n := dl.head.Next; n != dl.tail; n = n.Next {
        fmt.Printf("%d ", n.Val)
    }
    fmt.Println()
}

// PrintReverse traverses backward
func (dl *DoublyLinkedList) PrintReverse() {
    for n := dl.tail.Prev; n != dl.head; n = n.Prev {
        fmt.Printf("%d ", n.Val)
    }
    fmt.Println()
}

func dllDemo() {
    dl := NewDLL()
    dl.AddBack(1)
    dl.AddBack(2)
    dl.AddBack(3)
    n := dl.AddFront(0)

    dl.Print()        // 0 1 2 3
    dl.PrintReverse() // 3 2 1 0

    dl.MoveToFront(dl.Back()) // move 3 to front
    dl.Print()        // 3 0 1 2

    dl.remove(n)      // remove node with val=0
    dl.Print()        // 3 1 2

    fmt.Println("len:", dl.Len()) // 3
    fmt.Println("front:", dl.Front().Val) // 3
    fmt.Println("back:",  dl.Back().Val)  // 2
}
LRU Cache — DLL + HashMap
Get O(1) Put O(1) Space O(capacity)

The classic Staff+ interview problem. HashMap gives O(1) lookup of the node; DLL gives O(1) move-to-front and eviction of the least recently used (back of list).

lru_cache.go
package main

import "fmt"

// ── LRU Cache Node ────────────────────────────────────────
type LRUNode struct {
    key, val   int
    prev, next *LRUNode
}

// ── LRU Cache ─────────────────────────────────────────────
type LRUCache struct {
    capacity int
    cache    map[int]*LRUNode // key → node (O(1) lookup)
    head     *LRUNode         // MRU end (most recently used)
    tail     *LRUNode         // LRU end (least recently used)
}

func NewLRUCache(capacity int) *LRUCache {
    head := &LRUNode{} // sentinel
    tail := &LRUNode{} // sentinel
    head.next = tail
    tail.prev = head
    return &LRUCache{
        capacity: capacity,
        cache:    make(map[int]*LRUNode),
        head:     head,
        tail:     tail,
    }
}

// insertFront places node right after head sentinel (MRU position)
func (c *LRUCache) insertFront(n *LRUNode) {
    n.prev = c.head
    n.next = c.head.next
    c.head.next.prev = n
    c.head.next = n
}

// removeNode detaches node from wherever it is
func (c *LRUCache) removeNode(n *LRUNode) {
    n.prev.next = n.next
    n.next.prev = n.prev
}

// Get returns the value for key, or -1 if not found.
// Moves accessed node to front (most recently used).
func (c *LRUCache) Get(key int) int {
    node, ok := c.cache[key]
    if !ok { return -1 }
    // Move to front — mark as most recently used
    c.removeNode(node)
    c.insertFront(node)
    return node.val
}

// Put inserts or updates key→val.
// Evicts the LRU entry (back of list) if over capacity.
func (c *LRUCache) Put(key, val int) {
    if node, ok := c.cache[key]; ok {
        // Update existing — move to front
        node.val = val
        c.removeNode(node)
        c.insertFront(node)
        return
    }
    // Evict LRU if at capacity
    if len(c.cache) == c.capacity {
        lru := c.tail.prev   // node just before tail sentinel
        c.removeNode(lru)
        delete(c.cache, lru.key)
    }
    // Insert new node at front
    node := &LRUNode{key: key, val: val}
    c.insertFront(node)
    c.cache[key] = node
}

func lruDemo() {
    cache := NewLRUCache(3)
    cache.Put(1, 10)
    cache.Put(2, 20)
    cache.Put(3, 30)
    fmt.Println(cache.Get(1))  // 10 — 1 moves to MRU
    cache.Put(4, 40)           // evicts 2 (LRU)
    fmt.Println(cache.Get(2))  // -1 (evicted)
    fmt.Println(cache.Get(3))  // 30
    fmt.Println(cache.Get(4))  // 40
    cache.Put(5, 50)           // evicts 1 (now LRU after 3 and 4 accessed)
    fmt.Println(cache.Get(1))  // -1 (evicted)
}
LFU Cache — Harder Variant
Get O(1) Put O(1)

LFU evicts the least frequently used key. Tie-break: among equal-frequency keys, evict the least recently used. Uses a map of frequency → DLL and tracks minFreq.

lfu_cache.go
package main

import "fmt"

type LFUNode struct {
    key, val, freq int
    prev, next     *LFUNode
}

// freqList is a DLL for one frequency bucket
type freqList struct {
    head, tail *LFUNode
    size       int
}

func newFreqList() *freqList {
    h, t := &LFUNode{}, &LFUNode{}
    h.next = t; t.prev = h
    return &freqList{head: h, tail: t}
}

func (fl *freqList) addFront(n *LFUNode) {
    n.prev = fl.head; n.next = fl.head.next
    fl.head.next.prev = n; fl.head.next = n
    fl.size++
}

func (fl *freqList) remove(n *LFUNode) {
    n.prev.next = n.next; n.next.prev = n.prev
    fl.size--
}

func (fl *freqList) removeLast() *LFUNode {
    if fl.size == 0 { return nil }
    n := fl.tail.prev
    fl.remove(n)
    return n
}

// ── LFU Cache ─────────────────────────────────────────────
type LFUCache struct {
    capacity int
    minFreq  int
    keys     map[int]*LFUNode    // key  → node
    freqs    map[int]*freqList   // freq → DLL of nodes at that freq
}

func NewLFUCache(capacity int) *LFUCache {
    return &LFUCache{
        capacity: capacity,
        keys:     make(map[int]*LFUNode),
        freqs:    make(map[int]*freqList),
    }
}

func (c *LFUCache) promote(n *LFUNode) {
    oldFreq := n.freq
    c.freqs[oldFreq].remove(n)
    if c.freqs[oldFreq].size == 0 {
        delete(c.freqs, oldFreq)
        if c.minFreq == oldFreq { c.minFreq++ }
    }
    n.freq++
    if c.freqs[n.freq] == nil { c.freqs[n.freq] = newFreqList() }
    c.freqs[n.freq].addFront(n)
}

func (c *LFUCache) Get(key int) int {
    n, ok := c.keys[key]
    if !ok { return -1 }
    c.promote(n)
    return n.val
}

func (c *LFUCache) Put(key, val int) {
    if c.capacity == 0 { return }
    if n, ok := c.keys[key]; ok {
        n.val = val
        c.promote(n)
        return
    }
    if len(c.keys) == c.capacity {
        evicted := c.freqs[c.minFreq].removeLast()
        delete(c.keys, evicted.key)
    }
    n := &LFUNode{key: key, val: val, freq: 1}
    c.keys[key] = n
    if c.freqs[1] == nil { c.freqs[1] = newFreqList() }
    c.freqs[1].addFront(n)
    c.minFreq = 1
}

func lfuDemo() {
    cache := NewLFUCache(2)
    cache.Put(1, 1)
    cache.Put(2, 2)
    fmt.Println(cache.Get(1)) // 1  (freq[1]=2, freq[2]=1→ 2 is LFU)
    cache.Put(3, 3)           // evict key 2 (freq=1, LRU among freq-1)
    fmt.Println(cache.Get(2)) // -1 (evicted)
    fmt.Println(cache.Get(3)) // 3
    cache.Put(4, 4)           // evict key 3 (freq=1) — key 1 has freq=2
    fmt.Println(cache.Get(1)) // 1
    fmt.Println(cache.Get(3)) // -1 (evicted)
    fmt.Println(cache.Get(4)) // 4
}

func main() {
    dllDemo()
    fmt.Println("---")
    lruDemo()
    fmt.Println("---")
    lfuDemo()
}
⚠ Trap — Forgetting to Update the HashMap In LRU Put, when evicting the LRU node you must delete(c.cache, lru.key) — remove it from the map and the list. Missing either half corrupts the cache silently: the map returns a stale pointer to a detached node.
⚠ Trap — Sentinel Nodes Without head/tail sentinels, insert and remove need nil checks for the first and last node — doubling the edge cases. With sentinels, every real node always has a valid prev and next, making all 4-pointer rewires unconditional.
★ Staff+ — LRU Follow-ups Interviewers commonly ask: (1) Make it thread-safe → wrap Get/Put with sync.RWMutex (RLock for Get, Lock for Put). (2) Add TTL expiry → store expiry time in the node, check on Get. (3) Upgrade to LFU → the variant above. (4) Distributed LRU → consistent hashing across nodes, local LRU per shard.

20 Trees

What it is

A hierarchical, acyclic graph where every node has exactly one parent (except the root). A binary tree has at most 2 children per node. A BST adds the invariant: left subtree < node < right subtree, enabling O(log n) search on balanced trees.

Real-world analogy

A company org chart: CEO at root, managers below, employees as leaves. Each employee has exactly one boss but can have multiple direct reports. The BST is a filing system — labels are alphabetically ordered left-to-right, so you never need to search all drawers.

How it works internally

Trees are recursively defined — left/right subtrees are themselves trees. DFS traversals (in/pre/post-order) use the call stack implicitly. BFS uses a queue. Height = O(log n) for balanced, O(n) for skewed. Iterative traversal avoids stack overflow risk on degenerate inputs.

When to use

Tree problems almost always use recursion. Identify the return type pattern: bool (path exists), int (aggregate up the tree), void+closure (collect results). LCA: if both targets in different subtrees, current node is ancestor. Iterative inorder for BST validation/sorted output.

Complexity
DFS/BFS O(n)BST search O(log n) balancedBST worst case O(n) skewedSpace O(h) recursion stack (h = height)
Concept·Trees
What it is
A hierarchical data structure: one root node, each node has zero or more children, no cycles. A binary tree has at most 2 children (left, right). A BST adds the invariant: left subtree values < node < right subtree values, giving O(log n) search on balanced trees.
Real-world analogy
🌳A company org chart — the CEO is root, managers branch downward, employees are leaves. Every node has exactly one parent (except root). Decisions "flow down" and results "bubble up" — exactly how recursive tree algorithms work.
How it works
DFS traversals: inorder (L→root→R, gives sorted for BST), preorder (root→L→R), postorder (L→R→root). BFS: level-order with a queue. Recursive solutions: define what to return from a leaf, then combine left + right results at each node.
Complexity
Balanced: O(log n) Skewed: O(n)
Space: O(h) call stack where h = height. O(log n) balanced, O(n) worst case skewed tree.
When to use — recognition cues
hierarchical data BST search / insert / validate lowest common ancestor path sum / max path serialize / deserialize level-order output diameter / height / balance
trees.go
package main

import "fmt"

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

// ── DFS traversals ─────────────────────────────────────────
func inorder(root *TreeNode) []int {
    if root == nil { return nil }
    res := inorder(root.Left)
    res = append(res, root.Val)
    res = append(res, inorder(root.Right)...)
    return res
}

// Iterative inorder (common interview ask)
func inorderIter(root *TreeNode) []int {
    var res []int
    var stack []*TreeNode
    curr := root
    for curr != nil || len(stack) > 0 {
        for curr != nil { stack = append(stack, curr); curr = curr.Left }
        curr = stack[len(stack)-1]; stack = stack[:len(stack)-1]
        res = append(res, curr.Val)
        curr = curr.Right
    }
    return res
}

// ── BFS / Level-order ──────────────────────────────────────
func levelOrder(root *TreeNode) [][]int {
    if root == nil { return nil }
    var result [][]int
    queue := []*TreeNode{root}
    for len(queue) > 0 {
        level := make([]int, 0, len(queue))
        size := len(queue)
        for i := 0; i < size; i++ {
            node := queue[i]
            level = append(level, node.Val)
            if node.Left  != nil { queue = append(queue, node.Left)  }
            if node.Right != nil { queue = append(queue, node.Right) }
        }
        queue = queue[size:]
        result = append(result, level)
    }
    return result
}

// ── Max depth ──────────────────────────────────────────────
func maxDepth(root *TreeNode) int {
    if root == nil { return 0 }
    l, r := maxDepth(root.Left), maxDepth(root.Right)
    if l > r { return l + 1 }; return r + 1
}

// ── Diameter of binary tree ────────────────────────────────
func diameterOfBinaryTree(root *TreeNode) int {
    maxD := 0
    var depth func(*TreeNode) int
    depth = func(node *TreeNode) int {
        if node == nil { return 0 }
        l, r := depth(node.Left), depth(node.Right)
        if l+r > maxD { maxD = l + r }
        if l > r { return l + 1 }; return r + 1
    }
    depth(root)
    return maxD
}

// ── BST operations ─────────────────────────────────────────
func searchBST(root *TreeNode, val int) *TreeNode {
    for root != nil {
        switch {
        case val == root.Val: return root
        case val < root.Val:  root = root.Left
        default:              root = root.Right
        }
    }
    return nil
}

func isValidBST(root *TreeNode) bool {
    var validate func(*TreeNode, int, int) bool
    validate = func(node *TreeNode, min, max int) bool {
        if node == nil { return true }
        if node.Val <= min || node.Val >= max { return false }
        return validate(node.Left, min, node.Val) && validate(node.Right, node.Val, max)
    }
    return validate(root, -1<<62, 1<<62)
}

// ── Lowest Common Ancestor ─────────────────────────────────
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
    if root == nil || root == p || root == q { return root }
    left  := lowestCommonAncestor(root.Left, p, q)
    right := lowestCommonAncestor(root.Right, p, q)
    if left != nil && right != nil { return root }
    if left != nil { return left }; return right
}

func main() {
    //       4
    //      / \
    //     2   6
    //    / \ / \
    //   1  3 5  7
    root := &TreeNode{Val: 4,
        Left: &TreeNode{Val: 2,
            Left: &TreeNode{Val: 1}, Right: &TreeNode{Val: 3}},
        Right: &TreeNode{Val: 6,
            Left: &TreeNode{Val: 5}, Right: &TreeNode{Val: 7}},
    }
    fmt.Println(inorder(root))     // [1 2 3 4 5 6 7]
    fmt.Println(inorderIter(root)) // [1 2 3 4 5 6 7]
    fmt.Println(levelOrder(root))  // [[4] [2 6] [1 3 5 7]]
    fmt.Println(maxDepth(root))    // 3
    fmt.Println(isValidBST(root))  // true
}

21 Graphs

What it is

A set of nodes (vertices) connected by edges. Can be directed/undirected, weighted/unweighted, cyclic/acyclic (DAG). The most general data structure — trees, linked lists, and grids are all special cases of graphs.

Real-world analogy

A road map: cities are nodes, roads are edges. One-way streets are directed edges. Toll costs are weights. "Can I get from A to B?" is reachability. "What's the fastest route?" is shortest path. "Are all cities connected?" is connected components.

How it works internally

Adjacency list ([][]int): O(V+E) space, fast neighbour iteration — prefer for sparse graphs. Adjacency matrix ([][]bool): O(V²) space, O(1) edge lookup — prefer for dense graphs or when edge existence query dominates. DFS uses a visited set; BFS uses a queue. Topological sort only on DAGs.

When to use

Any "connection", "path", "dependency", "network", or "spreading" problem. DFS for connectivity, cycles, topological order. BFS for shortest path (unweighted). Dijkstra for weighted shortest path (non-negative). Union-Find for dynamic connectivity. Topological sort for dependency ordering.

Complexity
DFS/BFS O(V+E)Dijkstra O((V+E) log V)Bellman-Ford O(VE)Cycle detection required before topo sort
Concept·Graphs
What it is
A set of nodes (vertices) connected by edges. Trees are graphs with no cycles and one root. Graphs can be directed/undirected, weighted/unweighted, cyclic/acyclic. Represented as an adjacency list ([][]int) or adjacency matrix ([][]bool).
Real-world analogy
🗺A road map — cities are nodes, roads are edges. Finding the shortest route is shortest path. Checking if you can reach a city is connectivity. Scheduling tasks without conflicts is topological sort.
How it works
Build adjacency list from edge input. Track visited[] to avoid cycles. DFS with a recursion stack detects back-edges (cycles). BFS guarantees shortest path on unweighted graphs. Kahn's algorithm (in-degree queue) gives topological order for DAGs.
Complexity
DFS/BFS O(V+E) Matrix O(V²)
Adjacency list is standard. Matrix only for dense graphs or when edge lookup must be O(1).
When to use — recognition cues
connectivity / reachability shortest path (unweighted → BFS) shortest path (weighted → Dijkstra) topological sort / course schedule cycle detection connected components clone graph
graphs.go
package main

import "fmt"

type Graph struct {
    adj map[int][]int
}

func newGraph() *Graph { return &Graph{adj: make(map[int][]int)} }
func (g *Graph) AddEdge(u, v int) { g.adj[u] = append(g.adj[u], v) } // directed

// DFS — recursive
func (g *Graph) DFS(start int) []int {
    visited := make(map[int]bool)
    var result []int
    var dfs func(int)
    dfs = func(v int) {
        if visited[v] { return }
        visited[v] = true
        result = append(result, v)
        for _, n := range g.adj[v] { dfs(n) }
    }
    dfs(start)
    return result
}

// BFS
func (g *Graph) BFS(start int) []int {
    visited := map[int]bool{start: true}
    queue := []int{start}
    var result []int
    for len(queue) > 0 {
        v := queue[0]; queue = queue[1:]
        result = append(result, v)
        for _, n := range g.adj[v] {
            if !visited[n] { visited[n] = true; queue = append(queue, n) }
        }
    }
    return result
}

// Topological sort (Kahn's BFS — for DAG)
func topoSort(numNodes int, edges [][2]int) []int {
    inDegree := make([]int, numNodes)
    adj := make([][]int, numNodes)
    for _, e := range edges {
        adj[e[0]] = append(adj[e[0]], e[1])
        inDegree[e[1]]++
    }
    queue := []int{}
    for i := 0; i < numNodes; i++ { if inDegree[i] == 0 { queue = append(queue, i) } }
    var result []int
    for len(queue) > 0 {
        v := queue[0]; queue = queue[1:]
        result = append(result, v)
        for _, n := range adj[v] {
            inDegree[n]--
            if inDegree[n] == 0 { queue = append(queue, n) }
        }
    }
    if len(result) != numNodes { return nil } // cycle detected
    return result
}

// Cycle detection (directed graph — DFS with color)
// 0=unvisited, 1=in-stack, 2=done
func hasCycle(numNodes int, adj [][]int) bool {
    color := make([]int, numNodes)
    var dfs func(int) bool
    dfs = func(v int) bool {
        color[v] = 1
        for _, n := range adj[v] {
            if color[n] == 1 { return true }  // back edge
            if color[n] == 0 && dfs(n) { return true }
        }
        color[v] = 2
        return false
    }
    for i := 0; i < numNodes; i++ { if color[i] == 0 && dfs(i) { return true } }
    return false
}

// Number of islands (grid BFS — very common)
func numIslands(grid [][]byte) int {
    if len(grid) == 0 { return 0 }
    rows, cols := len(grid), len(grid[0])
    count := 0
    var bfs func(int, int)
    bfs = func(r, c int) {
        if r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] != '1' { return }
        grid[r][c] = '0' // mark visited
        bfs(r+1, c); bfs(r-1, c); bfs(r, c+1); bfs(r, c-1)
    }
    for r := 0; r < rows; r++ {
        for c := 0; c < cols; c++ {
            if grid[r][c] == '1' { count++; bfs(r, c) }
        }
    }
    return count
}

func main() {
    g := newGraph()
    g.AddEdge(0,1); g.AddEdge(0,2); g.AddEdge(1,3); g.AddEdge(2,3)
    fmt.Println("DFS:", g.DFS(0)) // [0 1 3 2]
    fmt.Println("BFS:", g.BFS(0)) // [0 1 2 3]

    edges := [][2]int{{5,2},{5,0},{4,0},{4,1},{2,3},{3,1}}
    fmt.Println("Topo:", topoSort(6, edges)) // valid topo order

    grid := [][]byte{
        {'1','1','0','0'},
        {'1','1','0','1'},
        {'0','0','0','1'},
    }
    fmt.Println("Islands:", numIslands(grid)) // 2
}
★ Staff+ — Union-Find (Disjoint Set) For connectivity problems. O(α(n)) amortized with path compression + union by rank. Used in Kruskal's MST, detecting cycles, network connectivity. Worth memorizing as a template.

22 Heap / Priority Queue

What it is

A complete binary tree stored as an array where every parent is ≤ its children (min-heap) or ≥ its children (max-heap). Provides O(1) access to the extreme element and O(log n) insert/remove. Go's container/heap requires implementing a 5-method interface.

Real-world analogy

A hospital emergency room triage queue. The most critical patient (max-priority) is always treated first, regardless of arrival order. When a new patient arrives or one is discharged, the queue reshuffles instantly to keep the most critical at the front.

How it works internally

Stored as an array. Parent of index i: (i-1)/2. Children of i: 2i+1, 2i+2. Push: append + sift-up (swap with parent while violating heap property). Pop: swap root with last element, shrink, sift-down. Build-heap from unsorted array: O(n) using bottom-up heapify (not N×push).

When to use

K largest/smallest elements (min-heap of size k). Median of stream (two heaps: maxHeap for lower half, minHeap for upper half). Dijkstra's algorithm. Merge K sorted lists. Task scheduling by priority. Whenever you need repeated access to the current minimum or maximum.

Complexity
Peek O(1)Push/Pop O(log n)Build-heap O(n)Go: must implement 5-method interface
Concept·Heap / Priority Queue
What it is
A complete binary tree stored as an array, satisfying the heap property: every parent is ≤ (min-heap) or ≥ (max-heap) its children. The minimum (or maximum) is always at index 0. In Go, use container/heap — you implement 5 interface methods and it does the rest.
Real-world analogy
🏥A hospital ER triage queue — patients aren't served in arrival order, but by severity. The most critical patient always goes next. Adding a new patient and re-prioritising takes O(log n), not O(n).
How it works
Stored as array: parent of i = (i-1)/2, children = 2i+1, 2i+2. Push: append + bubble up. Pop: swap root with last, remove last, bubble down. Go's heap.Push/Pop call your Swap/Less methods. Flip Less to switch min↔max.
Complexity
Push/Pop O(log n) Peek O(1) Build O(n)
Key insight: maintaining a heap of size k costs O(n log k) over n elements — much better than sorting O(n log n) for top-K problems.
When to use — recognition cues
K largest / K smallest K most frequent median of stream (two heaps) merge K sorted lists Dijkstra shortest path task scheduling "always process the min/max next"
heap_demo.go
package main

import (
    "container/heap"
    "fmt"
)

// ── Min-Heap (container/heap interface) ───────────────────
type MinHeap []int

func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] } // < = min, > = max
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x any)        { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() any {
    old := *h; n := len(old)
    x := old[n-1]; *h = old[:n-1]; return x
}

// ── Max-Heap — just flip Less ─────────────────────────────
type MaxHeap []int
func (h MaxHeap) Len() int           { return len(h) }
func (h MaxHeap) Less(i, j int) bool { return h[i] > h[j] } // flipped
func (h MaxHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *MaxHeap) Push(x any)        { *h = append(*h, x.(int)) }
func (h *MaxHeap) Pop() any          { old := *h; n := len(old); x := old[n-1]; *h = old[:n-1]; return x }

// ── Struct heap (priority queue with custom ordering) ─────
type Item struct { value string; priority int }
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int           { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].priority > pq[j].priority } // max-priority
func (pq PriorityQueue) Swap(i, j int)      { pq[i], pq[j] = pq[j], pq[i] }
func (pq *PriorityQueue) Push(x any)        { *pq = append(*pq, x.(*Item)) }
func (pq *PriorityQueue) Pop() any {
    old := *pq; n := len(old); x := old[n-1]; *pq = old[:n-1]; return x
}

// ── Kth largest element ───────────────────────────────────
// Min-heap of size k — O(n log k)
func findKthLargest(nums []int, k int) int {
    h := &MinHeap{}
    heap.Init(h)
    for _, n := range nums {
        heap.Push(h, n)
        if h.Len() > k { heap.Pop(h) } // remove smallest
    }
    return (*h)[0] // min of heap = kth largest
}

func main() {
    h := &MinHeap{3, 1, 4, 1, 5, 9}
    heap.Init(h)
    heap.Push(h, 2)
    fmt.Println(heap.Pop(h)) // 1 (min)
    fmt.Println(heap.Pop(h)) // 1

    pq := &PriorityQueue{
        {value: "low", priority: 1},
        {value: "high", priority: 10},
        {value: "med", priority: 5},
    }
    heap.Init(pq)
    fmt.Println(heap.Pop(pq).(*Item).value) // "high"

    fmt.Println(findKthLargest([]int{3,2,1,5,6,4}, 2)) // 5
    fmt.Println(findKthLargest([]int{3,2,3,1,2,4,5,5,6}, 4)) // 4
}
Heap Use Cases K largest/smallest, median of stream (two heaps), Dijkstra's SSSP, merge K sorted lists, task scheduling, top-K frequent elements.

23 Dynamic Programming

What it is

An optimisation technique for problems with overlapping subproblems and optimal substructure. Solve each subproblem once and cache the result. Avoids the exponential blowup of naive recursion by trading space for time.

Real-world analogy

A spreadsheet with formulas. Each cell is a subproblem; it references earlier cells (subproblem results) rather than recomputing from scratch. Fill in cells in the right order and the final answer appears in the last cell — that's bottom-up DP.

How it works internally

Top-down: recursion + memoisation map — natural to write, easy to reason about. Bottom-up: fill a dp table iteratively in dependency order — avoids recursion overhead, easier to optimise space. State = minimal set of variables that uniquely defines a subproblem. Transition = how to get dp[i] from smaller states.

When to use

Keywords: "count ways", "minimum cost", "maximum value", "can you achieve", "how many distinct" over sequences, grids, or strings. Confirm overlapping subproblems (draw the recursion tree — do branches repeat?). If yes, DP. If subproblems don't overlap, greedy or divide-and-conquer.

Complexity
Time O(states × transition)Space O(states) — often reducible1D rolling array when dp[i] depends only on dp[i-1]
Concept··Dynamic Programming
What it is
An optimisation technique that solves a problem by breaking it into overlapping subproblems and caching their results. Requires two properties: (1) optimal substructure — optimal solution built from optimal sub-solutions; (2) overlapping subproblems — same sub-problems recur many times.
Real-world analogy
📝Calculating your monthly salary: you don't recount every hour worked since joining — you cache last month's total and add this month's hours. DP is memoised recursion: solve once, look up forever.
How it works
Top-down: recursive + memo map (memoization). Bottom-up: fill a dp[] table from base cases up (tabulation). Both give same complexity. Bottom-up is usually faster (no call stack). Optimise space: if dp[i] only needs dp[i-1], use two variables.
Complexity
Time: O(states × work per state)
Space = O(states). Often reducible to O(1 row) by rolling array. DP converts exponential brute force to polynomial.
When to use — recognition cues
count ways to do X minimum / maximum cost to reach Y longest / shortest subsequence knapsack / subset selection can you reach / partition into decisions at each step with optimal outcome string edit distance
dp.go
package main

import "fmt"

// ── Memoization template (top-down) ──────────────────────
func fib(n int) int {
    memo := make(map[int]int)
    var dp func(int) int
    dp = func(n int) int {
        if n <= 1 { return n }
        if v, ok := memo[n]; ok { return v }
        memo[n] = dp(n-1) + dp(n-2)
        return memo[n]
    }
    return dp(n)
}

// ── Climbing stairs (bottom-up) ───────────────────────────
func climbStairs(n int) int {
    if n <= 2 { return n }
    prev, curr := 1, 2
    for i := 3; i <= n; i++ { prev, curr = curr, prev+curr }
    return curr
}

// ── 0/1 Knapsack ──────────────────────────────────────────
func knapsack(weights, values []int, capacity int) int {
    n := len(weights)
    dp := make([][]int, n+1)
    for i := range dp { dp[i] = make([]int, capacity+1) }
    for i := 1; i <= n; i++ {
        for w := 0; w <= capacity; w++ {
            dp[i][w] = dp[i-1][w] // don't take item
            if weights[i-1] <= w {
                take := dp[i-1][w-weights[i-1]] + values[i-1]
                if take > dp[i][w] { dp[i][w] = take }
            }
        }
    }
    return dp[n][capacity]
}

// ── Longest Common Subsequence ────────────────────────────
func lcs(s1, s2 string) int {
    m, n := len(s1), len(s2)
    dp := make([][]int, m+1)
    for i := range dp { dp[i] = make([]int, n+1) }
    for i := 1; i <= m; i++ {
        for j := 1; j <= n; j++ {
            if s1[i-1] == s2[j-1] { dp[i][j] = dp[i-1][j-1] + 1 } else
            if dp[i-1][j] > dp[i][j-1] { dp[i][j] = dp[i-1][j] } else { dp[i][j] = dp[i][j-1] }
        }
    }
    return dp[m][n]
}

// ── Longest Increasing Subsequence — O(n log n) ───────────
func lengthOfLIS(nums []int) int {
    tails := []int{} // tails[i] = smallest tail of LIS with length i+1
    for _, n := range nums {
        // binary search for insertion point
        lo, hi := 0, len(tails)
        for lo < hi { mid := lo + (hi-lo)/2; if tails[mid] < n { lo = mid+1 } else { hi = mid } }
        if lo == len(tails) { tails = append(tails, n) } else { tails[lo] = n }
    }
    return len(tails)
}

// ── Coin Change ───────────────────────────────────────────
func coinChange(coins []int, amount int) int {
    dp := make([]int, amount+1)
    for i := 1; i <= amount; i++ { dp[i] = amount + 1 } // "infinity"
    for i := 1; i <= amount; i++ {
        for _, c := range coins {
            if c <= i && dp[i-c]+1 < dp[i] { dp[i] = dp[i-c] + 1 }
        }
    }
    if dp[amount] > amount { return -1 }; return dp[amount]
}

// ── Word Break ────────────────────────────────────────────
func wordBreak(s string, wordDict []string) bool {
    words := make(map[string]bool)
    for _, w := range wordDict { words[w] = true }
    n := len(s)
    dp := make([]bool, n+1)
    dp[0] = true
    for i := 1; i <= n; i++ {
        for j := 0; j < i; j++ {
            if dp[j] && words[s[j:i]] { dp[i] = true; break }
        }
    }
    return dp[n]
}

func main() {
    fmt.Println(fib(10))          // 55
    fmt.Println(climbStairs(10))  // 89
    fmt.Println(knapsack([]int{2,3,4,5}, []int{3,4,5,6}, 5)) // 7
    fmt.Println(lcs("abcde", "ace"))          // 3
    fmt.Println(lengthOfLIS([]int{10,9,2,5,3,7,101,18})) // 4
    fmt.Println(coinChange([]int{1,5,10,25}, 41)) // 4 (25+10+5+1)
    fmt.Println(wordBreak("leetcode", []string{"leet","code"})) // true
}
★ Staff+ — DP Decision Framework (1) Identify overlapping subproblems + optimal substructure. (2) Define state clearly. (3) Write recurrence. (4) Top-down = memoization, Bottom-up = tabulation (usually more cache-friendly). (5) Optimize space: if dp[i] only depends on dp[i-1], use two variables.

29 Depth-First Search

What it is

A graph/tree traversal that explores as deep as possible along each branch before backtracking. Uses the call stack (recursive) or an explicit stack (iterative). Visits every reachable node exactly once — O(V+E).

Real-world analogy

Exploring a cave system with one rule: always take the first unexplored tunnel, mark it, and only backtrack when you hit a dead end. You'll eventually map the entire cave, even the deepest passages, before finishing any "side branch".

How it works internally

Recursive DFS uses the goroutine's call stack as its own stack — simple but risks stack overflow on deep inputs. Iterative DFS pushes neighbours onto an explicit []int stack — visit order can differ from recursive. The key: mark visited before recursing to avoid revisiting in cyclic graphs.

When to use

Path existence, all paths, cycle detection, topological sort, connected components, tree traversals, subtree aggregation. Choose DFS over BFS when depth matters more than distance, or when you need to enumerate all solutions (backtracking).

Complexity
Time O(V+E)Space O(h) recursive (h = max depth)Iterative: push neighbours in reverse for same orderMust copy path slice before appending to results
Concept·Depth-First Search
What it is
A traversal strategy that goes as deep as possible along one branch before backtracking. Uses the call stack (recursive) or an explicit stack (iterative). Forms the backbone of almost all tree algorithms, cycle detection, topological sort, and backtracking.
Real-world analogy
🧭Exploring a maze by always taking the first available turn, marking dead ends, and backtracking to the last junction when you hit a wall. You fully explore one corridor before trying others.
How it works
Mark node visited → process → recurse on unvisited neighbours → unmark (backtracking) or leave marked (path finding). Three return shapes: bool (path exists?), int (aggregate value up the tree), void + closure (collect results). Base case = nil node or visited.
Complexity
Time O(V+E) Space O(h) call stack
h = height for trees, V for graphs. Can overflow stack on degenerate input — prefer iterative for n > 10k.
When to use — recognition cues
does path exist? all paths / enumerate solutions tree height / diameter / balance cycle detection topological sort (DFS variant) connected components flood fill / island counting

DFS explores as deep as possible before backtracking. The key decision: recursive (implicit call stack) vs iterative (explicit stack). Both are O(V+E) for graphs.

Use DFS whenPrefer BFS when
Path existence, all paths, cyclesShortest path (unweighted)
Topological sort, connected componentsMinimum steps/hops
Tree traversals, subtree problemsLevel-order, layer-by-layer
Backtracking / exhaustive searchMulti-source spreading (rotting oranges)
Core Templates
dfs_templates.go
package main

import "fmt"

// ── Template 1: Recursive DFS on graph ───────────────────
func dfsRecursive(graph [][]int, node int, visited []bool) {
    visited[node] = true
    fmt.Println("visit:", node)
    for _, nei := range graph[node] {
        if !visited[nei] {
            dfsRecursive(graph, nei, visited)
        }
    }
}

// ── Template 2: Iterative DFS (explicit stack) ────────────
// NOTE: iterative DFS visits in REVERSE order vs recursive
func dfsIterative(graph [][]int, start int) []int {
    n := len(graph)
    visited := make([]bool, n)
    stack := []int{start}
    var order []int
    for len(stack) > 0 {
        node := stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        if visited[node] { continue }
        visited[node] = true
        order = append(order, node)
        for _, nei := range graph[node] { // push all neighbours
            if !visited[nei] { stack = append(stack, nei) }
        }
    }
    return order
}

// ── Template 3: DFS returning a value (path sum) ─────────
type TreeNode struct{ Val int; Left, Right *TreeNode }

func hasPathSum(root *TreeNode, target int) bool {
    if root == nil { return false }
    target -= root.Val
    if root.Left == nil && root.Right == nil { return target == 0 } // leaf
    return hasPathSum(root.Left, target) || hasPathSum(root.Right, target)
}

// ── Template 4: DFS collecting all paths ─────────────────
func allPaths(root *TreeNode, target int) [][]int {
    var result [][]int
    var dfs func(node *TreeNode, remaining int, path []int)
    dfs = func(node *TreeNode, remaining int, path []int) {
        if node == nil { return }
        path = append(path, node.Val) // path is a copy here (append may allocate)
        remaining -= node.Val
        if node.Left == nil && node.Right == nil && remaining == 0 {
            tmp := make([]int, len(path))
            copy(tmp, path)          // MUST copy — path slice will be reused
            result = append(result, tmp)
            return
        }
        dfs(node.Left,  remaining, path)
        dfs(node.Right, remaining, path)
    }
    dfs(root, target, []int{})
    return result
}

// ── Template 5: DFS with state / global max ───────────────
func maxPathSum(root *TreeNode) int {
    maxSum := -1 << 62
    var dfs func(*TreeNode) int
    dfs = func(node *TreeNode) int {
        if node == nil { return 0 }
        left  := max(0, dfs(node.Left))  // ignore negative subtrees
        right := max(0, dfs(node.Right))
        if path := node.Val + left + right; path > maxSum { maxSum = path }
        if left > right { return node.Val + left }; return node.Val + right
    }
    dfs(root)
    return maxSum
}

// ── Connected components count ────────────────────────────
func countComponents(n int, edges [][2]int) int {
    adj := make([][]int, n)
    for _, e := range edges {
        adj[e[0]] = append(adj[e[0]], e[1])
        adj[e[1]] = append(adj[e[1]], e[0])
    }
    visited := make([]bool, n)
    var dfs func(int)
    dfs = func(v int) {
        visited[v] = true
        for _, nei := range adj[v] { if !visited[nei] { dfs(nei) } }
    }
    count := 0
    for i := 0; i < n; i++ {
        if !visited[i] { dfs(i); count++ }
    }
    return count
}

func max(a, b int) int { if a > b { return a }; return b }

func main() {
    graph := [][]int{{1,2},{0,3},{0,3},{1,2}}
    visited := make([]bool, 4)
    dfsRecursive(graph, 0, visited)
    fmt.Println(dfsIterative(graph, 0))

    root := &TreeNode{5,
        &TreeNode{4, &TreeNode{11, &TreeNode{7,nil,nil}, &TreeNode{2,nil,nil}}, nil},
        &TreeNode{8, &TreeNode{13,nil,nil}, &TreeNode{4,nil,&TreeNode{1,nil,nil}}}}
    fmt.Println(hasPathSum(root, 22))   // true (5→4→11→2)
    fmt.Println(allPaths(root, 22))     // [[5 4 11 2]]
    fmt.Println(maxPathSum(root))       // 48

    fmt.Println(countComponents(5, [][2]int{{0,1},{1,2},{3,4}})) // 2
}
⚠ Trap — Copy the Path Slice When collecting paths in DFS, always copy(tmp, path) before appending to results. The path slice's underlying array is mutated as DFS unwinds. Without copy, all results point to the same backing array and end up identical.
⚠ Trap — Stack Overflow on Deep Input Go's goroutine stack starts at 2KB and grows automatically, but extremely deep recursion (e.g., a skewed tree with 100k nodes) can be slow. Prefer iterative DFS with an explicit stack for competitive problems with large N.
★ Must Know Three DFS return patterns: (1) bool — does path exist? Short-circuit with ||. (2) int — aggregate value up the tree (height, max sum). (3) void with closure — collect results into an outer slice. Recognising which pattern fits the problem is the interview skill.

30 Breadth-First Search

What it is

A traversal that explores level by level, visiting all nodes at distance d before any node at distance d+1. Uses a queue. Guarantees shortest path in unweighted graphs — the first time BFS reaches a node, it has found the minimum number of hops.

Real-world analogy

Ripples expanding from a stone dropped in a pond. All points at radius 1 are hit first, then radius 2, then 3. The first time the ripple reaches your boat is the shortest-distance path from the stone — BFS is exactly this wave expansion.

How it works internally

A queue holds nodes to visit. For each dequeued node, enqueue all unvisited neighbours and mark them visited immediately on enqueue (not dequeue) to prevent duplicates. Level boundaries are tracked by snapshotting size := len(queue) before each level loop.

When to use

Shortest path in unweighted graphs/grids. Minimum steps/transformations (Word Ladder). Level-order tree traversal. Multi-source spreading (Rotting Oranges, 01 Matrix). Any problem where "closest" or "minimum hops" is the goal.

Complexity
Time O(V+E)Space O(V) queueMark visited on ENQUEUE not dequeueOnly shortest path on unit-weight edges
Concept·Breadth-First Search
What it is
A traversal strategy that visits all nodes at the current depth level before moving deeper. Uses a queue (FIFO). The only algorithm that guarantees shortest path in an unweighted graph, because it explores by increasing distance from the source.
Real-world analogy
🌊Ripples in a pond — a stone dropped in the center creates rings that spread outward layer by layer. All nodes at distance 1 are visited before any node at distance 2. BFS is that expanding ring.
How it works
Seed queue with start nodes. Mark visited on enqueue. Each iteration: snapshot size := len(queue), dequeue exactly size nodes (one level). For each, enqueue unvisited neighbours. Increment level counter after each level batch.
Complexity
Time O(V+E) Space O(V) queue
Space can be O(width of widest level) in the worst case. In a complete binary tree that's O(n/2) = O(n). DFS uses O(h) which is better for wide trees.
When to use — recognition cues
shortest path (unweighted) minimum steps / hops level-order traversal multi-source spreading word ladder / implicit graph rotting oranges / 01 matrix "reach X in fewest moves"

BFS explores level by level. Guarantees shortest path in unweighted graphs. Always uses a queue. Go has no built-in queue — use a slice with a head pointer or just reslice.

Core Templates
bfs_templates.go
package main

import "fmt"

// ── Template 1: Basic BFS — shortest path ─────────────────
func bfsShortestPath(graph [][]int, start, end int) int {
    visited := make([]bool, len(graph))
    visited[start] = true
    queue := []int{start}
    dist := 0
    for len(queue) > 0 {
        size := len(queue) // process entire level
        for i := 0; i < size; i++ {
            node := queue[i]
            if node == end { return dist }
            for _, nei := range graph[node] {
                if !visited[nei] {
                    visited[nei] = true
                    queue = append(queue, nei)
                }
            }
        }
        queue = queue[size:] // dequeue whole level
        dist++
    }
    return -1 // unreachable
}

// ── Template 2: BFS with distance map ─────────────────────
func bfsDistances(graph [][]int, start int) []int {
    dist := make([]int, len(graph))
    for i := range dist { dist[i] = -1 }
    dist[start] = 0
    queue := []int{start}
    for len(queue) > 0 {
        node := queue[0]; queue = queue[1:]
        for _, nei := range graph[node] {
            if dist[nei] == -1 {
                dist[nei] = dist[node] + 1
                queue = append(queue, nei)
            }
        }
    }
    return dist
}

// ── Template 3: Multi-source BFS ──────────────────────────
// Start BFS from multiple sources simultaneously
// Classic: 01 Matrix (distance to nearest 0), Rotting Oranges
func multiSourceBFS(grid [][]int) [][]int {
    m, n := len(grid), len(grid[0])
    dist := make([][]int, m)
    for i := range dist { dist[i] = make([]int, n); for j := range dist[i] { dist[i][j] = -1 } }
    queue := [][2]int{}
    // Seed all sources at distance 0
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == 0 { dist[i][j] = 0; queue = append(queue, [2]int{i, j}) }
        }
    }
    dirs := [][2]int{{0,1},{0,-1},{1,0},{-1,0}}
    for len(queue) > 0 {
        cur := queue[0]; queue = queue[1:]
        for _, d := range dirs {
            r, c := cur[0]+d[0], cur[1]+d[1]
            if r >= 0 && r < m && c >= 0 && c < n && dist[r][c] == -1 {
                dist[r][c] = dist[cur[0]][cur[1]] + 1
                queue = append(queue, [2]int{r, c})
            }
        }
    }
    return dist
}

// ── Template 4: BFS on implicit graph (Word Ladder) ────────
// Shortest transformation sequence: "hit" → "cog"
func ladderLength(beginWord, endWord string, wordList []string) int {
    wordSet := make(map[string]bool)
    for _, w := range wordList { wordSet[w] = true }
    if !wordSet[endWord] { return 0 }

    queue := []string{beginWord}
    visited := map[string]bool{beginWord: true}
    steps := 1

    for len(queue) > 0 {
        size := len(queue)
        for i := 0; i < size; i++ {
            word := queue[i]
            bs := []byte(word)
            for pos := 0; pos < len(bs); pos++ {
                orig := bs[pos]
                for c := byte('a'); c <= byte('z'); c++ {
                    if c == orig { continue }
                    bs[pos] = c
                    next := string(bs)
                    if next == endWord { return steps + 1 }
                    if wordSet[next] && !visited[next] {
                        visited[next] = true
                        queue = append(queue, next)
                    }
                    bs[pos] = orig
                }
            }
        }
        queue = queue[size:]
        steps++
    }
    return 0
}

// ── Template 5: 0-1 BFS (deque, edge weight 0 or 1) ───────
// Faster than Dijkstra when edges are only 0 or 1
func zeroOneBFS(graph [][2]int, n, src int, adj [][]struct{ to, w int }) []int {
    dist := make([]int, n)
    for i := range dist { dist[i] = 1<<62 }
    dist[src] = 0
    deque := []int{src} // front for w=0, back for w=1
    for len(deque) > 0 {
        u := deque[0]; deque = deque[1:]
        for _, e := range adj[u] {
            if nd := dist[u] + e.w; nd < dist[e.to] {
                dist[e.to] = nd
                if e.w == 0 { deque = append([]int{e.to}, deque...) } else { deque = append(deque, e.to) }
            }
        }
    }
    return dist
}

func main() {
    graph := [][]int{{1,2},{0,3},{0,3},{1,2,4},{3}}
    fmt.Println(bfsShortestPath(graph, 0, 4))  // 2
    fmt.Println(bfsDistances(graph, 0))         // [0 1 1 2 3]

    grid := [][]int{{0,0,0},{0,1,0},{1,1,1}}
    fmt.Println(multiSourceBFS(grid))           // distances from nearest 0

    words := []string{"hot","dot","dog","lot","log","cog"}
    fmt.Println(ladderLength("hit", "cog", words)) // 5
}
⚠ Trap — Mark Visited Before Enqueue, Not After Dequeue If you mark visited when you dequeue, the same node gets added to the queue multiple times → O(V²) in worst case. Always visited[nei] = true right before queue = append(queue, nei).
⚠ Trap — BFS Doesn't Give Shortest Path on Weighted Graphs BFS gives shortest path only for unit-weight edges. For weighted graphs use Dijkstra (min-heap). For 0/1 weights use 0-1 BFS (deque). For negative weights use Bellman-Ford.
★ Must Know — Level Processing Pattern Snapshot size := len(queue) before the inner loop, then iterate exactly size times. This processes one complete level per outer iteration, which is how you get the level number (= distance) without storing it in each node.

31 Backtracking

What it is

DFS on a decision tree. At each node you make a choice, recurse to explore consequences, then undo the choice and try the next option. Systematically explores all possibilities while pruning branches that cannot lead to a valid solution.

Real-world analogy

Solving a maze by hand: walk forward, mark your path, hit a dead end, erase your last step, try a different direction. The "undo" is the backtrack. Good maze-solvers look ahead — they don't take a path that's already obviously blocked (pruning).

How it works internally

Three phases per recursive call: (1) base case — goal reached, collect result. (2) choose — pick the next option, modify state. (3) explore + undo — recurse, then restore state to exactly what it was before the choice. Pruning (validity checks before recursing) is the difference between usable and TLE.

When to use

Generate all subsets, combinations, permutations. Constraint satisfaction (N-Queens, Sudoku). Path finding on a grid (Word Search). Any "find all solutions" or "find one valid configuration" problem. If you find yourself writing nested loops of unknown depth — it's backtracking.

Complexity
Worst case O(b^d) — branching factor b, depth dPruning critical: sorted input + early breakSpace O(d) call stack
Concept·Backtracking
What it is
A systematic exhaustive search that builds candidates incrementally and abandons ("backtracks") a candidate as soon as it determines the candidate cannot lead to a valid solution. It is DFS on an implicit decision tree where each level represents one choice.
Real-world analogy
🔐Cracking a combination lock by trying digits one at a time — if the first digit is wrong, you immediately reset it and try the next, rather than completing the entire wrong combination. Pruning = not finishing combinations you know are wrong.
How it works
Template: make choice → recurse → undo choice. The undo step is what makes it "back"tracking. At each level, loop over valid choices, check constraints before recursing (pruning). Collect results at goal state (leaf of decision tree).
Complexity
Worst O(k^n) or O(n!)
Pruning is everything — it's the difference between timing out and passing. Without pruning = brute force. Sorting + early break can cut runtime drastically.
When to use — recognition cues
generate all subsets / permutations find all valid combinations N-Queens / Sudoku word search on grid "all possible" / "enumerate" constraint satisfaction partition into equal subsets

Backtracking is DFS on a decision tree. At each node you make a choice, recurse, then undo the choice. The art is pruning branches early to avoid exploring dead ends.

Universal Template func backtrack(state, choices) { if isGoal(state) { collect(); return }; for each choice { if isValid(choice) { make(choice); backtrack(state, choices); undo(choice) } } }
Subsets, Combinations, Permutations
subsets_combos.go
package main

import (
    "fmt"
    "sort"
)

// ── Subsets (no duplicates in input) ──────────────────────
func subsets(nums []int) [][]int {
    var result [][]int
    var backtrack func(start int, current []int)
    backtrack = func(start int, current []int) {
        tmp := make([]int, len(current))
        copy(tmp, current)
        result = append(result, tmp) // add at every node, not just leaves
        for i := start; i < len(nums); i++ {
            current = append(current, nums[i])
            backtrack(i+1, current)
            current = current[:len(current)-1] // undo
        }
    }
    backtrack(0, []int{})
    return result
}

// ── Subsets II (duplicates in input) ──────────────────────
func subsetsWithDup(nums []int) [][]int {
    sort.Ints(nums) // sort first to group duplicates
    var result [][]int
    var backtrack func(start int, current []int)
    backtrack = func(start int, current []int) {
        tmp := make([]int, len(current)); copy(tmp, current)
        result = append(result, tmp)
        for i := start; i < len(nums); i++ {
            if i > start && nums[i] == nums[i-1] { continue } // skip duplicate
            current = append(current, nums[i])
            backtrack(i+1, current)
            current = current[:len(current)-1]
        }
    }
    backtrack(0, []int{})
    return result
}

// ── Combinations: choose k from n ─────────────────────────
func combine(n, k int) [][]int {
    var result [][]int
    var backtrack func(start int, current []int)
    backtrack = func(start int, current []int) {
        if len(current) == k {
            tmp := make([]int, k); copy(tmp, current)
            result = append(result, tmp); return
        }
        // Pruning: need (k - len(current)) more, so stop if not enough remain
        for i := start; i <= n-(k-len(current))+1; i++ {
            current = append(current, i)
            backtrack(i+1, current)
            current = current[:len(current)-1]
        }
    }
    backtrack(1, []int{})
    return result
}

// ── Combination Sum (unlimited reuse, unique candidates) ──
func combinationSum(candidates []int, target int) [][]int {
    sort.Ints(candidates)
    var result [][]int
    var backtrack func(start, remaining int, current []int)
    backtrack = func(start, remaining int, current []int) {
        if remaining == 0 {
            tmp := make([]int, len(current)); copy(tmp, current)
            result = append(result, tmp); return
        }
        for i := start; i < len(candidates); i++ {
            if candidates[i] > remaining { break } // pruning: sorted, so rest are bigger
            current = append(current, candidates[i])
            backtrack(i, remaining-candidates[i], current) // i (not i+1) = allow reuse
            current = current[:len(current)-1]
        }
    }
    backtrack(0, target, []int{})
    return result
}

// ── Permutations (no duplicates) ─────────────────────────
func permute(nums []int) [][]int {
    var result [][]int
    var backtrack func(start int)
    backtrack = func(start int) {
        if start == len(nums) {
            tmp := make([]int, len(nums)); copy(tmp, nums)
            result = append(result, tmp); return
        }
        for i := start; i < len(nums); i++ {
            nums[start], nums[i] = nums[i], nums[start] // swap in
            backtrack(start + 1)
            nums[start], nums[i] = nums[i], nums[start] // swap out (undo)
        }
    }
    backtrack(0)
    return result
}

// ── Permutations II (with duplicates) ─────────────────────
func permuteUnique(nums []int) [][]int {
    sort.Ints(nums)
    used := make([]bool, len(nums))
    var result [][]int
    var backtrack func(current []int)
    backtrack = func(current []int) {
        if len(current) == len(nums) {
            tmp := make([]int, len(current)); copy(tmp, current)
            result = append(result, tmp); return
        }
        for i := 0; i < len(nums); i++ {
            if used[i] { continue }
            // Skip duplicate: same value, previous sibling not used
            if i > 0 && nums[i] == nums[i-1] && !used[i-1] { continue }
            used[i] = true
            backtrack(append(current, nums[i]))
            used[i] = false
        }
    }
    backtrack([]int{})
    return result
}

func main() {
    fmt.Println(len(subsets([]int{1,2,3})))          // 8
    fmt.Println(subsetsWithDup([]int{1,2,2}))         // 6 unique
    fmt.Println(combine(4, 2))                         // C(4,2) = 6
    fmt.Println(combinationSum([]int{2,3,6,7}, 7))    // [[2 2 3] [7]]
    fmt.Println(len(permute([]int{1,2,3})))            // 6
    fmt.Println(len(permuteUnique([]int{1,1,2})))      // 3
}
N-Queens, Sudoku, Word Search
nqueens_sudoku_wordsearch.go
package main

import (
    "fmt"
    "strings"
)

// ── N-Queens ──────────────────────────────────────────────
func solveNQueens(n int) [][]string {
    var result [][]string
    board := make([][]byte, n)
    for i := range board { board[i] = []byte(strings.Repeat(".", n)) }

    cols := make([]bool, n)
    diag1 := make([]bool, 2*n) // row-col+n: top-left diagonals
    diag2 := make([]bool, 2*n) // row+col:   top-right diagonals

    var backtrack func(row int)
    backtrack = func(row int) {
        if row == n {
            snapshot := make([]string, n)
            for i, r := range board { snapshot[i] = string(r) }
            result = append(result, snapshot)
            return
        }
        for col := 0; col < n; col++ {
            if cols[col] || diag1[row-col+n] || diag2[row+col] { continue }
            board[row][col] = 'Q'
            cols[col], diag1[row-col+n], diag2[row+col] = true, true, true
            backtrack(row + 1)
            board[row][col] = '.'
            cols[col], diag1[row-col+n], diag2[row+col] = false, false, false
        }
    }
    backtrack(0)
    return result
}

// ── Sudoku Solver ─────────────────────────────────────────
func solveSudoku(board [][]byte) {
    rows  := [9][9]bool{}
    cols  := [9][9]bool{}
    boxes := [9][9]bool{}
    var empty [][2]int

    for r := 0; r < 9; r++ {
        for c := 0; c < 9; c++ {
            if board[r][c] == '.' { empty = append(empty, [2]int{r, c}); continue }
            d := board[r][c] - '1'
            rows[r][d], cols[c][d], boxes[(r/3)*3+c/3][d] = true, true, true
        }
    }
    var solve func(idx int) bool
    solve = func(idx int) bool {
        if idx == len(empty) { return true }
        r, c := empty[idx][0], empty[idx][1]
        box := (r/3)*3 + c/3
        for d := 0; d < 9; d++ {
            if rows[r][d] || cols[c][d] || boxes[box][d] { continue }
            board[r][c] = byte('1' + d)
            rows[r][d], cols[c][d], boxes[box][d] = true, true, true
            if solve(idx + 1) { return true }
            board[r][c] = '.'
            rows[r][d], cols[c][d], boxes[box][d] = false, false, false
        }
        return false
    }
    solve(0)
}

// ── Word Search (grid) ────────────────────────────────────
func exist(board [][]byte, word string) bool {
    m, n := len(board), len(board[0])
    var dfs func(r, c, idx int) bool
    dfs = func(r, c, idx int) bool {
        if idx == len(word) { return true }
        if r < 0 || r >= m || c < 0 || c >= n { return false }
        if board[r][c] != word[idx] { return false }
        tmp := board[r][c]
        board[r][c] = '#'        // mark visited in-place
        found := dfs(r+1,c,idx+1) || dfs(r-1,c,idx+1) ||
                 dfs(r,c+1,idx+1) || dfs(r,c-1,idx+1)
        board[r][c] = tmp        // restore
        return found
    }
    for r := 0; r < m; r++ {
        for c := 0; c < n; c++ {
            if dfs(r, c, 0) { return true }
        }
    }
    return false
}

func main() {
    fmt.Println(len(solveNQueens(4))) // 2 solutions
    fmt.Println(solveNQueens(4)[0])

    grid := [][]byte{
        {'A','B','C','E'},
        {'S','F','C','S'},
        {'A','D','E','E'},
    }
    fmt.Println(exist(grid, "ABCCED")) // true
    fmt.Println(exist(grid, "SEE"))    // true
    fmt.Println(exist(grid, "ABCB"))   // false
}
⚠ Trap — Forgetting to Undo Every make(choice) must have a corresponding undo(choice) after the recursive call. Missing the undo is the #1 backtracking bug. Using boolean arrays (N-Queens cols/diags) makes undo trivial. For grids, temporarily mark with a sentinel character and restore it.
⚠ Trap — Duplicate Results Without Sorting + Skip For inputs with duplicates (Subsets II, Permutations II): sort first, then skip nums[i] == nums[i-1] at the same recursion level (same start). The condition is subtle: i > start && nums[i] == nums[i-1] for subsets; i > 0 && nums[i] == nums[i-1] && !used[i-1] for permutations.
★ Must Know — Pruning = Performance Backtracking without pruning is brute force. Key pruning techniques: (1) Sort + break early when candidate > remaining (combination sum). (2) Track constraint sets with O(1) lookup arrays instead of O(n) scans (N-Queens). (3) Bound checks before recursing (word search out-of-bounds). Staff+ interviewers will ask you to optimise naive backtracking.

32 Greedy Algorithms

What it is

An algorithm that makes the locally optimal choice at each step, trusting that local optima compose into a global optimum. Greedy is faster than DP but only correct when the problem has the greedy choice property — choosing greedily never eliminates the optimal solution.

Real-world analogy

Making change with coins: always pick the largest coin that fits. With standard denominations [25,10,5,1] this always gives the minimum coin count — that's why greedy works. With unusual denominations like [1,3,4] and target 6, greedy picks 4+1+1=3 coins, but optimal is 3+3=2 — greedy fails, DP required.

How it works internally

Two common structural patterns: (1) Sort then scan: sort by a key that enforces the greedy property (end time, frequency, weight), then make one linear pass of decisions. (2) Two-pass: enforce one directional constraint left-to-right, then the opposite right-to-left. Proving correctness uses an exchange argument: assume optimal differs, show swapping to greedy doesn't worsen it.

When to use

Scheduling/interval problems (sort by end time). Minimum spanning tree (Kruskal's, Prim's). Huffman encoding. Activity selection. Any problem where you can prove "always taking the best-looking option now can't hurt later." When uncertain — try DP first, then optimise.

Complexity
Usually O(n log n) — sort + O(n) scanSpace O(1) after sortCorrectness not automatic — needs proof or counterexample check
Concept·Greedy
What it is
An algorithm that makes the locally optimal choice at each step, never reconsidering past decisions. Works when the problem has the greedy choice property — a local optimum is always part of a global optimum. No dp table, no backtracking, just one forward pass.
Real-world analogy
💸Making change with the fewest coins using standard denominations (25¢, 10¢, 5¢, 1¢) — always grab the largest coin that fits. This works for standard denominations but fails for arbitrary ones (DP needed), which is exactly the greedy vs DP distinction.
How it works
Typically: sort by the greedy key (end time, weight, frequency), then do a single linear scan making the greedy decision at each step. Or: two passes (left-to-right + right-to-left) each enforcing one constraint direction independently.
Complexity
Time O(n log n) Space O(1)
Dominated by the sort. The scan itself is O(n). Much simpler code than equivalent DP — but only correct when greedy property holds.
When to use — recognition cues
interval scheduling / meeting rooms jump game / reachability minimum spanning tree Huffman encoding assign tasks / resources "minimum number of X to cover Y" always pick the best available next

Greedy makes the locally optimal choice at each step, trusting it leads to a global optimum. Proving correctness requires showing an exchange argument or greedy stays ahead. Hard to see; easy to code once you see it.

ProblemGreedy ChoiceWhy it works
Activity/Interval SchedulingPick earliest finish timeLeaves max room for future activities
Jump GameTrack max reachable indexIf reachable, always optimal to extend
Candy (ascending/descending)Two-pass left-right + right-leftEach pass enforces one direction's constraint
Gas StationStart where tank never goes negativeIf total gas ≥ total cost, solution exists
Task SchedulerMost frequent task fills idle slotsMost frequent determines minimum idle time
Partition LabelsExtend window to last occurrence of charGreedy window = tightest valid partition
greedy.go
package main

import (
    "fmt"
    "sort"
)

// ── Interval Scheduling — max non-overlapping intervals ───
func eraseOverlapIntervals(intervals [][2]int) int {
    sort.Slice(intervals, func(i, j int) bool {
        return intervals[i][1] < intervals[j][1] // sort by END time
    })
    removed, end := 0, -1<<62
    for _, iv := range intervals {
        if iv[0] >= end {
            end = iv[1] // take this interval
        } else {
            removed++ // overlaps — remove it
        }
    }
    return removed
}

// ── Gas Station ───────────────────────────────────────────
// If total gas >= total cost, solution exists. Find the start
// where running tank never dips below 0.
func canCompleteCircuit(gas, cost []int) int {
    total, tank, start := 0, 0, 0
    for i := range gas {
        diff := gas[i] - cost[i]
        total += diff
        tank  += diff
        if tank < 0 { start = i + 1; tank = 0 } // reset
    }
    if total < 0 { return -1 }
    return start
}

// ── Candy — each child gets min 1 candy; higher rating ────
// than neighbour gets more candy. Min candies total.
func candy(ratings []int) int {
    n := len(ratings)
    candies := make([]int, n)
    for i := range candies { candies[i] = 1 }
    // Left pass: right neighbour with higher rating gets +1
    for i := 1; i < n; i++ {
        if ratings[i] > ratings[i-1] { candies[i] = candies[i-1] + 1 }
    }
    // Right pass: left neighbour with higher rating gets max of current or +1
    for i := n - 2; i >= 0; i-- {
        if ratings[i] > ratings[i+1] && candies[i] <= candies[i+1] {
            candies[i] = candies[i+1] + 1
        }
    }
    total := 0
    for _, c := range candies { total += c }
    return total
}

// ── Task Scheduler ────────────────────────────────────────
// Minimum intervals to execute all tasks with cooldown n
func leastInterval(tasks []byte, n int) int {
    freq := [26]int{}
    for _, t := range tasks { freq[t-'A']++ }
    sort.Ints(freq[:])
    maxFreq := freq[25]
    // Slots = (maxFreq-1) * (n+1) + count of tasks with maxFreq
    idleSlots := (maxFreq - 1) * (n + 1)
    for _, f := range freq { if f == maxFreq { idleSlots++ } }
    if idleSlots > len(tasks) { return idleSlots }
    return len(tasks) // tasks fill all idle slots
}

// ── Partition Labels ──────────────────────────────────────
// Partition string so each letter appears in at most 1 part
func partitionLabels(s string) []int {
    last := [26]int{}
    for i, c := range s { last[c-'a'] = i } // last occurrence of each char
    var result []int
    start, end := 0, 0
    for i, c := range s {
        if pos := last[c-'a']; pos > end { end = pos } // extend window
        if i == end { // current window is complete
            result = append(result, end-start+1)
            start = end + 1
        }
    }
    return result
}

// ── Boats to Save People ──────────────────────────────────
// Each boat holds <= limit weight, at most 2 people
func numRescueBoats(people []int, limit int) int {
    sort.Ints(people)
    l, r, boats := 0, len(people)-1, 0
    for l <= r {
        if people[l]+people[r] <= limit { l++ } // lightest can share with heaviest
        r--; boats++ // heaviest always gets a boat
    }
    return boats
}

// ── Assign Cookies ────────────────────────────────────────
// Greedily satisfy smallest greed with smallest sufficient cookie
func findContentChildren(greed, size []int) int {
    sort.Ints(greed); sort.Ints(size)
    child, cookie := 0, 0
    for child < len(greed) && cookie < len(size) {
        if size[cookie] >= greed[child] { child++ } // satisfied
        cookie++
    }
    return child
}

// ── Wiggle Subsequence ────────────────────────────────────
// Length of longest wiggle subsequence (greedy count peaks/valleys)
func wiggleMaxLength(nums []int) int {
    if len(nums) < 2 { return len(nums) }
    length, prevDiff := 1, 0
    for i := 1; i < len(nums); i++ {
        diff := nums[i] - nums[i-1]
        if (diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0) {
            length++
            prevDiff = diff
        }
    }
    return length
}

func main() {
    ivs := [][2]int{{1,2},{2,3},{3,4},{1,3}}
    fmt.Println(eraseOverlapIntervals(ivs)) // 1

    fmt.Println(canCompleteCircuit([]int{1,2,3,4,5}, []int{3,4,5,1,2})) // 3
    fmt.Println(candy([]int{1,0,2}))       // 5
    fmt.Println(candy([]int{1,2,2}))       // 4
    fmt.Println(leastInterval([]byte("AABABC"), 2)) // 8 (wait: ABCAB_AB or similar)
    // Note: 'A'=3,'B'=2,'C'=1 → (3-1)*(2+1)+1=7, len=6 → max(7,6)=7
    fmt.Println(partitionLabels("ababcbacadefegdehijhklij")) // [9 7 8]
    fmt.Println(numRescueBoats([]int{3,2,2,1}, 3))           // 3
    fmt.Println(findContentChildren([]int{1,2,3}, []int{1,1})) // 1
    fmt.Println(wiggleMaxLength([]int{1,7,4,9,2,5}))          // 6
}
⚠ Trap — Confusing Greedy with DP Greedy works when the local optimum is always part of the global optimum. Coin change with arbitrary denominations requires DP (greedy fails with coins [1,3,4] and target 6). But coin change with standard denominations [1,5,10,25] is greedy-safe. Always verify with a counterexample.
★ Must Know — Two Greedy Patterns (1) Sort then scan: sort by a key that enforces the greedy property (end time, weight, frequency), then do one linear pass. (2) Two-pass: enforce left-to-right constraint in one pass, right-to-left constraint in another (Candy, Trapping Rain). Most greedy problems are one of these two shapes.

33 Trie (Prefix Tree)

What it is

A tree where each node represents a character in a string, and paths from root to marked nodes spell out stored words. All words sharing a prefix share the same path — making prefix operations O(L) regardless of how many words are stored.

Real-world analogy

Autocomplete in a search bar. As you type each letter, the system narrows down suggestions. Every keystroke is one level deeper in the trie — "app" leads to "apple", "application", "appetite" all sharing the "app" prefix node. No wasted work re-scanning previous characters.

How it works internally

Each node has an array (or map) of 26 child pointers plus an isEnd flag. Insert: walk one character at a time, creating nodes as needed. Search: walk and return false on missing child. Use [26]*TrieNode for O(1) lowercase-alpha access. Use map[rune]*Node for sparse/Unicode alphabets.

When to use

Autocomplete / prefix search. Word dictionary with wildcard (.). Word Search II (trie + DFS — far faster than brute-force one-word-at-a-time search). IP routing (longest prefix match). Counting words with a given prefix. Whenever multiple string operations share prefixes.

Complexity
Insert/Search/StartsWith O(L) — L = word lengthSpace O(ALPHABET × total chars)Map trie: flexible alphabet, higher constant
Concept·Trie / Prefix Tree
What it is
A tree where each edge represents one character and each node represents the prefix formed by the path from root to that node. Words sharing a prefix share the same path. Also called a prefix tree or digital tree. Enables O(L) search/insert where L = word length — independent of dictionary size.
Real-world analogy
📱Smartphone autocomplete — as you type each letter, the keyboard narrows suggestions by following one branch of the trie. "ca" instantly shows "cat", "car", "cake" because they all share the c→a path. The trie does not re-scan every word for each new character.
How it works
Each node has children [26]*TrieNode (or map) and isEnd bool. Insert: walk character by character, create missing nodes. Search: walk and check isEnd at the last char. StartsWith: walk and return true if path exists (don't check isEnd).
Complexity
Insert/Search O(L) Space O(ALPHABET × total chars)
Vs hashmap: hashmap is O(L) average but O(L²) with hashing. Trie wins for prefix queries — hashmap can't do StartsWith without scanning all keys.
When to use — recognition cues
autocomplete / prefix search word dictionary with wildcards count words with given prefix word search II (trie + DFS) IP routing (longest prefix match) multiple string pattern matching "does any word start with X?"

A Trie stores strings character by character, sharing prefixes. Insert/Search/StartsWith are all O(L) where L = word length. Space is O(ALPHABET × total_chars). Used for autocomplete, IP routing, spell check.

Implementation + Core Operations
trie.go
package main

import "fmt"

// ── Standard Trie (26 lowercase letters) ─────────────────
type TrieNode struct {
    children [26]*TrieNode
    isEnd    bool
    // Optional extras often needed in interviews:
    count    int    // how many words pass through this node
    word     string // store word at leaf (useful for Word Search II)
}

type Trie struct{ root *TrieNode }

func NewTrie() *Trie { return &Trie{root: &TrieNode{}} }

func (t *Trie) Insert(word string) {
    node := t.root
    for _, c := range word {
        idx := c - 'a'
        if node.children[idx] == nil {
            node.children[idx] = &TrieNode{}
        }
        node = node.children[idx]
        node.count++ // count words with this prefix
    }
    node.isEnd = true
    node.word = word
}

func (t *Trie) Search(word string) bool {
    node := t.find(word)
    return node != nil && node.isEnd
}

func (t *Trie) StartsWith(prefix string) bool {
    return t.find(prefix) != nil
}

func (t *Trie) CountWithPrefix(prefix string) int {
    node := t.find(prefix)
    if node == nil { return 0 }
    return node.count
}

func (t *Trie) find(prefix string) *TrieNode {
    node := t.root
    for _, c := range prefix {
        idx := c - 'a'
        if node.children[idx] == nil { return nil }
        node = node.children[idx]
    }
    return node
}

// Delete word from trie
func (t *Trie) Delete(word string) bool {
    return t.deleteHelper(t.root, word, 0)
}

func (t *Trie) deleteHelper(node *TrieNode, word string, depth int) bool {
    if node == nil { return false }
    if depth == len(word) {
        if !node.isEnd { return false }
        node.isEnd = false
        node.word = ""
        return true // caller may prune this node if no children
    }
    idx := word[depth] - 'a'
    if t.deleteHelper(node.children[idx], word, depth+1) {
        node.count--
        // Prune leaf if no longer needed
        if !node.children[idx].isEnd && node.children[idx].count == 0 {
            node.children[idx] = nil
        }
        return true
    }
    return false
}

// ── Trie with wildcard '.' (Word Dictionary) ─────────────
type WordDictionary struct{ root *TrieNode }

func NewWordDictionary() *WordDictionary { return &WordDictionary{&TrieNode{}} }

func (d *WordDictionary) AddWord(word string) {
    node := d.root
    for _, c := range word {
        idx := c - 'a'
        if node.children[idx] == nil { node.children[idx] = &TrieNode{} }
        node = node.children[idx]
    }
    node.isEnd = true
}

func (d *WordDictionary) Search(word string) bool {
    return d.searchFrom(d.root, word, 0)
}

func (d *WordDictionary) searchFrom(node *TrieNode, word string, idx int) bool {
    if node == nil { return false }
    if idx == len(word) { return node.isEnd }
    c := word[idx]
    if c != '.' {
        return d.searchFrom(node.children[c-'a'], word, idx+1)
    }
    // Wildcard: try all 26 children
    for _, child := range node.children {
        if d.searchFrom(child, word, idx+1) { return true }
    }
    return false
}

// ── Map-based Trie (simpler, supports any alphabet) ───────
type MapTrieNode struct {
    children map[rune]*MapTrieNode
    isEnd    bool
}

func NewMapTrie() *MapTrieNode { return &MapTrieNode{children: make(map[rune]*MapTrieNode)} }

func (n *MapTrieNode) Insert(word string) {
    curr := n
    for _, c := range word {
        if _, ok := curr.children[c]; !ok {
            curr.children[c] = &MapTrieNode{children: make(map[rune]*MapTrieNode)}
        }
        curr = curr.children[c]
    }
    curr.isEnd = true
}

func (n *MapTrieNode) Search(word string) bool {
    curr := n
    for _, c := range word {
        if _, ok := curr.children[c]; !ok { return false }
        curr = curr.children[c]
    }
    return curr.isEnd
}

func main() {
    t := NewTrie()
    t.Insert("apple"); t.Insert("app"); t.Insert("application")
    fmt.Println(t.Search("apple"))       // true
    fmt.Println(t.Search("app"))         // true
    fmt.Println(t.Search("appl"))        // false
    fmt.Println(t.StartsWith("appl"))    // true
    fmt.Println(t.CountWithPrefix("app")) // 3
    t.Delete("app")
    fmt.Println(t.Search("app"))         // false
    fmt.Println(t.StartsWith("app"))     // true (apple still exists)

    d := NewWordDictionary()
    d.AddWord("bad"); d.AddWord("dad"); d.AddWord("mad")
    fmt.Println(d.Search("pad"))  // false
    fmt.Println(d.Search("bad"))  // true
    fmt.Println(d.Search(".ad"))  // true
    fmt.Println(d.Search("b.."))  // true
}
Word Search II — Trie + DFS
Time: O(M·N·4·3^(L-1))vs O(words × M·N) brute force
word_search_ii.go
package main

import "fmt"

type WNode struct {
    children [26]*WNode
    word     string // non-empty at terminal node
}

// Find all words from list that exist in the grid
func findWords(board [][]byte, words []string) []string {
    root := &WNode{}
    // Build trie from word list
    for _, w := range words {
        node := root
        for _, c := range w {
            idx := c - 'a'
            if node.children[idx] == nil { node.children[idx] = &WNode{} }
            node = node.children[idx]
        }
        node.word = w
    }

    m, n := len(board), len(board[0])
    var result []string

    var dfs func(node *WNode, r, c int)
    dfs = func(node *WNode, r, c int) {
        if r < 0 || r >= m || c < 0 || c >= n { return }
        ch := board[r][c]
        if ch == '#' { return } // already visited
        next := node.children[ch-'a']
        if next == nil { return } // no words with this prefix

        if next.word != "" {
            result = append(result, next.word)
            next.word = "" // deduplicate: don't add same word twice
        }

        board[r][c] = '#'
        dfs(next, r+1, c); dfs(next, r-1, c)
        dfs(next, r, c+1); dfs(next, r, c-1)
        board[r][c] = ch
    }

    for r := 0; r < m; r++ {
        for c := 0; c < n; c++ {
            dfs(root, r, c)
        }
    }
    return result
}

func main() {
    board := [][]byte{
        {'o','a','a','n'},
        {'e','t','a','e'},
        {'i','h','k','r'},
        {'i','f','l','v'},
    }
    words := []string{"oath","pea","eat","rain"}
    fmt.Println(findWords(board, words)) // [oath eat]
}
⚠ Trap — Array vs Map children [26]*TrieNode is fast (O(1) access, cache-friendly) but wastes memory for sparse alphabets. map[rune]*TrieNode handles Unicode and sparse data but has higher constant. Pick [26] for lowercase-alpha problems, map for general strings.
★ Must Know — Trie Optimisations (1) Store the full word at terminal nodes to avoid re-constructing it during DFS. (2) After finding a word in Word Search II, set node.word = "" to deduplicate without removing from trie. (3) Prune trie branches as you find all words under them — reduces DFS overhead on large boards.

34 Matrices

What it is

A 2D grid of values, treated as an implicit graph where each cell (r,c) is a node and edges connect it to its 4 (or 8) neighbours. Most matrix problems reduce to graph traversal — choose DFS for component/path problems, BFS for shortest-distance problems.

Real-world analogy

A city grid: each intersection is a cell, roads go north/south/east/west (4-directional). Finding connected islands is finding city blocks. Rotting oranges is a virus spreading through the grid. Pacific Atlantic is water flowing downhill — reverse-BFS from the oceans uphill.

How it works internally

Cell (r,c) maps to node index r*cols + c for Union-Find. Direction arrays (dirs4, dirs8) loop over neighbours cleanly. Mark visited by overwriting the cell (common DFS trick). Multi-source BFS seeds the queue with all sources at distance 0 — the wave front expands simultaneously from all of them.

When to use

Island counting / flood fill → DFS overwrite. Shortest path / min steps → BFS. Multi-source spread (rotting, fire) → multi-source BFS. Border-connected cells (surrounded regions, Pacific-Atlantic) → reverse BFS/DFS from borders. Dynamic connectivity → Union-Find with r*cols+c node encoding.

Complexity
DFS/BFS O(M×N)Multi-source BFS O(M×N)Union-Find O(M×N × α)Mark visited on ENQUEUE for BFS correctness
Concept·Matrices / Grids
What it is
A 2D array where each cell [r][c] can be treated as a graph node. Edges connect each cell to its 4 (or 8) neighbours. Nearly all matrix problems reduce to a graph algorithm — the skill is choosing the right one (DFS, BFS, Union-Find) and encoding state efficiently.
Real-world analogy
🗺A city grid — streets run N/S/E/W. "Can you get from point A to B?" is BFS (fewest turns). "Are these buildings connected?" is Union-Find. "Which blocks are in the flood zone?" is DFS flood fill. Same grid, different questions → different algorithms.
How it works
Cell (r,c) → node index r*cols + c for Union-Find. Direction array dirs4 = [][2]int{{0,1},{0,-1},{1,0},{-1,0}} iterates neighbours. Always bounds-check before accessing. Mark visited by overwriting cell value (avoids extra boolean grid) — restore if needed (backtracking).
Complexity
Time O(M×N) Space O(M×N)
Every cell visited at most once. Some problems allow O(1) extra space by modifying the grid in-place (flood fill, number of islands, surrounded regions).
When to use — recognition cues
grid / board / image island counting shortest path on grid flood fill / connected region rotate / transpose matrix spiral / diagonal traversal border-connected cells multi-source spreading

Grid problems are graphs in disguise. Each cell (r,c) is a node; edges go to 4 (or 8) neighbours. The key is choosing DFS vs BFS and setting up the direction array correctly.

Direction Arrays & Grid Templates
grid_templates.go
package main

import "fmt"

// ── Direction arrays ──────────────────────────────────────
var dirs4 = [][2]int{{0,1},{0,-1},{1,0},{-1,0}}             // 4-directional
var dirs8 = [][2]int{{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}} // 8-directional

func inBounds(r, c, m, n int) bool { return r >= 0 && r < m && c >= 0 && c < n }

// ── DFS flood fill ────────────────────────────────────────
func floodFill(image [][]int, sr, sc, color int) [][]int {
    src := image[sr][sc]
    if src == color { return image }
    var dfs func(r, c int)
    dfs = func(r, c int) {
        if !inBounds(r, c, len(image), len(image[0])) { return }
        if image[r][c] != src { return }
        image[r][c] = color
        for _, d := range dirs4 { dfs(r+d[0], c+d[1]) }
    }
    dfs(sr, sc)
    return image
}

// ── BFS: 01 Matrix — distance to nearest 0 ───────────────
func updateMatrix(mat [][]int) [][]int {
    m, n := len(mat), len(mat[0])
    dist := make([][]int, m)
    queue := [][2]int{}
    for i := range dist {
        dist[i] = make([]int, n)
        for j := range dist[i] {
            if mat[i][j] == 0 {
                dist[i][j] = 0
                queue = append(queue, [2]int{i, j})
            } else {
                dist[i][j] = 1<<30
            }
        }
    }
    for len(queue) > 0 {
        cur := queue[0]; queue = queue[1:]
        for _, d := range dirs4 {
            r, c := cur[0]+d[0], cur[1]+d[1]
            if inBounds(r, c, m, n) && dist[r][c] > dist[cur[0]][cur[1]]+1 {
                dist[r][c] = dist[cur[0]][cur[1]] + 1
                queue = append(queue, [2]int{r, c})
            }
        }
    }
    return dist
}

// ── BFS: Rotting Oranges — minimum time ──────────────────
func orangesRotting(grid [][]int) int {
    m, n := len(grid), len(grid[0])
    fresh, minutes := 0, 0
    queue := [][2]int{}
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            switch grid[i][j] {
            case 1: fresh++
            case 2: queue = append(queue, [2]int{i, j})
            }
        }
    }
    for len(queue) > 0 && fresh > 0 {
        size := len(queue)
        for k := 0; k < size; k++ {
            r, c := queue[k][0], queue[k][1]
            for _, d := range dirs4 {
                nr, nc := r+d[0], c+d[1]
                if inBounds(nr, nc, m, n) && grid[nr][nc] == 1 {
                    grid[nr][nc] = 2
                    fresh--
                    queue = append(queue, [2]int{nr, nc})
                }
            }
        }
        queue = queue[size:]
        minutes++
    }
    if fresh > 0 { return -1 }
    return minutes
}

// ── Pacific Atlantic Water Flow ───────────────────────────
// Water can flow to cell with equal or lower height
// Find cells that can reach BOTH oceans
func pacificAtlantic(heights [][]int) [][2]int {
    m, n := len(heights), len(heights[0])
    pacific  := make([][]bool, m)
    atlantic := make([][]bool, m)
    for i := range pacific { pacific[i] = make([]bool, n); atlantic[i] = make([]bool, n) }

    var bfs func(queue [][2]int, visited [][]bool)
    bfs = func(queue [][2]int, visited [][]bool) {
        for len(queue) > 0 {
            cur := queue[0]; queue = queue[1:]
            for _, d := range dirs4 {
                r, c := cur[0]+d[0], cur[1]+d[1]
                if inBounds(r, c, m, n) && !visited[r][c] &&
                    heights[r][c] >= heights[cur[0]][cur[1]] { // reverse flow: go uphill
                    visited[r][c] = true
                    queue = append(queue, [2]int{r, c})
                }
            }
        }
    }

    // Seed Pacific (top row + left col) and Atlantic (bottom row + right col)
    pQueue, aQueue := [][2]int{}, [][2]int{}
    for i := 0; i < m; i++ {
        pacific[i][0] = true;   pQueue = append(pQueue, [2]int{i, 0})
        atlantic[i][n-1] = true; aQueue = append(aQueue, [2]int{i, n-1})
    }
    for j := 0; j < n; j++ {
        pacific[0][j] = true;   pQueue = append(pQueue, [2]int{0, j})
        atlantic[m-1][j] = true; aQueue = append(aQueue, [2]int{m-1, j})
    }
    bfs(pQueue, pacific); bfs(aQueue, atlantic)

    var result [][2]int
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if pacific[i][j] && atlantic[i][j] { result = append(result, [2]int{i, j}) }
        }
    }
    return result
}

// ── Surrounded Regions — capture 'O' not connected to edge ─
func solve(board [][]byte) {
    m, n := len(board), len(board[0])
    var dfs func(r, c int)
    dfs = func(r, c int) {
        if !inBounds(r, c, m, n) || board[r][c] != 'O' { return }
        board[r][c] = 'S' // safe — connected to border
        for _, d := range dirs4 { dfs(r+d[0], c+d[1]) }
    }
    // Mark all 'O's connected to border as safe
    for i := 0; i < m; i++ { dfs(i, 0); dfs(i, n-1) }
    for j := 0; j < n; j++ { dfs(0, j); dfs(m-1, j) }
    // Flip: remaining 'O' → 'X', safe 'S' → 'O'
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            switch board[i][j] {
            case 'O': board[i][j] = 'X'
            case 'S': board[i][j] = 'O'
            }
        }
    }
}

// ── Number of Distinct Islands (shape hashing) ───────────
func numDistinctIslands(grid [][]byte) int {
    m, n := len(grid), len(grid[0])
    shapes := make(map[string]bool)
    var shape []byte
    var dfs func(r, c, br, bc int)
    dfs = func(r, c, br, bc int) {
        if !inBounds(r, c, m, n) || grid[r][c] != '1' { return }
        grid[r][c] = '0'
        shape = append(shape, byte(r-br+'0'), byte(c-bc+'0'), ',') // relative position
        for _, d := range dirs4 { dfs(r+d[0], c+d[1], br, bc) }
        shape = append(shape, '!') // backtrack marker (distinguishes shapes)
    }
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == '1' {
                shape = shape[:0]
                dfs(i, j, i, j)
                shapes[string(shape)] = true
            }
        }
    }
    return len(shapes)
}

func main() {
    img := [][]int{{1,1,1},{1,1,0},{1,0,1}}
    fmt.Println(floodFill(img, 1, 1, 2)) // [[2 2 2] [2 2 0] [2 0 1]]

    mat := [][]int{{0,0,0},{0,1,0},{1,1,1}}
    fmt.Println(updateMatrix(mat)) // [[0 0 0] [0 1 0] [1 2 1]]

    grid := [][]int{{2,1,1},{1,1,0},{0,1,1}}
    fmt.Println(orangesRotting(grid)) // 4

    heights := [][]int{{1,2,2,3,5},{3,2,3,4,4},{2,4,5,3,1},{6,7,1,4,5},{5,1,1,2,4}}
    fmt.Println(pacificAtlantic(heights)) // cells reaching both oceans
}
Union-Find on Grid + Diagonal Traversals
grid_union_find.go
package main

import "fmt"

// ── Union-Find for grid connectivity ──────────────────────
type UnionFind struct {
    parent, rank []int
    components   int
}

func newUF(n int) *UnionFind {
    uf := &UnionFind{parent: make([]int, n), rank: make([]int, n), components: n}
    for i := range uf.parent { uf.parent[i] = i }
    return uf
}

func (uf *UnionFind) find(x int) int {
    if uf.parent[x] != x { uf.parent[x] = uf.find(uf.parent[x]) } // path compression
    return uf.parent[x]
}

func (uf *UnionFind) union(x, y int) {
    rx, ry := uf.find(x), uf.find(y)
    if rx == ry { return }
    uf.components--
    if uf.rank[rx] < uf.rank[ry] { rx, ry = ry, rx }
    uf.parent[ry] = rx
    if uf.rank[rx] == uf.rank[ry] { uf.rank[rx]++ }
}

// Number of Islands using Union-Find
func numIslandsUF(grid [][]byte) int {
    m, n := len(grid), len(grid[0])
    uf := newUF(m * n)
    islandCount := 0
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == '1' {
                islandCount++
                if i > 0 && grid[i-1][j] == '1' { uf.union(i*n+j, (i-1)*n+j); islandCount-- }
                if j > 0 && grid[i][j-1] == '1' { uf.union(i*n+j, i*n+j-1);   islandCount-- }
            }
        }
    }
    return islandCount
}

// ── Diagonal traversal ────────────────────────────────────
// Traverse all diagonals of m×n matrix (anti-diagonal: r+c=const)
func findDiagonalOrder(mat [][]int) []int {
    m, n := len(mat), len(mat[0])
    result := make([]int, 0, m*n)
    diags := make(map[int][]int)
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            diags[i+j] = append(diags[i+j], mat[i][j])
        }
    }
    for d := 0; d < m+n-1; d++ {
        if d%2 == 0 { // go up-right: reverse the diagonal
            for k := len(diags[d])-1; k >= 0; k-- { result = append(result, diags[d][k]) }
        } else {
            result = append(result, diags[d]...)
        }
    }
    return result
}

// ── Valid path in grid with obstacles ─────────────────────
// 0 = free, 1 = obstacle
func hasPathInGrid(grid [][]int) bool {
    m, n := len(grid), len(grid[0])
    if grid[0][0] == 1 || grid[m-1][n-1] == 1 { return false }
    queue := [][2]int{{0, 0}}
    grid[0][0] = 1 // mark visited
    for len(queue) > 0 {
        r, c := queue[0][0], queue[0][1]; queue = queue[1:]
        if r == m-1 && c == n-1 { return true }
        for _, d := range dirs4 {
            nr, nc := r+d[0], c+d[1]
            if inBounds(nr, nc, m, n) && grid[nr][nc] == 0 {
                grid[nr][nc] = 1
                queue = append(queue, [2]int{nr, nc})
            }
        }
    }
    return false
}

var dirs4 = [][2]int{{0,1},{0,-1},{1,0},{-1,0}}
func inBounds(r, c, m, n int) bool { return r >= 0 && r < m && c >= 0 && c < n }

func main() {
    g := [][]byte{{'1','1','0'},{'0','1','1'},{'0','0','1'}}
    fmt.Println(numIslandsUF(g)) // 1

    mat := [][]int{{1,2,3},{4,5,6},{7,8,9}}
    fmt.Println(findDiagonalOrder(mat)) // [1 2 4 7 5 3 6 8 9]
}
⚠ Trap — Modifying Grid During BFS In multi-source BFS (rotting oranges), you must mark cells visited when enqueued, not when dequeued. Otherwise cells get enqueued multiple times from different sources, giving wrong distances. Pattern: grid[nr][nc] = 2; queue = append(queue, ...) together.
⚠ Trap — Pacific Atlantic Reverse Flow Don't DFS from each cell trying to reach the ocean — that's O(M²N²). Instead, reverse the direction: BFS/DFS from the ocean borders inward, climbing uphill (heights[r][c] >= heights[parent]). Cells reachable from both seedings are the answer.
★ Must Know — Grid Problem Patterns (1) Flood fill / connected components: DFS, mark visited by overwriting cell. (2) Shortest path / min steps: BFS. (3) Multi-source spread: seed BFS queue with all sources. (4) Border-connected cells: DFS/BFS from borders, then flip (Surrounded Regions, Pacific Atlantic). (5) Connectivity counting: Union-Find when you need dynamic merging. Convert cell (r,c) to node index as r*cols + c.

24 Go Traps & Gotchas

What it is

Go-specific runtime panics, silent data bugs, and non-obvious language behaviours that trip up even experienced developers. Each trap has a mechanical root cause in Go's type system, memory model, or scheduler — understanding the cause makes the fix obvious.

Real-world analogy

Hidden potholes on a road you drive daily. Most journeys are fine. But certain combinations — nil map write, interface with nil pointer, send on closed channel — are specific coordinates that always crash. Knowing their exact locations means you never hit them.

How it works internally

Each trap has a specific cause: nil map panics because Go maps are reference types that must be initialised. Interface-nil mismatch because an interface stores (type, value) — a typed nil is not the zero interface. Closure capture shares the variable's memory address, not its current value.

When to use

Review this section before every Go interview. In code reviews, scan specifically for: unguarded map writes, slice append results that are discarded, goroutines in loops with captured variables, defer in loops, and functions returning concrete error types.

Complexity
Nil map write → panicSend on closed chan → panicTyped nil in interface ≠ nilClosure captures variable address, not value

Every item here has appeared in real Go interviews or caused production bugs. Know them cold.

all_traps.go — run each func independently
package main

import (
    "fmt"
    "reflect"
    "sync"
)

// ── TRAP 1: Nil map write ──────────────────────────────────
func trapNilMap() {
    var m map[string]int
    fmt.Println(m["key"]) // OK — returns 0
    // m["key"] = 1        // PANIC: assignment to entry in nil map
    m = make(map[string]int)
    m["key"] = 1  // now OK
}

// ── TRAP 2: Slice aliasing after append ────────────────────
func trapSliceAlias() {
    a := make([]int, 3, 6)  // len=3, cap=6
    b := a[:]               // shares underlying array
    b = append(b, 99)       // cap not exceeded → modifies a's backing array
    fmt.Println(a[:4])      // [0 0 0 99] — surprise!

    // Safe: use copy, or 3-index slice to cap sharing
    c := a[:3:3]            // cap == len → next append always allocates
    c = append(c, 99)       // new array allocated
    fmt.Println(a[:4])      // [0 0 0 99] — unchanged
    _ = b; _ = c
}

// ── TRAP 3: For range copies value ────────────────────────
func trapRangeCopy() {
    type Person struct{ Name string; Age int }
    people := []Person{{"Alice", 30}, {"Bob", 25}}

    // WRONG — v is a copy
    for _, v := range people {
        v.Age++ // modifies copy, not original
    }
    fmt.Println(people[0].Age) // 30 — unchanged!

    // CORRECT — use index
    for i := range people {
        people[i].Age++
    }
    fmt.Println(people[0].Age) // 31
}

// ── TRAP 4: Goroutine closure captures loop variable ───────
func trapClosureCapture() {
    var wg sync.WaitGroup
    results := make([]int, 5)

    // WRONG (pre Go 1.22)
    // for i := 0; i < 5; i++ {
    //     wg.Add(1)
    //     go func() { defer wg.Done(); results[i] = i }()
    // }

    // CORRECT: pass as argument
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) { defer wg.Done(); results[i] = i * i }(i)
    }
    wg.Wait()
    fmt.Println(results) // [0 1 4 9 16]
}

// ── TRAP 5: Interface nil vs typed nil ────────────────────
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func mightFail(fail bool) error {
    var e *MyError = nil
    if fail { e = &MyError{"oops"} }
    return e  // returns (*MyError, nil) — non-nil interface!
}

func trapInterfaceNil() {
    err := mightFail(false)
    fmt.Println(err)        // 
    fmt.Println(err == nil) // FALSE — interface holds (*MyError, nil)!
    fmt.Println(reflect.ValueOf(err).IsNil()) // true

    // FIX: return untyped nil
    // if fail { return &MyError{} }; return nil
}

// ── TRAP 6: Defer evaluates args immediately ───────────────
func trapDeferArgs() {
    x := 0
    defer fmt.Println("defer x:", x) // captures x=0 now
    x = 42
    fmt.Println("body x:", x) // 42
    // Output: body x: 42, then defer x: 0
}

// ── TRAP 7: Named return + defer ──────────────────────────
func trapNamedReturn() (result int) {
    defer func() {
        result++ // modifies the named return variable!
    }()
    return 10  // sets result=10, then defer runs → result=11
}

// ── TRAP 8: String indexing gives bytes, not runes ─────────
func trapStringIndex() {
    s := "日本語"
    fmt.Println(len(s))    // 9 bytes
    fmt.Println(s[0])      // 230 (first byte of 日)
    for i, r := range s {
        fmt.Printf("index=%d rune=%c\n", i, r)
    }
    // index=0 rune=日, index=3 rune=本, index=6 rune=語
}

// ── TRAP 9: Integer overflow ────────────────────────────────
func trapOverflow() {
    var i int32 = 2147483647  // MaxInt32
    i++                       // overflows to -2147483648 silently!
    fmt.Println(i)            // -2147483648

    // In interviews: always ask about constraints, use int64
    var safe int64 = 2147483647
    safe++
    fmt.Println(safe) // 2147483648 — fine
}

// ── TRAP 10: Defer in loop ─────────────────────────────────
func trapDeferLoop() {
    // WRONG: defers pile up, all run at function exit
    for i := 0; i < 3; i++ {
        defer fmt.Println("deferred:", i) // prints 2, 1, 0 at END
    }
    // FIX: wrap in closure/function
    for i := 0; i < 3; i++ {
        func(i int) {
            defer fmt.Println("wrapped defer:", i) // runs immediately
        }(i)
    }
}

func main() {
    fmt.Println("=== Nil Map ===");       trapNilMap()
    fmt.Println("=== Slice Alias ===");   trapSliceAlias()
    fmt.Println("=== Range Copy ===");    trapRangeCopy()
    fmt.Println("=== Closure ===");       trapClosureCapture()
    fmt.Println("=== Interface Nil ==="); trapInterfaceNil()
    fmt.Println("=== Defer Args ===");    trapDeferArgs()
    fmt.Println("=== Named Return ===");  fmt.Println(trapNamedReturn()) // 11
    fmt.Println("=== String Index ===");  trapStringIndex()
    fmt.Println("=== Overflow ===");      trapOverflow()
    fmt.Println("=== Defer Loop ===");    trapDeferLoop()
}

24b CLI Input / Reading Stdin

What it is

Mechanisms for a Go program to read structured input from stdin (fmt.Scan, bufio.Scanner), parse command-line arguments (os.Args, flag package), and write output efficiently. Critical for competitive programming and CLI tooling.

Real-world analogy

The keyboard and screen of your program. Stdin is the keyboard — your program blocks waiting for the user to type. Stdout is the screen — output appears there. Buffering is like typing a whole sentence before hitting send instead of transmitting one character at a time — far faster.

How it works internally

fmt.Scan makes a syscall per token — slow for large input. bufio.NewReader wraps stdin with a 4KB buffer — reads a large chunk once, then parses from memory. bufio.NewWriter accumulates output in memory and flushes in one write. Must call Flush() explicitly — data is lost otherwise.

When to use

Competitive programming → always use the bufio.NewReader + bufio.NewWriter + defer Flush() template. Interactive problems → fmt.Scan is fine. Unknown number of lines → bufio.Scanner with Scan() loop. CLI tools with named flags → flag package with flag.Parse().

Complexity
bufio read: O(n/bufSize) syscallsfmt.Scan: O(n) syscalls — slow for large inputMissing Flush() = no output written

Go has several ways to read input. Knowing which to reach for — and when — matters in interviews, competitive coding, and real CLI tools.

MethodBest ForGotcha
fmt.Scan / Scanln / ScanfSimple, small input; interactive programsSlow on large input; Scanln stops at newline
bufio.ScannerLine-by-line reading; unknown number of linesDefault buffer 64KB — must resize for long lines
bufio.NewReader + fmt.FscanFast whitespace-delimited token readingMust Flush buffered writer
os.ArgsCommand-line argumentsos.Args[0] is the binary name, args start at [1]
flag packageNamed CLI flags (--port 8080)Must call flag.Parse() before reading values
① fmt.Scan / Scanln / Scanf

Simplest option. Reads from stdin, splits on whitespace. Good for small input or interactive sessions.

fmt_scan.go
package main

import "fmt"

func main() {
    // ── fmt.Scan — reads space/newline-separated tokens ──
    var a, b int
    fmt.Scan(&a, &b)         // reads two ints; works across newlines
    fmt.Println(a + b)

    // ── fmt.Scanln — stops at newline ───────────────────
    var x, y float64
    fmt.Scanln(&x, &y)       // both must be on the SAME line
    fmt.Printf("%.2f\n", x+y)

    // ── fmt.Scanf — formatted, like C scanf ─────────────
    var name string
    var age int
    fmt.Scanf("%s %d", &name, &age)
    fmt.Printf("name=%s age=%d\n", name, age)

    // ── Read N numbers on one line ───────────────────────
    var n int
    fmt.Scan(&n)
    nums := make([]int, n)
    for i := range nums {
        fmt.Scan(&nums[i])   // works even if spread across lines
    }
    fmt.Println(nums)

    // ── Read until EOF (unknown count) ───────────────────
    var v int
    var values []int
    for {
        _, err := fmt.Scan(&v)
        if err != nil { break } // io.EOF or parse error
        values = append(values, v)
    }
    fmt.Println(values)
}
⚠ Trap — Scan vs Scanln fmt.Scan treats newlines as whitespace and reads across lines. fmt.Scanln stops at the newline — if you only read 1 of 3 values on a line, the rest are discarded. Use fmt.Scan for most interview problems.
② bufio.Scanner — Line-by-Line

Best when you need to read a whole line as a string, process unknown number of lines, or split on custom delimiters.

bufio_scanner.go
package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func main() {
    scanner := bufio.NewScanner(os.Stdin)

    // ── Increase buffer for long lines (default = 64KB) ─
    // Required if a single line can be very long
    const maxBuf = 1024 * 1024 // 1MB
    scanner.Buffer(make([]byte, maxBuf), maxBuf)

    // ── Read line by line ────────────────────────────────
    var lines []string
    for scanner.Scan() {
        line := scanner.Text()   // current line, no trailing \n
        if line == "" { break }  // stop on blank line (optional)
        lines = append(lines, line)
    }
    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "read error:", err)
    }
    fmt.Println(lines)

    // ── Read first line as single integer ────────────────
    scanner2 := bufio.NewScanner(os.Stdin)
    scanner2.Scan()
    n, _ := strconv.Atoi(scanner2.Text())
    fmt.Println(n)

    // ── Read line of space-separated ints ────────────────
    scanner3 := bufio.NewScanner(os.Stdin)
    scanner3.Scan()
    parts := strings.Fields(scanner3.Text()) // splits on any whitespace
    nums := make([]int, len(parts))
    for i, p := range parts {
        nums[i], _ = strconv.Atoi(p)
    }
    fmt.Println(nums)

    // ── Read a grid (matrix) ─────────────────────────────
    scanner4 := bufio.NewScanner(os.Stdin)
    var grid [][]int
    for scanner4.Scan() {
        row := []int{}
        for _, p := range strings.Fields(scanner4.Text()) {
            v, _ := strconv.Atoi(p)
            row = append(row, v)
        }
        if len(row) > 0 { grid = append(grid, row) }
    }
    fmt.Println(grid)

    // ── Split by words instead of lines ─────────────────
    scanner5 := bufio.NewScanner(os.Stdin)
    scanner5.Split(bufio.ScanWords) // default is ScanLines
    for scanner5.Scan() {
        fmt.Println(scanner5.Text()) // each word
    }
}
③ bufio Fast I/O — Competitive / Large Input

The go-to pattern for competitive programming. fmt.Fscan with a buffered reader is ~5-10x faster than plain fmt.Scan on large inputs because it avoids per-call syscalls.

fast_io.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

// ── Standard fast I/O template ────────────────────────────
// Copy this boilerplate at the top of every competitive solution

var (
    reader = bufio.NewReader(os.Stdin)
    writer = bufio.NewWriter(os.Stdout)
)

func main() {
    defer writer.Flush() // CRITICAL: flush buffered output at end

    // Read a single integer
    var n int
    fmt.Fscan(reader, &n)

    // Read n integers
    nums := make([]int, n)
    for i := range nums {
        fmt.Fscan(reader, &nums[i])
    }

    // Read a string
    var s string
    fmt.Fscan(reader, &s) // stops at whitespace

    // Read multiple values in one call
    var a, b, c int
    fmt.Fscan(reader, &a, &b, &c)

    // Read a grid of n rows, m cols
    var rows, cols int
    fmt.Fscan(reader, &rows, &cols)
    grid := make([][]int, rows)
    for i := range grid {
        grid[i] = make([]int, cols)
        for j := range grid[i] {
            fmt.Fscan(reader, &grid[i][j])
        }
    }

    // Write output (always to writer, not fmt.Println)
    fmt.Fprintln(writer, n)
    for _, v := range nums {
        fmt.Fprintf(writer, "%d ", v)
    }
    fmt.Fprintln(writer)
    fmt.Fprintln(writer, grid)
}
⚠ Never Forget defer writer.Flush() Without Flush(), buffered output stays in memory and is never written to stdout. Your program produces no output. Always defer writer.Flush() as the very first line of main().
④ Complete Interview Input Patterns

Common input shapes you'll encounter. Each is a self-contained runnable program — pipe input via echo "..." | go run file.go or type interactively.

input_patterns.go
package main

import (
    "bufio"
    "fmt"
    "os"
    "sort"
    "strings"
)

var in  = bufio.NewReader(os.Stdin)
var out = bufio.NewWriter(os.Stdout)

func main() {
    defer out.Flush()
    // ── Choose the pattern matching your problem input ───

    pattern1() // "read N, then N numbers"
    // pattern2() // "read until EOF"
    // pattern3() // "read T test cases"
    // pattern4() // "read a grid"
    // pattern5() // "read edges for a graph"
    // pattern6() // "read key-value pairs"
}

// Pattern 1: N followed by N space-separated integers
// Input:
//   5
//   3 1 4 1 5
func pattern1() {
    var n int
    fmt.Fscan(in, &n)
    nums := make([]int, n)
    for i := range nums { fmt.Fscan(in, &nums[i]) }
    sort.Ints(nums)
    fmt.Fprintln(out, nums)
}

// Pattern 2: Read until EOF — unknown number of integers
// Input:
//   1 2 3
//   4 5 6
func pattern2() {
    var nums []int
    var v int
    for {
        _, err := fmt.Fscan(in, &v)
        if err != nil { break }
        nums = append(nums, v)
    }
    fmt.Fprintln(out, nums)
}

// Pattern 3: T test cases
// Input:
//   3
//   5
//   10
//   15
func pattern3() {
    var t int
    fmt.Fscan(in, &t)
    for ; t > 0; t-- {
        var n int
        fmt.Fscan(in, &n)
        fmt.Fprintln(out, n*2) // solve per test case
    }
}

// Pattern 4: R x C grid of integers
// Input:
//   3 4
//   1 2 3 4
//   5 6 7 8
//   9 0 1 2
func pattern4() {
    var r, c int
    fmt.Fscan(in, &r, &c)
    grid := make([][]int, r)
    for i := range grid {
        grid[i] = make([]int, c)
        for j := range grid[i] { fmt.Fscan(in, &grid[i][j]) }
    }
    for _, row := range grid { fmt.Fprintln(out, row) }
}

// Pattern 4b: R x C grid of characters (maze, board)
// Input:
//   3 3
//   #.#
//   .S.
//   #.#
func pattern4b() {
    var r, c int
    fmt.Fscan(in, &r, &c)
    scanner := bufio.NewScanner(in)
    scanner.Scan() // consume trailing newline after "r c"
    grid := make([]string, r)
    for i := range grid {
        scanner.Scan()
        grid[i] = scanner.Text()
    }
    for _, row := range grid { fmt.Fprintln(out, row) }
}

// Pattern 5: Graph as edge list
// Input:
//   4 5        ← n nodes, m edges
//   0 1
//   0 2
//   1 3
//   2 3
//   1 2
func pattern5() {
    var n, m int
    fmt.Fscan(in, &n, &m)
    adj := make([][]int, n)
    for i := 0; i < m; i++ {
        var u, v int
        fmt.Fscan(in, &u, &v)
        adj[u] = append(adj[u], v)
        adj[v] = append(adj[v], u) // undirected; remove for directed
    }
    for i, neighbors := range adj {
        fmt.Fprintf(out, "%d: %v\n", i, neighbors)
    }
}

// Pattern 6: Read words / string tokens per line
// Input:
//   hello world foo
//   bar baz
func pattern6() {
    scanner := bufio.NewScanner(in)
    for scanner.Scan() {
        words := strings.Fields(scanner.Text())
        fmt.Fprintln(out, len(words), words)
    }
}
⑤ os.Args & flag — CLI Tool Inputs

Used in real CLI tools, not competitive problems. Know this for system design / platform engineering rounds where you're building a tool.

cli_args.go
package main

import (
    "flag"
    "fmt"
    "os"
    "strconv"
)

func main() {
    // ── os.Args — raw positional arguments ───────────────
    // Usage: go run cli_args.go hello 42
    fmt.Println("program:", os.Args[0])
    if len(os.Args) > 1 {
        fmt.Println("args:", os.Args[1:])
        for i, arg := range os.Args[1:] {
            if n, err := strconv.Atoi(arg); err == nil {
                fmt.Printf("  arg[%d] is int: %d\n", i, n)
            } else {
                fmt.Printf("  arg[%d] is string: %s\n", i, arg)
            }
        }
    }

    // ── flag package — named flags ────────────────────────
    // Usage: go run cli_args.go -port 8080 -verbose -name gopher
    port    := flag.Int("port", 3000, "server port")
    verbose := flag.Bool("verbose", false, "enable verbose output")
    name    := flag.String("name", "world", "name to greet")
    timeout := flag.Duration("timeout", 0, "request timeout e.g. 30s")

    flag.Parse() // MUST call before reading any flag values

    // flag.Args() returns remaining non-flag arguments
    rest := flag.Args()

    fmt.Printf("port=%d verbose=%v name=%s timeout=%v\n",
        *port, *verbose, *name, *timeout)
    fmt.Println("remaining args:", rest)

    // Custom usage message
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
        flag.PrintDefaults()
    }
}
⑥ Testing Your Input Code Locally
shell — how to feed input
# Pipe a string directly
echo "5\n3 1 4 1 5" | go run solution.go

# Multi-line input (use $'...' for actual newlines in bash)
echo $'5\n3 1 4 1 5' | go run solution.go

# Redirect from a file (best for complex test cases)
go run solution.go < input.txt

# Redirect output too
go run solution.go < input.txt > output.txt

# Interactive: just run and type, Ctrl+D to send EOF
go run solution.go

# Create a test input file
cat > input.txt << 'EOF'
5
3 1 4 1 5
EOF
go run solution.go < input.txt
Quick Stdin Mock in Tests In unit tests, replace os.Stdin by piping through a strings.Reader: old := os.Stdin; r, w, _ := os.Pipe(); os.Stdin = r; w.WriteString("5\n"); w.Close(); // run; os.Stdin = old

25 fmt — Format Verbs

What it is

Go's formatted I/O package — analogous to C's stdio.h. Provides printing (Print/Println/Printf), string formatting (Sprintf), scanning (Scan/Scanf), and error wrapping (Errorf with %w).

Real-world analogy

A printing press with templates. The format string is the template with blank slots (%d, %s); the arguments fill in the slots. %v is the universal slot — it uses the value's default format, like a template that just writes whatever you hand it.

How it works internally

%v uses reflection to format any type. Printf writes to stdout (os.Stdout writer). Fprintf writes to any io.Writer — use this for buffered output. Errorf with %w wraps an error, preserving the chain for errors.Is and errors.As unwrapping.

When to use

%+v for structs with field names (debugging). %T to print a value's type. %w in fmt.Errorf for wrapping errors with context. fmt.Fprintln(writer, ...) instead of fmt.Println for competitive programming with buffered output.

Complexity
Sprintf O(n output length)%v uses reflection — slower than explicit formatFprintf to bufio.Writer = O(1) syscalls
VerbMeaningExample
%vDefault formatfmt.Sprintf("%v", []int{1,2,3})[1 2 3]
%+vStruct with field namesfmt.Sprintf("%+v", p){Name:Alice Age:30}
%#vGo syntax representationmain.Person{Name:"Alice"}
%TTypefmt.Sprintf("%T", 42)int
%dInteger decimal%05d → zero-padded
%bBinaryfmt.Sprintf("%b", 6)110
%xHex lowercasefmt.Sprintf("%x", 255)ff
%fFloat%.2f → 2 decimal places
%sString
%qQuoted string"hello"
%pPointer address
%wWrap error (fmt.Errorf only)fmt.Errorf("db: %w", err)

26 strings & strconv

What it is

strings: operations on immutable string values (split, join, replace, trim, search). strconv: conversions between strings and other primitive types (int, float, bool). Together they cover virtually all string processing needs in Go.

Real-world analogy

strings is a text editor's toolbar — cut, copy, find, replace, capitalise. strconv is the "Save As" dialog — it changes the file format (type), not the content: "42" (string) ↔ 42 (int), "3.14" ↔ 3.14, "true" ↔ true.

How it works internally

Most strings functions return new strings (immutable). strings.Builder uses an internal []byte and a WriteString method — O(1) amortised per append, O(n) total. strconv.Atoi parses base-10; use ParseInt(s, base, bitSize) for hex/binary. FormatInt(n, base) for the reverse.

When to use

strings.Fields to split on any whitespace (competitive input parsing). strings.Builder for building strings in loops. strconv.Atoi/Itoa for int↔string. strconv.ParseInt(s, 16, 64) for hex parsing. Never string(65) for int→string — use Itoa.

Complexity
Most strings ops O(n)Builder O(1) amortised per Writes += piece in loop = O(n²) — use Builder
strings_strconv.go
package main

import (
    "fmt"
    "strings"
    "strconv"
)

func main() {
    s := "  Hello, World!  "

    // ── strings package ──────────────────────────────────
    fmt.Println(strings.TrimSpace(s))           // "Hello, World!"
    fmt.Println(strings.ToLower("ABC"))          // "abc"
    fmt.Println(strings.ToUpper("abc"))          // "ABC"
    fmt.Println(strings.Contains("hello", "ell")) // true
    fmt.Println(strings.HasPrefix("hello", "he")) // true
    fmt.Println(strings.HasSuffix("hello", "lo")) // true
    fmt.Println(strings.Index("hello", "ll"))    // 2 (-1 if not found)
    fmt.Println(strings.Count("hello", "l"))     // 2
    fmt.Println(strings.Replace("oink oink", "oink", "moo", 1)) // "moo oink"
    fmt.Println(strings.ReplaceAll("aaa", "a", "b")) // "bbb"

    parts := strings.Split("a,b,c", ",")  // ["a" "b" "c"]
    fmt.Println(strings.Join(parts, "-")) // "a-b-c"

    fmt.Println(strings.Fields("  foo bar  baz  ")) // ["foo" "bar" "baz"]

    // Efficient concatenation — O(n), not O(n²)
    var sb strings.Builder
    for i := 0; i < 5; i++ { fmt.Fprintf(&sb, "%d", i) }
    fmt.Println(sb.String()) // "01234"

    // strings.NewReader for byte-by-byte processing
    r := strings.NewReader("Hello")
    fmt.Println(r.Len()) // 5

    // ── strconv package ──────────────────────────────────
    // int ↔ string
    n, err := strconv.Atoi("123")
    fmt.Println(n, err)          // 123 
    _, err = strconv.Atoi("abc")
    fmt.Println(err)             // invalid syntax

    fmt.Println(strconv.Itoa(42)) // "42"

    // ParseInt(s, base, bitSize)
    i64, _ := strconv.ParseInt("FF", 16, 64) // hex
    fmt.Println(i64) // 255
    i64, _ = strconv.ParseInt("1010", 2, 64)  // binary
    fmt.Println(i64) // 10

    // FormatInt(n, base)
    fmt.Println(strconv.FormatInt(255, 16)) // "ff"
    fmt.Println(strconv.FormatInt(255, 2))  // "11111111"

    // float
    f, _ := strconv.ParseFloat("3.14", 64)
    fmt.Println(f)
    fmt.Println(strconv.FormatFloat(f, 'f', 2, 64)) // "3.14"

    // bool
    b, _ := strconv.ParseBool("true")
    fmt.Println(b) // true
}

27 sort & container/heap

What it is

sort: in-place sorting with comparison closures, binary search on sorted slices, and checked sort predicates. container/heap: heap operations (push/pop/fix) on any type implementing the 5-method heap.Interface.

Real-world analogy

sort is a personal organiser that rearranges items by any rule you specify. container/heap is an always-sorted priority inbox — every time you add or remove an item, the inbox instantly re-sorts so the most important item is always on top.

How it works internally

sort.Slice uses pdqsort (pattern-defeating quicksort) — O(n log n) average and worst case. sort.Search is binary search over an index range with a monotonic predicate — returns leftmost index where predicate is true. heap.Push appends then sifts up; heap.Pop swaps root with last, shrinks, sifts down.

When to use

sort.Slice for custom struct ordering. sort.Ints/Strings for primitives. sort.Search for lower/upper bound on sorted data. container/heap whenever you need a priority queue with arbitrary struct ordering — flip Less to toggle min/max heap.

Complexity
sort O(n log n)sort.Search O(log n)heap Push/Pop O(log n)heap Init (build-heap) O(n)
sort_demo.go
package main

import (
    "fmt"
    "sort"
)

func main() {
    // Primitive slice sorting
    ints := []int{5, 2, 8, 1, 9, 3}
    sort.Ints(ints)       // ascending in-place
    fmt.Println(ints)     // [1 2 3 5 8 9]
    sort.Sort(sort.Reverse(sort.IntSlice(ints))) // descending
    fmt.Println(ints)     // [9 8 5 3 2 1]

    strs := []string{"banana", "apple", "cherry"}
    sort.Strings(strs)
    fmt.Println(strs) // [apple banana cherry]

    // Custom sort with sort.Slice (unstable) / sort.SliceStable
    type Person struct{ Name string; Age int }
    people := []Person{{"Bob", 25}, {"Alice", 30}, {"Charlie", 25}}

    // Sort by age asc, then name asc
    sort.Slice(people, func(i, j int) bool {
        if people[i].Age != people[j].Age {
            return people[i].Age < people[j].Age
        }
        return people[i].Name < people[j].Name
    })
    fmt.Println(people) // [{Bob 25} {Charlie 25} {Alice 30}]

    // Binary search (on sorted slice)
    sorted := []int{1, 3, 5, 7, 9}
    idx := sort.SearchInts(sorted, 5)  // returns index where 5 is/would be
    fmt.Println(idx, sorted[idx] == 5) // 2 true

    // sort.Search — binary search with predicate
    // "find first i in [0,n) where f(i) is true"
    n := sort.Search(len(sorted), func(i int) bool { return sorted[i] >= 6 })
    fmt.Println(n) // 3 (first index with value >= 6)

    // Check if sorted
    fmt.Println(sort.IntsAreSorted(sorted)) // true

    // 2D sort — sort rows by first element
    matrix := [][]int{{3,1},{1,4},{2,2},{1,3}}
    sort.Slice(matrix, func(i, j int) bool {
        if matrix[i][0] != matrix[j][0] { return matrix[i][0] < matrix[j][0] }
        return matrix[i][1] < matrix[j][1]
    })
    fmt.Println(matrix) // [[1 3] [1 4] [2 2] [3 1]]
}

28 math, errors & Fast I/O

What it is

math: floating-point functions and constants (sqrt, pow, ceil, floor, min, max, Inf, MaxInt64). math/bits: CPU-level bit manipulation. errors: error creation and chain unwrapping. bufio: buffered I/O for performance-critical reading/writing.

Real-world analogy

A scientific calculator (math), a binary wrench for hardware-level tightening (bits), an error message decoder ring (errors), and a loading dock with a staging area (bufio — batches work instead of making a trip per item).

How it works internally

math.MaxInt64 is a compile-time constant. bits.OnesCount compiles to a single CPU POPCNT instruction. errors.Is walks the error chain via Unwrap(). bufio.Writer accumulates writes in a 4KB buffer — Flush() commits to the underlying writer in one syscall.

When to use

math.MaxInt64 / 1<<62 as infinity sentinel in graph algorithms. bits.OnesCount for popcount in bitmask DP. errors.Is(err, target) to check sentinel errors across wrapping layers. bufio always for competitive programming output — and never forget defer writer.Flush().

Complexity
math funcs O(1)bits ops O(1) — single CPU instructionerrors.Is O(chain depth)bufio: missing Flush = silent data loss
math_misc.go
package main

import (
    "bufio"
    "errors"
    "fmt"
    "math"
    "math/bits"
    "os"
)

func main() {
    // ── math ─────────────────────────────────────────────
    fmt.Println(math.MaxInt64)      // 9223372036854775807
    fmt.Println(math.MinInt64)      // -9223372036854775808
    fmt.Println(math.MaxFloat64)
    fmt.Println(math.Abs(-3.14))    // 3.14
    fmt.Println(math.Sqrt(16))      // 4
    fmt.Println(math.Pow(2, 10))    // 1024
    fmt.Println(math.Log2(1024))    // 10
    fmt.Println(math.Ceil(3.1))     // 4
    fmt.Println(math.Floor(3.9))    // 3
    fmt.Println(math.Min(3.0, 5.0)) // 3
    fmt.Println(math.Max(3.0, 5.0)) // 5

    // Integer min/max — Go 1.21+
    fmt.Println(min(3, 5)) // builtin since 1.21
    fmt.Println(max(3, 5))

    // Inf and NaN
    inf := math.Inf(1)      // +Inf
    fmt.Println(math.IsInf(inf, 1)) // true

    // ── bits package (bit manipulation) ──────────────────
    fmt.Println(bits.OnesCount(6))     // 2 (binary: 110)
    fmt.Println(bits.Len(255))         // 8
    fmt.Println(bits.TrailingZeros(8)) // 3

    // Common bit tricks
    x := 13 // 1101
    fmt.Println(x & (x-1))   // 1100 — clear lowest set bit
    fmt.Println(x & -x)      // 0001 — isolate lowest set bit
    fmt.Println(x | (x+1))   // 1111 — set lowest unset bit
    isPow2 := x > 0 && x & (x-1) == 0
    fmt.Println("isPow2:", isPow2) // false

    // ── Error handling ────────────────────────────────────
    sentinel := errors.New("not found")
    wrapped  := fmt.Errorf("db query: %w", sentinel)
    fmt.Println(errors.Is(wrapped, sentinel))  // true — unwraps chain

    var target *os.PathError
    fmt.Println(errors.As(wrapped, &target))   // false — wrong type

    // ── Fast I/O (competitive / large input) ─────────────
    reader := bufio.NewReader(os.Stdin)
    writer := bufio.NewWriter(os.Stdout)
    defer writer.Flush() // MUST flush buffered writer

    // Read line
    // line, _ := reader.ReadString('\n')
    // line = strings.TrimRight(line, "\r\n")

    // Scan integers fast
    var n int
    fmt.Fscan(reader, &n) // reads from buffered reader
    _ = n
    // Print fast
    fmt.Fprintln(writer, "result")

    // ── Modular arithmetic (common in DP/combinatorics) ──
    const MOD = 1_000_000_007
    a, b := 999_999_999, 999_999_999
    product := (a % MOD * (b % MOD)) % MOD
    fmt.Println(product)
}
★ Staff+ — Integer Min/Max in Older Go Before Go 1.21 builtins, use: const INF = int(^uint(0) >> 1) for MaxInt, and -INF for MinInt. Or define func min(a, b int) int manually.
Interview Closing Tips (1) Always clarify input constraints before coding. (2) Handle edge cases: empty input, single element, all same values, overflow. (3) Talk through complexity before and after. (4) In Go, prefer int or int64 over int32. (5) Use named variables over clever one-liners — readability matters at Staff level.