Understanding data structures can be challenging when they’re presented as abstract concepts. Let’s make them tangible by exploring them through something we all know: a coffee shop. From the order queue to delivery routes, every aspect of running a coffee shop mirrors fundamental computer science concepts.

In this comprehensive guide, we’ll explore 25+ data structures through the lens of coffee shop operations, complete with visualizations and Go implementations.


Part 1: The Basics Revisited

The Order Line - Queue

Every morning at the coffee shop, customers line up in order. First in, first out (FIFO). The person who arrives first gets served first. This is a queue.

graph LR A[Customer 1] --> B[Customer 2] B --> C[Customer 3] C --> D[Customer 4] style A fill:#90EE90 style D fill:#FFB6C1 classDef default fill:#E8F4F8,stroke:#333,stroke-width:2px E[← Dequeue
Service] -.-> A D -.-> F[Enqueue →
New Customer]

Go Implementation:

package main

import "fmt"

type OrderQueue struct {
    orders []string
}

func (q *OrderQueue) Enqueue(order string) {
    q.orders = append(q.orders, order)
    fmt.Printf("📝 Added order: %s (Queue size: %d)\n", order, len(q.orders))
}

func (q *OrderQueue) Dequeue() string {
    if len(q.orders) == 0 {
        return ""
    }
    order := q.orders[0]
    q.orders = q.orders[1:]
    fmt.Printf("✅ Serving order: %s (Remaining: %d)\n", order, len(q.orders))
    return order
}

func (q *OrderQueue) IsEmpty() bool {
    return len(q.orders) == 0
}

func main() {
    queue := &OrderQueue{}

    // Morning rush
    queue.Enqueue("Latte")
    queue.Enqueue("Cappuccino")
    queue.Enqueue("Espresso")

    // Serve customers
    queue.Dequeue() // Latte
    queue.Dequeue() // Cappuccino

    queue.Enqueue("Mocha")
    queue.Dequeue() // Espresso
}

Time Complexity:

  • Enqueue: O(1)
  • Dequeue: O(1) with proper implementation
  • Peek: O(1)

The Cup Stack - Stack

Behind the counter, clean cups are stacked. You always take from the top and add to the top. Last in, first out (LIFO). This is a stack.

graph TB A[Cup 4
Last In] B[Cup 3] C[Cup 2] D[Cup 1
First In] A --> B B --> C C --> D style A fill:#FFB6C1 style D fill:#90EE90 E[← Pop
Take Cup] -.-> A A -.-> F[Push →
Add Cup]

Go Implementation:

package main

import "fmt"

type CupStack struct {
    cups []string
}

func (s *CupStack) Push(cup string) {
    s.cups = append(s.cups, cup)
    fmt.Printf("➕ Stacked cup: %s (Height: %d)\n", cup, len(s.cups))
}

func (s *CupStack) Pop() string {
    if len(s.cups) == 0 {
        return ""
    }
    cup := s.cups[len(s.cups)-1]
    s.cups = s.cups[:len(s.cups)-1]
    fmt.Printf("➖ Took cup: %s (Remaining: %d)\n", cup, len(s.cups))
    return cup
}

func (s *CupStack) Peek() string {
    if len(s.cups) == 0 {
        return ""
    }
    return s.cups[len(s.cups)-1]
}

func main() {
    stack := &CupStack{}

    // Stack cups
    stack.Push("Small Cup #1")
    stack.Push("Medium Cup #2")
    stack.Push("Large Cup #3")

    // Use cups (LIFO order)
    fmt.Println("Top cup:", stack.Peek())
    stack.Pop() // Large Cup #3
    stack.Pop() // Medium Cup #2
}

Real-world use case: Undo functionality (Ctrl+Z) in editors works like a stack.


The Loyalty Card Chain - Linked List

Each loyalty card points to the next customer in the rewards program. Each node (customer) contains data and a reference to the next node.

graph LR A[Alice
Points: 50] -->|next| B[Bob
Points: 30] B -->|next| C[Carol
Points: 80] C -->|next| D[null] style A fill:#90EE90 style D fill:#FFB6C1

Go Implementation:

package main

import "fmt"

type Customer struct {
    Name   string
    Points int
    Next   *Customer
}

type LoyaltyChain struct {
    Head *Customer
}

func (l *LoyaltyChain) AddCustomer(name string, points int) {
    newCustomer := &Customer{
        Name:   name,
        Points: points,
    }

    if l.Head == nil {
        l.Head = newCustomer
        return
    }

    current := l.Head
    for current.Next != nil {
        current = current.Next
    }
    current.Next = newCustomer
    fmt.Printf("🎁 Added %s to loyalty program (%d points)\n", name, points)
}

func (l *LoyaltyChain) Display() {
    current := l.Head
    fmt.Println("\n📋 Loyalty Chain:")
    for current != nil {
        fmt.Printf("  → %s (%d points)\n", current.Name, current.Points)
        current = current.Next
    }
}

func main() {
    chain := &LoyaltyChain{}

    chain.AddCustomer("Alice", 50)
    chain.AddCustomer("Bob", 30)
    chain.AddCustomer("Carol", 80)

    chain.Display()
}

Advantages:

  • Dynamic size
  • Efficient insertions/deletions at the beginning: O(1)
  • No wasted space

Disadvantages:

  • No random access (must traverse): O(n)
  • Extra memory for pointers

The Name on the Cup - Hash Table

The barista writes your name on the cup for instant identification. Hash tables provide O(1) average lookup time.

