red-black-tree 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +146 -5
- data/lib/red-black-tree.rb +85 -13
- data/lib/red_black_tree/node/data_delegation.rb +17 -0
- data/lib/red_black_tree/node/left_right_element_referencers.rb +2 -2
- data/lib/red_black_tree/node.rb +2 -0
- data/lib/red_black_tree/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b536af387a65d00a6d05596a8b209b32b500abf5cf156d5ab7169ab0149f6b2
|
4
|
+
data.tar.gz: 0bc6b9317ee4659228b201d5feb0c3146b356c5831ebe00a003d5d4f491020dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea60c092e82dcecf019933df7348db451e10922437d4ac90c325fb1654ad5226738513b4a4d2e2ee45f19d2af86753c7f5932584a66ec0e90bf8c3ea0ce4836a
|
7
|
+
data.tar.gz: c022d30679141753341dee36142b6690756d1797fe11e2dab34b4ddeeaafcc163693a3686bf4e2a324bd60951b869d9cd6d9496d49fed0c2a9fa4c7c3335751a
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.3] - 2024-10-21
|
4
|
+
|
5
|
+
- Make `RedBlackTree#left_most_node` public
|
6
|
+
- Add `RedBlackTree#traverse_pre_order`
|
7
|
+
- Add `RedBlackTree#traverse_in_order`
|
8
|
+
- Add `RedBlackTree#traverse_post_order`
|
9
|
+
- Add `RedBlackTree#traverse_level_order`
|
10
|
+
- Add `RedBlackTree#traverse`, alias of `RedBlackTree#traverse_in_order`
|
11
|
+
- Extend `RedBlackTree#search` to accept a block
|
12
|
+
- Delegate missing `RedBlackTree::Node` methods to its `#data`
|
13
|
+
|
3
14
|
## [0.1.2] - 2024-09-08
|
4
15
|
|
5
16
|
- Fix a bunch of issues in `RedBlackTree#insert!` and `RedBlackTree#delete!` algorithms
|
data/README.md
CHANGED
@@ -37,13 +37,154 @@ until tree.empty?
|
|
37
37
|
end
|
38
38
|
```
|
39
39
|
|
40
|
+
## Performance
|
41
|
+
|
42
|
+
> [!NOTE]
|
43
|
+
> Red-black trees are designed for specific use cases and are not intended as a general-purpose data structure. The
|
44
|
+
comparisons below are provided merely to illustrate the performance characteristics of the gem. However, it is important
|
45
|
+
to note that the benchmarks do not take into account the self-balancing nature of red-black trees.
|
46
|
+
|
47
|
+
### Sort and iterate 10,000 elements
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
require 'benchmark/ips'
|
51
|
+
|
52
|
+
Work = Struct.new :min_latency, keyword_init: true
|
53
|
+
|
54
|
+
class WorkNode < RedBlackTree::Node
|
55
|
+
def <=> other
|
56
|
+
self.data.min_latency <=> other.data.min_latency
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
sample_data = 10_000.times.map { Work.new(min_latency: rand(1_000)) }
|
61
|
+
|
62
|
+
Benchmark.ips do |x|
|
63
|
+
x.report("RedBlackTree") do
|
64
|
+
tree = RedBlackTree.new
|
65
|
+
sample_data.each { |work| tree << WorkNode.new(work); }
|
66
|
+
tree.shift until tree.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
# 1:1 comparison
|
70
|
+
x.report("Array (gradual sort)") do
|
71
|
+
array = []
|
72
|
+
sample_data.each { |work| array << work; array.sort_by!(&:min_latency); }
|
73
|
+
array.shift until array.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
x.report("Array (single sort)") do
|
77
|
+
array = []
|
78
|
+
sample_data.each { |work| array << work; }
|
79
|
+
array.sort_by!(&:min_latency)
|
80
|
+
array.shift until array.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
x.compare!
|
84
|
+
end
|
85
|
+
|
86
|
+
#=> ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
|
87
|
+
#=> Warming up --------------------------------------
|
88
|
+
#=> RedBlackTree 1.000 i/100ms
|
89
|
+
#=> Array (gradual sort) 1.000 i/100ms
|
90
|
+
#=> Array (single sort) 78.000 i/100ms
|
91
|
+
#=> Calculating -------------------------------------
|
92
|
+
#=> RedBlackTree 5.417 (± 0.0%) i/s (184.61 ms/i) - 28.000 in 5.172532s
|
93
|
+
#=> Array (gradual sort) 0.268 (± 0.0%) i/s (3.74 s/i) - 2.000 in 7.473005s
|
94
|
+
#=> Array (single sort) 768.691 (± 2.2%) i/s (1.30 ms/i) - 3.900k in 5.076337s
|
95
|
+
#=>
|
96
|
+
#=> Comparison:
|
97
|
+
#=> Array (single sort): 768.7 i/s
|
98
|
+
#=> RedBlackTree: 5.4 i/s - 141.91x slower
|
99
|
+
#=> Array (gradual sort): 0.3 i/s - 2872.03x slower
|
100
|
+
```
|
101
|
+
|
102
|
+
### Sort and search 10,000 elements
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
require 'benchmark/ips'
|
106
|
+
|
107
|
+
Work = Struct.new :min_latency, keyword_init: true
|
108
|
+
|
109
|
+
class WorkNode < RedBlackTree::Node
|
110
|
+
def <=> other
|
111
|
+
self.data.min_latency <=> other.data.min_latency
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
sample_data = 10_000.times.map { Work.new(min_latency: rand(1_000)) }
|
116
|
+
search_sample = sample_data.sample
|
117
|
+
|
118
|
+
Benchmark.ips do |x|
|
119
|
+
x.report("RedBlackTree#search") do
|
120
|
+
tree = RedBlackTree.new
|
121
|
+
sample_data.each { |work| tree << WorkNode.new(work); }
|
122
|
+
raise unless tree.search search_sample
|
123
|
+
end
|
124
|
+
|
125
|
+
# 1:1 comparison
|
126
|
+
x.report("Array#find (gradual sort)") do
|
127
|
+
array = []
|
128
|
+
sample_data.each { |work| array << work; array.sort_by!(&:min_latency); }
|
129
|
+
raise unless array.find { |work| work.min_latency == search_sample.min_latency }
|
130
|
+
end
|
131
|
+
|
132
|
+
x.report("Array#find (single sort)") do
|
133
|
+
array = []
|
134
|
+
sample_data.each { |work| array << work; }
|
135
|
+
array.sort_by!(&:min_latency)
|
136
|
+
raise unless array.find { |work| work.min_latency == search_sample.min_latency }
|
137
|
+
end
|
138
|
+
|
139
|
+
# 1:1 comparison
|
140
|
+
x.report("Array#bsearch (gradual sort)") do
|
141
|
+
array = []
|
142
|
+
sample_data.each { |work| array << work; array.sort_by!(&:min_latency); }
|
143
|
+
raise unless array.bsearch { |work| search_sample.min_latency <= work.min_latency }
|
144
|
+
end
|
145
|
+
|
146
|
+
x.report("Array#bsearch (single sort)") do
|
147
|
+
array = []
|
148
|
+
sample_data.each { |work| array << work; }
|
149
|
+
array.sort_by!(&:min_latency)
|
150
|
+
raise unless array.bsearch { |work| search_sample.min_latency <= work.min_latency }
|
151
|
+
end
|
152
|
+
|
153
|
+
x.compare!
|
154
|
+
end
|
155
|
+
|
156
|
+
#=> ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
|
157
|
+
#=> Warming up --------------------------------------
|
158
|
+
#=> RedBlackTree#search 1.000 i/100ms
|
159
|
+
#=> Array#find (gradual sort)
|
160
|
+
#=> 1.000 i/100ms
|
161
|
+
#=> Array#find (single sort)
|
162
|
+
#=> 69.000 i/100ms
|
163
|
+
#=> Array#bsearch (gradual sort)
|
164
|
+
#=> 1.000 i/100ms
|
165
|
+
#=> Array#bsearch (single sort)
|
166
|
+
#=> 89.000 i/100ms
|
167
|
+
#=> Calculating -------------------------------------
|
168
|
+
#=> RedBlackTree#search 12.926 (± 0.0%) i/s (77.36 ms/i) - 65.000 in 5.030736s
|
169
|
+
#=> Array#find (gradual sort)
|
170
|
+
#=> 0.262 (± 0.0%) i/s (3.81 s/i) - 2.000 in 7.623953s
|
171
|
+
#=> Array#find (single sort)
|
172
|
+
#=> 690.631 (± 1.0%) i/s (1.45 ms/i) - 3.519k in 5.095823s
|
173
|
+
#=> Array#bsearch (gradual sort)
|
174
|
+
#=> 0.267 (± 0.0%) i/s (3.75 s/i) - 2.000 in 7.492482s
|
175
|
+
#=> Array#bsearch (single sort)
|
176
|
+
#=> 895.413 (± 1.7%) i/s (1.12 ms/i) - 4.539k in 5.070590s
|
177
|
+
#=>
|
178
|
+
#=> Comparison:
|
179
|
+
#=> Array#bsearch (single sort): 895.4 i/s
|
180
|
+
#=> Array#find (single sort): 690.6 i/s - 1.30x slower
|
181
|
+
#=> RedBlackTree#search: 12.9 i/s - 69.27x slower
|
182
|
+
#=> Array#bsearch (gradual sort): 0.3 i/s - 3354.39x slower
|
183
|
+
#=> Array#find (gradual sort): 0.3 i/s - 3412.57x slower
|
184
|
+
```
|
185
|
+
|
40
186
|
## WIP Features
|
41
187
|
|
42
|
-
- `RedBlackTree#traverse_in_order`
|
43
|
-
- `RedBlackTree#traverse_pre_order`
|
44
|
-
- `RedBlackTree#traverse_post_order`
|
45
|
-
- `RedBlackTree#traverse_level_order`
|
46
|
-
- `RedBlackTree#traverse` (default in-order)
|
47
188
|
- `RedBlackTree#max`
|
48
189
|
- `RedBlackTree#height`
|
49
190
|
- `RedBlackTree#clear`
|
data/lib/red-black-tree.rb
CHANGED
@@ -14,7 +14,6 @@ class RedBlackTree
|
|
14
14
|
# @return [RedBlackTree::Node, nil] the root node
|
15
15
|
attr_reader :root
|
16
16
|
|
17
|
-
# @private
|
18
17
|
# @return [RedBlackTree::Node, nil] the left most node
|
19
18
|
attr_reader :left_most_node
|
20
19
|
alias_method :min, :left_most_node
|
@@ -78,8 +77,8 @@ class RedBlackTree
|
|
78
77
|
#
|
79
78
|
# @private ideally this is only used internally e.g. in #<< which has context on the ideal location for the node
|
80
79
|
# @param node [RedBlackTree::Node] the node to be inserted
|
81
|
-
# @param target_parent [RedBlackTree::Node
|
82
|
-
# @param direction ["left", "right"
|
80
|
+
# @param target_parent [RedBlackTree::Node] the parent under which the node should be inserted
|
81
|
+
# @param direction ["left", "right"] the direction of the node relative to the parent
|
83
82
|
# @return [RedBlackTree] self
|
84
83
|
def insert! node, target_parent = nil, direction = nil
|
85
84
|
raise ArgumentError, "cannot insert leaf node" if node.instance_of? LeafNode
|
@@ -221,11 +220,18 @@ class RedBlackTree
|
|
221
220
|
# Searches for a node which matches the given data/value.
|
222
221
|
#
|
223
222
|
# @param data [any] the data to search for
|
223
|
+
# @yield [RedBlackTree::Node] the block to be used for comparison
|
224
224
|
# @return [RedBlackTree::Node, nil] the matching node
|
225
|
-
def search data
|
226
|
-
|
225
|
+
def search data = nil, &block
|
226
|
+
if block_given?
|
227
|
+
raise ArgumentError, "provide either data or block, not both" if data
|
228
|
+
|
229
|
+
_search_by_block block, @root
|
230
|
+
else
|
231
|
+
raise ArgumentError, "data must be provided for search" unless data
|
227
232
|
|
228
|
-
|
233
|
+
_search_by_data data, @root
|
234
|
+
end
|
229
235
|
end
|
230
236
|
|
231
237
|
# Returns true if there is a node which matches the given data/value, and false if there is not.
|
@@ -235,6 +241,64 @@ class RedBlackTree
|
|
235
241
|
!!search(data)
|
236
242
|
end
|
237
243
|
|
244
|
+
# Traverses the tree in pre-order and yields each node.
|
245
|
+
#
|
246
|
+
# @param node [RedBlackTree::Node] the node to start the traversal from
|
247
|
+
# @yield [RedBlackTree::Node] the block to be executed for each node
|
248
|
+
# @return [void]
|
249
|
+
def traverse_pre_order node = @root, &block
|
250
|
+
return if node.nil? || node.leaf?
|
251
|
+
|
252
|
+
block.call node
|
253
|
+
traverse_pre_order node.left, &block
|
254
|
+
traverse_pre_order node.right, &block
|
255
|
+
end
|
256
|
+
|
257
|
+
# Traverses the tree in in-order and yields each node.
|
258
|
+
#
|
259
|
+
# @param node [RedBlackTree::Node] the node to start the traversal from
|
260
|
+
# @yield [RedBlackTree::Node] the block to be executed for each node
|
261
|
+
# @return [void]
|
262
|
+
def traverse_in_order node = @root, &block
|
263
|
+
return if node.nil? || node.leaf?
|
264
|
+
|
265
|
+
traverse_in_order node.left, &block
|
266
|
+
block.call node
|
267
|
+
traverse_in_order node.right, &block
|
268
|
+
end
|
269
|
+
alias_method :traverse, :traverse_in_order
|
270
|
+
|
271
|
+
# Traverses the tree in post-order and yields each node.
|
272
|
+
#
|
273
|
+
# @param node [RedBlackTree::Node] the node to start the traversal from
|
274
|
+
# @yield [RedBlackTree::Node] the block to be executed for each node
|
275
|
+
# @return [void]
|
276
|
+
def traverse_post_order node = @root, &block
|
277
|
+
return if node.nil? || node.leaf?
|
278
|
+
|
279
|
+
traverse_post_order node.left, &block
|
280
|
+
traverse_post_order node.right, &block
|
281
|
+
block.call node
|
282
|
+
end
|
283
|
+
|
284
|
+
# Traverses the tree in level-order and yields each node.
|
285
|
+
#
|
286
|
+
# @yield [RedBlackTree::Node] the block to be executed for each node
|
287
|
+
# @return [void]
|
288
|
+
def traverse_level_order &block
|
289
|
+
return if @root.nil?
|
290
|
+
|
291
|
+
queue = [@root]
|
292
|
+
until queue.empty?
|
293
|
+
node = queue.shift
|
294
|
+
next if node.nil? || node.leaf?
|
295
|
+
|
296
|
+
block.call node
|
297
|
+
queue << node.left
|
298
|
+
queue << node.right
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
238
302
|
private
|
239
303
|
|
240
304
|
# Rotates a (sub-)tree starting from the given node in the given direction.
|
@@ -263,15 +327,23 @@ class RedBlackTree
|
|
263
327
|
opp_direction_child
|
264
328
|
end
|
265
329
|
|
266
|
-
def
|
267
|
-
|
268
|
-
|
330
|
+
def _search_by_block block, node
|
331
|
+
traverse node do |current|
|
332
|
+
next if current.leaf?
|
333
|
+
|
334
|
+
return current if block.call current
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def _search_by_data data, node
|
339
|
+
return if node.nil? || node.leaf?
|
340
|
+
return node if data == node.data
|
269
341
|
|
270
|
-
mock_node =
|
271
|
-
if mock_node >=
|
272
|
-
|
342
|
+
mock_node = node.class.new data
|
343
|
+
if mock_node >= node
|
344
|
+
_search_by_data data, node.right
|
273
345
|
else
|
274
|
-
|
346
|
+
_search_by_data data, node.left
|
275
347
|
end
|
276
348
|
end
|
277
349
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedBlackTree
|
4
|
+
module DataDelegation
|
5
|
+
def method_missing method_name, *args, &block
|
6
|
+
if @data.respond_to? method_name
|
7
|
+
@data.public_send method_name, *args, &block
|
8
|
+
else
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def respond_to_missing? method_name, include_private = false
|
14
|
+
@data.respond_to? method_name, include_private || super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -7,7 +7,7 @@ class RedBlackTree
|
|
7
7
|
case direction
|
8
8
|
when Node::LEFT then @left
|
9
9
|
when Node::RIGHT then @right
|
10
|
-
else raise ArgumentError, "Direction must be one of #{DIRECTIONS}"
|
10
|
+
else raise ArgumentError, "Direction must be one of #{Implementation::DIRECTIONS}"
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
@@ -15,7 +15,7 @@ class RedBlackTree
|
|
15
15
|
case direction
|
16
16
|
when Node::LEFT then @left = node
|
17
17
|
when Node::RIGHT then @right = node
|
18
|
-
else raise ArgumentError, "Direction must be one of #{DIRECTIONS}"
|
18
|
+
else raise ArgumentError, "Direction must be one of #{Implementation::DIRECTIONS}"
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
data/lib/red_black_tree/node.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative "utils"
|
4
4
|
require_relative "node/leaf_node_comparable"
|
5
|
+
require_relative "node/data_delegation"
|
5
6
|
require_relative "node/left_right_element_referencers"
|
6
7
|
|
7
8
|
class RedBlackTree
|
@@ -13,6 +14,7 @@ class RedBlackTree
|
|
13
14
|
end
|
14
15
|
|
15
16
|
include Comparable
|
17
|
+
include DataDelegation
|
16
18
|
|
17
19
|
# @return [any] the data/value representing the node
|
18
20
|
attr_reader :data
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: red-black-tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Young
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -23,6 +23,7 @@ files:
|
|
23
23
|
- README.md
|
24
24
|
- lib/red-black-tree.rb
|
25
25
|
- lib/red_black_tree/node.rb
|
26
|
+
- lib/red_black_tree/node/data_delegation.rb
|
26
27
|
- lib/red_black_tree/node/leaf_node.rb
|
27
28
|
- lib/red_black_tree/node/leaf_node_comparable.rb
|
28
29
|
- lib/red_black_tree/node/left_right_element_referencers.rb
|
@@ -51,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
52
|
- !ruby/object:Gem::Version
|
52
53
|
version: '0'
|
53
54
|
requirements: []
|
54
|
-
rubygems_version: 3.5.
|
55
|
+
rubygems_version: 3.5.16
|
55
56
|
signing_key:
|
56
57
|
specification_version: 4
|
57
58
|
summary: Red-Black Tree Data Structure for Ruby
|