Skip to content

Commit 3b2338e

Browse files
author
Dennis Kuhnert
committed
Add RangeSearch, Add package kdrange
1 parent 13f0706 commit 3b2338e

File tree

5 files changed

+277
-24
lines changed

5 files changed

+277
-24
lines changed

README.md

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@
1010
A [k-d tree](https://en.wikipedia.org/wiki/K-d_tree) implementation in Go with:
1111
- n-dimensional points
1212
- k-nearest neighbor search
13+
- range search
1314
- remove without rebuilding the whole subtree
1415
- data attached to the points
1516
- using own structs by implementing a simple 2 function interface
1617

17-
Future features:
18-
- range search
19-
2018

2119
## Usage
2220

@@ -29,7 +27,6 @@ import "github.com/kyroy/kdtree"
2927
````
3028

3129

32-
3330
### Implement the `kdtree.Point` interface
3431

3532
```go
@@ -64,12 +61,27 @@ func main() {
6461
// KNN (k-nearest neighbor)
6562
fmt.Println(tree.KNN(&points.Point{Coordinates: []float64{1, 1, 1}}, 2))
6663
// [{3.00 1.00} {5.00 0.00}]
67-
68-
// other
69-
fmt.Println(tree)
70-
// [[[<nil> {3.00 1.00} {1.00 8.00}] {5.00 0.00} [<nil> {8.00 3.00} {7.00 5.00}]]]
64+
65+
// RangeSearch
66+
fmt.Println(tree.RangeSearch(kdrange.New(1, 8, 0, 2)))
67+
// [{5.00 0.00} {3.00 1.00}]
68+
69+
// Points
7170
fmt.Println(tree.Points())
7271
// [{3.00 1.00} {1.00 8.00} {5.00 0.00} {8.00 3.00} {7.00 5.00}]
72+
73+
// Remove
74+
fmt.Println(tree.Remove(&points.Point2D{X: 5, Y: 0}))
75+
// {5.00 0.00}
76+
77+
// String
78+
fmt.Println(tree)
79+
// [[{1.00 8.00} {3.00 1.00} [<nil> {8.00 3.00} {7.00 5.00}]]]
80+
81+
// Balance
82+
tree.Balance()
83+
fmt.Println(tree)
84+
// [[[{3.00 1.00} {1.00 8.00} <nil>] {7.00 5.00} {8.00 3.00}]]
7385
}
7486
```
7587

@@ -81,9 +93,9 @@ type Data struct {
8193
8294
func main() {
8395
tree := kdtree.New([]kdtree.Point{
84-
points.NewPoint([]float64{7, 2, 3}, Data{value: "first"}),
85-
points.NewPoint([]float64{3, 7, 10}, Data{value: "second"}),
86-
points.NewPoint([]float64{4, 6, 1}, Data{value: "third"}),
96+
points.NewPoint([]float64{7, 2, 3}, Data{value: "first"}),
97+
points.NewPoint([]float64{3, 7, 10}, Data{value: "second"}),
98+
points.NewPoint([]float64{4, 6, 1}, Data{value: "third"}),
8799
})
88100
89101
// Insert
@@ -94,10 +106,25 @@ func main() {
94106
fmt.Println(tree.KNN(&points.Point{Coordinates: []float64{1, 1, 1}}, 2))
95107
// [{[4 6 1] {third}} {[7 2 3] {first}}]
96108
97-
// other
98-
fmt.Println(tree)
99-
// [[{[3 7 10] {second}} {[4 6 1] {third}} [{[8 1 0] {fifth}} {[7 2 3] {first}} {[12 4 6] {fourth}}]]]
109+
// RangeSearch
110+
fmt.Println(tree.RangeSearch(kdrange.New(1, 15, 1, 5, 0, 5)))
111+
// [{[7 2 3] {first}} {[8 1 0] {fifth}}]
112+
113+
// Points
100114
fmt.Println(tree.Points())
101115
// [{[3 7 10] {second}} {[4 6 1] {third}} {[8 1 0] {fifth}} {[7 2 3] {first}} {[12 4 6] {fourth}}]
116+
117+
// Remove
118+
fmt.Println(tree.Remove(points.NewPoint([]float64{3, 7, 10}, nil)))
119+
// {[3 7 10] {second}}
120+
121+
// String
122+
fmt.Println(tree)
123+
// [[<nil> {[4 6 1] {third}} [{[8 1 0] {fifth}} {[7 2 3] {first}} {[12 4 6] {fourth}}]]]
124+
125+
// Balance
126+
tree.Balance()
127+
fmt.Println(tree)
128+
// [[[{[7 2 3] {first}} {[4 6 1] {third}} <nil>] {[8 1 0] {fifth}} {[12 4 6] {fourth}}]]
102129
}
103130
```

kdrange/range.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2018 Dennis Kuhnert
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Package kdrange contains k-dimensional range struct and helpers.
18+
package kdrange
19+
20+
// Range represents a range in k-dimensional space.
21+
type Range [][2]float64
22+
23+
// New creates a new Range.
24+
//
25+
// It accepts a sequence of min/max pairs that define the Range.
26+
// For example a 2-dimensional rectangle with the with 2 and height 3, starting at (1,2):
27+
//
28+
// r := NewRange(1, 3, 2, 5)
29+
//
30+
// I.e.:
31+
// x (dim 0): 1 <= x <= 3
32+
// y (dim 1): 2 <= y <= 5
33+
func New(limits ...float64) Range {
34+
if limits == nil || len(limits)%2 != 0 {
35+
return nil
36+
}
37+
r := make([][2]float64, len(limits)/2)
38+
for i := range r {
39+
r[i] = [2]float64{limits[2*i], limits[2*i+1]}
40+
}
41+
return r
42+
}

kdrange/range_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2018 Dennis Kuhnert
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package kdrange_test
18+
19+
import (
20+
"testing"
21+
22+
"github.com/kyroy/kdtree/kdrange"
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestNewRange(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
input []float64
30+
output kdrange.Range
31+
}{
32+
{
33+
name: "nil",
34+
input: nil,
35+
output: nil,
36+
},
37+
{
38+
name: "uneven",
39+
input: []float64{0},
40+
output: nil,
41+
},
42+
{
43+
name: "empty",
44+
input: []float64{},
45+
output: [][2]float64{},
46+
},
47+
{
48+
name: "2d",
49+
input: []float64{1, 2, 3, 4},
50+
output: [][2]float64{{1, 2}, {3, 4}},
51+
},
52+
{
53+
name: "3d",
54+
input: []float64{4, 1, 6, 19, 3, 1},
55+
output: [][2]float64{{4, 1}, {6, 19}, {3, 1}},
56+
},
57+
}
58+
for _, test := range tests {
59+
t.Run(test.name, func(t *testing.T) {
60+
assert.Equal(t, test.output, kdrange.New(test.input...))
61+
})
62+
}
63+
}

kdtree.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"math"
2323
"sort"
2424

25+
"github.com/kyroy/kdtree/kdrange"
2526
"github.com/kyroy/priority-queue"
2627
)
2728

@@ -116,6 +117,7 @@ func (t *KDTree) Points() []Point {
116117
}
117118

118119
// KNN returns the k-nearest neighbours of the given point.
120+
// The points are sorted by the distance to the given points. Starting with the nearest.
119121
func (t *KDTree) KNN(p Point, k int) []Point {
120122
if t.root == nil || p == nil || k == 0 {
121123
return []Point{}
@@ -133,10 +135,22 @@ func (t *KDTree) KNN(p Point, k int) []Point {
133135
return points
134136
}
135137

138+
// RangeSearch returns all points in the given range r.
139+
//
140+
// Returns an empty slice when input is nil or len(r) does not equal Point.Dimensions().
141+
func (t *KDTree) RangeSearch(r kdrange.Range) []Point {
142+
if t.root == nil || r == nil || len(r) != t.root.Dimensions() {
143+
return []Point{}
144+
}
145+
146+
return t.root.RangeSearch(r, 0)
147+
}
148+
136149
func knn(p Point, k int, start *node, currentAxis int, nearestPQ *pq.PriorityQueue) {
137150
if p == nil || k == 0 || start == nil {
138151
return
139152
}
153+
140154
var path []*node
141155
currentNode := start
142156

@@ -353,3 +367,24 @@ func (n *node) FindLargest(axis int, largest *node) *node {
353367
}
354368
return largest
355369
}
370+
371+
func (n *node) RangeSearch(r kdrange.Range, axis int) []Point {
372+
points := []Point{}
373+
374+
for dim, limit := range r {
375+
if limit[0] > n.Dimension(dim) || limit[1] < n.Dimension(dim) {
376+
goto checkChildren
377+
}
378+
}
379+
points = append(points, n.Point)
380+
381+
checkChildren:
382+
if n.Left != nil && n.Dimension(axis) >= r[axis][0] {
383+
points = append(points, n.Left.RangeSearch(r, (axis+1)%n.Dimensions())...)
384+
}
385+
if n.Right != nil && n.Dimension(axis) <= r[axis][1] {
386+
points = append(points, n.Right.RangeSearch(r, (axis+1)%n.Dimensions())...)
387+
}
388+
389+
return points
390+
}

0 commit comments

Comments
 (0)