Skip to content

Iterating Maps — Optimization Exercises


Exercise 1 🟢 — Repeated Full-Map Scan for Single Key

package main

import "fmt"

func findByValue(m map[string]int, target int) string {
    for k, v := range m { // O(n) scan
        if v == target {
            return k
        }
    }
    return ""
}

// Called many times with the same m but different targets
func main() {
    m := map[string]int{"alice": 95, "bob": 87, "carol": 95, "dave": 72}
    for i := 0; i < 10000; i++ {
        _ = findByValue(m, 95)
    }
    fmt.Println("done")
}

Problem: O(n) scan repeated 10K times.

Optimized Solution Build an inverted index (value → []key) once:
// Build once
type BiMap struct {
    forward  map[string]int    // key -> value
    inverted map[int][]string  // value -> []key
}

func NewBiMap(m map[string]int) *BiMap {
    inv := make(map[int][]string, len(m))
    for k, v := range m {
        inv[v] = append(inv[v], k)
    }
    return &BiMap{forward: m, inverted: inv}
}

func (b *BiMap) FindByValue(target int) []string {
    return b.inverted[target] // O(1)
}

// Now: 10000 lookups = 10000 O(1) operations instead of O(n) each

Exercise 2 🟢 — Repeated Map Building

package main

import (
    "fmt"
    "strings"
)

func normalize(input string) map[string]int {
    m := map[string]int{} // allocates every call
    for _, word := range strings.Fields(input) {
        m[strings.ToLower(word)]++
    }
    return m
}

func compare(a, b string) bool {
    ma := normalize(a) // two map allocations per call
    mb := normalize(b)
    if len(ma) != len(mb) { return false }
    for k, v := range ma {
        if mb[k] != v { return false }
    }
    return true
}

func main() {
    for i := 0; i < 1000; i++ {
        _ = compare("Hello World", "world hello")
    }
    fmt.Println("done")
}
Optimized Solution
// Option 1: Reuse a pre-allocated map (clear between uses)
func normalizeInto(input string, m map[string]int) {
    for k := range m { delete(m, k) } // clear
    for _, word := range strings.Fields(input) {
        m[strings.ToLower(word)]++
    }
}

// Option 2: For comparison, avoid building second map
// Count words in first, then subtract for second:
func compare(a, b string) bool {
    ma := map[string]int{}
    for _, w := range strings.Fields(strings.ToLower(a)) { ma[w]++ }
    for _, w := range strings.Fields(strings.ToLower(b)) { ma[w]-- }
    for _, v := range ma {
        if v != 0 { return false }
    }
    return true
    // One map, not two — 50% fewer allocations
}

Exercise 3 🟢 — Converting Map to Sorted Slice Repeatedly

package main

import (
    "fmt"
    "sort"
)

func topScorers(scores map[string]int, n int) []string {
    type kv struct{ k string; v int }
    pairs := make([]kv, 0, len(scores))
    for k, v := range scores {
        pairs = append(pairs, kv{k, v})
    }
    sort.Slice(pairs, func(i, j int) bool {
        return pairs[i].v > pairs[j].v
    })
    result := make([]string, min(n, len(pairs)))
    for i := range result {
        result[i] = pairs[i].k
    }
    return result
}

// Called every second with essentially the same data
func main() {
    scores := map[string]int{"Alice": 95, "Bob": 82, "Carol": 91}
    for i := 0; i < 1000; i++ {
        _ = topScorers(scores, 3)
    }
    fmt.Println("done")
}

func min(a, b int) int { if a < b { return a }; return b }
Optimized Solution
// Cache the sorted result; only re-sort when map changes
type RankedScores struct {
    scores  map[string]int
    sorted  []string // cached sorted keys
    version int
}

func (rs *RankedScores) UpdateScore(name string, score int) {
    rs.scores[name] = score
    rs.version++ // invalidate cache
    rs.sorted = nil
}

