red-black-tree 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 291198bed7dd82b504c47b1c6fa047674c9302a7c529d33409d61797a0677929
4
- data.tar.gz: ab8b8d513625de270d0126c0c3f1d5d787260b32f1bd0f13927332e9e2640aab
3
+ metadata.gz: 06ad66aa31594940e116b62cd872c61dc65ebb90964e097d84b4d540e9eefa2d
4
+ data.tar.gz: 74ae5eae6622755db6e1df89fe03e598e8c9c3bd22ce20ebe363464c60c6f7f6
5
5
  SHA512:
6
- metadata.gz: aed7c4bd38468df481ebf815605f8564bc66ecfb8b7a53ee5babefaad2f68fc0a08994a9e91922f161f628a23d63eff02e57c7418bd7ce80b6db4f95723aa421
7
- data.tar.gz: 2ec1c08f09e3ef1ca7bf608032cfb4a28042e57ec357dd40ee1c8951c64e0c7d944f7900db4bf9acc758513070f5533745e4b6a3fa4477567644de73cc62787c
6
+ metadata.gz: a17c8c5c6e2aab3cd53736d00ba3367b402450383b9f8a078669887d2590eed699e4d559dd588c18d90a0ce4fcde64b6a4c9aee8fc6452ab2d663e50c79b11bb
7
+ data.tar.gz: cfcd5d572e43089fce345cab92d8327c3002f6c282f422316dc16d48020672ac8a62f878b33a67d73f55027498df1f36fa5d1f7526f31a943b0860338a7f00c5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.9] - 2026-05-10
4
+
5
+ - Add `Enumerable` to `RedBlackTree`
6
+ - Fix `RedBlackTree#clear!` not detaching nodes
7
+ - Fix `LeafNodeComparable` being prepended to `LeafNode`
8
+
9
+ ## [0.1.8] - 2025-07-26
10
+
11
+ - Refactor to slim down and de-duplicate some methods
12
+
3
13
  ## [0.1.7] - 2025-01-05
4
14
 
5
15
  - Change `RedBlackTree#delete!` return value to the deleted node
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # RedBlackTree
2
2
 
