diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 01ab831..f9d6a65 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -33,7 +33,6 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos files: ./coverage/cover.out flags: unittests name: codecov-umbrella diff --git a/.idea/runConfigurations/Test_all.xml b/.idea/runConfigurations/Test_all.xml new file mode 100644 index 0000000..299d2a5 --- /dev/null +++ b/.idea/runConfigurations/Test_all.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile index 321561f..a80eb2e 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ bench: cover: # This runs the benchmarks just once, as unit tests, for coverage reporting only. # It does not replace running "make bench". + mkdir -p coverage go test -v -race -run=. -bench=. -benchtime=1x -coverprofile=coverage/cover.out -covermode=atomic ./... .PHONY: test diff --git a/README.md b/README.md index 546462b..7b79915 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Since this is a library, it has no install process, but you can build it to ensu #### Running normal tests: unit and benchmarks -For the simple version, without generating coverage reports, run: +For the simple version, run: ``` make test # Unit tests, fast make coverage # Coverage report in cover.out. A bit longer. diff --git a/binarysearchtree/intrinsic.go b/binarysearchtree/intrinsic.go new file mode 100644 index 0000000..9189acf --- /dev/null +++ b/binarysearchtree/intrinsic.go @@ -0,0 +1,230 @@ +package binarysearchtree + +import ( + "cmp" + "errors" + + "github.com/fgm/container" +) + +type node[E cmp.Ordered] struct { + data *E + left, right *node[E] +} + +func (n *node[E]) delete(e *E) *node[E] { + switch { + case n == nil, e == nil: + n = nil + case *e < *n.data: + n.left = n.left.delete(e) + case *e > *n.data: + n.right = n.right.delete(e) + // Matched childless node: just drop it. + case n.left == nil && n.right == nil: + n = nil + // Matched node with only one right child: promote that child. + case n.left == nil: + n = n.right + // Matched node with only one left child: promote that child. + case n.right == nil: + n = n.left + // Matched node with two children: promote leftmost child of right child. + // + // We could also have promoted the rightmost child of the left child. + default: + promovendum := n.right // Cannot be nil: that case was handled ed above. + for { + if promovendum.left == nil { + break + } + promovendum = promovendum.left // Not nil either, per previous statement. + } + n.data = promovendum.data + n.right = n.right.delete(promovendum.data) // As the leftmost child, it won't have two children. + } + return n +} + +func (n *node[E]) upsert(m *node[E]) *E { + switch { + case *m.data < *n.data: + if n.left == nil { + n.left = m + return nil + } else { + return n.left.upsert(m) + } + case *m.data > *n.data: + if n.right == nil { + n.right = m + return nil + } else { + return n.right.upsert(m) + } + default: // *m.data == *n.data + data := n.data + n.data = m.data + return data + } +} + +func (n *node[E]) walkInOrder(cb container.WalkCB[E]) error { + var err error + if n == nil { + return nil + } + if n.left != nil { + if err = n.left.walkInOrder(cb); err != nil { + return err + } + } + if err := cb(n.data); err != nil { + return err + } + if n.right != nil { + if err = n.right.walkInOrder(cb); err != nil { + return err + } + } + return nil +} + +func (n *node[E]) walkPostOrder(cb container.WalkCB[E]) error { + var err error + if n == nil { + return nil + } + if n.left != nil { + if err = n.left.walkPostOrder(cb); err != nil { + return err + } + } + if n.right != nil { + if err = n.right.walkPostOrder(cb); err != nil { + return err + } + } + return cb(n.data) +} + +func (n *node[E]) walkPreOrder(cb container.WalkCB[E]) error { + var err error + if n == nil { + return nil + } + if err := cb(n.data); err != nil { + return err + } + if n.left != nil { + if err = n.left.walkPreOrder(cb); err != nil { + return err + } + } + if n.right != nil { + if err = n.right.walkPreOrder(cb); err != nil { + return err + } + } + return nil +} + +// Intrinsic holds nodes which are their own ordering key. +type Intrinsic[E cmp.Ordered] struct { + root *node[E] +} + +// Len returns the number of nodes in the tree, for the container.Countable interface. +// Complexity is O(n). +func (t *Intrinsic[E]) Len() int { + l := 0 + t.WalkPostOrder(func(_ *E) error { l++; return nil }) + return l +} + +func (t *Intrinsic[E]) Elements() []E { + var sl []E + t.WalkPreOrder(func(e *E) error { sl = append(sl, *e); return nil }) + return sl +} + +// Upsert adds a value to the tree, replacing and returning the previous one if any. +// If none existed, it returns nil. +func (t *Intrinsic[E]) Upsert(e ...*E) []*E { + results := make([]*E, 0, len(e)) + var result *E + for _, oneE := range e { + n := &node[E]{data: oneE} + + switch { + case t == nil, e == nil: + result = nil + case t.root == nil: + t.root = n + result = nil + default: + result = t.root.upsert(n) + } + results = append(results, result) + } + return results +} + +func (t *Intrinsic[E]) Delete(e *E) { + if t == nil || e == nil { + return + } + t.root.delete(e) +} + +// IndexOf returns the position of the value among those in the tree. +// If the value cannot be found, it will return 0, false, otherwise the position +// starting at 0, and true. +func (t *Intrinsic[E]) IndexOf(e *E) (int, bool) { + errFound := errors.New("found") + index := 0 + err := t.WalkInOrder(func(x *E) error { + if *x == *e { + return errFound + } + index++ + return nil + }) + if err != errFound { + return 0, false + } + return index, true +} + +// WalkInOrder is useful for search and listing nodes in order. +func (t *Intrinsic[E]) WalkInOrder(cb container.WalkCB[E]) error { + if t == nil { + return nil + } + return t.root.walkInOrder(cb) +} + +// WalkPostOrder in useful for deleting subtrees. +func (t *Intrinsic[E]) WalkPostOrder(cb container.WalkCB[E]) error { + if t == nil { + return nil + } + return t.root.walkPostOrder(cb) +} + +// WalkPreOrder is useful to clone the tree. +func (t *Intrinsic[E]) WalkPreOrder(cb container.WalkCB[E]) error { + if t == nil { + return nil + } + return t.root.walkPreOrder(cb) +} + +func (t *Intrinsic[E]) Clone() container.BinarySearchTree[E] { + clone := &Intrinsic[E]{} + t.WalkPreOrder(func(e *E) error { + clone.Upsert(e) + return nil + }) + return clone +} diff --git a/binarysearchtree/intrinsic_opaque_test.go b/binarysearchtree/intrinsic_opaque_test.go new file mode 100644 index 0000000..723e725 --- /dev/null +++ b/binarysearchtree/intrinsic_opaque_test.go @@ -0,0 +1,156 @@ +package binarysearchtree_test + +import ( + "fmt" + "log" + "strconv" + "testing" + + "github.com/fgm/container" + bst "github.com/fgm/container/binarysearchtree" +) + +func TestIntrinsic_nil(t *testing.T) { + var tree *bst.Intrinsic[int] + tree.WalkInOrder(bst.P) + tree.WalkPostOrder(bst.P) + tree.WalkPreOrder(bst.P) + tree.Upsert(nil) + tree.Delete(nil) + + tree = &bst.Intrinsic[int]{} + tree.WalkInOrder(bst.P) + tree.WalkPostOrder(bst.P) + tree.WalkPreOrder(bst.P) + // Output: +} + +func TestIntrinsic_Upsert(t *testing.T) { + tree := bst.Simple() + actual := tree.Upsert(&bst.One) + if len(actual) != 1 { + t.Fatalf("expected overwriting upsert to return one value, got %v", actual) + } + if *actual[0] != bst.One { + t.Fatalf("expected overwriting upsert to return %d, got %d", bst.One, *actual[0]) + } + + actual = tree.Upsert(&bst.Six) + if len(actual) != 1 { + t.Fatalf("expected non-overwriting upsert to return one value, got %v", actual) + } + if actual[0] != nil { + t.Fatalf("expected non-overwriting upsert to return one nil, got %v", actual[0]) + } +} + +func TestIntrinsic_IndexOf(t *testing.T) { + tree := bst.Simple() + checks := [...]struct { + input int + expectedOK bool + expectedIndex int + }{ + {bst.One, true, 0}, + {bst.Two, true, 1}, + {bst.Three, true, 2}, + {bst.Four, true, 3}, + {bst.Five, true, 4}, + {bst.Six, false, 0}, + } + for _, check := range checks { + t.Run(strconv.Itoa(check.input), func(t *testing.T) { + actualIndex, actualOK := tree.IndexOf(&check.input) + if actualOK != check.expectedOK { + t.Fatalf("%d found: %t but expected %t", check.input, actualOK, check.expectedOK) + } + if actualIndex != check.expectedIndex { + t.Fatalf("%d at index %d but expected %d", check.input, actualIndex, check.expectedIndex) + } + }) + } +} + +func TestIntrinsic_Len(t *testing.T) { + si := bst.Simple().(container.Enumerable[int]).Elements() + hf := bst.HalfFull().(container.Enumerable[int]).Elements() + + checks := [...]struct { + name string + input []int + deletions []int + expected int + }{ + {"empty", nil, nil, 0}, + {"simple", si, nil, 5}, + {"half-full", hf, nil, 6}, + {"overwrite element", append(si, bst.Three), nil, 5}, + {"delete nonexistent", si, []int{bst.Six}, 5}, + {"delete existing childless", si, []int{bst.One}, 4}, + {"delete existing with 1 left child", si, []int{bst.Two}, 4}, + {"delete existing with 1 right child", si, []int{bst.Four}, 4}, + {"delete existing with 2 children", hf, []int{bst.Three}, 5}, + } + for _, check := range checks { + t.Run(check.name, func(t *testing.T) { + tree := bst.Intrinsic[int]{} + // In these loops, e is always the same variable: without cloning, + // each iteration reuses the same pointer, overwriting the tree. + for _, e := range check.input { + clone := e + tree.Upsert(&clone) + } + for _, e := range check.deletions { + clone := e + tree.Delete(&clone) + } + if tree.Len() != check.expected { + t.Fatalf("Found len %d, but expected %d", tree.Len(), check.expected) + } + }) + } +} + +func TestIntrinsic_Walk_canceling(t *testing.T) { + tree := bst.Simple() + checks := [...]struct { + name string + walker func(cb container.WalkCB[int]) error + calls1, calls3, calls5 int + }{ + {"in order", tree.WalkInOrder, 1, 3, 5}, + {"post order", tree.WalkPostOrder, 1, 5, 3}, + {"pre order", tree.WalkPreOrder, 3, 1, 5}, + } + tree.WalkInOrder(func(e *int) error { log.Println(*e); return nil }) + stopper := func(at int) container.WalkCB[int] { + called := 0 + return container.WalkCB[int](func(e *int) error { + called++ + if *e == at { + return fmt.Errorf("%d", called) + } + return nil + }) + } + for _, check := range checks { + t.Run(check.name, func(t *testing.T) { + for _, val := range []struct { + input, expected int + }{ + {1, check.calls1}, + {3, check.calls3}, + {5, check.calls5}, + } { + err := check.walker(stopper(val.input)) + actual, err := strconv.Atoi(err.Error()) + if err != nil { + t.Fatalf("unexpected non-int error: %v", err) + } + if actual != val.expected { + t.Fatalf("got %d but expected %d", actual, val.expected) + } + } + }) + } +} diff --git a/binarysearchtree/intrinsic_transparent_test.go b/binarysearchtree/intrinsic_transparent_test.go new file mode 100644 index 0000000..21b92a5 --- /dev/null +++ b/binarysearchtree/intrinsic_transparent_test.go @@ -0,0 +1,125 @@ +package binarysearchtree + +import ( + "fmt" + "testing" + + "github.com/fgm/container" +) + +var ( + One, Two, Three, Four, Five, Six = 1, 2, 3, 4, 5, 6 +) + +// Simple builds this tree: +// +// 3 +// / \ +// 2 4 +// / \ +// 1 5 +func Simple() container.BinarySearchTree[int] { + simple := Intrinsic[int]{} + simple.Upsert(&Three, &Two, &Four, &One, &Five) + return &simple +} + +// HalfFull builds this tree, which contains all deletion cases +// +// 3 +// / \ +// 2 5 +// / / \ +// 1 4 6 +func HalfFull() container.BinarySearchTree[int] { + hf := Intrinsic[int]{} + hf.Upsert(&Three, &Two, &Five, &One, &Four, &Six) + return &hf +} + +func P(e *int) error { + _, err := fmt.Println(*e) + return err +} + +func ExampleIntrinsic_WalkInOrder() { + bst := Simple() + bst.WalkInOrder(P) + // Output: + // 1 + // 2 + // 3 + // 4 + // 5 +} + +func ExampleIntrinsic_WalkPostOrder() { + bst := Simple() + bst.WalkPostOrder(P) + // Output: + // 1 + // 2 + // 5 + // 4 + // 3 +} + +func ExampleIntrinsic_WalkPreOrder() { + bst := Simple() + bst.WalkPreOrder(P) + // Output: + // 3 + // 2 + // 1 + // 4 + // 5 +} + +func TestIntrinsic_Clone(t *testing.T) { + bst := Simple().(*Intrinsic[int]) + clone := bst.Clone().(*Intrinsic[int]) + input := bst.root + output := clone.root + checks := [...]struct { + name string + expected, actual any + }{ + {"root", *input.data, *output.data}, + {"left.data", *input.left.data, *output.left.data}, + {"left.left.data", *input.left.left.data, *output.left.left.data}, + {"left.left.left", input.left.left.left, output.left.left.left}, + {"left.left.right", input.left.left.right, output.left.left.right}, + {"left.right", input.left.right, output.left.right}, + {"right.data", *input.right.data, *output.right.data}, + {"right.left", input.right.left, output.right.left}, + {"right.right.data", *input.right.right.data, *output.right.right.data}, + {"right.right.left", input.right.right.left, output.right.right.left}, + {"right.right.right", input.right.right.right, output.right.right.right}, + } + for _, check := range checks { + t.Run(check.name, func(t *testing.T) { + if check.actual != check.expected { + t.Fatalf("got %v but expected %d", check.actual, check.expected) + } + }) + } +} + +func TestTree_Delete(t *testing.T) { + checks := [...]struct { + name string + delendum int + }{ + {"one: leaf", One}, + {"two: one child", Two}, + {"five: two children", Five}, + {"three: root with two children", Three}, + } + + for _, check := range checks { + t.Run(check.name, func(t *testing.T) { + bst := HalfFull() + bst.Delete(&check.delendum) + }) + } +} diff --git a/types.go b/types.go index 59ac71b..a7673c7 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,9 @@ package container -import "iter" +import ( + "cmp" + "iter" +) // OrderedMap has the same API as a sync.Map for the specific case of OrderedMap[any, any]. type OrderedMap[K comparable, V any] interface { @@ -73,6 +76,8 @@ type Stack[E any] interface { // For concurrency-safe types, it is not atomic vs other operations, // meaning it MUST NOT be used to take decisions, but only as an observability/debugging tool. type Countable interface { + // Len returns the number of elements in a structure. + // Its complexity may be higher than O(1), e.g. O(n) when it relies on Enumerable. Len() int } @@ -87,3 +92,22 @@ type Set[E comparable] interface { Intersection(other Set[E]) Set[E] Difference(other Set[E]) Set[E] } + +// FIXME replace by an iterator-based version like the one in Set.Items. +type Enumerable[E any] interface { + Elements() []E +} + +// BinarySearchTree is a generic binary search tree implementation with no concurrency guarantees. +// Instantiate by a zero value of the implementation. +type BinarySearchTree[E cmp.Ordered] interface { + Clone() BinarySearchTree[E] + Delete(*E) + IndexOf(*E) (int, bool) + Upsert(...*E) []*E + WalkInOrder(cb WalkCB[E]) error + WalkPostOrder(cb WalkCB[E]) error + WalkPreOrder(cb WalkCB[E]) error +} + +type WalkCB[E any] func(*E) error