graph TB subgraph "Hash Table" H["Hash Function
hash(name) % size"] end subgraph "Buckets" B0["0: Alice → Latte"] B1["1: (empty)"] B2["2: Bob → Espresso"] B3["3: Carol → Mocha"] B4["4: (empty)"] end H --> B0 H --> B1 H --> B2 H --> B3 H --> B4 style H fill:#FFD700 style B0 fill:#90EE90 style B2 fill:#90EE90 style B3 fill:#90EE90

Go Implementation:

package main

import "fmt"

type OrderMap struct {
    orders map[string]string
}

func NewOrderMap() *OrderMap {
    return &OrderMap{
        orders: make(map[string]string),
    }
}

func (m *OrderMap) PlaceOrder(name, drink string) {
    m.orders[name] = drink
    fmt.Printf("☕ Order placed: %s → %s\n", name, drink)
}

func (m *OrderMap) GetOrder(name string) (string, bool) {
    drink, exists := m.orders[name]
    return drink, exists
}

func (m *OrderMap) CompleteOrder(name string) {
    if drink, exists := m.orders[name]; exists {
        fmt.Printf("✅ Completed: %s's %s\n", name, drink)
        delete(m.orders, name)
    }
}

func main() {
    orders := NewOrderMap()

    // Take orders
    orders.PlaceOrder("Alice", "Latte")
    orders.PlaceOrder("Bob", "Espresso")
    orders.PlaceOrder("Carol", "Mocha")

    // Barista calls: "Order for Alice!"
    if drink, found := orders.GetOrder("Alice"); found {
        fmt.Printf("📢 %s is ready!\n", drink)
        orders.CompleteOrder("Alice")
    }
}

When Names Collide - Hash Collisions

What happens when two customers have the same name? We need collision resolution strategies.

graph TB subgraph "Chaining Method" B0["Bucket 2:
Bob → Latte"] B0 --> B01["Bobby → Espresso"] B01 --> B02["Robert → Mocha"] end subgraph "Open Addressing" O2["2: Bob → Latte"] O3["3: Bobby → Espresso
(moved from 2)"] O4["4: Robert → Mocha
(moved from 2)"] end style B0 fill:#FFB6C1 style B01 fill:#FFB6C1 style B02 fill:#FFB6C1 style O2 fill:#90EE90 style O3 fill:#FFA500 style O4 fill:#FFA500

Chaining Implementation:

package main

import "fmt"

type Order struct {
    Name  string
    Drink string
    Next  *Order
}

type HashTableWithChaining struct {
    buckets []*Order
    size    int
}

func NewHashTable(size int) *HashTableWithChaining {
    return &HashTableWithChaining{
        buckets: make([]*Order, size),
        size:    size,
    }
}

func (ht *HashTableWithChaining) hash(name string) int {
    hash := 0
    for _, ch := range name {
        hash += int(ch)
    }
    return hash % ht.size
}

func (ht *HashTableWithChaining) Insert(name, drink string) {
    index := ht.hash(name)
    newOrder := &Order{Name: name, Drink: drink}

    if ht.buckets[index] == nil {
        ht.buckets[index] = newOrder
        fmt.Printf("☕ Order: %s → %s (bucket %d)\n", name, drink, index)
    } else {
        // Collision! Add to chain
        fmt.Printf("⚠️  Collision at bucket %d! Adding %s to chain\n", index, name)
        current := ht.buckets[index]
        for current.Next != nil {
            current = current.Next
        }
        current.Next = newOrder
    }
}

func (ht *HashTableWithChaining) Get(name string) string {
    index := ht.hash(name)
    current := ht.buckets[index]

    for current != nil {
        if current.Name == name {
            return current.Drink
        }
        current = current.Next
    }
    return ""
}

func main() {
    ht := NewHashTable(5)

    // These might collide depending on hash function
    ht.Insert("Bob", "Latte")
    ht.Insert("Bobby", "Espresso")
    ht.Insert("Robert", "Mocha")

    fmt.Println("\n🔍 Finding Bob's order:", ht.Get("Bob"))
}

Part 2: Trees in the Shop

The Menu Categories - Binary Search Tree

The menu is organized hierarchically: Drinks → Hot/Cold → Coffee/Tea → Specific drinks. A BST keeps items sorted for fast lookup.

graph TB A[Hot Drinks
$5] B[Cold Brew
$4] C[Specialty
$7] D[Americano
$3] E[Latte
$5] F[Frappuccino
$6] G[Macchiato
$8] A --> B A --> C B --> D B --> E C --> F C --> G style A fill:#FFD700 style B fill:#90EE90 style C fill:#90EE90 style D fill:#E8F4F8 style E fill:#E8F4F8 style F fill:#E8F4F8 style G fill:#E8F4F8

Go Implementation:

package main

import "fmt"

type MenuItem struct {
    Name  string
    Price int
    Left  *MenuItem
    Right *MenuItem
}

type MenuBST struct {
    Root *MenuItem
}

func (m *MenuBST) Insert(name string, price int) {
    newItem := &MenuItem{Name: name, Price: price}

    if m.Root == nil {
        m.Root = newItem
        return
    }

    m.insertNode(m.Root, newItem)
}

func (m *MenuBST) insertNode(current, newItem *MenuItem) {
    if newItem.Price < current.Price {
        if current.Left == nil {
            current.Left = newItem
        } else {
            m.insertNode(current.Left, newItem)
        }
    } else {
        if current.Right == nil {
            current.Right = newItem
        } else {
            m.insertNode(current.Right, newItem)
        }
    }
}

func (m *MenuBST) FindInRange(min, max int) {
    fmt.Printf("\n☕ Items between $%d and $%d:\n", min, max)
    m.inRangeSearch(m.Root, min, max)
}

func (m *MenuBST) inRangeSearch(node *MenuItem, min, max int) {
    if node == nil {
        return
    }

    if node.Price > min {
        m.inRangeSearch(node.Left, min, max)
    }

    if node.Price >= min && node.Price <= max {
        fmt.Printf("  - %s: $%d\n", node.Name, node.Price)
    }

    if node.Price < max {
        m.inRangeSearch(node.Right, min, max)
    }
}

func main() {
    menu := &MenuBST{}

    menu.Insert("Latte", 5)
    menu.Insert("Americano", 3)
    menu.Insert("Macchiato", 8)
    menu.Insert("Cold Brew", 4)
    menu.Insert("Frappuccino", 6)

    menu.FindInRange(4, 6) // Find drinks $4-$6
}

Time Complexity:

  • Search: O(log n) average, O(n) worst case
  • Insert: O(log n) average
  • Delete: O(log n) average

The Balanced Menu - AVL Tree

To prevent the menu from becoming lopsided (all expensive items on one side), we use self-balancing trees.

graph TB subgraph "Unbalanced BST" U1[Item $3] --> U2[Item $4] U2 --> U3[Item $5] U3 --> U4[Item $6] U4 --> U5[Item $7] end subgraph "Balanced AVL Tree" A1[Item $5] A1 --> A2[Item $3] A1 --> A3[Item $6] A2 --> A4[Item $4] A3 --> A5[Item $7] end style U1 fill:#FFB6C1 style A1 fill:#90EE90 style A2 fill:#90EE90 style A3 fill:#90EE90

Key Concept: AVL trees maintain balance by rotating nodes when the height difference exceeds 1.

Go Implementation (simplified):

package main

import (
    "fmt"
    "math"
)

type AVLNode struct {
    Name   string
    Price  int
    Height int
    Left   *AVLNode
    Right  *AVLNode
}

func height(node *AVLNode) int {
    if node == nil {
        return 0
    }
    return node.Height
}

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

func getBalance(node *AVLNode) int {
    if node == nil {
        return 0
    }
    return height(node.Left) - height(node.Right)
}

func rightRotate(y *AVLNode) *AVLNode {
    x := y.Left
    T2 := x.Right

    x.Right = y
    y.Left = T2

    y.Height = max(height(y.Left), height(y.Right)) + 1
    x.Height = max(height(x.Left), height(x.Right)) + 1

    return x
}

func leftRotate(x *AVLNode) *AVLNode {
    y := x.Right
    T2 := y.Left

    y.Left = x
    x.Right = T2

    x.Height = max(height(x.Left), height(x.Right)) + 1
    y.Height = max(height(y.Left), height(y.Right)) + 1

    return y
}

func insert(node *AVLNode, name string, price int) *AVLNode {
    if node == nil {
        return &AVLNode{Name: name, Price: price, Height: 1}
    }

    if price < node.Price {
        node.Left = insert(node.Left, name, price)
    } else if price > node.Price {
        node.Right = insert(node.Right, name, price)
    } else {
        return node
    }

    node.Height = 1 + max(height(node.Left), height(node.Right))

    balance := getBalance(node)

    // Left-Left Case
    if balance > 1 && price < node.Left.Price {
        return rightRotate(node)
    }

    // Right-Right Case
    if balance < -1 && price > node.Right.Price {
        return leftRotate(node)
    }

    // Left-Right Case
    if balance > 1 && price > node.Left.Price {
        node.Left = leftRotate(node.Left)
        return rightRotate(node)
    }

    // Right-Left Case
    if balance < -1 && price < node.Right.Price {
        node.Right = rightRotate(node.Right)
        return leftRotate(node)
    }

    return node
}

func main() {
    var root *AVLNode

    items := []struct {
        name  string
        price int
    }{
        {"Espresso", 3},
        {"Latte", 5},
        {"Cappuccino", 4},
        {"Mocha", 6},
        {"Americano", 3},
    }

    for _, item := range items {
        root = insert(root, item.name, item.price)
        fmt.Printf("✅ Added %s ($%d) - Tree height: %d\n", item.name, item.price, height(root))
    }
}

The VIP Queue - Heap

Some customers are VIPs and should be served first, regardless of arrival time. A heap maintains priority order efficiently.

graph TB A["Priority: 10
(VIP Customer)"] B["Priority: 7
(Regular)"] C["Priority: 8
(Member)"] D["Priority: 3
(New)"] E["Priority: 5
(Regular)"] A --> B A --> C B --> D B --> E style A fill:#FFD700 style C fill:#90EE90 style B fill:#90EE90

Go Implementation (Max Heap):

package main

import (
    "container/heap"
    "fmt"
)

type Customer struct {
    Name     string
    Priority int
    Index    int
}

type PriorityQueue []*Customer

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].Priority > pq[j].Priority // Max heap
}

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].Index = i
    pq[j].Index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    customer := x.(*Customer)
    customer.Index = n
    *pq = append(*pq, customer)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    customer := old[n-1]
    customer.Index = -1
    *pq = old[0 : n-1]
    return customer
}