func (rs *RankedScores) TopN(n int) []string {
    if rs.sorted == nil { // rebuild only when needed
        type kv struct{ k string; v int }
        pairs := make([]kv, 0, len(rs.scores))
        for k, v := range rs.scores { pairs = append(pairs, kv{k, v}) }
        sort.Slice(pairs, func(i, j int) bool { return pairs[i].v > pairs[j].v })
        rs.sorted = make([]string, len(pairs))
        for i, p := range pairs { rs.sorted[i] = p.k }
    }
    if n > len(rs.sorted) { n = len(rs.sorted) }
    return rs.sorted[:n]
}

Exercise 4 🟡 — Map Scan Instead of Direct Lookup

package main

import "fmt"

type Config struct {
    settings map[string]string
}

func (c *Config) IsProduction() bool {
    for k, v := range c.settings {
        if k == "env" && v == "production" {
            return true
        }
    }
    return false
}

func main() {
    cfg := &Config{settings: map[string]string{
        "env":     "production",
        "version": "1.0",
        "region":  "us-east",
    }}
    for i := 0; i < 1_000_000; i++ {
        _ = cfg.IsProduction()
    }
    fmt.Println("done")
}
Optimized Solution
// Direct lookup is O(1) — no need to range
func (c *Config) IsProduction() bool {
    return c.settings["env"] == "production"
}
// If key might not exist:
func (c *Config) IsProduction() bool {
    v, ok := c.settings["env"]
    return ok && v == "production"
}
// Speedup: O(n) → O(1). For n=1M calls with map size 3: 3M→1M operations

Exercise 5 🟡 — Inefficient Map Union

package main

import "fmt"

func union(maps ...map[string]int) map[string]int {
    result := map[string]int{}
    for _, m := range maps {
        for k, v := range m {
            result[k] += v
        }
    }
    return result
}

// Called with many small maps
func main() {
    // 1000 maps, each with 100 entries
    mapsSlice := make([]map[string]int, 1000)
    for i := range mapsSlice {
        mapsSlice[i] = map[string]int{}
        for j := 0; j < 100; j++ {
            mapsSlice[i][fmt.Sprintf("key%d", j)] = j + i
        }
    }
    _ = union(mapsSlice...)
    fmt.Println("done")
}
Optimized Solution
func union(maps ...map[string]int) map[string]int {
    // Calculate total unique keys estimate for pre-allocation
    totalSize := 0
    for _, m := range maps { totalSize += len(m) }
    // Pre-allocate with estimated capacity
    result := make(map[string]int, totalSize) // overestimates but prevents rehashing
    for _, m := range maps {
        for k, v := range m {
            result[k] += v
        }
    }
    return result
}
// Without pre-allocation: map rehashes ~log2(100) = 7 times for 100 entries
// With pre-allocation: 0 rehashes — significant for large inputs

Exercise 6 🟡 — Iterating Map of Channels Serially

package main

import (
    "fmt"
    "time"
)

func drainAll(channels map[string]chan int) map[string][]int {
    results := map[string][]int{}
    for name, ch := range channels {
        close(ch) // signal done
        for v := range ch { // serial drain — one at a time
            results[name] = append(results[name], v)
        }
    }
    return results
}

func main() {
    channels := map[string]chan int{}
    for _, name := range []string{"a", "b", "c", "d"} {
        ch := make(chan int, 100)
        for i := 0; i < 100; i++ { ch <- i }
        channels[name] = ch
    }
    start := time.Now()
    _ = drainAll(channels)
    fmt.Println(time.Since(start))
}
Optimized Solution
import "sync"

func drainAll(channels map[string]chan int) map[string][]int {
    var mu sync.Mutex
    results := map[string][]int{}
    var wg sync.WaitGroup

    for name, ch := range channels {
        name, ch := name, ch
        wg.Add(1)
        go func() {
            defer wg.Done()
            close(ch)
            local := []int{}
            for v := range ch { local = append(local, v) }
            mu.Lock()
            results[name] = local
            mu.Unlock()
        }()
    }
    wg.Wait()
    return results
}
// Parallel drain: 4 channels simultaneously instead of sequentially

Exercise 7 🟡 — String Key with Repeated Allocation

package main