3
+ ![Version](https://img.shields.io/gem/v/red-black-tree)
4
+ ![Build](https://badge.buildkite.com/6ccbd68c23960899d1deafdb4cfaa96f1e8c04ad6e198e193b.svg)
5
+
3
6
  [Red-black tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) data structure for Ruby.
4
7
 
5
8
  ## Installation
@@ -83,20 +86,20 @@ Benchmark.ips do |x|
83
86
  x.compare!
84
87
  end
85
88
 
86
- #=> ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin24]
89
+ #=> ruby 4.0.3 (2026-04-21 revision 85ddef263a) +YJIT +PRISM [arm64-darwin23]
87
90
  #=> Warming up --------------------------------------
88
91
  #=> RedBlackTree 1.000 i/100ms
89
92
  #=> Array (gradual sort) 1.000 i/100ms
90
- #=> Array (single sort) 100.000 i/100ms
93
+ #=> Array (single sort) 81.000 i/100ms
91
94
  #=> Calculating -------------------------------------
92
- #=> RedBlackTree 16.6846.0%) i/s (59.94 ms/i) - 84.000 in 5.053688s
93
- #=> Array (gradual sort) 0.270 (± 0.0%) i/s (3.71 s/i) - 2.000 in 7.418650s
94
- #=> Array (single sort) 1.006k (± 2.4%) i/s (994.28 μs/i) - 5.100k in 5.073834s
95
- #=>
95
+ #=> RedBlackTree 12.1708.2%) i/s (82.17 ms/i) - 61.000 in 5.023274s
96
+ #=> Array (gradual sort) 0.208 (± 0.0%) i/s (4.82 s/i) - 2.000 in 9.632733s
97
+ #=> Array (single sort) 815.7621.2%) i/s (1.23 ms/i) - 4.131k in 5.064788s
98
+ #=>
96
99
  #=> Comparison:
97
- #=> Array (single sort): 1005.8 i/s
98
- #=> RedBlackTree: 16.7 i/s - 60.28x slower
99
- #=> Array (gradual sort): 0.3 i/s - 3729.44x slower
100
+ #=> Array (single sort): 815.8 i/s
101
+ #=> RedBlackTree: 12.2 i/s - 67.03x slower
102
+ #=> Array (gradual sort): 0.2 i/s - 3928.64x slower
100
103
  ```
101
104
 
102
105
  ### Sort and search 10,000 elements
@@ -119,7 +122,7 @@ Benchmark.ips do |x|
119
122
  x.report("RedBlackTree#search") do
120
123
  tree = RedBlackTree.new
121
124
  sample_data.each { |work| tree << WorkNode.new(work); }
122
- raise unless tree.search search_sample
125
+ raise unless tree.search { |node| node.data == search_sample }
123
126
  end
124
127
 
125
128
  # 1:1 comparison
@@ -6,6 +6,7 @@ require_relative "red_black_tree/node/leaf_node"
6
6
 
7
7
  class RedBlackTree
8
8
  include Utils
9
+ include Enumerable
9
10
 
10
11
  # @return [Integer] the number of valid/non-leaf nodes
11
12
  attr_reader :size
@@ -106,27 +107,7 @@ class RedBlackTree
106
107
  node.parent = target_parent
107
108
  node.red!
108
109
 
109
- while node.parent && node.parent.red? do
110
- if node.parent.sibling && node.parent.sibling.red?
111
- node.parent.black!
112
- node.parent.sibling.black!
113
- node.parent.parent.red!
114
- node = node.parent.parent
115
- else
116
- opp_direction = node.opposite_position
117
- if node.parent.position == opp_direction
118
- rotate_sub_tree! node.parent, opp_direction
119
- node = node[opp_direction]
120
- end
121
-
122
- opp_direction = node.opposite_position
123
- rotate_sub_tree! node.parent.parent, opp_direction
124
- node.parent.black!
125
- node.parent[opp_direction].red!
126
- end
127
-
128
- @root.black!
129
- end
110
+ rebalance_after_insertion! node
130
111
  end
131
112
 
132
113
  node.validate! @root == node
@@ -153,71 +134,11 @@ class RedBlackTree
153
134
  original_node = node
154
135
 
155
136
  if node.children_are_valid?
156
- is_root = is_root? node
157
-
158
- successor = node.left
159
- successor = successor.left until successor.left.leaf?
160
- node.swap_colour_with! successor
161
- node.swap_position_with! successor
162
- node.swap_position_with! LeafNode.new
163
-
164
- @root = successor if is_root
137
+ delete_node_with_two_children! node
165
138
  elsif node.single_child_is_valid?
166
- is_root = is_root? node
167
-
168
- valid_child = node.children.find(&:valid?)
169
- valid_child.black!
170
- node.swap_position_with! valid_child
171
- node.swap_position_with! LeafNode.new
172
-
173
- @root = valid_child if is_root
139
+ delete_node_with_one_child! node
174
140
  elsif node.children_are_leaves?
175
- if is_root? node
176
- @root = nil
177
- elsif node.red?
178
- node.swap_position_with! LeafNode.new
179
- else
180
- loop do
181
- if node.sibling && node.sibling.valid? && node.sibling.red?
182
- node.parent.red!
183
- node.sibling.black!
184
- rotate_sub_tree! node.parent, node.position
185
- end
186
-
187
- if node.close_nephew && node.close_nephew.valid? && node.close_nephew.red?
188
- node.sibling.red! unless node.sibling.leaf?
189
- node.close_nephew.black!
190
- rotate_sub_tree! node.sibling, node.opposite_position
191
- end
192
-
193
- if node.distant_nephew && node.distant_nephew.valid? && node.distant_nephew.red?
194
- case node.parent.colour
195
- when Node::RED then node.sibling.red!
196
- when Node::BLACK then node.sibling.black!
197
- end
198
- node.parent.black!
199
- node.distant_nephew.black!
200
- rotate_sub_tree! node.parent, node.position
201
-
202
- break
203
- end
204
-
205
- if node.parent && node.parent.red?
206
- node.sibling.red! unless node.sibling.leaf?
207
- node.parent.black!
208
-
209
- break
210
- end
211
-
212
- if node.sibling && !node.sibling.leaf?
213
- node.sibling.red!
214
- end
215
-
216
- break unless node = node.parent
217
- end
218
-
219
- original_node.swap_position_with! LeafNode.new
220
- end
141
+ delete_leaf_node! node, original_node
221
142
  end
222
143
 
223
144
  original_node.tree = nil
@@ -233,6 +154,11 @@ class RedBlackTree
233
154
  #
234
155
  # @return [RedBlackTree] self
235
156
  def clear!
157
+ traverse_post_order do |node|
158
+ node.tree = nil
159
+ node.colour = node.parent = node.left = node.right = nil
160
+ end
161
+
236
162
  @root = nil
237
163
  @size = 0
238
164
  @left_most_node = nil
@@ -259,7 +185,7 @@ class RedBlackTree
259
185
  alias_method :find, :search
260
186
 
261
187
  def select &block
262
- raise ArgumentError, "block must be provided for select" unless block
188
+ raise ArgumentError, "block must be provided for select" unless block_given?
263
189
 
264
190
  _select_by_block block, @root
265
191
  end
@@ -270,7 +196,18 @@ class RedBlackTree
270
196
  #
271
197
  # @return [true, false]
272
198
  def include? data
273
- !!search(data)
199
+ !search(data).nil?
200
+ end
201
+
202
+ # Traverses the tree in in-order and yields each node.
203
+ # When called without a block, returns an {Enumerator}.
204
+ #
205
+ # @yield [RedBlackTree::Node] the block to be executed for each node
206
+ # @return [void, Enumerator]
207
+ def each &block
208
+ return enum_for(:each) unless block_given?
209
+
210
+ traverse_in_order(&block)
274
211
  end
275
212
 
276
213
  # Traverses the tree in pre-order and yields each node.
@@ -323,16 +260,37 @@ class RedBlackTree
323
260
  queue = [@root]
324
261
  until queue.empty?
325
262
  node = queue.shift
326
- next if node.nil? || node.leaf?
327
-
328
263
  block.call node
329
- queue << node.left
330
- queue << node.right
264
+ queue << node.left unless node.left.leaf?
265
+ queue << node.right unless node.right.leaf?
331
266
  end
332
267
  end
333
268
 
334
269
  private
335
270
 
271
+ def is_root? node
272
+ node && @root && node.object_id == @root.object_id
273
+ end
274
+
275
+ def increment_size!
276
+ @size += 1
277
+ end
278
+
279
+ def decrement_size!
280
+ @size -= 1
281
+ end
282
+
283
+ def update_left_most_node!
284
+ unless @root
285
+ @left_most_node = nil
286
+ return
287
+ end
288
+
289
+ current = @root
290
+ current = current.left until current.left.leaf?
291
+ @left_most_node = current
292
+ end
293
+
336
294
  # Rotates a (sub-)tree starting from the given node in the given direction.
337
295
  #
338
296
  # @param node [RedBlackTree::Node] the root node of the sub-tree
@@ -359,12 +317,111 @@ class RedBlackTree
359
317
  opp_direction_child
360
318
  end
361
319
 
320
+ def rebalance_after_insertion! node
321
+ while node.parent && node.parent.red? do
322
+ if node.parent.sibling && node.parent.sibling.red?
323
+ node.parent.black!
324
+ node.parent.sibling.black!
325
+ node.parent.parent.red!
326
+ node = node.parent.parent
327
+ else
328
+ opp_direction = node.opposite_position
329
+ if node.parent.position == opp_direction
330
+ rotate_sub_tree! node.parent, opp_direction
331
+ node = node[opp_direction]
332
+ end
333
+
334
+ opp_direction = node.opposite_position
335
+ rotate_sub_tree! node.parent.parent, opp_direction
336
+ node.parent.black!
337
+ node.parent[opp_direction].red!
338
+ end
339
+
340
+ @root.black!
341
+ end
342
+ end
343
+
344
+ def delete_node_with_two_children! node
345
+ is_root = is_root? node
346
+
347
+ predecessor = node.left
348
+ predecessor = predecessor.right until predecessor.right.leaf?
349
+ node.swap_colour_with! predecessor
350
+ node.swap_position_with! predecessor
351
+ node.swap_position_with! LeafNode.new
352
+
353
+ @root = predecessor if is_root
354
+ end
355
+
356
+ def delete_node_with_one_child! node
357
+ is_root = is_root? node
358
+
359
+ valid_child = node.children.find(&:valid?)
360
+ valid_child.black!
361
+ node.swap_position_with! valid_child
362
+ node.swap_position_with! LeafNode.new
363
+
364
+ @root = valid_child if is_root
365
+ end
366
+
367
+ def delete_leaf_node! node, original_node
368
+ if is_root? node
369
+ @root = nil
370
+ elsif node.red?
371
+ node.swap_position_with! LeafNode.new
372
+ else
373
+ rebalance_after_deletion! node
374
+ original_node.swap_position_with! LeafNode.new
375
+ end
376
+ end
377
+
378
+ def rebalance_after_deletion! node
379
+ loop do
380
+ if node.sibling && node.sibling.valid? && node.sibling.red?
381
+ node.parent.red!
382
+ node.sibling.black!
383
+ rotate_sub_tree! node.parent, node.position
384
+ end
385
+
386
+ if node.close_nephew && node.close_nephew.valid? && node.close_nephew.red?
387
+ node.sibling.red! unless node.sibling.leaf?
388
+ node.close_nephew.black!
389
+ rotate_sub_tree! node.sibling, node.opposite_position
390
+ end
391
+
392
+ if node.distant_nephew && node.distant_nephew.valid? && node.distant_nephew.red?
393
+ case node.parent.colour
394
+ when Node::RED then node.sibling.red!
395
+ when Node::BLACK then node.sibling.black!
396
+ end
397
+ node.parent.black!
398
+ node.distant_nephew.black!
399
+ rotate_sub_tree! node.parent, node.position
400
+
401
+ break
402
+ end
403
+
404
+ if node.parent && node.parent.red?
405
+ node.sibling.red! unless node.sibling.leaf?
406
+ node.parent.black!
407
+
408
+ break
409
+ end
410
+
411
+ if node.sibling && !node.sibling.leaf?
412
+ node.sibling.red!
413
+ end
414
+
415
+ break unless node = node.parent
416
+ end
417
+ end
418
+
362
419
  def _search_by_data data, node
363
420
  return if node.nil? || node.leaf?
364
421
  return node if data == node.data
365
422
 
366
- mock_node = node.class.new data
367
- if mock_node >= node
423
+ comparison = data <=> node.data
424
+ if comparison && comparison >= 0
368
425
  _search_by_data data, node.right
369
426
  else
370
427
  _search_by_data data, node.left
@@ -388,27 +445,4 @@ class RedBlackTree
388
445
  end
389
446
  end
390
447
  end
391
-
392
- def is_root? node
393
- node && @root && node.object_id == @root.object_id
394
- end
395
-
396
- def increment_size!
397
- @size += 1
398
- end
399
-
400
- def decrement_size!
401
- @size -= 1
402
- end
403
-
404
- def update_left_most_node!
405
- unless @root
406
- @left_most_node = nil
407
- return
408
- end
409
-
410
- current = @root
411
- current = current.left until current.left.leaf?
412
- @left_most_node = current
413
- end
414
448
  end
@@ -4,20 +4,27 @@ class RedBlackTree
4
4
  class Node
5
5
  module LeftRightElementReferencers # @private
6
6
  def [] direction
7
+ validate_direction! direction
7
8
  case direction
8
9
  when Node::LEFT then @left
9
10
  when Node::RIGHT then @right
10
- else raise ArgumentError, "Direction must be one of #{Implementation::DIRECTIONS}, got #{direction}"
11
11
  end
12
12
  end
13
13
 
14
14
  def []= direction, node
15
+ validate_direction! direction
15
16
  case direction
16
17
  when Node::LEFT then @left = node
17
18
  when Node::RIGHT then @right = node
18
- else raise ArgumentError, "Direction must be one of #{Implementation::DIRECTIONS}, got #{direction}"
19
19
  end
20
20
  end
21
+
22
+ private
23
+
24
+ def validate_direction! direction
25
+ return if [Node::LEFT, Node::RIGHT].include?(direction)
26
+ raise ArgumentError, "Direction must be one of #{Implementation::DIRECTIONS}, got #{direction}"
27
+ end
21
28
  end
22
29
  end
23
30
  end
@@ -9,7 +9,7 @@ class RedBlackTree
9
9
  class Node
10
10
  class << self
11
11
  def inherited subclass
12
- subclass.prepend LeafNodeComparable
12
+ subclass.prepend LeafNodeComparable unless subclass == ::RedBlackTree::LeafNode
13
13
  end
14
14
  end
15
15
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedBlackTree
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.9"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: red-black-tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  email:
13
13
  - djry1999@gmail.com
@@ -49,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
49
  - !ruby/object:Gem::Version
50
50
  version: '0'
51
51
  requirements: []
52
- rubygems_version: 3.6.2
52
+ rubygems_version: 4.0.6
53
53
  specification_version: 4
54
54
  summary: Red-Black Tree Data Structure for Ruby
55
55
  test_files: []