func main() {
    pq := make(PriorityQueue, 0)
    heap.Init(&pq)

    customers := []*Customer{
        {Name: "Alice (Regular)", Priority: 5},
        {Name: "Bob (VIP)", Priority: 10},
        {Name: "Carol (New)", Priority: 3},
        {Name: "Dave (Member)", Priority: 7},
    }

    // Add customers
    for _, customer := range customers {
        heap.Push(&pq, customer)
        fmt.Printf("➕ Added: %s (Priority: %d)\n", customer.Name, customer.Priority)
    }

    // Serve in priority order
    fmt.Println("\n🎯 Serving order:")
    for pq.Len() > 0 {
        customer := heap.Pop(&pq).(*Customer)
        fmt.Printf("  → %s (Priority: %d)\n", customer.Name, customer.Priority)
    }
}

Time Complexity:

  • Insert: O(log n)
  • Extract Max/Min: O(log n)
  • Peek: O(1)

The Autocomplete - Trie

When typing “lat” in the order system, we want to suggest: “latte”, “latté”, “latte macchiato”. A trie enables efficient prefix matching.

graph TB ROOT((root)) L[l] LA[a] LAT[t] LATT[t] LATTE[e ✓] LATTER[é ✓] ROOT --> L L --> LA LA --> LAT LAT --> LATT LATT --> LATTE LATT --> LATTER style ROOT fill:#FFD700 style LATTE fill:#90EE90 style LATTER fill:#90EE90

Go Implementation:

package main

import "fmt"

type TrieNode struct {
    children map[rune]*TrieNode
    isEnd    bool
    word     string
}

type Trie struct {
    root *TrieNode
}

func NewTrie() *Trie {
    return &Trie{
        root: &TrieNode{
            children: make(map[rune]*TrieNode),
        },
    }
}

func (t *Trie) Insert(word string) {
    node := t.root
    for _, ch := range word {
        if node.children[ch] == nil {
            node.children[ch] = &TrieNode{
                children: make(map[rune]*TrieNode),
            }
        }
        node = node.children[ch]
    }
    node.isEnd = true
    node.word = word
}

func (t *Trie) AutoComplete(prefix string) []string {
    node := t.root

    // Navigate to prefix
    for _, ch := range prefix {
        if node.children[ch] == nil {
            return []string{}
        }
        node = node.children[ch]
    }

    // Collect all words with this prefix
    results := []string{}
    t.collectWords(node, &results)
    return results
}

func (t *Trie) collectWords(node *TrieNode, results *[]string) {
    if node.isEnd {
        *results = append(*results, node.word)
    }

    for _, child := range node.children {
        t.collectWords(child, results)
    }
}

func main() {
    trie := NewTrie()

    drinks := []string{
        "latte",
        "latté",
        "latte macchiato",
        "espresso",
        "espresso macchiato",
        "cappuccino",
        "coffee",
    }

    for _, drink := range drinks {
        trie.Insert(drink)
    }

    // Autocomplete
    prefixes := []string{"lat", "esp", "cof"}
    for _, prefix := range prefixes {
        suggestions := trie.AutoComplete(prefix)
        fmt.Printf("🔍 '%s' → %v\n", prefix, suggestions)
    }
}

Use Cases:

  • Autocomplete search
  • Spell checkers
  • IP routing tables

The Time-Based Sales - Segment Tree

“What were our total sales between 9am and 11am?” A segment tree answers range queries efficiently.

