Skip to content

Commit

Permalink
Add Delete and DeletePrefix
Browse files Browse the repository at this point in the history
  • Loading branch information
snorwin committed Jun 13, 2021
1 parent c3812dc commit 44dad5e
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 176 deletions.
74 changes: 52 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,14 @@
# gorax
**gorax** is a Go [radix tree](https://en.wikipedia.org/wiki/Radix_tree) implementation inspired by the ANSI C [Rax](https://github.com/antirez/rax) radix tree.

## Example Tree
![example](example.svg)
```go
t := gorax.FromMap(map[string]interface{}{
"alligator": nil,
"alien": 1,
"baloon": 2,
"chromodynamic": 3,
"romane": 4,
"romanus": 5,
"romulus": 6,
"rubens": 7,
"ruber": 8,
"rubicon": 9,
"rubicundus": "a",
"all": "b",
"rub": "c",
"ba": "d",
})
```
## Documentation
The full documentation is available on [Godoc](https://pkg.go.dev/github.com/snorwin/gorax).

## Performance
The **gorax** `Insert` and `Get` are `O(k)` operations. In many cases, this can be faster than a hash table because the hash function is an `O(k)` operation too and in the worst case depending on how collisions are handled it may take even longer. Furthermore, hash tables have very poor cache locality.
The **gorax** `Insert`, `Delete` and `Get` are `O(k)` operations. In many cases, this can be faster than a hash table because the hash function is an `O(k)` operation too and in the worst case depending on how collisions are handled it may take even longer. Furthermore, hash tables have very poor cache locality.

### Benchmark
The benchmark demonstrates well that the insertion and get times are independent of the number of elements stored in the **gorax** radix tree. In average insert and operations are taking less than `500ns/op` measured on an 8 Core (11th Gen i7) Intel CPU.
The benchmark demonstrates well that the insertion, deletion and get times are independent of the number of elements stored in the **gorax** radix tree. In average insert and operations are taking less than `500ns/op` measured on an 8 Core (11th Gen i7) Intel CPU.
```
goos: linux
goarch: amd64
Expand All @@ -52,7 +34,55 @@ BenchmarkGet10-8 2489961 466.6 ns/op
BenchmarkGet100-8 306944 3282 ns/op
BenchmarkGet1000-8 60535 31144 ns/op
BenchmarkGet10000-8 5689 358072 ns/op
BenchmarkDelete1-8 6051354 267.8 ns/op
BenchmarkDelete10-8 2085792 581.9 ns/op
BenchmarkDelete100-8 342584 3543 ns/op
BenchmarkDelete1000-8 38193 31167 ns/op
BenchmarkDelete10000-8 3726 326149 ns/op
```
## Examples
### Find longest prefix
```go
// Create a tree
t := gorax.New()
_ = t.Insert("foo", nil)
_ = t.Insert("bar", 1)
_ = t.Insert("foobar", 2)

// Find the longest prefix match
prefix, _, _ := t.LongestPrefix("foojin")
fmt.Println(prefix)
```
```
foo
```

### Create a gorax Tree from `map`
```go
// Create a tree
t := gorax.FromMap(map[string]interface{}{
"alligator": nil,
"alien": 1,
"baloon": 2,
"chromodynamic": 3,
"romane": 4,
"romanus": 5,
"romulus": 6,
"rubens": 7,
"ruber": 8,
"rubicon": 9,
"rubicundus": "a",
"all": "b",
"rub": "c",
"ba": "d",
})

// Convert to DOT graph
fmt.Println(t.ToDOTGraph().String())
```
#### [DOT Graph](example.dot) representation of the Tree
![](example.svg)
*(leaf nodes: blue color, compressed nodes: green color, key nodes: rectangular shape)*

## Trivia
In Star Wars **gorax** are a seldom-seen species of humanoids of gigantic proportion that are native to the mountains of Endor.
4 changes: 2 additions & 2 deletions dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"github.com/emicklei/dot"
)

// ToDotGraph walks the Tree and converts it into a dot.Graph
func (t *Tree) ToDotGraph() *dot.Graph {
// ToDOTGraph walks the Tree and converts it into a dot.Graph
func (t *Tree) ToDOTGraph() *dot.Graph {
// create new dot graph
graph := dot.NewGraph(dot.Directed)

Expand Down
4 changes: 2 additions & 2 deletions dot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
var example string

var _ = Describe("Tree", func() {
Context("ToDotGraph", func() {
Context("ToDOTGraph", func() {
It("should_generate_graph_regression_test", func() {
t := gorax.FromMap(map[string]interface{}{
"alligator": nil,
Expand All @@ -33,7 +33,7 @@ var _ = Describe("Tree", func() {
"ba": "d",
})

Ω(strings.ReplaceAll(t.ToDotGraph().String(), "\t\n", "\n")).Should(Equal(example))
Ω(strings.ReplaceAll(t.ToDOTGraph().String(), "\t\n", "\n")).Should(Equal(example))
})
})
})
42 changes: 42 additions & 0 deletions benchmark_test.go → gorax_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ func BenchmarkGet10000(b *testing.B) {
benchmarkGet(b, 10000)
}

func BenchmarkDelete1(b *testing.B) {
benchmarkDelete(b, 1)
}

func BenchmarkDelete10(b *testing.B) {
benchmarkDelete(b, 10)
}

func BenchmarkDelete100(b *testing.B) {
benchmarkDelete(b, 100)
}

func BenchmarkDelete1000(b *testing.B) {
benchmarkDelete(b, 1000)
}
func BenchmarkDelete10000(b *testing.B) {
benchmarkDelete(b, 10000)
}

func benchmarkInsert(b *testing.B, size int) {
keys := make([]string, size)
for i := 0; i < size; i++ {
Expand Down Expand Up @@ -93,3 +112,26 @@ func benchmarkGet(b *testing.B, size int) {
b.StopTimer()
}
}

func benchmarkDelete(b *testing.B, size int) {
keys := make([]string, size)
for i := 0; i < size; i++ {
keys[i] = randString(rand.Intn(BenchmarkMaxKeySize))
}

t := gorax.New()
for j := 0; j < size; j++ {
t.Insert(keys[j], "")
}

b.StopTimer()
b.ResetTimer()

for i := 0; i < b.N; i++ {
b.StartTimer()
for j := 0; j < size; j++ {
t.Delete(keys[j])
}
b.StopTimer()
}
}
193 changes: 193 additions & 0 deletions gorax_fuzzy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package gorax_test

import (
"math/rand"
"sort"
"strings"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"github.com/snorwin/gorax"
)

const (
FuzzyTestSize = 1000
FuzzyMaxKeySize = 256
)

var _ = Describe("Fuzzy Tests", func() {
Context("Insert/Get/Delete", func() {
It("should_insert_get_and_delete", func() {
t := gorax.New()

m := make(map[string]interface{}, FuzzyTestSize)
for i := 0; i < FuzzyTestSize; i++ {
key := randString(rand.Intn(FuzzyMaxKeySize))
value := randInteface()

_, expected := m[key]
m[key] = value

actual := !t.Insert(key, value)
Ω(actual).Should(Equal(expected))

Ω(t.ToMap()).Should(Equal(m))
Ω(t.Len()).Should(Equal(len(m)))
}

for key, value := range m {
actual, ok := t.Get(key)
Ω(ok).Should(BeTrue())

if value == nil {
Ω(actual).Should(BeNil())
} else {
Ω(actual).Should(Equal(value))
}
}

for key := range m {
expected := m[key]
delete(m, key)

actual, ok := t.Delete(key)
Ω(ok).Should(BeTrue())

if expected == nil {
Ω(actual).Should(BeNil())
} else {
Ω(actual).Should(Equal(expected))
}

Ω(t.ToMap()).Should(Equal(m))
Ω(t.Len()).Should(Equal(len(m)))
}
})
})
Context("Minimum/Maximum", func() {
var (
t *gorax.Tree
m map[string]interface{}

keys []string
)
BeforeEach(func() {
m = make(map[string]interface{}, FuzzyTestSize)
for i := 0; i < FuzzyTestSize; i++ {
m[randString(rand.Intn(FuzzyMaxKeySize))] = randInteface()
}

t = gorax.FromMap(m)

keys = []string{}
for key := range m {
keys = append(keys, key)
}

sort.Strings(keys)
})
It("should_find_minimum", func() {
for i := 0; i < len(keys); i++ {
expected := m[keys[i]]

key, actual, ok := t.Minimum()
Ω(ok).Should(BeTrue())
Ω(key).Should(Equal(keys[i]))

if expected == nil {
Ω(actual).Should(BeNil())
} else {
Ω(actual).Should(Equal(expected))
}

t.Delete(key)
}
})
It("should_find_maximum", func() {
for i := len(keys) - 1; i >= 0; i-- {
expected := m[keys[i]]

key, actual, ok := t.Maximum()
Ω(ok).Should(BeTrue())
Ω(key).Should(Equal(keys[i]))

if expected == nil {
Ω(actual).Should(BeNil())
} else {
Ω(actual).Should(Equal(expected))
}

t.Delete(key)
}
})
})
Context("Walk", func() {
It("should_walk_prefix", func() {
for i := 0; i < FuzzyTestSize; i++ {
prefix := randString(rand.Intn(24))

size := 200
m := make(map[string]interface{}, 2*size)
expected := make(map[string]interface{})
for j := 0; j < size; j++ {
key := randString(rand.Intn(FuzzyMaxKeySize))
m[key] = randInteface()

if strings.HasPrefix(key, prefix) {
expected[key] = m[key]
}
}
for j := 0; j < size; j++ {
key := prefix + randString(rand.Intn(FuzzyMaxKeySize))
m[key] = randInteface()
expected[key] = m[key]
}

t := gorax.FromMap(m)

actual := make(map[string]interface{})
t.WalkPrefix(prefix, func(key string, value interface{}) bool {
actual[key] = value

return false
})

Ω(actual).Should(Equal(expected))
}
})
It("should_walk_path", func() {
for i := 0; i < FuzzyTestSize; i++ {
slice := make([]string, 24)
for j := 0; j < len(slice); j++ {
slice[j] = randString(rand.Intn(FuzzyMaxKeySize))
}
m := map[string]interface{}{}
for j := len(slice); j >= 0; j-- {
m[strings.Join(slice[:j], "")] = i
}

t := gorax.FromMap(m)

path := strings.Join(slice, "")
for j := 0; j <= len(path); j++ {
expected := map[string]interface{}{}
for k, v := range m {
if strings.HasPrefix(path[:j], k) {
expected[k] = v
}
}

actual := make(map[string]interface{})
t.WalkPath(path[:j], func(key string, value interface{}) bool {
actual[key] = value

return false
})

Ω(actual).Should(Equal(expected))
}
}
})
})
})
Loading

0 comments on commit 44dad5e

Please sign in to comment.