0.0
No release in over 3 years
A high-performance pure Ruby Red-Black Tree implementation. Features: O(1) key lookup via hybrid hash index, O(log n) insert/delete, lazy Enumerator-based range queries (lt/gt/between), nearest/prev/succ search, memory-efficient node pooling, and MultiRBTree for duplicate keys with first/last value access.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

rbtree-ruby

🌍 English | ζ—₯本θͺž

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.

Features

  • 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: MultiRBTree class 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: true to safely modify the tree (insert/delete) during iteration.

Installation

Add this line to your application's Gemfile:

gem 'rbtree-ruby'

And then execute:

bundle install

Or install it yourself as:

gem install rbtree-ruby

Usage

Basic RBTree

require '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         # => 2

MultiRBTree with Duplicate Keys

require '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 values

Nearest Key Search

tree = 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"]

Predecessor/Successor Search

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)

Reverse Range Queries

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)

Conversion and Merging

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]]

Filtering and Copying

Note: dup, select, reject, delete_if, reject!, keep_if, invert, and merge are 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 }  # => nil

For 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 removed

MultiRBTree Value Array Access

For 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)

Performance

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.

RBTree vs Hash vs Array (Overwhelming Power)

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

Memory Efficiency & Custom Allocators

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 AutoShrinkNodePool automatically 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)

When to Use RBTree

βœ… 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.

API Documentation

Full RDoc documentation is available. Generate it locally with:

rdoc lib/rbtree.rb

Then open doc/index.html in your browser.

Development

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 install

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/firelzrd/rbtree-ruby.

License

The gem is available as open source under the terms of the MIT License.

Author

Masahito Suzuki (firelzrd@gmail.com)

Copyright Β© 2026 Masahito Suzuki