graph TB A["[0-7]
Total: $500"] B["[0-3]
$280"] C["[4-7]
$220"] D["[0-1]
$150"] E["[2-3]
$130"] F["[4-5]
$100"] G["[6-7]
$120"] A --> B A --> C B --> D B --> E C --> F C --> G style A fill:#FFD700 style B fill:#90EE90 style C fill:#90EE90

Go Implementation:

package main

import "fmt"

type SegmentTree struct {
    tree []int
    n    int
}

func NewSegmentTree(sales []int) *SegmentTree {
    n := len(sales)
    st := &SegmentTree{
        tree: make([]int, 4*n),
        n:    n,
    }
    st.build(sales, 0, 0, n-1)
    return st
}

func (st *SegmentTree) build(sales []int, node, start, end int) {
    if start == end {
        st.tree[node] = sales[start]
        return
    }

    mid := (start + end) / 2
    leftChild := 2*node + 1
    rightChild := 2*node + 2

    st.build(sales, leftChild, start, mid)
    st.build(sales, rightChild, mid+1, end)

    st.tree[node] = st.tree[leftChild] + st.tree[rightChild]
}

func (st *SegmentTree) Query(left, right int) int {
    return st.query(0, 0, st.n-1, left, right)
}

func (st *SegmentTree) query(node, start, end, left, right int) int {
    if right < start || end < left {
        return 0
    }

    if left <= start && end <= right {
        return st.tree[node]
    }

    mid := (start + end) / 2
    leftChild := 2*node + 1
    rightChild := 2*node + 2

    leftSum := st.query(leftChild, start, mid, left, right)
    rightSum := st.query(rightChild, mid+1, end, left, right)

    return leftSum + rightSum
}

func main() {
    // Hourly sales from 8am to 4pm
    hourlySales := []int{50, 80, 100, 50, 60, 40, 70, 50}
    hours := []string{"8am", "9am", "10am", "11am", "12pm", "1pm", "2pm", "3pm"}

    st := NewSegmentTree(hourlySales)

    // Query: Total sales between 9am (index 1) and 11am (index 3)
    total := st.Query(1, 3)
    fmt.Printf("💰 Sales from %s to %s: $%d\n", hours[1], hours[3], total)

    // Query: Morning sales (8am-12pm)
    morning := st.Query(0, 4)
    fmt.Printf("🌅 Morning sales (8am-12pm): $%d\n", morning)
}

Time Complexity:

  • Build: O(n)
  • Query: O(log n)
  • Update: O(log n)

Part 3: Graphs and Connections

The Ingredient Dependencies - Directed Graph

Making a latte requires: beans → ground coffee → espresso shot → steamed milk → latte. These dependencies form a directed graph.

graph LR A[Coffee Beans] --> B[Ground Coffee] B --> C[Espresso Shot] D[Milk] --> E[Steamed Milk] C --> F[Latte] E --> F style A fill:#8B4513 style D fill:#F0F8FF style F fill:#90EE90

Go Implementation:

package main

import "fmt"

type Graph struct {
    adjList map[string][]string
}

func NewGraph() *Graph {
    return &Graph{
        adjList: make(map[string][]string),
    }
}

func (g *Graph) AddEdge(from, to string) {
    g.adjList[from] = append(g.adjList[from], to)
}

func (g *Graph) GetDependencies(ingredient string) {
    visited := make(map[string]bool)
    fmt.Printf("📋 Dependencies for %s:\n", ingredient)
    g.dfs(ingredient, visited, 0)
}

func (g *Graph) dfs(node string, visited map[string]bool, level int) {
    if visited[node] {
        return
    }

    visited[node] = true
    indent := ""
    for i := 0; i < level; i++ {
        indent += "  "
    }
    fmt.Printf("%s→ %s\n", indent, node)

    for _, neighbor := range g.adjList[node] {
        g.dfs(neighbor, visited, level+1)
    }
}

func main() {
    g := NewGraph()

    // Build latte dependency graph
    g.AddEdge("Latte", "Espresso Shot")
    g.AddEdge("Latte", "Steamed Milk")
    g.AddEdge("Espresso Shot", "Ground Coffee")
    g.AddEdge("Ground Coffee", "Coffee Beans")
    g.AddEdge("Steamed Milk", "Milk")

    g.GetDependencies("Latte")
}

The Delivery Routes - Weighted Graph

Finding the shortest delivery route between coffee shops uses weighted graphs and Dijkstra’s algorithm.

graph LR A[Shop A] -->|2km| B[Shop B] A -->|4km| C[Shop C] B -->|1km| C B -->|7km| D[Shop D] C -->|3km| D style A fill:#90EE90 style D fill:#FFB6C1

Go Implementation (Dijkstra’s Algorithm):

package main

import (
    "container/heap"
    "fmt"
    "math"
)

type Edge struct {
    to     string
    weight int
}

type Item struct {
    node     string
    distance int
    index    int
}

type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].distance < pq[j].distance
}
func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index = i
    pq[j].index = j
}
func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    item.index = -1
    *pq = old[0 : n-1]
    return item
}

type WeightedGraph struct {
    adjList map[string][]Edge
}

func NewWeightedGraph() *WeightedGraph {
    return &WeightedGraph{
        adjList: make(map[string][]Edge),
    }
}

func (g *WeightedGraph) AddEdge(from, to string, weight int) {
    g.adjList[from] = append(g.adjList[from], Edge{to: to, weight: weight})
    g.adjList[to] = append(g.adjList[to], Edge{to: from, weight: weight})
}

func (g *WeightedGraph) Dijkstra(start, end string) (int, []string) {
    distances := make(map[string]int)
    previous := make(map[string]string)

    for node := range g.adjList {
        distances[node] = math.MaxInt32
    }
    distances[start] = 0

    pq := make(PriorityQueue, 0)
    heap.Init(&pq)
    heap.Push(&pq, &Item{node: start, distance: 0})

    for pq.Len() > 0 {
        current := heap.Pop(&pq).(*Item)

        if current.node == end {
            break
        }

        if current.distance > distances[current.node] {
            continue
        }

        for _, edge := range g.adjList[current.node] {
            distance := distances[current.node] + edge.weight

            if distance < distances[edge.to] {
                distances[edge.to] = distance
                previous[edge.to] = current.node
                heap.Push(&pq, &Item{node: edge.to, distance: distance})
            }
        }
    }

    // Reconstruct path
    path := []string{}
    for at := end; at != ""; at = previous[at] {
        path = append([]string{at}, path...)
        if at == start {
            break
        }
    }

    return distances[end], path
}

func main() {
    g := NewWeightedGraph()

    g.AddEdge("Shop A", "Shop B", 2)
    g.AddEdge("Shop A", "Shop C", 4)
    g.AddEdge("Shop B", "Shop C", 1)
    g.AddEdge("Shop B", "Shop D", 7)
    g.AddEdge("Shop C", "Shop D", 3)

    distance, path := g.Dijkstra("Shop A", "Shop D")
    fmt.Printf("🚗 Shortest route: %v\n", path)
    fmt.Printf("📏 Total distance: %d km\n", distance)
}

The Table Groups - Union-Find

“Are these customers sitting together?” Union-Find (Disjoint Set Union) tracks connected components.

graph LR subgraph "Group 1" A[Alice] --- B[Bob] B --- C[Carol] end subgraph "Group 2" D[Dave] --- E[Eve] end subgraph "Alone" F[Frank] end style A fill:#90EE90 style B fill:#90EE90 style C fill:#90EE90 style D fill:#FFB6C1 style E fill:#FFB6C1 style F fill:#FFA500

