diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 582037c..f470204 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,39 +1,59 @@ name: CI - on: push: - branches: [master] + branches: ["main", "master", "devel"] paths: - - '**.go' - - '**.yml' - + - '**.go' + - '**.yml' pull_request: - workflow_dispatch: - -jobs: + branches: ["main"] - build: +jobs: + test: + strategy: + matrix: + go-version: ['1.20'] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - uses: actions/checkout@v3 + - run: go test -v ./... + + govulncheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.18 - check-latest: true + - uses: golang/govulncheck-action@v1 + with: + go-version-input: 1.20 + check-latest: true + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: 1.20 - - name: Build - run: go build -v ./... + - name: Test Coverage + run: go test -v -coverprofile=profile.cov ./... - - name: Test Coverage - run: go test -v -coverprofile=profile.cov ./... + - uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: profile.cov - - uses: shogo82148/actions-goveralls@v1 - with: - path-to-profile: profile.cov - - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: latest + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: 1.20 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest diff --git a/README.md b/README.md index 2e5b8e5..cd7d039 100644 --- a/README.md +++ b/README.md @@ -6,32 +6,16 @@ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +## API CHANGE !!! + +The API has changed from v0.1.0 to v0.2.0 + ## Overview `package interval` is an immutable datastructure for fast lookups in one dimensional intervals. The implementation is based on treaps, augmented for intervals. Treaps are randomized self balancing binary search trees. -Immutability is achieved because insert/delete will return a new treap which will share some nodes with the original treap. -All nodes are read-only after creation, allowing concurrent readers to operate safely with concurrent writers. - -The time complexity is **O(log(n))** or **O(k+log(n))** where k is the number of returned items, the space complexity is **O(n)**. - -``` -Insert() O(log(n)) -Delete() O(log(n)) -Find() O(log(n)) -Intersects() O(log(n)) -CoverLCP() O(log(n)) -CoverSCP() O(log(n)) - -Covers() O(k+log(n)) -CoveredBy() O(k+log(n)) -Precedes() O(k+log(n)) -PrecededBy() O(k+log(n)) -Intersections() O(k+log(n)) -``` - The author is propably the first (in december 2022) using augmented treaps as a very promising [data structure] for the representation of dynamic IP address tables for arbitrary ranges, that enables most and least specific range matching and even more lookup methods @@ -79,15 +63,18 @@ To apply this library to types of one-dimensional intervals, you must provide a import "github.com/gaissmai/interval" type Tree[T any] struct{ ... } - func NewTree[T any](cmp func(a, b T) (ll, rr, lr, rl int), items ...T) Tree[T] + func NewTree[T any](cmp func(a, b T) (ll, rr, lr, rl int), items ...T) *Tree[T] - func NewTreeConcurrent[T any](jobs int, cmp func(a, b T) (ll, rr, lr, rl int), items ...T) Tree[T] + func NewTreeConcurrent[T any](jobs int, cmp func(a, b T) (ll, rr, lr, rl int), items ...T) *Tree[T] - func (t Tree[T]) Insert(items ...T) Tree[T] - func (t Tree[T]) Delete(item T) (Tree[T], bool) + func (t *Tree[T]) Insert(items ...T) + func (t *Tree[T]) Delete(item T) bool + func (t *Tree[T]) Union(other *Tree[T], overwrite bool) - func (t *Tree[T]) InsertMutable(items ...T) - func (t *Tree[T]) DeleteMutable(item T) bool + func (t Tree[T]) InsertImmutable(items ...T) *Tree[T] + func (t Tree[T]) DeleteImmutable(item T) (*Tree[T], bool) + func (t Tree[T]) UnionImmutable(other *Tree[T], overwrite bool) *Tree[T] + func (t Tree[T]) Clone() *Tree[T] func (t Tree[T]) Find(item T) (result T, ok bool) func (t Tree[T]) CoverLCP(item T) (result T, ok bool) @@ -102,9 +89,6 @@ To apply this library to types of one-dimensional intervals, you must provide a func (t Tree[T]) Intersections(item T) []T - func (t Tree[T]) Clone() Tree[T] - func (t Tree[T]) Union(other Tree[T], overwrite bool, immutable bool) Tree[T] - func (t Tree[T]) Visit(start, stop T, visitFn func(item T) bool) func (t Tree[T]) Fprint(w io.Writer) error func (t Tree[T]) String() string @@ -135,21 +119,14 @@ goos: linux goarch: amd64 pkg: github.com/gaissmai/interval cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz -BenchmarkInsert/Into1-8 7488871 189 ns/op 128 B/op 2 allocs/op -BenchmarkInsert/Into10-8 3276116 398 ns/op 256 B/op 4 allocs/op -BenchmarkInsert/Into100-8 1496136 951 ns/op 512 B/op 8 allocs/op -BenchmarkInsert/Into1_000-8 533299 1959 ns/op 960 B/op 15 allocs/op -BenchmarkInsert/Into10_000-8 727627 1398 ns/op 832 B/op 13 allocs/op -BenchmarkInsert/Into100_000-8 342596 3339 ns/op 1600 B/op 25 allocs/op -BenchmarkInsert/Into1_000_000-8 396890 3681 ns/op 1728 B/op 27 allocs/op - -BenchmarkInsertMutable/Into1-8 10415164 128 ns/op 64 B/op 1 allocs/op -BenchmarkInsertMutable/Into10-8 5160836 245 ns/op 64 B/op 1 allocs/op -BenchmarkInsertMutable/Into100-8 2707705 443 ns/op 64 B/op 1 allocs/op -BenchmarkInsertMutable/Into1_000-8 1694250 723 ns/op 64 B/op 1 allocs/op -BenchmarkInsertMutable/Into10_000-8 1204220 963 ns/op 64 B/op 1 allocs/op -BenchmarkInsertMutable/Into100_000-8 966566 1249 ns/op 64 B/op 1 allocs/op -BenchmarkInsertMutable/Into1_000_000-8 645523 1863 ns/op 64 B/op 1 allocs/op + +BenchmarkInsert/Into1-8 10415164 128 ns/op 64 B/op 1 allocs/op +BenchmarkInsert/Into10-8 5160836 245 ns/op 64 B/op 1 allocs/op +BenchmarkInsert/Into100-8 2707705 443 ns/op 64 B/op 1 allocs/op +BenchmarkInsert/Into1_000-8 1694250 723 ns/op 64 B/op 1 allocs/op +BenchmarkInsert/Into10_000-8 1204220 963 ns/op 64 B/op 1 allocs/op +BenchmarkInsert/Into100_000-8 966566 1249 ns/op 64 B/op 1 allocs/op +BenchmarkInsert/Into1_000_000-8 645523 1863 ns/op 64 B/op 1 allocs/op ``` ### Delete @@ -162,24 +139,19 @@ goos: linux goarch: amd64 pkg: github.com/gaissmai/interval cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz -BenchmarkDelete/DeleteFrom10-8 8018518 150 ns/op 128 B/op 2 allocs/op -BenchmarkDelete/DeleteFrom100-8 2349710 708 ns/op 448 B/op 7 allocs/op -BenchmarkDelete/DeleteFrom1_000-8 616173 2051 ns/op 1536 B/op 24 allocs/op -BenchmarkDelete/DeleteFrom10_000-8 446180 2362 ns/op 1856 B/op 29 allocs/op -BenchmarkDelete/DeleteFrom100_000-8 272798 4224 ns/op 2816 B/op 44 allocs/op -BenchmarkDelete/DeleteFrom1_000_000-8 231808 5897 ns/op 3520 B/op 55 allocs/op - -BenchmarkDeleteMutable/DeleteFrom10-8 7682869 156 ns/op 0 B/op 0 allocs/op -BenchmarkDeleteMutable/DeleteFrom100-8 13009023 92 ns/op 0 B/op 0 allocs/op -BenchmarkDeleteMutable/DeleteFrom1_000-8 1912417 627 ns/op 0 B/op 0 allocs/op -BenchmarkDeleteMutable/DeleteFrom10_000-8 1362752 889 ns/op 0 B/op 0 allocs/op -BenchmarkDeleteMutable/DeleteFrom100_000-8 893157 1334 ns/op 0 B/op 0 allocs/op -BenchmarkDeleteMutable/DeleteFrom1_000_000-8 647199 1828 ns/op 0 B/op 0 allocs/op + +BenchmarkDelete/DeleteFrom10-8 7682869 156 ns/op 0 B/op 0 allocs/op +BenchmarkDelete/DeleteFrom100-8 13009023 92 ns/op 0 B/op 0 allocs/op +BenchmarkDelete/DeleteFrom1_000-8 1912417 627 ns/op 0 B/op 0 allocs/op +BenchmarkDelete/DeleteFrom10_000-8 1362752 889 ns/op 0 B/op 0 allocs/op +BenchmarkDelete/DeleteFrom100_000-8 893157 1334 ns/op 0 B/op 0 allocs/op +BenchmarkDelete/DeleteFrom1_000_000-8 647199 1828 ns/op 0 B/op 0 allocs/op ``` ### Lookups -The benchmark for `CoverLCP()` (a.k.a. longest-prefix-match if the interval is an IP CIDR prefix) is very promising: +The benchmark for `CoverLCP()` (also known as longest-prefix-match when the interval is an IP-CIDR prefix) is very +promising, as it is a generalized algorithm that is not specifically optimized only for IP address lookup: ``` $ go test -benchmem -bench='CoverLCP' @@ -187,6 +159,7 @@ goos: linux goarch: amd64 pkg: github.com/gaissmai/interval cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz + BenchmarkCoverLCP/In100-8 4833073 239 ns/op 0 B/op 0 allocs/op BenchmarkCoverLCP/In1_000-8 3714054 323 ns/op 0 B/op 0 allocs/op BenchmarkCoverLCP/In10_000-8 1897353 630 ns/op 0 B/op 0 allocs/op @@ -203,6 +176,7 @@ goos: linux goarch: amd64 pkg: github.com/gaissmai/interval cpu: Intel(R) Core(TM) i5-7500T CPU @ 2.70GHz + BenchmarkIntersects/In1-4 58757540 19 ns/op 0 B/op 0 allocs/op BenchmarkIntersects/In10-4 27267051 40 ns/op 0 B/op 0 allocs/op BenchmarkIntersects/In100-4 17559418 60 ns/op 0 B/op 0 allocs/op @@ -220,6 +194,7 @@ goos: linux goarch: amd64 pkg: github.com/gaissmai/interval cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz + BenchmarkFind/In100-8 17299449 63 ns/op 0 B/op 0 allocs/op BenchmarkFind/In1_000-8 17327350 69 ns/op 0 B/op 0 allocs/op BenchmarkFind/In10_000-8 12858908 90 ns/op 0 B/op 0 allocs/op diff --git a/bench_test.go b/bench_test.go index 572542a..0cded2b 100644 --- a/bench_test.go +++ b/bench_test.go @@ -17,7 +17,7 @@ var intMap = map[int]string{ 1_000_000: "1_000_000", } -func BenchmarkInsert(b *testing.B) { +func BenchmarkInsertImmutable(b *testing.B) { for n := 1; n <= 1_000_000; n *= 10 { tree := interval.NewTree(cmpUintInterval, genUintIvals(n)...) @@ -27,13 +27,13 @@ func BenchmarkInsert(b *testing.B) { b.Run(name, func(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _ = tree.Insert(probe) + _ = tree.InsertImmutable(probe) } }) } } -func BenchmarkInsertMutable(b *testing.B) { +func BenchmarkInsert(b *testing.B) { for n := 1; n <= 1_000_000; n *= 10 { tree := interval.NewTree(cmpUintInterval, genUintIvals(n)...) probe := genUintIvals(1)[0] @@ -42,13 +42,13 @@ func BenchmarkInsertMutable(b *testing.B) { b.Run(name, func(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - (&tree).InsertMutable(probe) + tree.Insert(probe) } }) } } -func BenchmarkDelete(b *testing.B) { +func BenchmarkDeleteImmutable(b *testing.B) { for n := 1; n <= 1_000_000; n *= 10 { ivals := genUintIvals(n) probe := ivals[rand.Intn(len(ivals))] @@ -59,13 +59,13 @@ func BenchmarkDelete(b *testing.B) { b.Run(name, func(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = tree.Delete(probe) + _, _ = tree.DeleteImmutable(probe) } }) } } -func BenchmarkDeleteMutable(b *testing.B) { +func BenchmarkDelete(b *testing.B) { for n := 1; n <= 1_000_000; n *= 10 { ivals := genUintIvals(n) probe := ivals[rand.Intn(len(ivals))] @@ -76,7 +76,7 @@ func BenchmarkDeleteMutable(b *testing.B) { b.Run(name, func(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _ = (&tree).DeleteMutable(probe) + _ = tree.Delete(probe) } }) } @@ -105,13 +105,13 @@ func BenchmarkUnionImmutable(b *testing.B) { b.Run(name, func(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _ = this100_000.Union(tree, false, true) + _ = this100_000.UnionImmutable(tree, false) } }) } } -func BenchmarkUnionMutable(b *testing.B) { +func BenchmarkUnion(b *testing.B) { for n := 10; n <= 100_000; n *= 10 { this100_000 := interval.NewTree(cmpUintInterval, genUintIvals(100_000)...) tree := interval.NewTree(cmpUintInterval, genUintIvals(n)...) @@ -120,7 +120,7 @@ func BenchmarkUnionMutable(b *testing.B) { b.Run(name, func(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _ = this100_000.Union(tree, false, false) + this100_000.Union(tree, false) } }) } diff --git a/go.mod b/go.mod index 18dacb9..5f70dea 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/gaissmai/interval -go 1.18 +go 1.20 diff --git a/helpers.go b/helpers.go index 58eb666..af6d14f 100644 --- a/helpers.go +++ b/helpers.go @@ -71,28 +71,27 @@ func (t Tree[T]) String() string { // // example: IP CIDRs as intervals // -// ▼ -// ├─ 0.0.0.0/0 -// │ ├─ 10.0.0.0/8 -// │ │ ├─ 10.0.0.0/24 -// │ │ └─ 10.0.1.0/24 -// │ ├─ 127.0.0.0/8 -// │ │ └─ 127.0.0.1/32 -// │ ├─ 169.254.0.0/16 -// │ ├─ 172.16.0.0/12 -// │ └─ 192.168.0.0/16 -// │ └─ 192.168.1.0/24 -// └─ ::/0 -// ├─ ::1/128 -// ├─ 2000::/3 -// │ └─ 2001:db8::/32 -// ├─ fc00::/7 -// ├─ fe80::/10 -// └─ ff00::/8 +// ▼ +// ├─ 0.0.0.0/0 +// │ ├─ 10.0.0.0/8 +// │ │ ├─ 10.0.0.0/24 +// │ │ └─ 10.0.1.0/24 +// │ ├─ 127.0.0.0/8 +// │ │ └─ 127.0.0.1/32 +// │ ├─ 169.254.0.0/16 +// │ ├─ 172.16.0.0/12 +// │ └─ 192.168.0.0/16 +// │ └─ 192.168.1.0/24 +// └─ ::/0 +// ├─ ::1/128 +// ├─ 2000::/3 +// │ └─ 2001:db8::/32 +// ├─ fc00::/7 +// ├─ fe80::/10 +// └─ ff00::/8 // // If the interval items don't implement fmt.Stringer they are stringified with // their default format %v. -// func (t Tree[T]) Fprint(w io.Writer) error { // pcm = parent-child-mapping var pcm parentChildsMap[T] @@ -159,18 +158,17 @@ func (t *Tree[T]) hierarchyStringify(w io.Writer, n *node[T], pcm parentChildsMa // // e.g. with left/right, item priority [prio] and pointers [this|left|right] // -// R 0...5 [prio:0.9405] [0xc000024940|l:0xc000024140|r:0xc000024980] -// ├─l 0...6 [prio:0.6047] [0xc000024140|l:0x0|r:0x0] -// └─r 1...4 [prio:0.6868] [0xc000024980|l:0xc000024440|r:0xc000024900] -// ├─l 1...8 [prio:0.6646] [0xc000024440|l:0x0|r:0xc000024480] -// │ └─r 1...7 [prio:0.4377] [0xc000024480|l:0x0|r:0xc0000244c0] -// │ └─r 1...5 [prio:0.4246] [0xc0000244c0|l:0x0|r:0x0] -// └─r 7...9 [prio:0.5152] [0xc000024900|l:0xc0000249c0|r:0x0] -// └─l 6...7 [prio:0.3009] [0xc0000249c0|l:0xc000024880|r:0x0] -// └─l 2...7 [prio:0.1565] [0xc000024880|l:0xc000024680|r:0xc0000248c0] -// ├─l 2...8 [prio:0.06564] [0xc000024680|l:0x0|r:0x0] -// └─r 4...8 [prio:0.09697] [0xc0000248c0|l:0x0|r:0x0] -// +// R 0...5 [prio:0.9405] [0xc000024940|l:0xc000024140|r:0xc000024980] +// ├─l 0...6 [prio:0.6047] [0xc000024140|l:0x0|r:0x0] +// └─r 1...4 [prio:0.6868] [0xc000024980|l:0xc000024440|r:0xc000024900] +// ├─l 1...8 [prio:0.6646] [0xc000024440|l:0x0|r:0xc000024480] +// │ └─r 1...7 [prio:0.4377] [0xc000024480|l:0x0|r:0xc0000244c0] +// │ └─r 1...5 [prio:0.4246] [0xc0000244c0|l:0x0|r:0x0] +// └─r 7...9 [prio:0.5152] [0xc000024900|l:0xc0000249c0|r:0x0] +// └─l 6...7 [prio:0.3009] [0xc0000249c0|l:0xc000024880|r:0x0] +// └─l 2...7 [prio:0.1565] [0xc000024880|l:0xc000024680|r:0xc0000248c0] +// ├─l 2...8 [prio:0.06564] [0xc000024680|l:0x0|r:0x0] +// └─r 4...8 [prio:0.09697] [0xc0000248c0|l:0x0|r:0x0] func (t Tree[T]) FprintBST(w io.Writer) error { if t.root == nil { return nil @@ -232,19 +230,19 @@ func (t *Tree[T]) binarytreeStringify(w io.Writer, n *node[T], pad string) error // parentChildsMap, needed for interval tree printing, this is not BST printing! // // Interval tree, parent->childs relation printed. A parent interval covers a child interval. -// ▼ -// ├─ 0...6 -// │ └─ 0...5 -// ├─ 1...8 -// │ ├─ 1...7 -// │ │ └─ 1...5 -// │ │ └─ 1...4 -// │ └─ 2...8 -// │ ├─ 2...7 -// │ └─ 4...8 -// │ └─ 6...7 -// └─ 7...9 // +// ▼ +// ├─ 0...6 +// │ └─ 0...5 +// ├─ 1...8 +// │ ├─ 1...7 +// │ │ └─ 1...5 +// │ │ └─ 1...4 +// │ └─ 2...8 +// │ ├─ 2...7 +// │ └─ 4...8 +// │ └─ 6...7 +// └─ 7...9 type parentChildsMap[T any] struct { pcMap map[*node[T]][]*node[T] // parent -> []child map stack []*node[T] // just needed for the algo @@ -300,7 +298,6 @@ func (t *Tree[T]) pcmForNode(n *node[T], pcm parentChildsMap[T]) parentChildsMap // Note: This is for debugging and testing purposes only during development in semver // 0.x.y. In future versions this will be removed without increasing the main // semantic version, so please do not rely on it for now. -// func (t Tree[T]) Statistics() (size int, maxDepth int, average, deviation float64) { // key is depth, value is the sum of nodes with this depth depths := make(map[int]int) @@ -363,13 +360,14 @@ func (t Tree[T]) Max() (max T) { // or if start > stop, then the order is reversed. The visit function is called for each item. // // For example, the entire tree can be traversed as follows -// t.Visit(t.Min(), t.Max(), visitFn) +// +// t.Visit(t.Min(), t.Max(), visitFn) // // or in reverse order by -// t.Visit(t.Max(), t.Min(), visitFn). // -// The traversion terminates prematurely if the visit function returns false. +// t.Visit(t.Max(), t.Min(), visitFn). // +// The traversion terminates prematurely if the visit function returns false. func (t Tree[T]) Visit(start, stop T, visitFn func(item T) bool) { if t.root == nil { return @@ -393,9 +391,10 @@ func (t Tree[T]) Visit(start, stop T, visitFn func(item T) bool) { } // Clone, deep cloning of the tree structure. -func (t Tree[T]) Clone() Tree[T] { - t.root = t.clone(t.root) - return t +func (t Tree[T]) Clone() *Tree[T] { + c := NewTree[T](t.cmp) + c.root = t.clone(t.root) + return c } // clone rec-descent diff --git a/treap.go b/treap.go index ea28e16..16b354a 100644 --- a/treap.go +++ b/treap.go @@ -35,16 +35,15 @@ type Tree[T any] struct { // NewTree initializes the interval tree with the compare function and items from type T. // -// cmp(a, b T) (ll, rr, lr, rl int) +// cmp(a, b T) (ll, rr, lr, rl int) // // The result of cmp() must be four int values: // -// ll: left point interval a compared with left point interval b (-1, 0, +1) -// rr: right point interval a compared with right point interval b (-1, 0, +1) -// lr: left point interval a compared with right point interval b (-1, 0, +1) -// rl: right point interval a compared with left point interval b (-1, 0, +1) -// -func NewTree[T any](cmp func(a, b T) (ll, rr, lr, rl int), items ...T) Tree[T] { +// ll: left point interval a compared with left point interval b (-1, 0, +1) +// rr: right point interval a compared with right point interval b (-1, 0, +1) +// lr: left point interval a compared with right point interval b (-1, 0, +1) +// rl: right point interval a compared with left point interval b (-1, 0, +1) +func NewTree[T any](cmp func(a, b T) (ll, rr, lr, rl int), items ...T) *Tree[T] { var t Tree[T] t.cmp = cmp @@ -53,12 +52,12 @@ func NewTree[T any](cmp func(a, b T) (ll, rr, lr, rl int), items ...T) Tree[T] { t.root = t.insert(t.root, t.makeNode(items[i]), false) } - return t + return &t } // NewTreeConcurrent, convenience function for initializing the interval tree for large inputs (> 100_000). // A good value reference for jobs is the number of logical CPUs usable by the current process. -func NewTreeConcurrent[T any](jobs int, cmp func(a, b T) (ll, rr, lr, rl int), items ...T) Tree[T] { +func NewTreeConcurrent[T any](jobs int, cmp func(a, b T) (ll, rr, lr, rl int), items ...T) *Tree[T] { // define a min chunk size, don't split in too small chunks const minChunkSize = 25_000 @@ -75,7 +74,7 @@ func NewTreeConcurrent[T any](jobs int, cmp func(a, b T) (ll, rr, lr, rl int), i var wg sync.WaitGroup var chunk []T - partialTrees := make(chan Tree[T]) + partialTrees := make(chan *Tree[T]) // fan out for ; l > 0; l = len(items) { @@ -105,8 +104,8 @@ func NewTreeConcurrent[T any](jobs int, cmp func(a, b T) (ll, rr, lr, rl int), i // fan in t := NewTree[T](cmp) for other := range partialTrees { - // fast union, immutable is false - t = t.Union(other, false, false) + // fast union + t.Union(other, false) } return t } @@ -127,19 +126,19 @@ func (n *node[T]) copyNode() *node[T] { return &c } -// Insert elements into the tree, returns the new Tree. +// InsertImmutable elements into the tree, returns the new Tree. // If an element is a duplicate, it replaces the previous element. -func (t Tree[T]) Insert(items ...T) Tree[T] { +func (t Tree[T]) InsertImmutable(items ...T) *Tree[T] { for i := range items { t.root = t.insert(t.root, t.makeNode(items[i]), true) } - return t + return &t } -// InsertMutable inserts items into the tree, changing the original tree. +// Insert inserts items into the tree, changing the original tree. // If the original tree does not need to be preserved then this is much faster than the immutable insert. -func (t *Tree[T]) InsertMutable(items ...T) { +func (t *Tree[T]) Insert(items ...T) { for i := range items { t.root = t.insert(t.root, t.makeNode(items[i]), false) } @@ -214,19 +213,19 @@ func (t *Tree[T]) insert(n, m *node[T], immutable bool) *node[T] { return n } -// Delete removes an item if it exists, returns the new tree and true, false if not found. -func (t Tree[T]) Delete(item T) (Tree[T], bool) { +// DeleteImmutable removes an item if it exists, returns the new tree and true, false if not found. +func (t Tree[T]) DeleteImmutable(item T) (*Tree[T], bool) { // split/join must be immutable l, m, r := t.split(t.root, item, true) t.root = (&t).join(l, r, true) ok := m != nil - return t, ok + return &t, ok } -// DeleteMutable removes an item from tree, returns true if it exists, false otherwise. +// Delete removes an item from tree, returns true if it exists, false otherwise. // If the original tree does not need to be preserved then this is much faster than the immutable delete. -func (t *Tree[T]) DeleteMutable(item T) bool { +func (t *Tree[T]) Delete(item T) bool { l, m, r := t.split(t.root, item, false) t.root = t.join(l, r, false) @@ -240,9 +239,13 @@ func (t *Tree[T]) DeleteMutable(item T) bool { // // To create very large trees, it may be time-saving to slice the input data into chunks, // fan out for creation and combine the generated subtrees with non-immutable unions. -func (t Tree[T]) Union(other Tree[T], overwrite bool, immutable bool) Tree[T] { - t.root = t.union(t.root, other.root, overwrite, immutable) - return t +func (t *Tree[T]) Union(other *Tree[T], overwrite bool) { + t.root = t.union(t.root, other.root, overwrite, false) +} + +func (t Tree[T]) UnionImmutable(other *Tree[T], overwrite bool) *Tree[T] { + t.root = t.union(t.root, other.root, overwrite, true) + return &t } // union combines to treaps. @@ -358,59 +361,58 @@ func (t Tree[T]) Find(item T) (result T, ok bool) { // // The meaning of 'LCP' is best explained with examples: // -// A, B and C covers the item, but B has longest-common-prefix (LCP) with item. +// A, B and C covers the item, but B has longest-common-prefix (LCP) with item. // -// ------LCP--->| +// ------LCP--->| // -// Item |----| +// Item |----| // -// A |------------------------| -// B |---------------------------| -// C |---------------| -// D |--| +// A |------------------------| +// B |---------------------------| +// C |---------------| +// D |--| // -// e.g. for this interval tree +// e.g. for this interval tree // -// ▼ -// ├─ 0...6 -// │ └─ 0...5 -// ├─ 1...8 -// │ ├─ 1...7 -// │ │ └─ 1...5 -// │ │ └─ 1...4 -// │ └─ 2...8 -// │ ├─ 2...7 -// │ └─ 4...8 -// │ └─ 6...7 -// └─ 7...9 +// ▼ +// ├─ 0...6 +// │ └─ 0...5 +// ├─ 1...8 +// │ ├─ 1...7 +// │ │ └─ 1...5 +// │ │ └─ 1...4 +// │ └─ 2...8 +// │ ├─ 2...7 +// │ └─ 4...8 +// │ └─ 6...7 +// └─ 7...9 // -// tree.CoverLCP(ival{0,5}) returns ival{0,5}, true -// tree.CoverLCP(ival{3,6}) returns ival{2,7}, true -// tree.CoverLCP(ival{6,9}) returns ival{}, false +// tree.CoverLCP(ival{0,5}) returns ival{0,5}, true +// tree.CoverLCP(ival{3,6}) returns ival{2,7}, true +// tree.CoverLCP(ival{6,9}) returns ival{}, false // // If the interval tree consists of IP CIDRs, CoverLCP is identical to the // longest-prefix-match. // -// example: IP CIDRs as intervals -// -// ▼ -// ├─ 0.0.0.0/0 -// │ ├─ 10.0.0.0/8 -// │ │ ├─ 10.0.0.0/24 -// │ │ └─ 10.0.1.0/24 -// │ └─ 127.0.0.0/8 -// │ └─ 127.0.0.1/32 -// └─ ::/0 -// ├─ ::1/128 -// ├─ 2000::/3 -// │ └─ 2001:db8::/32 -// ├─ fc00::/7 -// ├─ fe80::/10 -// └─ ff00::/8 -// -// tree.CoverLCP("10.0.1.17/32") returns "10.0.1.0/24", true -// tree.CoverLCP("2001:7c0:3100::/40") returns "2000::/3", true -// +// example: IP CIDRs as intervals +// +// ▼ +// ├─ 0.0.0.0/0 +// │ ├─ 10.0.0.0/8 +// │ │ ├─ 10.0.0.0/24 +// │ │ └─ 10.0.1.0/24 +// │ └─ 127.0.0.0/8 +// │ └─ 127.0.0.1/32 +// └─ ::/0 +// ├─ ::1/128 +// ├─ 2000::/3 +// │ └─ 2001:db8::/32 +// ├─ fc00::/7 +// ├─ fe80::/10 +// └─ ff00::/8 +// +// tree.CoverLCP("10.0.1.17/32") returns "10.0.1.0/24", true +// tree.CoverLCP("2001:7c0:3100::/40") returns "2000::/3", true func (t Tree[T]) CoverLCP(item T) (result T, ok bool) { return t.lcp(t.root, item) } @@ -462,37 +464,36 @@ func (t *Tree[T]) lcp(n *node[T], item T) (result T, ok bool) { // // The meaning of 'SCP' is best explained with examples: // -// A, B and C covers the item, but A has shortest-common-prefix (SCP) with item. -// -// --SCP-->| +// A, B and C covers the item, but A has shortest-common-prefix (SCP) with item. // -// Item |----| +// --SCP-->| // -// A |------------------------| -// B |---------------------------| -// C |---------------| -// D |-----------------| +// Item |----| // -// e.g. for this interval tree +// A |------------------------| +// B |---------------------------| +// C |---------------| +// D |-----------------| // -// ▼ -// ├─ 0...6 -// │ └─ 0...5 -// ├─ 1...8 -// │ ├─ 1...7 -// │ │ └─ 1...5 -// │ │ └─ 1...4 -// │ └─ 2...8 -// │ ├─ 2...7 -// │ └─ 4...8 -// │ └─ 6...7 -// └─ 7...9 +// e.g. for this interval tree // -// tree.CoverSCP(ival{0,6}) returns ival{0,6}, true -// tree.CoverSCP(ival{0,5}) returns ival{0,6}, true -// tree.CoverSCP(ival{3,7}) returns ival{1,8}, true -// tree.CoverSCP(ival{6,9}) returns ival{}, false +// ▼ +// ├─ 0...6 +// │ └─ 0...5 +// ├─ 1...8 +// │ ├─ 1...7 +// │ │ └─ 1...5 +// │ │ └─ 1...4 +// │ └─ 2...8 +// │ ├─ 2...7 +// │ └─ 4...8 +// │ └─ 6...7 +// └─ 7...9 // +// tree.CoverSCP(ival{0,6}) returns ival{0,6}, true +// tree.CoverSCP(ival{0,5}) returns ival{0,6}, true +// tree.CoverSCP(ival{3,7}) returns ival{1,8}, true +// tree.CoverSCP(ival{6,9}) returns ival{}, false func (t Tree[T]) CoverSCP(item T) (result T, ok bool) { l, m, _ := t.split(t.root, item, true) result, ok = t.scp(l, item) @@ -685,17 +686,16 @@ func (t *Tree[T]) intersections(n *node[T], item T) (result []T) { // Precedes returns all intervals that precedes the item. // The returned intervals are in sorted order. // -// example: +// example: // -// Item |-----------------| +// Item |-----------------| // -// A |---------------------------------------| -// B |-----| -// C |-----------------| -// D |-----------------| -// -// Precedes(item) => [D, B] +// A |---------------------------------------| +// B |-----| +// C |-----------------| +// D |-----------------| // +// Precedes(item) => [D, B] func (t Tree[T]) Precedes(item T) []T { // split, reduce the search space l, _, _ := t.split(t.root, item, true) @@ -728,17 +728,16 @@ func (t *Tree[T]) precedes(n *node[T], item T) (result []T) { // PrecededBy returns all intervals that are preceded by the item. // The returned intervals are in sorted order. // -// example: -// -// Item |-----| +// example: // -// A |---------------------------------------| -// B |-----| -// C |-----------------| -// D |-----------------| +// Item |-----| // -// PrecededBy(item) => [B, D] +// A |---------------------------------------| +// B |-----| +// C |-----------------| +// D |-----------------| // +// PrecededBy(item) => [B, D] func (t Tree[T]) PrecededBy(item T) []T { // split, reduce the search space _, _, r := t.split(t.root, item, true) diff --git a/treap_test.go b/treap_test.go index 3cca902..b4b33f1 100644 --- a/treap_test.go +++ b/treap_test.go @@ -60,13 +60,13 @@ func equals(a, b uintInterval) bool { return a[0] == b[0] && a[1] == b[1] } -func equalStatistics(t1, t2 interval.Tree[uintInterval]) bool { +func equalStatistics(t1, t2 *interval.Tree[uintInterval]) bool { a1, b1, c1, d1 := t1.Statistics() a2, b2, c2, d2 := t2.Statistics() return a1 == a2 && b1 == b2 && c1 == c2 && d1 == d2 } -func equalsSizeAndOrder[T any](t1, t2 interval.Tree[T]) bool { +func equalsSizeAndOrder[T any](t1, t2 *interval.Tree[T]) bool { var t1InOrder []T t1.Visit(t1.Min(), t1.Max(), func(item T) bool { t1InOrder = append(t1InOrder, item) @@ -120,7 +120,7 @@ func TestNewTree(t *testing.T) { t.Errorf("Find(), got: %v, want: false", ok) } - if _, ok := tree.Delete(zeroItem); ok { + if _, ok := tree.DeleteImmutable(zeroItem); ok { t.Errorf("Delete(), got: %v, want: false", ok) } @@ -132,7 +132,7 @@ func TestNewTree(t *testing.T) { t.Errorf("CoverSCP(), got: %v, want: false", ok) } - if size, _, _, _ := tree.Insert(zeroItem).Statistics(); size != 1 { + if size, _, _, _ := tree.InsertImmutable(zeroItem).Statistics(); size != 1 { t.Errorf("Insert(), got: %v, want: 1", size) } @@ -262,15 +262,15 @@ func TestImmutable(t *testing.T) { t.Parallel() tree1 := interval.NewTree(cmpUintInterval, ps...) - if _, ok := tree1.Delete(tree1.Min()); !ok { + if _, ok := tree1.DeleteImmutable(tree1.Min()); !ok { t.Fatal("Delete, could not delete min item") } - if _, ok := tree1.Delete(tree1.Min()); !ok { + if _, ok := tree1.DeleteImmutable(tree1.Min()); !ok { t.Fatal("Delete changed receiver") } item := uintInterval{111, 666} - _ = tree1.Insert(item) + _ = tree1.InsertImmutable(item) if _, ok := tree1.Find(item); ok { t.Fatal("Insert changed receiver") @@ -288,16 +288,16 @@ func TestMutable(t *testing.T) { min := tree1.Min() var ok bool - if ok = (&tree1).DeleteMutable(min); !ok { - t.Fatal("DeleteMutable, could not delete min item") + if ok = tree1.Delete(min); !ok { + t.Fatal("Delete, could not delete min item") } if equalStatistics(tree1, clone) { - t.Fatal("DeleteMutable didn't change receiver") + t.Fatal("Delete didn't change receiver") } - if ok = (&tree1).DeleteMutable(min); ok { - t.Fatal("DeleteMutable didn't change receiver") + if ok = tree1.Delete(min); ok { + t.Fatal("Delete didn't change receiver") } // reset @@ -309,10 +309,10 @@ func TestMutable(t *testing.T) { } item := uintInterval{111, 666} - (&tree1).InsertMutable(item) + tree1.Insert(item) if _, ok := tree1.Find(item); !ok { - t.Fatal("InsertMutable didn't changed receiver") + t.Fatal("Insert didn't changed receiver") } } @@ -831,8 +831,8 @@ func TestUnion(t *testing.T) { tree1 := interval.NewTree(cmpUintInterval) for i := range ps { - b := tree1.Insert(ps[i]) - tree1 = tree1.Union(b, false, true) + b := tree1.InsertImmutable(ps[i]) + tree1 = tree1.UnionImmutable(b, false) } asStr := `▼ @@ -855,8 +855,8 @@ func TestUnion(t *testing.T) { // now with dupe overwrite for i := range ps { - b := tree1.Insert(ps[i]) - tree1 = tree1.Union(b, true, true) + b := tree1.InsertImmutable(ps[i]) + tree1 = tree1.UnionImmutable(b, true) } if tree1.String() != asStr { @@ -877,8 +877,8 @@ func TestUnion(t *testing.T) { {7, 9}, } - tree2 := tree1.Insert(ps2...) - tree1 = tree1.Union(tree2, false, false) + tree2 := tree1.InsertImmutable(ps2...) + tree1.Union(tree2, false) asStr = `▼ ├─ 0...6 @@ -959,7 +959,7 @@ func TestMatch(t *testing.T) { t.Run(probe.String(), func(t *testing.T) { t.Parallel() - tree1 := tree1.Insert(probe) + tree1 := tree1.InsertImmutable(probe) if _, ok := tree1.Find(probe); !ok { t.Fatalf("inserted item not found in tree: %v", probe) @@ -1011,14 +1011,14 @@ func TestMissing(t *testing.T) { t.Run(probe.String(), func(t *testing.T) { t.Parallel() - tree1 := tree1.Insert(probe) + tree1 := tree1.InsertImmutable(probe) var ok bool if _, ok = tree1.Find(probe); !ok { t.Fatalf("inserted item not found in tree: %v", probe) } - if tree1, ok = tree1.Delete(probe); !ok { + if tree1, ok = tree1.DeleteImmutable(probe); !ok { t.Fatalf("delete, inserted item not found in tree: %v", probe) }