A pure Ruby implementation of the Red-Black Tree data structure, providing efficient ordered key-value storage with O(log n) time complexity for insertion, deletion, and lookup operations.
- Self-Balancing Binary Search Tree: Maintains optimal performance through red-black tree properties. All insertions, deletions, and lookups run in O(log n).
- Ordered Operations: Efficient sorted iteration, range queries (
lt,gt,between), min/max retrieval. - Multi-Value Support:
MultiRBTreeclass stores multiple values per key, with access to first or last value individually. - Pure Ruby: No C extensions required. Works on MRI, JRuby, TruffleRuby, and all Ruby implementations.
- Hybrid Indexing: Internal hash index enables O(1) key lookup and membership checks — matching standard Hash performance.
- Memory Efficiency: Node recycling with auto-shrinking pool (
AutoShrinkNodePool) drastically reduces GC pressure in long-running apps. - Nearest Key Search: Finds the closest numeric key in O(log n) time — ideal for spatial or temporal queries.
- Safe Iteration: Use
safe: trueto safely modify the tree (insert/delete) during iteration.
Add this line to your application's Gemfile:
gem 'rbtree-ruby'And then execute:
bundle installOr install it yourself as:
gem install rbtree-rubyrequire 'rbtree'
# Create an empty tree
tree = RBTree.new
# Or initialize with data (Bulk Insert)
tree = RBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
tree = RBTree[[5, 'five'], [4, 'four']]
tree = RBTree.new do # Block initialization
data_source.each { |data| [data.time, data.content] }
end
# Insert and retrieve values
tree.insert(10, 'ten')
tree[20] = 'twenty'
# Bulk insert
tree.insert({30 => 'thirty', 40 => 'forty'})
puts tree[10] # => "ten"
# Iterate in sorted order
tree.each { |key, value| puts "#{key}: #{value}" }
# Output:
# 1: one
# 2: two
# 3: three
# 10: ten
# 20: twenty
# Modification during iteration
# Unlike standard Ruby Hash/Array, modification during iteration is fully supported
# with the `safe: true` option. This allows you to delete or insert keys safely while iterating.
tree.each(safe: true) { |k, v| tree.delete(k) if k.even? }
tree.each(reverse: true) { |k, v| puts k } # Same as reverse_each
# Min and max
tree.min # => [1, "one"]
tree.max # => [20, "twenty"]
# Range queries (return Enumerator, use .to_a for Array)
tree.lt(10).to_a # => [[1, "one"], [2, "two"], [3, "three"]]
tree.gte(10).each { |k, v| puts k } # Block iteration
tree.between(2, 10).to_a # => [[2, "two"], [3, "three"], [10, "ten"]]
# Range objects in [] (v0.3.4+)
tree[..10].to_a # lte(10)
tree[2..10].each { |k, v| ... } # Block iteration on Range
tree[2...10].to_a # between(2, 10, include_max: false)
tree[10..].to_a # gte(10)
tree[2..10, reverse: true].to_a # with options
# Shift and pop
tree.shift # => [1, "one"] (removes minimum)
tree.pop # => [20, "twenty"] (removes maximum)
# Delete
tree.delete(3) # => "three"
# Check membership
tree.has_key?(2) # => true
tree.size # => 2require 'rbtree'
tree = MultiRBTree.new
# Insert multiple values for the same key
tree.insert(1, 'first one')
tree.insert(1, 'second one')
tree.insert(1, 'third one')
tree.insert(2, 'two')
tree.size # => 4 (total number of key-value pairs)
# Get first value
tree.value(1) # => "first one"
tree[1] # => "first one"
# Get all values for a key (returns Enumerator)
tree.values(1).to_a # => ["first one", "second one", "third one"]
# Iterate over all key-value pairs
tree.each { |k, v| puts "#{k}: #{v}" }
# Output:
# 1: first one
# 1: second one
# 1: third one
# 2: two
# Delete only first value
tree.delete_value(1) # => "first one"
tree.value(1) # => "second one"
# Delete all values for a key
tree.delete_key(1) # removes all remaining valuestree = RBTree.new({1 => 'one', 5 => 'five', 10 => 'ten'})
tree.nearest(4) # => [5, "five"] (closest key to 4)
tree.nearest(7) # => [5, "five"] (same distance, returns smaller key)
tree.nearest(8) # => [10, "ten"]Find the next or previous key in the tree:
tree = RBTree.new({1 => 'one', 3 => 'three', 5 => 'five', 7 => 'seven'})
tree.prev(5) # => [3, "three"] (largest key < 5)
tree.succ(5) # => [7, "seven"] (smallest key > 5)
# Works even if the key doesn't exist
tree.prev(4) # => [3, "three"] (4 doesn't exist, returns largest key < 4)
tree.succ(4) # => [5, "five"] (4 doesn't exist, returns smallest key > 4)
# Returns nil at boundaries
tree.prev(1) # => nil (no key smaller than 1)
tree.succ(7) # => nil (no key larger than 7)All range queries return an Enumerator (use .to_a for Array) and support a :reverse option:
tree = RBTree.new({1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four'})
tree.lt(3).to_a # => [[1, "one"], [2, "two"]]
tree.lt(3, reverse: true).to_a # => [[2, "two"], [1, "one"]]
tree.lt(3).first # => [1, "one"] (lazy, no array created)
# Lazy evaluation
tree.gt(0).lazy.take(2).to_a # => [[1, "one"], [2, "two"]] (only computes first 2)Seamlessly convert to standard Ruby objects or merge other collections:
tree = RBTree.new({1 => 'one', 2 => 'two'})
# Convert to Array (via Enumerable)
tree.to_a # => [[1, "one"], [2, "two"]]
# Convert to Hash
tree.to_h # => {1 => "one", 2 => "two"}
# Merge (destructive)
other = {3 => 'three'}
tree.merge!(other)
tree.size # => 3
# Merge (non-destructive) — returns a new tree
merged = tree.merge({4 => 'four'})
# Merge with block for duplicate key resolution
merged = tree.merge({1 => 'ONE'}) { |key, old_val, new_val| old_val }
# Invert keys and values
tree = RBTree.new({1 => 'a', 2 => 'b', 3 => 'c'})
tree.invert.to_a # => [["a", 1], ["b", 2], ["c", 3]]Note:
dup,select,reject,delete_if,reject!,keep_if,invert, andmergeare convenience methods composed from existing primitives. They provide no speed advantage over manual composition — their value is in readability and API completeness.
Create copies or filter trees using familiar Ruby idioms:
tree = RBTree.new({1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four'})
# Deep copy — independent of original
copy = tree.dup
copy.delete(1)
tree.size # => 4 (unchanged)
# select / reject — return a new tree
evens = tree.select { |k, _| k.even? } # => {2=>"two", 4=>"four"}
odds = tree.reject { |k, _| k.even? } # => {1=>"one", 3=>"three"}
# delete_if / keep_if — modify in place
tree.delete_if { |k, _| k > 2 }
tree.to_a # => [[1, "one"], [2, "two"]]
# reject! — like delete_if, but returns nil if nothing changed
tree.reject! { |_, _| false } # => nilFor MultiRBTree, delete_if and keep_if operate at individual value granularity:
tree = MultiRBTree.new
tree.insert(1, 'a')
tree.insert(1, 'b')
tree.insert(2, 'c')
tree.delete_if { |k, v| k == 1 && v == 'a' }
tree.to_a # => [[1, "b"], [2, "c"]] — only 'a' was removedFor keys with multiple values, choose which value to access:
tree = MultiRBTree.new
tree.insert(1, 'first')
tree.insert(1, 'second')
tree.insert(1, 'third')
# Access first or last value
tree.value(1) # => "first"
tree.value(1, last: true) # => "third"
tree.first_value(1) # => "first"
tree.last_value(1) # => "third"
# Delete from either end
tree.delete_first_value(1) # => "first"
tree.delete_last_value(1) # => "third"
tree.value(1) # => "second"
# min/max with :last option
tree.insert(2, 'a')
tree.insert(2, 'b')
tree.min # => [1, "second"] (first value of min key)
tree.max(last: true) # => [2, "b"] (last value of max key)All major operations run in O(log n) time:
insert(key, value)- O(log n)delete(key)- O(log n)value(key)/[]- O(1) (hybrid hash index)has_key?- O(1) (hybrid hash index)min/max- O(1)shift/pop- O(log n)prev/succ- O(log n) with O(1) hash check and faster startup
Iteration over all elements takes O(n) time.
For ordered and spatial operations, RBTree is not just faster—it is in a completely different class. The following benchmarks were conducted with 500,000 items:
| Operation | RBTree | Hash/Array | Speedup | Why? |
|---|---|---|---|---|
| Nearest Key Search | O(log n) | O(n) scan | ~8,600x faster | Spatial binary search vs full scan |
| Range Queries | O(log n + k) | O(n) filter | ~540x faster | Direct subtree jump vs full scan |
| Min Extraction | O(log n) | O(n) search | ~160x faster | Continuous rebalancing vs full scan |
| Sorted Iteration | O(n) | O(n log n) | FREE | Always sorted vs explicit sort |
| Key Lookup | O(1) | O(1) | Equal | Hybrid Hash Index provides O(1) access like standard Hash |
RBTree uses an internal Memory Pool to recycle node objects.
- Significantly reduces Garbage Collection (GC) pressure during frequent insertions and deletions.
- Auto-Shrinking: The default
AutoShrinkNodePoolautomatically releases unused nodes back to Ruby's GC when the pool gets too large relative to current usage, preventing memory leaks in long-running applications with fluctuating workloads. - Customization: You can customize the pool behavior or provide your own allocator:
# Customize auto-shrink parameters
pool = RBTree::AutoShrinkNodePool.new(
history_size: 60, # 1 minute history
buffer_factor: 1.5, # Keep 50% buffer above fluctuation
reserve_ratio: 0.2 # Always keep 20% reserve
)
tree = RBTree.new(node_allocator: pool)✅ Use RBTree when you need:
- Ordered iteration by key
- Fast min/max retrieval
- Range queries (
between,lt,gt,lte,gte) - Nearest key search
- Priority queue behavior (shift/pop by key order)
✅ Use Hash when you only need:
- Fast key-value lookup (RBTree is now equally fast!)
- No ordering requirements
Run ruby demo.rb for a full benchmark demonstration.
Full RDoc documentation is available. Generate it locally with:
rdoc lib/rbtree.rbThen open doc/index.html in your browser.
After checking out the repo, run bundle install to install dependencies.can then run:
# Generate RDoc documentation
rake rdoc
# Build the gem
rake build
# Install locally
rake installBug reports and pull requests are welcome on GitHub at https://github.com/firelzrd/rbtree-ruby.
The gem is available as open source under the terms of the MIT License.
Masahito Suzuki (firelzrd@gmail.com)
Copyright © 2026 Masahito Suzuki