import "fmt"

func hotFunction(userID int, action string) {
    key := fmt.Sprintf("user:%d:action:%s", userID, action)
    // use key to access map
    _ = key
}

func main() {
    for i := 0; i < 1_000_000; i++ {
        hotFunction(i%100, "click")
    }
    fmt.Println("done")
}
Optimized Solution
// Option 1: Use struct as map key (no allocation)
type UserAction struct {
    UserID int
    Action string
}
var cache = map[UserAction]struct{}{}
func hotFunction(userID int, action string) {
    key := UserAction{userID, action} // stack-allocated struct, no alloc
    _ = cache[key]
}

// Option 2: Pre-compute/intern strings if userID and action sets are small
var actionCache = map[string]struct{}{"click": {}, "view": {}, "buy": {}}
// Use canonical string references

// Option 3: Integer encoding for small bounded sets
// encode userID*1000 + actionIndex as a single int key
var actionIdx = map[string]int{"click": 0, "view": 1, "buy": 2}
var cache2 = map[int]struct{}{}
func hotFunction2(userID int, action string) {
    key := userID*1000 + actionIdx[action]
    _ = cache2[key]
}

Exercise 8 🔴 — Full Map Scan for Existence Check

package main

import "fmt"

var blacklist = []string{"spam@evil.com", "bad@actor.net", "fraud@scam.org"}
// ... potentially thousands of entries

func isBlacklisted(email string) bool {
    for _, b := range blacklist { // O(n) scan!
        if b == email {
            return true
        }
    }
    return false
}

func main() {
    emails := []string{"user@good.com", "spam@evil.com", "another@good.com"}
    for _, email := range emails {
        if isBlacklisted(email) {
            fmt.Println("BLOCKED:", email)
        }
    }
}
Optimized Solution
// Build a set (map) for O(1) lookup
var blacklistSet map[string]struct{}

func init() {
    blacklist := []string{"spam@evil.com", "bad@actor.net", "fraud@scam.org"}
    blacklistSet = make(map[string]struct{}, len(blacklist))
    for _, email := range blacklist {
        blacklistSet[email] = struct{}{}
    }
}

func isBlacklisted(email string) bool {
    _, ok := blacklistSet[email] // O(1)
    return ok
}
// Speedup: O(n) → O(1) per check
// For 1000-entry blacklist checking 1M emails: 1B ops → 1M ops (1000x)

Exercise 9 🔴 — Unnecessary Map for Counting When Slice Suffices

package main

import "fmt"

func countGrades(grades []int) map[int]int {
    freq := map[int]int{}
    for _, g := range grades {
        freq[g]++ // grades are 0-100
    }
    return freq
}

func printDistribution(grades []int) {
    freq := countGrades(grades)
    for score := 0; score <= 100; score++ {
        if freq[score] > 0 {
            fmt.Printf("%d: %d\n", score, freq[score])
        }
    }
}
Optimized Solution
// Grades are bounded integers 0-100: use array (slice) instead of map
func countGrades(grades []int) [101]int {
    var freq [101]int
    for _, g := range grades {
        if g >= 0 && g <= 100 {
            freq[g]++
        }
    }
    return freq
}

func printDistribution(grades []int) {
    freq := countGrades(grades)
    for score, count := range freq { // array range: cache-friendly!
        if count > 0 {
            fmt.Printf("%d: %d\n", score, count)
        }
    }
}
// Benefits:
// - [101]int on stack: 0 heap allocations
// - Array access: O(1) with perfect cache behavior
// - Iteration: cache-friendly sequential access
// - 100x more memory-efficient than map for dense integer ranges

Exercise 10 🔴 — Deep Copy in Hot Loop

package main

import "fmt"

func processBatch(templates map[string]map[string]int, multiplier int) []map[string]int {
    results := make([]map[string]int, 0, len(templates))
    for _, tmpl := range templates {
        copy := map[string]int{} // allocation per template!
        for k, v := range tmpl {
            copy[k] = v * multiplier // copy + transform
        }
        results = append(results, copy)
    }
    return results
}

