package main
import (
stdheap "container/heap"
"flag"
"fmt"
"sort"
"time"
"algo/metrics"
)
// stdItems is the adapter container/heap requires. Note the shape of Push and
// Pop: they take and return `any`, so every element crosses an interface
// boundary on the way in and out.
type stdItems []Item
func (s stdItems) Len() int { return len(s) }
func (s stdItems) Less(i, j int) bool { return s[i].Rating < s[j].Rating }
func (s stdItems) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s *stdItems) Push(x any) { *s = append(*s, x.(Item)) }
func (s *stdItems) Pop() any { old := *s; n := len(old); it := old[n-1]; *s = old[:n-1]; return it }
// topKStd is TopKHeap written against container/heap. Same algorithm, same
// comparisons — a different way of reaching the elements.
func topKStd(items []Item, k int) []Item {
if k <= 0 {
return nil
}
h := &stdItems{}
stdheap.Init(h)
for _, it := range items {
if h.Len() < k {
stdheap.Push(h, it)
continue
}
if it.Rating <= (*h)[0].Rating {
continue
}
stdheap.Pop(h)
stdheap.Push(h, it)
}
out := make([]Item, 0, h.Len())
for h.Len() > 0 {
out = append(out, stdheap.Pop(h).(Item))
}
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return out
}
// topKSort is the obvious answer: sort everything, take the first k.
func topKSort(items []Item, k int, c *metrics.Counter) []Item {
cp := append([]Item(nil), items...)
sort.Slice(cp, func(i, j int) bool {
c.Hit()
return cp[i].Rating > cp[j].Rating
})
if k > len(cp) {
k = len(cp)
}
return cp[:k]
}
// cmdTop is the partial-order argument: you do not have to sort to find the best
// ten.
func cmdTop(args []string) error {
fs := flag.NewFlagSet("top", flag.ContinueOnError)
in := fs.String("in", "library.jsonl", "library file")
k := fs.Int("k", 10, "how many to take")
show := fs.Bool("show", false, "print the k items")
if err := fs.Parse(args); err != nil {
return err
}
items, err := load(*in)
if err != nil {
return err
}
n := len(items)
var hc metrics.Counter
t0 := time.Now()
byHeap := TopKHeap(items, *k, &hc)
heapTime := time.Since(t0)
var sc metrics.Counter
t0 = time.Now()
bySort := topKSort(items, *k, &sc)
sortTime := time.Since(t0)
fmt.Printf("n = %d, k = %d\n\n", n, *k)
fmt.Printf(" %-22s %14s %12s %14s\n", "", "comparisons", "time", "per item")
fmt.Printf(" %-22s %14d %12s %14.2f\n", "heap of size k", hc.N(), heapTime.Round(time.Microsecond), float64(hc.N())/float64(n))
fmt.Printf(" %-22s %14d %12s %14.2f\n", "sort.Slice, take k", sc.N(), sortTime.Round(time.Microsecond), float64(sc.N())/float64(n))
fmt.Printf("\n ratio: %.1fx fewer comparisons\n", float64(sc.N())/float64(hc.N()))
same := len(byHeap) == len(bySort)
for i := range byHeap {
if i < len(bySort) && byHeap[i].Rating != bySort[i].Rating {
same = false
}
}
fmt.Printf(" same k ratings? %v\n", same)
if *show {
fmt.Println()
for i, it := range byHeap {
fmt.Printf(" %2d. %3d %s\n", i+1, it.Rating, it.Title)
}
}
return nil
}
// cmdPq is lesson 6's unanswered question: the hand-written heap against
// container/heap, on the workload a priority queue actually gets.
func cmdPq(args []string) error {
fs := flag.NewFlagSet("pq", flag.ContinueOnError)
in := fs.String("in", "library.jsonl", "library file")
rounds := fs.Int("rounds", 20, "how many times to repeat each arm")
if err := fs.Parse(args); err != nil {
return err
}
items, err := load(*in)
if err != nil {
return err
}
// push everything, then drain — the shape of Dijkstra's frontier.
var mine, std time.Duration
var c metrics.Counter
for r := 0; r < *rounds; r++ {
t0 := time.Now()
h := NewMinHeap(len(items))
for _, it := range items {
h.Push(it, &c)
}
for h.Len() > 0 {
h.Pop(&c)
}
mine += time.Since(t0)
t0 = time.Now()
s := &stdItems{}
stdheap.Init(s)
for _, it := range items {
stdheap.Push(s, it)
}
for s.Len() > 0 {
stdheap.Pop(s)
}
std += time.Since(t0)
}
n := time.Duration(*rounds)
fmt.Printf("n = %d, push-all then drain, %d rounds\n\n", len(items), *rounds)
fmt.Printf(" %-22s %14s\n", "", "per round")
fmt.Printf(" %-22s %14s\n", "hand-written heap", (mine / n).Round(time.Microsecond))
fmt.Printf(" %-22s %14s\n", "container/heap", (std / n).Round(time.Microsecond))
fmt.Printf("\n container/heap is %.2fx slower\n", float64(std)/float64(mine))
return nil
}