618 lines
16 KiB
Go
618 lines
16 KiB
Go
package tests
|
|
|
|
import (
|
|
"bufio"
|
|
"io"
|
|
"math"
|
|
"math/rand"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"datastructures/trees/avl"
|
|
)
|
|
|
|
// AvlTree exposes Root and AvlNode.Data/Height, so these tests drive the tree
|
|
// through Insert and observe it through the two traversals. Inorder of a BST
|
|
// must come back sorted, which makes it a cheap oracle for the whole structure:
|
|
// if a rotation dropped or duplicated a node, the sorted comparison catches it.
|
|
//
|
|
// Helpers uniqueSorted, equalInts and validBSTPreorder live in
|
|
// binarySearchTree_test.go, same package.
|
|
|
|
func equalStrings(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestAvlTraversalsOnEmptyTree(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
|
|
if got := tree.TraverseInorder(); len(got) != 0 {
|
|
t.Errorf("TraverseInorder() on empty tree = %v, want empty", got)
|
|
}
|
|
|
|
if got := tree.TraversePreorder(); len(got) != 0 {
|
|
t.Errorf("TraversePreorder() on empty tree = %v, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestAvlTraversalsOnSingleNode(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
tree.Insert(42)
|
|
|
|
if got := tree.TraverseInorder(); !equalInts(got, []int{42}) {
|
|
t.Errorf("TraverseInorder() = %v, want [42]", got)
|
|
}
|
|
|
|
if got := tree.TraversePreorder(); !equalInts(got, []int{42}) {
|
|
t.Errorf("TraversePreorder() = %v, want [42]", got)
|
|
}
|
|
}
|
|
|
|
func TestAvlInorderIsSorted(t *testing.T) {
|
|
inputs := [][]int{
|
|
{5, 3, 8, 1, 4, 7, 9},
|
|
{1, 2, 3, 4, 5, 6, 7}, // ascending, forces left rotations
|
|
{7, 6, 5, 4, 3, 2, 1}, // descending, forces right rotations
|
|
{10, 20, 30, 40, 50, 25}, // mixes both, hits the double rotations
|
|
{-5, 0, -10, 3, -3, 8}, // negatives
|
|
{42}, // single node
|
|
{1, 1, 1, 1}, // all duplicates
|
|
}
|
|
|
|
for _, in := range inputs {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range in {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
got := tree.TraverseInorder()
|
|
want := uniqueSorted(in)
|
|
|
|
if !equalInts(got, want) {
|
|
t.Errorf("insert %v: TraverseInorder() = %v, want %v", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The insert sequence below is worked out by hand: it ends as
|
|
// 30(20(10,25), 40(_,50)) after a left-right double rotation at the root, so
|
|
// the exact preorder pins down the tree shape, not just its contents.
|
|
func TestAvlPreorderMatchesKnownShape(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{10, 20, 30, 40, 50, 25} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
want := []int{30, 20, 10, 25, 40, 50}
|
|
if got := tree.TraversePreorder(); !equalInts(got, want) {
|
|
t.Errorf("TraversePreorder() = %v, want %v", got, want)
|
|
}
|
|
|
|
wantInorder := []int{10, 20, 25, 30, 40, 50}
|
|
if got := tree.TraverseInorder(); !equalInts(got, wantInorder) {
|
|
t.Errorf("TraverseInorder() = %v, want %v", got, wantInorder)
|
|
}
|
|
}
|
|
|
|
func TestAvlPreorderStartsAtRoot(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{50, 25, 75, 10, 30, 60, 90} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
got := tree.TraversePreorder()
|
|
if len(got) == 0 {
|
|
t.Fatal("TraversePreorder() returned nothing")
|
|
}
|
|
|
|
if got[0] != tree.Root.Data {
|
|
t.Errorf("TraversePreorder()[0] = %d, want root %d", got[0], tree.Root.Data)
|
|
}
|
|
}
|
|
|
|
func TestAvlPreorderIsValidBSTPreorder(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{15, 3, 27, 1, 9, 20, 40, 6, 12, 35, 50} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
pre := tree.TraversePreorder()
|
|
if !validBSTPreorder(pre) {
|
|
t.Errorf("TraversePreorder() = %v is not a valid BST pre-order", pre)
|
|
}
|
|
}
|
|
|
|
// Both traversals must visit every node exactly once, so they agree on length
|
|
// and on contents once sorted.
|
|
func TestAvlTraversalsVisitSameNodes(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{8, 2, 19, 4, 11, 25, 1, 6, 30, 15} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
inorder := tree.TraverseInorder()
|
|
preorder := tree.TraversePreorder()
|
|
|
|
if len(inorder) != len(preorder) {
|
|
t.Fatalf("length mismatch: inorder %d, preorder %d", len(inorder), len(preorder))
|
|
}
|
|
|
|
sortedPre := append([]int(nil), preorder...)
|
|
sort.Ints(sortedPre)
|
|
|
|
if !equalInts(inorder, sortedPre) {
|
|
t.Errorf("traversals disagree: inorder %v, sorted preorder %v", inorder, sortedPre)
|
|
}
|
|
}
|
|
|
|
func TestAvlTraversalsDoNotMutateTree(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{5, 2, 9, 1, 7, 12} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
rootBefore := tree.Root
|
|
heightBefore := tree.Root.Height
|
|
|
|
first := tree.TraverseInorder()
|
|
second := tree.TraverseInorder()
|
|
firstPre := tree.TraversePreorder()
|
|
secondPre := tree.TraversePreorder()
|
|
|
|
if !equalInts(first, second) {
|
|
t.Errorf("repeated TraverseInorder() differ: %v then %v", first, second)
|
|
}
|
|
|
|
if !equalInts(firstPre, secondPre) {
|
|
t.Errorf("repeated TraversePreorder() differ: %v then %v", firstPre, secondPre)
|
|
}
|
|
|
|
if tree.Root != rootBefore || tree.Root.Height != heightBefore {
|
|
t.Error("traversal changed the tree root")
|
|
}
|
|
}
|
|
|
|
// The constraint moved from a numeric-only interface to cmp.Ordered, so strings
|
|
// have to work end to end.
|
|
func TestAvlStringKeys(t *testing.T) {
|
|
tree := avl.NewAvlTree[string]()
|
|
for _, w := range []string{"pear", "apple", "fig", "date", "banana", "cherry"} {
|
|
tree.Insert(w)
|
|
}
|
|
|
|
want := []string{"apple", "banana", "cherry", "date", "fig", "pear"}
|
|
if got := tree.TraverseInorder(); !equalStrings(got, want) {
|
|
t.Errorf("TraverseInorder() = %v, want %v", got, want)
|
|
}
|
|
|
|
if got := tree.TraversePreorder(); len(got) != len(want) {
|
|
t.Errorf("TraversePreorder() returned %d values, want %d", len(got), len(want))
|
|
}
|
|
}
|
|
|
|
func TestAvlFloatKeys(t *testing.T) {
|
|
tree := avl.NewAvlTree[float64]()
|
|
for _, v := range []float64{3.5, -1.25, 0, 9.75, 2.5} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
want := []float64{-1.25, 0, 2.5, 3.5, 9.75}
|
|
got := tree.TraverseInorder()
|
|
|
|
if len(got) != len(want) {
|
|
t.Fatalf("TraverseInorder() = %v, want %v", got, want)
|
|
}
|
|
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("TraverseInorder() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Degenerate insert orders are what the rotations exist for: 1..n ascending
|
|
// would be a linked list in a plain BST. Height must stay within the AVL bound
|
|
// of 1.44*log2(n+2).
|
|
func TestAvlStaysBalancedOnSortedInput(t *testing.T) {
|
|
const count = 1000
|
|
|
|
for _, name := range []string{"ascending", "descending"} {
|
|
tree := avl.NewAvlTree[int]()
|
|
|
|
for i := 0; i < count; i++ {
|
|
if name == "ascending" {
|
|
tree.Insert(i)
|
|
} else {
|
|
tree.Insert(count - i)
|
|
}
|
|
}
|
|
|
|
got := tree.TraverseInorder()
|
|
if len(got) != count {
|
|
t.Fatalf("%s: TraverseInorder() returned %d values, want %d", name, len(got), count)
|
|
}
|
|
|
|
if !sort.IntsAreSorted(got) {
|
|
t.Fatalf("%s: TraverseInorder() is not sorted", name)
|
|
}
|
|
|
|
maxHeight := int(1.44 * math.Log2(float64(count+2)))
|
|
if tree.Root.Height > maxHeight {
|
|
t.Errorf("%s: height %d exceeds AVL bound %d", name, tree.Root.Height, maxHeight)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAvlTraversalsRandomized(t *testing.T) {
|
|
const inserts = 3000
|
|
|
|
tree := avl.NewAvlTree[int]()
|
|
var raw []int
|
|
|
|
for i := 0; i < inserts; i++ {
|
|
v := rand.Intn(750)
|
|
tree.Insert(v)
|
|
raw = append(raw, v)
|
|
}
|
|
|
|
want := uniqueSorted(raw)
|
|
|
|
if got := tree.TraverseInorder(); !equalInts(got, want) {
|
|
t.Errorf("TraverseInorder() has %d values, want %d unique", len(got), len(want))
|
|
}
|
|
|
|
pre := tree.TraversePreorder()
|
|
if len(pre) != len(want) {
|
|
t.Errorf("TraversePreorder() returned %d values, want %d", len(pre), len(want))
|
|
}
|
|
|
|
if !validBSTPreorder(pre) {
|
|
t.Error("TraversePreorder() is not a valid BST pre-order")
|
|
}
|
|
}
|
|
|
|
// --- Insert ---
|
|
|
|
// checkAvlInvariants walks the tree recomputing height from the leaves up and
|
|
// returns the true height. It fails the test if any node's stored Height is
|
|
// stale, if any balance factor leaves [-1,1], or if the BST ordering is broken.
|
|
func checkAvlInvariants(t *testing.T, node *avl.AvlNode[int], low, high int) int {
|
|
t.Helper()
|
|
|
|
if node == nil {
|
|
return 0
|
|
}
|
|
|
|
if node.Data <= low || node.Data >= high {
|
|
t.Errorf("node %d violates BST ordering, must be in (%d, %d)", node.Data, low, high)
|
|
}
|
|
|
|
leftHeight := checkAvlInvariants(t, node.Left, low, node.Data)
|
|
rightHeight := checkAvlInvariants(t, node.Right, node.Data, high)
|
|
|
|
realHeight := 1 + max(leftHeight, rightHeight)
|
|
if node.Height != realHeight {
|
|
t.Errorf("node %d has stored Height %d, recomputed %d", node.Data, node.Height, realHeight)
|
|
}
|
|
|
|
if balance := leftHeight - rightHeight; balance < -1 || balance > 1 {
|
|
t.Errorf("node %d has balance factor %d, want within [-1,1]", node.Data, balance)
|
|
}
|
|
|
|
return realHeight
|
|
}
|
|
|
|
// Each sequence below triggers exactly one of the four rebalancing cases, and
|
|
// all four settle into the same 2(1,3) shape.
|
|
func TestAvlInsertRotationCases(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
insert []int
|
|
}{
|
|
{"left-left", []int{3, 2, 1}},
|
|
{"right-right", []int{1, 2, 3}},
|
|
{"left-right", []int{3, 1, 2}},
|
|
{"right-left", []int{1, 3, 2}},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range c.insert {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
if got := tree.TraversePreorder(); !equalInts(got, []int{2, 1, 3}) {
|
|
t.Errorf("%s: inserting %v gave preorder %v, want [2 1 3]", c.name, c.insert, got)
|
|
}
|
|
|
|
if tree.Root.Height != 2 {
|
|
t.Errorf("%s: root height = %d, want 2", c.name, tree.Root.Height)
|
|
}
|
|
|
|
checkAvlInvariants(t, tree.Root, math.MinInt, math.MaxInt)
|
|
}
|
|
}
|
|
|
|
func TestAvlInsertIgnoresDuplicates(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
|
|
for _, v := range []int{5, 3, 8, 5, 3, 8, 5} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
want := []int{3, 5, 8}
|
|
if got := tree.TraverseInorder(); !equalInts(got, want) {
|
|
t.Errorf("TraverseInorder() = %v, want %v", got, want)
|
|
}
|
|
|
|
if tree.Root.Height != 2 {
|
|
t.Errorf("root height = %d, want 2 (duplicates must not deepen the tree)", tree.Root.Height)
|
|
}
|
|
}
|
|
|
|
func TestAvlInsertSetsRootAndLeafHeights(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
|
|
if tree.Root != nil {
|
|
t.Fatal("NewAvlTree() should start with a nil Root")
|
|
}
|
|
|
|
tree.Insert(1)
|
|
|
|
if tree.Root == nil {
|
|
t.Fatal("Insert() did not set Root")
|
|
}
|
|
|
|
if tree.Root.Data != 1 {
|
|
t.Errorf("Root.Data = %d, want 1", tree.Root.Data)
|
|
}
|
|
|
|
if tree.Root.Height != 1 {
|
|
t.Errorf("leaf Height = %d, want 1", tree.Root.Height)
|
|
}
|
|
|
|
if tree.Root.Left != nil || tree.Root.Right != nil {
|
|
t.Error("a single-node tree should have no children")
|
|
}
|
|
}
|
|
|
|
// Insert order must not matter: the same set of keys always produces the same
|
|
// AVL tree only if the rotations are right, but at minimum every order has to
|
|
// yield the same contents and hold the invariants.
|
|
func TestAvlInsertOrderIndependence(t *testing.T) {
|
|
orders := [][]int{
|
|
{1, 2, 3, 4, 5, 6, 7},
|
|
{7, 6, 5, 4, 3, 2, 1},
|
|
{4, 2, 6, 1, 3, 5, 7},
|
|
{1, 7, 2, 6, 3, 5, 4},
|
|
}
|
|
|
|
want := []int{1, 2, 3, 4, 5, 6, 7}
|
|
|
|
for _, order := range orders {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range order {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
if got := tree.TraverseInorder(); !equalInts(got, want) {
|
|
t.Errorf("insert %v: TraverseInorder() = %v, want %v", order, got, want)
|
|
}
|
|
|
|
checkAvlInvariants(t, tree.Root, math.MinInt, math.MaxInt)
|
|
}
|
|
}
|
|
|
|
func TestAvlInsertKeepsInvariantsRandomized(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
|
|
for i := 0; i < 2000; i++ {
|
|
tree.Insert(rand.Intn(5000))
|
|
}
|
|
|
|
checkAvlInvariants(t, tree.Root, math.MinInt, math.MaxInt)
|
|
}
|
|
|
|
// --- Find ---
|
|
|
|
func TestAvlFindOnEmptyTree(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
|
|
node, ok := tree.Find(1)
|
|
if ok {
|
|
t.Error("Find() on an empty tree returned true")
|
|
}
|
|
|
|
if node == nil {
|
|
t.Fatal("Find() returned a nil node; the contract is a zero node and false")
|
|
}
|
|
|
|
if node.Data != 0 {
|
|
t.Errorf("Find() miss returned Data %d, want the zero value", node.Data)
|
|
}
|
|
}
|
|
|
|
func TestAvlFindLocatesEveryInsertedValue(t *testing.T) {
|
|
values := []int{50, 25, 75, 10, 30, 60, 90, 5, 15, 27, 40}
|
|
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range values {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
for _, v := range values {
|
|
node, ok := tree.Find(v)
|
|
if !ok {
|
|
t.Errorf("Find(%d) = false, want true", v)
|
|
continue
|
|
}
|
|
|
|
if node.Data != v {
|
|
t.Errorf("Find(%d) returned node holding %d", v, node.Data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAvlFindMissingValues(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{50, 25, 75, 10, 30} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
for _, missing := range []int{-1, 0, 11, 26, 49, 51, 74, 76, 1000} {
|
|
if _, ok := tree.Find(missing); ok {
|
|
t.Errorf("Find(%d) = true, want false", missing)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find walks the tree by reassigning t.Root on a value receiver. That is only
|
|
// safe because the receiver is a copy; this test would catch a switch to a
|
|
// pointer receiver, which would leave the tree truncated after one lookup.
|
|
func TestAvlFindDoesNotMutateTree(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{50, 25, 75, 10, 30, 60, 90} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
before := tree.TraverseInorder()
|
|
rootBefore := tree.Root
|
|
|
|
tree.Find(90)
|
|
tree.Find(10)
|
|
tree.Find(-1) // miss, walks all the way to a nil child
|
|
|
|
if tree.Root != rootBefore {
|
|
t.Error("Find() moved the tree Root")
|
|
}
|
|
|
|
if after := tree.TraverseInorder(); !equalInts(before, after) {
|
|
t.Errorf("tree changed after Find(): %v then %v", before, after)
|
|
}
|
|
}
|
|
|
|
func TestAvlFindStringKeys(t *testing.T) {
|
|
tree := avl.NewAvlTree[string]()
|
|
for _, w := range []string{"pear", "apple", "fig"} {
|
|
tree.Insert(w)
|
|
}
|
|
|
|
if node, ok := tree.Find("apple"); !ok || node.Data != "apple" {
|
|
t.Errorf(`Find("apple") = %v, %v; want the apple node and true`, node, ok)
|
|
}
|
|
|
|
if _, ok := tree.Find("kiwi"); ok {
|
|
t.Error(`Find("kiwi") = true, want false`)
|
|
}
|
|
}
|
|
|
|
// --- Display ---
|
|
|
|
// captureAvlDisplay runs tree.Display() with stdout redirected and returns the
|
|
// printed lines. captureDisplay in binarySearchTree_test.go is for BSTree.
|
|
func captureAvlDisplay(tree *avl.AvlTree[int]) []string {
|
|
old := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
tree.Display()
|
|
|
|
w.Close()
|
|
os.Stdout = old
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
_, _ = io.Copy(io.Discard, r)
|
|
|
|
return lines
|
|
}
|
|
|
|
func TestAvlDisplayEmptyTree(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
|
|
lines := captureAvlDisplay(tree)
|
|
if len(lines) != 1 || lines[0] != "<empty>" {
|
|
t.Errorf("Display() on empty tree printed %q, want [<empty>]", lines)
|
|
}
|
|
}
|
|
|
|
func TestAvlDisplaySingleNode(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
tree.Insert(7)
|
|
|
|
lines := captureAvlDisplay(tree)
|
|
if len(lines) != 1 || lines[0] != "7" {
|
|
t.Errorf("Display() printed %q, want [7] with no connector", lines)
|
|
}
|
|
}
|
|
|
|
// Golden output for the hand-checked 30(20(10,25), 40(_,50)) tree. Display
|
|
// draws it rotated 90° left, so the right subtree sits on top.
|
|
func TestAvlDisplayKnownTree(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{10, 20, 30, 40, 50, 25} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
want := []string{
|
|
" ┌── 50",
|
|
" ┌── 40",
|
|
"30",
|
|
" │ ┌── 25",
|
|
" └── 20",
|
|
" └── 10",
|
|
}
|
|
|
|
got := captureAvlDisplay(tree)
|
|
if !equalStrings(got, want) {
|
|
t.Errorf("Display() printed:\n%s\nwant:\n%s",
|
|
strings.Join(got, "\n"), strings.Join(want, "\n"))
|
|
}
|
|
}
|
|
|
|
// Rotated 90° left means reading the lines top to bottom yields a reverse
|
|
// in-order walk, which ties Display back to the traversals.
|
|
func TestAvlDisplayReadsAsReverseInorder(t *testing.T) {
|
|
tree := avl.NewAvlTree[int]()
|
|
for _, v := range []int{15, 3, 27, 1, 9, 20, 40, 6, 12, 35, 50} {
|
|
tree.Insert(v)
|
|
}
|
|
|
|
lines := captureAvlDisplay(tree)
|
|
|
|
var printed []int
|
|
for _, line := range lines {
|
|
fields := strings.Fields(line)
|
|
value, err := strconv.Atoi(fields[len(fields)-1])
|
|
if err != nil {
|
|
t.Fatalf("could not read a value from Display() line %q: %v", line, err)
|
|
}
|
|
printed = append(printed, value)
|
|
}
|
|
|
|
inorder := tree.TraverseInorder()
|
|
|
|
want := make([]int, 0, len(inorder))
|
|
for i := len(inorder) - 1; i >= 0; i-- {
|
|
want = append(want, inorder[i])
|
|
}
|
|
|
|
if !equalInts(printed, want) {
|
|
t.Errorf("Display() values top to bottom = %v, want reverse inorder %v", printed, want)
|
|
}
|
|
}
|