package main
import (
"fmt"
"math"
"sort"
"testing"
"algo/metrics"
)
// avlBound is the proven AVL height limit: h <= 1.4405 * log2(n+2) - 0.3277.
// Every test below checks against this rather than against a number someone
// eyeballed, because the point of this lesson is that the bound is a GUARANTEE.
func avlBound(n int) float64 {
return 1.4405*math.Log2(float64(n)+2) - 0.3277
}
func TestAVLStoresAndFinds(t *testing.T) {
items := shuffledLib(1000)
avl := NewAVLIndex()
var c metrics.Counter
for _, it := range items {
avl.Put(it, &c)
}
if avl.Len() != 1000 {
t.Fatalf("stored %d, want 1000", avl.Len())
}
for _, it := range items {
got, ok := avl.Get(it.Title, &c)
if !ok || got.ID != it.ID {
t.Fatalf("Get(%q): ok=%v id=%d, want id=%d", it.Title, ok, got.ID, it.ID)
}
}
if _, ok := avl.Get("nėra tokio pavadinimo", &c); ok {
t.Error("found a title that was never stored")
}
if avl.Len() != 1000 {
t.Errorf("Len drifted to %d during lookups", avl.Len())
}
}
// scrambledKeys is a deterministic xorshift key set — no math/rand, identical
// every run.
//
// The keys are DELIBERATELY not sequential. An orderly generator like
// fmt.Sprintf("t%05d", (i*7919)%n) turns out to exercise only the two SINGLE
// rotations; a version of rebalance with the left-right case deleted passes a
// whole suite built on such keys. These scrambled keys drive all four cases, and
// that omission then shows up as a node with balance 2.
func scrambledKeys(n int) []string {
out := make([]string, n)
x := uint64(88172645463325252)
for i := range out {
x ^= x << 13
x ^= x >> 7
x ^= x << 17
out[i] = fmt.Sprintf("k%016x", x)
}
return out
}
// Rotations rearrange the tree, so the thing lesson 11 was built for must
// survive every one of them.
func TestRotationsPreserveTheOrdering(t *testing.T) {
keys := scrambledKeys(2000)
avl := NewAVLIndex()
var c metrics.Counter
for i, k := range keys {
avl.Put(Item{ID: i, Title: k}, &c)
if i%97 == 0 { // check DURING the build, not only at the end
if !sort.StringsAreSorted(avl.Keys()) {
t.Fatalf("a rotation broke the BST invariant after %d inserts", i+1)
}
if b := avl.WorstBalance(); b > 1 {
t.Fatalf("balance %d after %d inserts, want <= 1", b, i+1)
}
}
}
if b := avl.WorstBalance(); b > 1 {
t.Fatalf("balance %d in the finished tree, want <= 1", b)
}
out := avl.Keys()
lo, _ := avl.Min()
hi, _ := avl.Max()
if lo != out[0] || hi != out[len(out)-1] {
t.Errorf("Min/Max = %q/%q, want %q/%q", lo, hi, out[0], out[len(out)-1])
}
if avl.Rotations == 0 {
t.Error("no rotations happened at all — nothing was being balanced")
}
}
// THE CONTRACT OF THIS LESSON, AND IT IS A PROPERTY RATHER THAN AN OUTPUT.
//
// The input is the one that destroyed lesson 11's tree: strictly ascending.
// A plain BST answers every query correctly here and still fails this test by
// two orders of magnitude — height 10000 against a bound of about 19.
//
// You cannot pass this by producing the right keys. Only the shape passes it.
func TestHeightStaysBoundedOnSortedInput(t *testing.T) {
for _, n := range []int{1000, 10000, 100000} {
avl := NewAVLIndex()
var c metrics.Counter
for i := 0; i < n; i++ { // strictly ascending
avl.Put(Item{ID: i, Title: fmt.Sprintf("t%08d", i)}, &c)
}
bound := avlBound(n)
if float64(avl.Height()) > bound {
t.Errorf("n=%d: height %d exceeds the AVL bound %.1f", n, avl.Height(), bound)
}
if b := avl.WorstBalance(); b > 1 {
t.Errorf("n=%d: some node has balance %d, want <= 1", n, b)
}
var l metrics.Counter
for i := 0; i < n; i++ {
avl.Get(fmt.Sprintf("t%08d", i), &l)
}
if per := float64(l.N()) / float64(n); per > bound {
t.Errorf("n=%d: %.1f comparisons per lookup, bound is %.1f", n, per, bound)
}
t.Logf("n=%6d: height %2d, bound %.1f, %.3f cmps/lookup", n, avl.Height(), bound, float64(l.N())/float64(n))
}
}
// The same bound under DESCENDING input, which is the mirror case, and under a
// zigzag that also killed the plain tree (homework 4 of lesson 11).
func TestHeightStaysBoundedOnEveryAdversarialOrder(t *testing.T) {
const n = 20000
keys := make([]string, n)
for i := range keys {
keys[i] = fmt.Sprintf("t%08d", i)
}
orders := map[string][]string{}
orders["ascending"] = keys
desc := make([]string, n)
for i, k := range keys {
desc[n-1-i] = k
}
orders["descending"] = desc
zig := make([]string, 0, n)
for lo, hi := 0, n-1; lo <= hi; lo++ {
zig = append(zig, keys[lo])
if lo < hi {
zig = append(zig, keys[hi])
hi--
}
}
orders["zigzag"] = zig
bound := avlBound(n)
for name, order := range orders {
avl := NewAVLIndex()
var c metrics.Counter
for _, k := range order {
avl.Put(Item{Title: k}, &c)
}
if avl.Len() != n {
t.Fatalf("%s: stored %d, want %d", name, avl.Len(), n)
}
if float64(avl.Height()) > bound {
t.Errorf("%s: height %d exceeds the bound %.1f", name, avl.Height(), bound)
}
if !sort.StringsAreSorted(avl.Keys()) {
t.Errorf("%s: the tree is not in order", name)
}
t.Logf("%-11s height %2d (bound %.1f), %.3f rotations per insert",
name, avl.Height(), bound, float64(avl.Rotations)/float64(n))
}
}
// REBALANCING MUST BE AMORTIZED CONSTANT. If it were not, the guarantee would be
// paid for with a cost that grows, and the whole trade would collapse.
//
// The ceiling is 2.0 rather than 1.0 because the zigzag order above needs ~1.62
// rotations per insert — the most expensive order measured. What matters is that
// the figure does not GROW with n.
func TestRotationsAreAmortizedConstant(t *testing.T) {
for _, n := range []int{1000, 10000, 100000} {
avl := NewAVLIndex()
var c metrics.Counter
for i := 0; i < n; i++ {
avl.Put(Item{Title: fmt.Sprintf("t%08d", i)}, &c)
}
if per := float64(avl.Rotations) / float64(n); per > 2.0 {
t.Errorf("n=%d: %.3f rotations per insert, want a small constant", n, per)
}
}
}
// THE PRICE, measured with the clock rather than the counter.
//
// The counter cannot see a rotation — rotations are not comparisons — so these
// benchmarks are the only place the cost of balancing is visible at all. Run
// them with:
//
// go test -run XXX -bench 'Build|Lookup' -benchtime=20x -count=5
//
// They need s10k.jsonl (algo gen -n 10000 -seed 7 -o s10k.jsonl) and skip
// without it.
func benchItems(b *testing.B) []Item {
items, err := load("s10k.jsonl")
if err != nil {
b.Skip(err)
}
return items
}
func BenchmarkBuildBST(b *testing.B) {
items := benchItems(b)
var c metrics.Counter
b.ResetTimer()
for i := 0; i < b.N; i++ {
t := NewTreeIndex()
for _, it := range items {
t.Put(it, &c)
}
}
}
func BenchmarkBuildAVL(b *testing.B) {
items := benchItems(b)
var c metrics.Counter
b.ResetTimer()
for i := 0; i < b.N; i++ {
t := NewAVLIndex()
for _, it := range items {
t.Put(it, &c)
}
}
}
func BenchmarkLookupBST(b *testing.B) {
items := benchItems(b)
var c metrics.Counter
t := NewTreeIndex()
for _, it := range items {
t.Put(it, &c)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, it := range items {
t.Get(it.Title, &c)
}
}
}
func BenchmarkLookupAVL(b *testing.B) {
items := benchItems(b)
var c metrics.Counter
t := NewAVLIndex()
for _, it := range items {
t.Put(it, &c)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, it := range items {
t.Get(it.Title, &c)
}
}
}