Go Implementation:

package main

import "fmt"

type UnionFind struct {
    parent map[string]string
    rank   map[string]int
}

func NewUnionFind() *UnionFind {
    return &UnionFind{
        parent: make(map[string]string),
        rank:   make(map[string]int),
    }
}

func (uf *UnionFind) MakeSet(customer string) {
    if _, exists := uf.parent[customer]; !exists {
        uf.parent[customer] = customer
        uf.rank[customer] = 0
    }
}

func (uf *UnionFind) Find(customer string) string {
    if uf.parent[customer] != customer {
        uf.parent[customer] = uf.Find(uf.parent[customer]) // Path compression
    }
    return uf.parent[customer]
}

func (uf *UnionFind) Union(customer1, customer2 string) {
    root1 := uf.Find(customer1)
    root2 := uf.Find(customer2)

    if root1 == root2 {
        return
    }

    // Union by rank
    if uf.rank[root1] < uf.rank[root2] {
        uf.parent[root1] = root2
    } else if uf.rank[root1] > uf.rank[root2] {
        uf.parent[root2] = root1
    } else {
        uf.parent[root2] = root1
        uf.rank[root1]++
    }

    fmt.Printf("🔗 Joined %s and %s at same table\n", customer1, customer2)
}

func (uf *UnionFind) AreTogether(customer1, customer2 string) bool {
    return uf.Find(customer1) == uf.Find(customer2)
}

func main() {
    uf := NewUnionFind()

    customers := []string{"Alice", "Bob", "Carol", "Dave", "Eve", "Frank"}
    for _, customer := range customers {
        uf.MakeSet(customer)
    }

    // Group customers
    uf.Union("Alice", "Bob")
    uf.Union("Bob", "Carol")
    uf.Union("Dave", "Eve")

    // Check if customers are together
    fmt.Println("\n🔍 Checking groups:")
    fmt.Printf("Alice and Carol together? %v\n", uf.AreTogether("Alice", "Carol"))
    fmt.Printf("Alice and Dave together? %v\n", uf.AreTogether("Alice", "Dave"))
    fmt.Printf("Dave and Eve together? %v\n", uf.AreTogether("Dave", "Eve"))
}

Time Complexity: Nearly O(1) with path compression and union by rank


The Recipe Steps - Topological Sort

Making a cappuccino has ordered steps. Topological sort ensures tasks happen in the correct sequence.

graph LR A[Grind Beans] --> B[Pull Espresso] C[Heat Milk] --> D[Froth Milk] B --> E[Combine] D --> E E --> F[Add Foam] F --> G[Serve] style A fill:#90EE90 style C fill:#90EE90 style G fill:#FFB6C1

Go Implementation:

package main

import "fmt"

type Task struct {
    name         string
    dependencies []string
}

func TopologicalSort(tasks []Task) []string {
    // Build adjacency list and in-degree map
    adjList := make(map[string][]string)
    inDegree := make(map[string]int)

    for _, task := range tasks {
        if _, exists := inDegree[task.name]; !exists {
            inDegree[task.name] = 0
        }

        for _, dep := range task.dependencies {
            adjList[dep] = append(adjList[dep], task.name)
            inDegree[task.name]++
        }
    }

    // Find tasks with no dependencies
    queue := []string{}
    for task, degree := range inDegree {
        if degree == 0 {
            queue = append(queue, task)
        }
    }

    result := []string{}

    for len(queue) > 0 {
        current := queue[0]
        queue = queue[1:]
        result = append(result, current)

        for _, neighbor := range adjList[current] {
            inDegree[neighbor]--
            if inDegree[neighbor] == 0 {
                queue = append(queue, neighbor)
            }
        }
    }

    return result
}

func main() {
    tasks := []Task{
        {name: "Grind Beans", dependencies: []string{}},
        {name: "Heat Milk", dependencies: []string{}},
        {name: "Pull Espresso", dependencies: []string{"Grind Beans"}},
        {name: "Froth Milk", dependencies: []string{"Heat Milk"}},
        {name: "Combine", dependencies: []string{"Pull Espresso", "Froth Milk"}},
        {name: "Add Foam", dependencies: []string{"Combine"}},
        {name: "Serve", dependencies: []string{"Add Foam"}},
    }

    order := TopologicalSort(tasks)
    fmt.Println("📋 Recipe steps in order:")
    for i, step := range order {
        fmt.Printf("  %d. %s\n", i+1, step)
    }
}

Part 4: Probabilistic and Cache

The Regular Customer Check - Bloom Filter

“Has this customer been here before?” A Bloom filter gives fast, space-efficient probabilistic answers.

graph TB subgraph "Bloom Filter (Bit Array)" B["[0,1,0,1,1,0,1,0,1,0]"] end H1[Hash1] --> B H2[Hash2] --> B H3[Hash3] --> B INPUT["Customer: Alice"] --> H1 INPUT --> H2 INPUT --> H3 style INPUT fill:#90EE90 style B fill:#FFD700

Properties:

  • False positives possible: “Probably yes”
  • No false negatives: “Definitely no”
  • Space efficient: Much smaller than a set

Go Implementation:

package main

import (
    "fmt"
    "hash/fnv"
)

type BloomFilter struct {
    bits []bool
    size int
    k    int // Number of hash functions
}

func NewBloomFilter(size, k int) *BloomFilter {
    return &BloomFilter{
        bits: make([]bool, size),
        size: size,
        k:    k,
    }
}

func (bf *BloomFilter) hash(s string, seed int) int {
    h := fnv.New32a()
    h.Write([]byte(fmt.Sprintf("%s%d", s, seed)))
    return int(h.Sum32()) % bf.size
}

func (bf *BloomFilter) Add(customer string) {
    for i := 0; i < bf.k; i++ {
        index := bf.hash(customer, i)
        bf.bits[index] = true
    }
    fmt.Printf("✅ Added %s to bloom filter\n", customer)
}

func (bf *BloomFilter) MightContain(customer string) bool {
    for i := 0; i < bf.k; i++ {
        index := bf.hash(customer, i)
        if !bf.bits[index] {
            return false
        }
    }
    return true
}

func main() {
    bf := NewBloomFilter(100, 3)

    // Regular customers
    regulars := []string{"Alice", "Bob", "Carol"}
    for _, customer := range regulars {
        bf.Add(customer)
    }

    // Check customers
    checks := []string{"Alice", "Dave", "Bob", "Eve"}
    fmt.Println("\n🔍 Checking customers:")
    for _, customer := range checks {
        if bf.MightContain(customer) {
            fmt.Printf("  %s: Probably a regular ✓\n", customer)
        } else {
            fmt.Printf("  %s: Definitely new ✗\n", customer)
        }
    }
}

Use Cases:

  • Web crawlers (avoid revisiting URLs)
  • Database query optimization
  • Spell checkers