func main() {
    templates := map[string]map[string]int{
        "t1": {"x": 1, "y": 2},
        "t2": {"x": 3, "y": 4},
    }
    for i := 0; i < 10000; i++ {
        _ = processBatch(templates, i)
    }
    fmt.Println("done")
}
Optimized Solution
// Option 1: Pre-allocate with capacity hint per template
func processBatch(templates map[string]map[string]int, multiplier int) []map[string]int {
    results := make([]map[string]int, 0, len(templates))
    for _, tmpl := range templates {
        result := make(map[string]int, len(tmpl)) // pre-sized — no rehash
        for k, v := range tmpl {
            result[k] = v * multiplier
        }
        results = append(results, result)
    }
    return results
}

// Option 2: If multiplier 1 (pure copy): use maps.Clone (optimized)
import "maps"
func cloneBatch(templates map[string]map[string]int) []map[string]int {
    results := make([]map[string]int, 0, len(templates))
    for _, tmpl := range templates {
        results = append(results, maps.Clone(tmpl))
    }
    return results
}

// Option 3: Structural sharing — return views with lazy multiplier
type MultipliedMap struct {
    base       map[string]int
    multiplier int
}
func (m *MultipliedMap) Get(k string) int { return m.base[k] * m.multiplier }
// Zero copy! Multiplier applied on read only

Exercise 11 🔴 — Unbounded Cache Growth

package main

import (
    "fmt"
    "sync"
)

var (
    mu    sync.RWMutex
    cache = map[string][]byte{}
)

func getOrCompute(key string, compute func() []byte) []byte {
    mu.RLock()
    if v, ok := cache[key]; ok {
        mu.RUnlock()
        return v
    }
    mu.RUnlock()

    v := compute()
    mu.Lock()
    cache[key] = v // grows without bound!
    mu.Unlock()
    return v
}

func main() {
    for i := 0; i < 1_000_000; i++ {
        key := fmt.Sprintf("key-%d", i)
        _ = getOrCompute(key, func() []byte { return make([]byte, 1024) })
    }
    // cache now holds 1GB of data!
    fmt.Println("cache size:", len(cache))
}
Optimized Solution
// LRU cache with bounded size
type LRUCache struct {
    mu       sync.Mutex
    capacity int
    cache    map[string]*entry
    order    []string // simple LRU tracking (use container/list for O(1))
}

type entry struct {
    value []byte
}

func NewLRU(capacity int) *LRUCache {
    return &LRUCache{
        capacity: capacity,
        cache:    make(map[string]*entry, capacity),
        order:    make([]string, 0, capacity),
    }
}

func (c *LRUCache) GetOrCompute(key string, compute func() []byte) []byte {
    c.mu.Lock()
    defer c.mu.Unlock()

    if e, ok := c.cache[key]; ok {
        return e.value
    }

    if len(c.cache) >= c.capacity {
        // Evict oldest (first in order)
        oldest := c.order[0]
        c.order = c.order[1:]
        delete(c.cache, oldest)
    }

    v := compute()
    c.cache[key] = &entry{v}
    c.order = append(c.order, key)
    return v
}

// Production: use github.com/hashicorp/golang-lru or similar

Optimization Summary

Exercise Problem Fix Impact
1 O(n) scan for value lookup Build inverted index O(n)→O(1) per lookup
2 Two maps per comparison One map + subtract 50% fewer allocations
3 Re-sort on every call Cache sorted result O(n log n)→O(1) when unchanged
4 Range scan for direct key Direct m[key] lookup O(n)→O(1)
5 Map without pre-allocation make(map, totalSize) Eliminate rehashing
6 Serial channel drain Parallel goroutines Nx speedup (N channels)
7 String key allocation Struct key or int encoding 0 allocs per call
8 Slice scan for blacklist Map set O(1) O(n)→O(1) per check
9 Map for bounded ints Array/slice 0 allocs, cache-friendly
10 Deep copy in hot loop Pre-sized map, structural sharing 50-90% fewer allocs
11 Unbounded cache LRU with capacity O(1) bounded memory