Drills
Five tasks.
1. Heapsort
Draining a heap gives ascending order, which is a sorting algorithm. Write
heapSort(items []Item, c *metrics.Counter) []Item: build a heap, then Pop
everything into a slice.
It is O(n log n), in place if you want it to be, and needs no extra memory. So
how does it compare with lessons 7 to 9?
n=100000: heapsort 3002497 comparisons, sort.Slice 813236 (3.69x)
3.69× more comparisons than sort.Slice. Explain where they come from —
count how many comparisons one Pop does per level and compare with one merge
step in lesson 8.
Then answer: heapsort is O(n log n) and needs no extra memory, which merge sort
does. Why does nobody use it as their default sort anyway?
2. Build the heap the other way
There are two ways to turn n items into a heap: n calls to Push, or place
them all in the array and sift each one down, starting at n/2 - 1. The second is
called heapify.
The textbook says n Pushes is O(n log n) and heapify is O(n). Measure it:
random data
n= 1000: 1000 Pushes cost 2234 cmps (2.23/item); heapify 1867 (1.87/item)
n=100000: 100000 Pushes cost 223155 cmps (2.23/item); heapify 187550 (1.88/item)
Both are linear, and heapify wins by only 16%. The textbook's log n has
gone missing.
Now feed it ordered input:
n=100000: Push ascending 99999 (1.00/item) · Push descending 1468946 (14.69/item) · heapify 199978 (2.00/item)
There it is. Explain all three columns: why ascending input is Push's best case
for a min-heap, why descending is its worst, and why heapify does not care.
Then say which of the three numbers the O(n log n) in the textbook refers to,
and why random data landed so close to the best case.
3. A max-heap without a second implementation
algo top wants the highest ratings and you built a min-heap. There are three
ways to get a max-heap:
- copy
heap.goand flip every<to>; - store
-Ratingand negate on the way out; - take a comparison function as a field on the struct.
Implement one. Then argue for or against each of the other two — including what
the third one costs, given what step 6 measured. (A func field is not an
interface. Does that matter? Measure it.)
4. Boxing or dispatch?
Step 6 named two suspects and did not separate them. Separate them.
Write a third heap: container/heap again, but over []*Item instead of
[]Item. A pointer is pointer-shaped, so it fits inside an interface value and is
not copied to the heap — you get the dispatch without the boxing.
BenchmarkHeapMine-32 20 1152790 ns/op 565248 B/op 1 allocs/op
BenchmarkHeapStdlibPtr-32 20 1158620 ns/op 81944 B/op 2 allocs/op
BenchmarkHeapStdlib-32 20 1932750 ns/op 3934116 B/op 20020 allocs/op
With pointers, container/heap ties the hand-written heap. Every bit of the
1.66× was boxing; the dispatch cost nothing measurable.
Explain why the dispatch turned out to be free — the call site is monomorphic and
the branch is perfectly predicted. Then answer the question that matters: does
this mean container/heap is fine after all? What did the pointer version cost
you that the value version did not? (Look at where the Items now live, and at
what lesson 11 measured about scattered memory.)
5. The crossover, precisely
Step 5 put the top-k crossover between k = 1,000 and k = 10,000 at
n = 100,000.
Find it exactly, by bisection. Then repeat at n = 10,000 and n = 1,000,000.
Is the crossover a fixed k, a fixed ratio k/n, or something else? State a rule
for when to use the heap and when to sort, and show the measurement it rests on.