The Recently Made Drinks - LRU Cache

Keep recently made drinks warm for quick serving. LRU Cache evicts least recently used items.

graph LR subgraph "LRU Cache (Capacity: 3)" A["Most Recent:
Latte"] --> B["Cappuccino"] --> C["Least Recent:
Espresso"] end NEW["New: Mocha"] -->|Push| A C -->|Evict| OUT[Removed] style A fill:#90EE90 style C fill:#FFB6C1 style NEW fill:#FFD700

Go Implementation:

package main

import (
    "container/list"
    "fmt"
)

type LRUCache struct {
    capacity int
    cache    map[string]*list.Element
    lru      *list.List
}

type cacheEntry struct {
    key   string
    value string
}

func NewLRUCache(capacity int) *LRUCache {
    return &LRUCache{
        capacity: capacity,
        cache:    make(map[string]*list.Element),
        lru:      list.New(),
    }
}

func (c *LRUCache) Get(key string) (string, bool) {
    if elem, found := c.cache[key]; found {
        c.lru.MoveToFront(elem)
        return elem.Value.(*cacheEntry).value, true
    }
    return "", false
}

func (c *LRUCache) Put(key, value string) {
    if elem, found := c.cache[key]; found {
        c.lru.MoveToFront(elem)
        elem.Value.(*cacheEntry).value = value
        fmt.Printf("🔄 Updated cache: %s = %s\n", key, value)
        return
    }

    if c.lru.Len() >= c.capacity {
        oldest := c.lru.Back()
        if oldest != nil {
            c.lru.Remove(oldest)
            delete(c.cache, oldest.Value.(*cacheEntry).key)
            fmt.Printf("🗑️  Evicted: %s\n", oldest.Value.(*cacheEntry).key)
        }
    }

    entry := &cacheEntry{key: key, value: value}
    elem := c.lru.PushFront(entry)
    c.cache[key] = elem
    fmt.Printf("➕ Cached: %s = %s\n", key, value)
}

func main() {
    cache := NewLRUCache(3)

    cache.Put("drink1", "Latte")
    cache.Put("drink2", "Cappuccino")
    cache.Put("drink3", "Espresso")

    fmt.Println()

    // Access drink1 (moves to front)
    if drink, found := cache.Get("drink1"); found {
        fmt.Printf("🔍 Found: %s\n", drink)
    }

    fmt.Println()

    // Add new drink (should evict drink2)
    cache.Put("drink4", "Mocha")

    fmt.Println("\n📋 Checking what's in cache:")
    for _, key := range []string{"drink1", "drink2", "drink3", "drink4"} {
        if drink, found := cache.Get(key); found {
            fmt.Printf("  ✓ %s: %s\n", key, drink)
        } else {
            fmt.Printf("  ✗ %s: not in cache\n", key)
        }
    }
}

Time Complexity: O(1) for both get and put operations


The Fast Menu Search - Skip List

A skip list is a probabilistic alternative to balanced trees, providing O(log n) search with simpler implementation.

graph LR subgraph "Level 2 (Express)" L2A[Header] --> L2B[$3] --> L2C[$7] end subgraph "Level 1" L1A[Header] --> L1B[$3] --> L1C[$5] --> L1D[$7] end subgraph "Level 0 (Base)" L0A[Header] --> L0B[$3] --> L0C[$4] --> L0D[$5] --> L0E[$6] --> L0F[$7] end style L2B fill:#FFD700 style L1C fill:#90EE90 style L0D fill:#90EE90

Go Implementation (simplified):

package main

import (
    "fmt"
    "math/rand"
)

const MaxLevel = 4
const Probability = 0.5

type SkipNode struct {
    value   int
    forward []*SkipNode
}

type SkipList struct {
    header *SkipNode
    level  int
}

func NewSkipList() *SkipList {
    return &SkipList{
        header: &SkipNode{forward: make([]*SkipNode, MaxLevel)},
        level:  0,
    }
}

func (sl *SkipList) randomLevel() int {
    level := 0
    for rand.Float64() < Probability && level < MaxLevel-1 {
        level++
    }
    return level
}

func (sl *SkipList) Insert(value int) {
    update := make([]*SkipNode, MaxLevel)
    current := sl.header

    for i := sl.level; i >= 0; i-- {
        for current.forward[i] != nil && current.forward[i].value < value {
            current = current.forward[i]
        }
        update[i] = current
    }

    level := sl.randomLevel()
    if level > sl.level {
        for i := sl.level + 1; i <= level; i++ {
            update[i] = sl.header
        }
        sl.level = level
    }

    newNode := &SkipNode{
        value:   value,
        forward: make([]*SkipNode, level+1),
    }

    for i := 0; i <= level; i++ {
        newNode.forward[i] = update[i].forward[i]
        update[i].forward[i] = newNode
    }

    fmt.Printf("➕ Inserted $%d at level %d\n", value, level)
}

func (sl *SkipList) Search(value int) bool {
    current := sl.header

    for i := sl.level; i >= 0; i-- {
        for current.forward[i] != nil && current.forward[i].value < value {
            current = current.forward[i]
        }
    }

    current = current.forward[0]
    return current != nil && current.value == value
}

func main() {
    sl := NewSkipList()

    prices := []int{3, 5, 7, 4, 6}
    for _, price := range prices {
        sl.Insert(price)
    }

    fmt.Println("\n🔍 Searching:")
    for _, price := range []int{5, 8} {
        if sl.Search(price) {
            fmt.Printf("  $%d: Found ✓\n", price)
        } else {
            fmt.Printf("  $%d: Not found ✗\n", price)
        }
    }
}

How Many Unique Customers? - HyperLogLog

Counting unique customers exactly requires storing all IDs. HyperLogLog provides approximate counts using minimal memory.

Key Idea: Count leading zeros in hash values to estimate cardinality.

Go Implementation (simplified concept):

package main

import (
    "fmt"
    "hash/fnv"
    "math"
    "math/bits"
)

type HyperLogLog struct {
    registers []uint8
    m         int
}

func NewHyperLogLog(precision int) *HyperLogLog {
    m := 1 << precision
    return &HyperLogLog{
        registers: make([]uint8, m),
        m:         m,
    }
}

func (hll *HyperLogLog) hash(s string) uint64 {
    h := fnv.New64a()
    h.Write([]byte(s))
    return h.Sum64()
}

func (hll *HyperLogLog) Add(customer string) {
    hash := hll.hash(customer)
    index := hash & uint64(hll.m-1)

    // Count leading zeros in remaining bits
    w := hash >> uint(math.Log2(float64(hll.m)))
    leadingZeros := uint8(bits.LeadingZeros64(w)) + 1

    if leadingZeros > hll.registers[index] {
        hll.registers[index] = leadingZeros
    }
}

func (hll *HyperLogLog) Count() int {
    sum := 0.0
    for _, val := range hll.registers {
        sum += 1.0 / math.Pow(2, float64(val))
    }

    alpha := 0.7213 / (1 + 1.079/float64(hll.m))
    estimate := alpha * float64(hll.m*hll.m) / sum

    return int(estimate)
}

func main() {
    hll := NewHyperLogLog(10)

    // Simulate customer visits
    customers := []string{
        "Alice", "Bob", "Carol", "Alice", "Dave",
        "Bob", "Eve", "Frank", "Alice", "Grace",
    }

    for _, customer := range customers {
        hll.Add(customer)
    }

    actualUnique := 7 // Alice, Bob, Carol, Dave, Eve, Frank, Grace
    estimated := hll.Count()

    fmt.Printf("👥 Actual unique customers: %d\n", actualUnique)
    fmt.Printf("📊 HyperLogLog estimate: %d\n", estimated)
    fmt.Printf("📉 Error: %.1f%%\n", math.Abs(float64(estimated-actualUnique))/float64(actualUnique)*100)
}

Memory Usage: ~1.5KB for counting billions of unique items (with ~2% error)


Part 5: Specialized Structures

The Order Modifications - Persistent Data Structures

“Actually, can I change my latte to oat milk?” Persistent data structures preserve history without copying everything.

graph TB V1["Version 1:
Latte (Whole Milk)"] V2["Version 2:
Latte (Oat Milk)"] SHARED["Shared:
Espresso Shot
Size: Grande"] V1 --> SHARED V2 --> SHARED style V1 fill:#FFB6C1 style V2 fill:#90EE90 style SHARED fill:#FFD700

Concept: Instead of modifying, create new versions that share structure.

Go Implementation (simplified):

package main

import "fmt"

type Order struct {
    drink       string
    milk        string
    size        string
    previousVer *Order
    version     int
}

type OrderHistory struct {
    versions []*Order
}

func NewOrderHistory(drink, milk, size string) *OrderHistory {
    initial := &Order{
        drink:   drink,
        milk:    milk,
        size:    size,
        version: 1,
    }
    return &OrderHistory{
        versions: []*Order{initial},
    }
}

func (oh *OrderHistory) Modify(field, newValue string) *Order {
    current := oh.versions[len(oh.versions)-1]
    newOrder := &Order{
        drink:       current.drink,
        milk:        current.milk,
        size:        current.size,
        previousVer: current,
        version:     current.version + 1,
    }

    switch field {
    case "milk":
        newOrder.milk = newValue
    case "size":
        newOrder.size = newValue
    }

    oh.versions = append(oh.versions, newOrder)
    fmt.Printf("🔄 Modified order (v%d → v%d): Changed %s to %s\n",
        current.version, newOrder.version, field, newValue)

    return newOrder
}

func (oh *OrderHistory) GetVersion(version int) *Order {
    if version > 0 && version <= len(oh.versions) {
        return oh.versions[version-1]
    }
    return nil
}

func (oh *OrderHistory) PrintHistory() {
    fmt.Println("\n📜 Order History:")
    for _, order := range oh.versions {
        fmt.Printf("  v%d: %s %s with %s\n",
            order.version, order.size, order.drink, order.milk)
    }
}

func main() {
    order := NewOrderHistory("Latte", "Whole Milk", "Grande")

    order.Modify("milk", "Oat Milk")
    order.Modify("size", "Venti")

    order.PrintHistory()

    // Access old version
    fmt.Println("\n🔙 Going back to version 1:")
    v1 := order.GetVersion(1)
    fmt.Printf("  %s %s with %s\n", v1.size, v1.drink, v1.milk)
}

The Time Window Orders - Sliding Window

“How many orders in the last 5 minutes?” A sliding window tracks recent events efficiently.

graph LR T1[9:00] --- T2[9:01] --- T3[9:02] --- T4[9:03] --- T5[9:04] --- T6[9:05] subgraph "Window [9:01-9:05]" T2 T3 T4 T5 end style T1 fill:#FFB6C1 style T2 fill:#90EE90 style T5 fill:#90EE90 style T6 fill:#FFA500

Go Implementation:

package main

import (
    "fmt"
    "time"
)

type Order struct {
    id        int
    timestamp time.Time
}

type SlidingWindow struct {
    orders     []Order
    windowSize time.Duration
}

func NewSlidingWindow(windowSize time.Duration) *SlidingWindow {
    return &SlidingWindow{
        orders:     []Order{},
        windowSize: windowSize,
    }
}

func (sw *SlidingWindow) AddOrder(id int) {
    order := Order{
        id:        id,
        timestamp: time.Now(),
    }
    sw.orders = append(sw.orders, order)
    sw.cleanup()
    fmt.Printf("➕ Order #%d added (Window size: %d)\n", id, len(sw.orders))
}

func (sw *SlidingWindow) cleanup() {
    cutoff := time.Now().Add(-sw.windowSize)
    validStart := 0

    for i, order := range sw.orders {
        if order.timestamp.After(cutoff) {
            validStart = i
            break
        }
    }

    sw.orders = sw.orders[validStart:]
}

func (sw *SlidingWindow) Count() int {
    sw.cleanup()
    return len(sw.orders)
}

func (sw *SlidingWindow) GetOrders() []int {
    sw.cleanup()
    ids := make([]int, len(sw.orders))
    for i, order := range sw.orders {
        ids[i] = order.id
    }
    return ids
}

func main() {
    window := NewSlidingWindow(3 * time.Second)

    window.AddOrder(1)
    window.AddOrder(2)

    time.Sleep(2 * time.Second)
    window.AddOrder(3)

    fmt.Printf("\n📊 Orders in window: %v\n", window.GetOrders())

    time.Sleep(2 * time.Second)
    fmt.Printf("📊 Orders in window after 4s: %v\n", window.GetOrders())
}

Use Cases:

  • Rate limiting
  • Real-time analytics
  • Moving averages

The Nearby Shops - KD-Tree

“Find all coffee shops within 5km.” A KD-Tree efficiently searches multi-dimensional space.

graph TB A["Split on X
(x=5)"] B["Split on Y
(y=3)"] C["Split on Y
(y=7)"] D["Shop A
(2,2)"] E["Shop B
(4,5)"] F["Shop C
(7,4)"] G["Shop D
(8,8)"] A --> B A --> C B --> D B --> E C --> F C --> G style A fill:#FFD700 style B fill:#90EE90 style C fill:#90EE90

Go Implementation (2D):

package main

import (
    "fmt"
    "math"
)

type Point struct {
    x, y float64
    name string
}

type KDNode struct {
    point Point
    left  *KDNode
    right *KDNode
}

func buildKDTree(points []Point, depth int) *KDNode {
    if len(points) == 0 {
        return nil
    }

    axis := depth % 2

    // Sort points by current axis
    if axis == 0 {
        // Sort by x
        for i := 0; i < len(points)-1; i++ {
            for j := i + 1; j < len(points); j++ {
                if points[i].x > points[j].x {
                    points[i], points[j] = points[j], points[i]
                }
            }
        }
    } else {
        // Sort by y
        for i := 0; i < len(points)-1; i++ {
            for j := i + 1; j < len(points); j++ {
                if points[i].y > points[j].y {
                    points[i], points[j] = points[j], points[i]
                }
            }
        }
    }

    median := len(points) / 2

    return &KDNode{
        point: points[median],
        left:  buildKDTree(points[:median], depth+1),
        right: buildKDTree(points[median+1:], depth+1),
    }
}

func distance(p1, p2 Point) float64 {
    dx := p1.x - p2.x
    dy := p1.y - p2.y
    return math.Sqrt(dx*dx + dy*dy)
}

func findWithinRadius(node *KDNode, target Point, radius float64, depth int, results *[]Point) {
    if node == nil {
        return
    }

    dist := distance(node.point, target)
    if dist <= radius {
        *results = append(*results, node.point)
    }

    axis := depth % 2
    var axisValue, targetValue float64

    if axis == 0 {
        axisValue = node.point.x
        targetValue = target.x
    } else {
        axisValue = node.point.y
        targetValue = target.y
    }

    if targetValue-radius <= axisValue {
        findWithinRadius(node.left, target, radius, depth+1, results)
    }
    if targetValue+radius >= axisValue {
        findWithinRadius(node.right, target, radius, depth+1, results)
    }
}

func main() {
    shops := []Point{
        {x: 2, y: 3, name: "Shop A"},
        {x: 5, y: 4, name: "Shop B"},
        {x: 9, y: 6, name: "Shop C"},
        {x: 4, y: 7, name: "Shop D"},
        {x: 8, y: 1, name: "Shop E"},
    }

    tree := buildKDTree(shops, 0)

    myLocation := Point{x: 5, y: 5, name: "My Location"}
    radius := 3.0

    results := []Point{}
    findWithinRadius(tree, myLocation, radius, 0, &results)

    fmt.Printf("🗺️  Shops within %.1fkm of %s:\n", radius, myLocation.name)
    for _, shop := range results {
        dist := distance(myLocation, shop)
        fmt.Printf("  - %s (%.2fkm away)\n", shop.name, dist)
    }
}

The Shift Schedule - Interval Tree

“Who’s working at 3pm?” An interval tree efficiently queries overlapping time ranges.

graph TB A["[9am-5pm]
Max: 5pm"] B["[9am-1pm]
Alice"] C["[2pm-6pm]
Bob"] D["[9am-11am]
Carol"] E["[4pm-8pm]
Dave"] A --> B A --> C B --> D C --> E style A fill:#FFD700 style B fill:#90EE90 style C fill:#90EE90

Go Implementation (simplified):

package main

import "fmt"

type Shift struct {
    employee string
    start    int
    end      int
}

type IntervalNode struct {
    shift Shift
    max   int
    left  *IntervalNode
    right *IntervalNode
}

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

func insert(node *IntervalNode, shift Shift) *IntervalNode {
    if node == nil {
        return &IntervalNode{
            shift: shift,
            max:   shift.end,
        }
    }

    if shift.start < node.shift.start {
        node.left = insert(node.left, shift)
    } else {
        node.right = insert(node.right, shift)
    }

    node.max = max(node.max, shift.end)
    if node.left != nil {
        node.max = max(node.max, node.left.max)
    }
    if node.right != nil {
        node.max = max(node.max, node.right.max)
    }

    return node
}

func findOverlapping(node *IntervalNode, time int, results *[]string) {
    if node == nil {
        return
    }

    if node.shift.start <= time && time <= node.shift.end {
        *results = append(*results, node.shift.employee)
    }

    if node.left != nil && node.left.max >= time {
        findOverlapping(node.left, time, results)
    }

    findOverlapping(node.right, time, results)
}

func main() {
    var root *IntervalNode

    shifts := []Shift{
        {employee: "Alice", start: 9, end: 13},   // 9am-1pm
        {employee: "Bob", start: 14, end: 18},    // 2pm-6pm
        {employee: "Carol", start: 9, end: 11},   // 9am-11am
        {employee: "Dave", start: 16, end: 20},   // 4pm-8pm
        {employee: "Eve", start: 12, end: 16},    // 12pm-4pm
    }

    for _, shift := range shifts {
        root = insert(root, shift)
        fmt.Printf("➕ Scheduled: %s (%d:00-%d:00)\n",
            shift.employee, shift.start, shift.end)
    }

    checkTimes := []int{10, 15, 19}
    for _, time := range checkTimes {
        results := []string{}
        findOverlapping(root, time, &results)

        fmt.Printf("\n🕐 At %d:00, working: %v\n", time, results)
    }
}

Conclusion

From the simple order queue to complex interval trees, data structures are everywhere in a coffee shop. Understanding these concepts through real-world metaphors makes them more intuitive and memorable.

Key Takeaways

Basic Structures:

  • Queue: First in, first out (order line)
  • Stack: Last in, first out (cup stack)
  • Hash Table: O(1) lookup (name on cup)

Trees:

  • BST: Organized hierarchy (menu categories)
  • Heap: Priority-based access (VIP queue)
  • Trie: Prefix matching (autocomplete)

Graphs:

  • Directed Graph: Dependencies (ingredients)
  • Weighted Graph: Shortest paths (delivery routes)
  • Union-Find: Connected components (table groups)

Probabilistic:

  • Bloom Filter: Space-efficient membership test
  • LRU Cache: Recently used items
  • HyperLogLog: Cardinality estimation

Specialized:

  • Persistent DS: Version history
  • Sliding Window: Time-based queries
  • KD-Tree: Spatial search
  • Interval Tree: Range queries

Choosing the Right Structure

Requirement Structure Time Complexity
FIFO ordering Queue O(1)
LIFO ordering Stack O(1)
Fast lookup by key Hash Table O(1) avg
Sorted data + range queries BST/AVL O(log n)
Always get min/max Heap O(log n)
Prefix matching Trie O(m)
Shortest path Dijkstra O(E log V)
Membership test (space-efficient) Bloom Filter O(k)
Recent items LRU Cache O(1)
Spatial queries KD-Tree O(log n) avg

Next Steps

  1. Implement these structures from scratch to understand internals
  2. Solve problems on LeetCode/HackerRank using these concepts
  3. Profile your code to see which structure performs best
  4. Read source code of production implementations (Go stdlib, Redis, etc.)

Remember: The best data structure is the one that fits your specific use case. Start simple, measure, then optimize.


Happy coding, and may your coffee be strong and your data structures efficient!