├── .bnsignore ├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── History.txt ├── README.md ├── Rakefile ├── lib ├── btree.rb └── btree │ ├── node.rb │ └── tree.rb ├── test └── test_btree.rb └── version.txt /.bnsignore: -------------------------------------------------------------------------------- 1 | # The list of files that should be ignored by Mr Bones. 2 | # Lines that start with '#' are comments. 3 | # 4 | # A .gitignore file can be used instead by setting it as the ignore 5 | # file in your Rakefile: 6 | # 7 | # Bones { 8 | # ignore_file '.gitignore' 9 | # } 10 | # 11 | # For a project with a C extension, the following would be a good set of 12 | # exclude patterns (uncomment them if you want to use them): 13 | # *.[oa] 14 | # *~ 15 | announcement.txt 16 | coverage 17 | doc 18 | pkg 19 | *.swp 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/ 2 | /doc/ 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | btree 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.2.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | rvm: 9 | - 2.4.1 10 | branches: 11 | except: 12 | - "/^v\\d+\\.\\d+\\.\\d+$/" 13 | deploy: 14 | api_key: 15 | secure: pDZgw7xhVcsEHA9pNrww1hexa50UqZa5mn2JvnnSw6JejLGhPFP0V520/p5GrdW/wZpHuWSd5L5WuM1Uz6ft0Q+LOeIuHC80WHFNgY6E+D7jS06yX2pELh94lWMlFefZvZbT/qv0LQx8fdDh1Dou6abkXHWCApC6VMJ7vBhi8kw= 16 | provider: rubygems 17 | on: 18 | tags: true 19 | gem: btree 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | #repo_name "seifertd/Ruby-BTree" 4 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 5 | 6 | # gem "rails" 7 | 8 | # Added at 2017-11-09 08:39:45 -0800 by doug: 9 | gem "rake", "~> 13", :group => [:development, :test] 10 | 11 | # Added at 2017-11-09 08:40:02 -0800 by doug: 12 | gem "bones", "~> 3.9", :group => [:development, :test] 13 | 14 | gem "minitest", :group => [:development, :test] 15 | # Added at 2017-11-09 08:40:12 -0800 by doug: 16 | gem "shoulda", "~> 3.5", :group => [:development, :test] 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (7.0.8) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 1.6, < 2) 7 | minitest (>= 5.1) 8 | tzinfo (~> 2.0) 9 | bones (3.9.0) 10 | little-plugger (~> 1.1) 11 | loquacious (~> 1.9) 12 | rake (~> 13.0) 13 | rdoc (~> 6.0) 14 | concurrent-ruby (1.2.2) 15 | i18n (1.14.1) 16 | concurrent-ruby (~> 1.0) 17 | little-plugger (1.1.4) 18 | loquacious (1.9.1) 19 | minitest (5.20.0) 20 | psych (5.1.2) 21 | stringio 22 | rake (13.0.6) 23 | rdoc (6.6.3.1) 24 | psych (>= 4.0.0) 25 | shoulda (3.6.0) 26 | shoulda-context (~> 1.0, >= 1.0.1) 27 | shoulda-matchers (~> 3.0) 28 | shoulda-context (1.2.2) 29 | shoulda-matchers (3.1.3) 30 | activesupport (>= 4.0.0) 31 | stringio (3.1.0) 32 | tzinfo (2.0.6) 33 | concurrent-ruby (~> 1.0) 34 | 35 | PLATFORMS 36 | x86_64-linux 37 | 38 | DEPENDENCIES 39 | bones (~> 3.9) 40 | minitest 41 | rake (~> 13) 42 | shoulda (~> 3.5) 43 | 44 | BUNDLED WITH 45 | 2.4.12 46 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 0.0.0 / 2010-12-20 2 | 3 | * 1 major enhancement 4 | * Initial Release! 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | btree 2 | ===== 3 | 4 | Pure ruby implementation of a btree as described in Introduction to Algorithms by 5 | Cormen, Leiserson, Rivest and Stein, Chapter 18. 6 | 7 | Features 8 | -------- 9 | 10 | * Create B-Tree of arbitrary degree (2 by default) 11 | * Insert key value pairs using a Map like interface 12 | * Query for values associated with keys using a Map like interface 13 | * Keys can be any object supporting comparison operators: <, > and == 14 | 15 | Examples 16 | -------- 17 | 18 | require 'btree' 19 | tree = Btree.create # default degree = 2 20 | tree = Btree.create(5) # degree = 5 21 | tree['foo'] = 'foo value' 22 | tree['bar'] = 'bar value' 23 | 24 | puts "key 'foo' has value: #{tree['foo']}" 25 | puts "BTree has #{tree.size} key-value pairs" 26 | 27 | Future 28 | ------ 29 | 30 | * Deletion is not implemented. TODO: Implement deletion 31 | * Attempt to insert existing key raises RuntimeError. 32 | TODO: Allow replacement of value associated with existing key instead of raising RuntimeError. 33 | * Persist state to backing store 34 | 35 | Install 36 | ------- 37 | 38 | * gem install btree 39 | 40 | Author 41 | ------ 42 | 43 | Original author: Douglas A. Seifert (doug@dseifert.net) 44 | 45 | License 46 | ------- 47 | 48 | (The MIT License) 49 | 50 | Copyright (c) 2010,2011 Douglas A. Seifert 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining 53 | a copy of this software and associated documentation files (the 54 | 'Software'), to deal in the Software without restriction, including 55 | without limitation the rights to use, copy, modify, merge, publish, 56 | distribute, sublicense, and/or sell copies of the Software, and to 57 | permit persons to whom the Software is furnished to do so, subject to 58 | the following conditions: 59 | 60 | The above copyright notice and this permission notice shall be 61 | included in all copies or substantial portions of the Software. 62 | 63 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 64 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 65 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 66 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 67 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 68 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 69 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 70 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | begin 3 | require 'bones' 4 | rescue LoadError 5 | abort '### Please install the "bones" gem ###' 6 | end 7 | 8 | task :default => 'test:run' 9 | task 'gem:release' => 'test:run' 10 | 11 | Bones { 12 | name 'btree' 13 | authors 'Douglas A. Seifert' 14 | email 'doug@dseifert.net' 15 | url 'https://github.com/seifertd/Ruby-BTree' 16 | readme_file 'README.md' 17 | exclude ['.bnsignore', '.gitignore', '.ruby-gemset', '.ruby-version', '.travis.yml', 'vendor', '.git'] 18 | } 19 | 20 | -------------------------------------------------------------------------------- /lib/btree.rb: -------------------------------------------------------------------------------- 1 | # :main: README.md 2 | module Btree 3 | 4 | # :stopdoc: 5 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR 6 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR 7 | # :startdoc: 8 | 9 | # Returns the version string for the library. 10 | # 11 | def self.version 12 | @version ||= File.read(path('version.txt')).strip 13 | end 14 | 15 | # Returns the library path for the module. If any arguments are given, 16 | # they will be joined to the end of the libray path using 17 | # File.join. 18 | # 19 | def self.libpath( *args, &block ) 20 | rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten) 21 | if block 22 | begin 23 | $LOAD_PATH.unshift LIBPATH 24 | rv = block.call 25 | ensure 26 | $LOAD_PATH.shift 27 | end 28 | end 29 | return rv 30 | end 31 | 32 | # Returns the lpath for the module. If any arguments are given, 33 | # they will be joined to the end of the path using 34 | # File.join. 35 | # 36 | def self.path( *args, &block ) 37 | rv = args.empty? ? PATH : ::File.join(PATH, args.flatten) 38 | if block 39 | begin 40 | $LOAD_PATH.unshift PATH 41 | rv = block.call 42 | ensure 43 | $LOAD_PATH.shift 44 | end 45 | end 46 | return rv 47 | end 48 | 49 | # Utility method used to require all files ending in .rb that lie in the 50 | # directory below this file that has the same name as the filename passed 51 | # in. Optionally, a specific _directory_ name can be passed in such that 52 | # the _filename_ does not have to be equivalent to the directory. 53 | # 54 | def self.require_all_libs_relative_to( fname, dir = nil ) 55 | dir ||= ::File.basename(fname, '.*') 56 | search_me = ::File.expand_path( 57 | ::File.join(::File.dirname(fname), dir, '**', '*.rb')) 58 | 59 | Dir.glob(search_me).sort.each {|rb| require rb} 60 | end 61 | 62 | def self.create(degree) 63 | raise "Degree of Btree must be >= 2" if degree < 2 64 | return Btree::Tree.new(degree) 65 | end 66 | 67 | end # module Btree 68 | 69 | Btree.require_all_libs_relative_to(__FILE__) 70 | 71 | -------------------------------------------------------------------------------- /lib/btree/node.rb: -------------------------------------------------------------------------------- 1 | class Btree::Node 2 | def initialize(degree) 3 | @degree = degree 4 | @keys = [] 5 | @children = [] 6 | end 7 | 8 | def dump(level = 0) 9 | @keys.each_with_index do |key, idx| 10 | puts "LEVEL: #{level} => #{key.first}: full? #{full?} leaf? #{leaf?} children: #{values.inspect}" 11 | if @children[idx] 12 | @children[idx].dump(level + 1) 13 | end 14 | end 15 | (@children[@keys.size..-1] || []).each do |c| 16 | c.dump(level+1) 17 | end 18 | nil 19 | end 20 | 21 | def add_child(node) 22 | @children << node 23 | end 24 | 25 | def children 26 | @children.dup.freeze 27 | end 28 | 29 | def keys 30 | @keys.map(&:first).freeze 31 | end 32 | 33 | def values 34 | @keys.map(&:last).freeze 35 | end 36 | 37 | def full? 38 | size >= 2 * @degree - 1 39 | end 40 | 41 | def leaf? 42 | @children.length == 0 43 | end 44 | 45 | def size 46 | @keys.size 47 | end 48 | 49 | def values_of(range) 50 | 51 | result = Array.new 52 | 53 | i = 1 54 | while i <= size && range.end >= @keys[i-1].first 55 | if range.cover? @keys[i-1].first 56 | result << @keys[i-1].last 57 | child = @children[i-1].values_of(range) unless leaf? 58 | result += child if child 59 | end 60 | i += 1 61 | end 62 | 63 | result 64 | 65 | end 66 | 67 | 68 | def value_of(key) 69 | 70 | return values_of(key) if key.kind_of? Range 71 | 72 | i = 1 73 | while i <= size && key > @keys[i-1].first 74 | i += 1 75 | end 76 | 77 | #puts "Getting value of key #{key}, i = #{i}, keys = #{@keys.inspect}, leaf? #{leaf?}, numchildren: #{@children.size}" 78 | 79 | if i <= size && key == @keys[i-1].first 80 | #puts "Found key: #{key.inspect}" 81 | return @keys[i-1].last 82 | elsif leaf? 83 | #puts "We are a leaf, no more children, so val is nil" 84 | return nil 85 | else 86 | #puts "Looking into child #{i}" 87 | return @children[i-1].value_of(key) 88 | end 89 | end 90 | 91 | def insert(key, value) 92 | i = size - 1 93 | #puts "INSERTING #{key} INTO NODE: #{self.inspect}" 94 | if leaf? 95 | raise "Duplicate key" if @keys.any?{|(k,v)| k == key } #OPTIMIZE: This is inefficient 96 | while i >= 0 && @keys[i] && key < @keys[i].first 97 | @keys[i+1] = @keys[i] 98 | i -= 1 99 | end 100 | @keys[i+1] = [key, value] 101 | else 102 | while i >= 0 && @keys[i] && key < @keys[i].first 103 | i -= 1 104 | end 105 | #puts " -- INSERT KEY INDEX #{i}" 106 | if @children[i+1] && @children[i+1].full? 107 | split(i+1) 108 | if key > @keys[i+1].first 109 | i += 1 110 | end 111 | end 112 | @children[i+1].insert(key, value) 113 | end 114 | end 115 | 116 | def split(child_idx) 117 | raise "Invalid child index #{child_idx} in split, num_children = #{@children.size}" if child_idx < 0 || child_idx >= @children.size 118 | #puts "SPLIT1: #{self.inspect}" 119 | splitee = @children[child_idx] 120 | y = Btree::Node.new(@degree) 121 | z = Btree::Node.new(@degree) 122 | (@degree-1).times do |j| 123 | z._keys[j] = splitee._keys[j+@degree] 124 | y._keys[j] = splitee._keys[j] 125 | end 126 | if !splitee.leaf? 127 | @degree.times do |j| 128 | z._children[j] = splitee._children[j+@degree] 129 | y._children[j] = splitee._children[j] 130 | end 131 | end 132 | mid_val = splitee._keys[@degree-1] 133 | #puts "SPLIT2: #{self.inspect}" 134 | (@keys.size).downto(child_idx) do |j| 135 | @children[j+1] = @children[j] 136 | end 137 | 138 | @children[child_idx+1] = z 139 | @children[child_idx] = y 140 | 141 | #puts "SPLIT3: #{self.inspect}" 142 | 143 | (@keys.size - 1).downto(child_idx) do |j| 144 | @keys[j+1] = @keys[j] 145 | end 146 | 147 | #puts "SPLIT4: #{self.inspect}" 148 | 149 | @keys[child_idx] = mid_val 150 | #puts "SPLIT5: #{self.inspect}" 151 | end 152 | 153 | protected 154 | 155 | def _keys 156 | @keys 157 | end 158 | 159 | def _children 160 | @children 161 | end 162 | 163 | end 164 | -------------------------------------------------------------------------------- /lib/btree/tree.rb: -------------------------------------------------------------------------------- 1 | class Btree::Tree 2 | attr_reader :root, :degree, :size 3 | 4 | # Creates a BTree of degree 2 by default. Keys 5 | # Must support being compared using >, < and == methods. 6 | def initialize(degree = 2) 7 | @degree = degree 8 | @root = Btree::Node.new(@degree) 9 | @size = 0 10 | end 11 | 12 | # Insert a key-value pair into the btree 13 | def insert(key, value = nil) 14 | node = @root 15 | if node.full? 16 | @root = Btree::Node.new(@degree) 17 | @root.add_child(node) 18 | @root.split(@root.children.size - 1) 19 | #puts "After split, root = #{@root.inspect}" 20 | # split child(@root, 1) 21 | node = @root 22 | end 23 | node.insert(key, value) 24 | @size += 1 25 | return self 26 | end 27 | 28 | # puts internal state 29 | def dump 30 | @root.dump 31 | end 32 | 33 | # Get value associated with the specified key 34 | def value_of(key) 35 | @root.value_of(key) 36 | end 37 | 38 | # Support map like access 39 | alias_method :"[]", :value_of 40 | 41 | # Support map like key-value setting 42 | alias_method :"[]=", :insert 43 | 44 | end 45 | -------------------------------------------------------------------------------- /test/test_btree.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'btree' 3 | require 'shoulda' 4 | 5 | class TestBtree < Minitest::Test 6 | def test_insert_notfull 7 | t = Btree.create(5) 8 | t.insert(5, "5") 9 | assert_equal 1, t.root.size 10 | assert !t.root.full? 11 | assert_equal 5, t.degree 12 | assert_equal 1, t.size 13 | end 14 | 15 | def test_degree_too_small 16 | assert_raises(RuntimeError) do 17 | Btree.create(1) 18 | end 19 | end 20 | 21 | def test_insert_duplicate_key 22 | t = Btree.create(2) 23 | t.insert(1, "1") 24 | assert_raises(RuntimeError) do 25 | t.insert(1, "1") 26 | end 27 | end 28 | 29 | def test_insert_a_lot 30 | t = Btree.create(2) 31 | 45.times do |i| 32 | t.insert i, i*i 33 | end 34 | assert_equal 45, t.size 35 | 45.times do |i| 36 | assert_equal i*i, t.value_of(i) 37 | end 38 | end 39 | 40 | def test_value_of 41 | t = Btree.create(5) 42 | t.insert(1, "foo") 43 | t.insert(5, "bar") 44 | t.insert(7, "baz") 45 | t.insert(3, "findme") 46 | assert_equal "findme", t.value_of(3) 47 | assert_equal "baz", t.value_of(7) 48 | assert_equal "bar", t.value_of(5) 49 | assert_equal "foo", t.value_of(1) 50 | assert_nil t.value_of(11) 51 | assert_equal 4, t.size 52 | assert_equal ["findme", "bar"], t.value_of(3..6) 53 | end 54 | 55 | def test_fill_root 56 | t = Btree.create(2) 57 | 3.times {|n| t.insert(n, n.to_s)} 58 | assert t.root.full? 59 | assert_equal [0,1,2], t.root.keys 60 | assert_equal ['0','1','2'], t.root.values 61 | assert_equal 3, t.size 62 | end 63 | 64 | context 'full root' do 65 | setup do 66 | @t = Btree.create(2) 67 | 3.times {|n| @t.insert(n*2, n.to_s)} 68 | end 69 | should "be able to insert at end" do 70 | @t.insert(10, "10") 71 | assert_equal "10", @t.value_of(10) 72 | assert_equal "0", @t.value_of(0) 73 | assert_equal "1", @t.value_of(2) 74 | assert_equal "2", @t.value_of(4) 75 | assert_equal 4, @t.size 76 | end 77 | should "be able to insert at front" do 78 | @t.insert(-1, "10") 79 | assert_equal "10", @t.value_of(-1) 80 | assert_equal "0", @t.value_of(0) 81 | assert_equal "1", @t.value_of(2) 82 | assert_equal "2", @t.value_of(4) 83 | assert_equal 4, @t.size 84 | end 85 | should "be able to insert in the middle" do 86 | @t.insert(3, "10") 87 | assert_equal "10", @t.value_of(3) 88 | assert_equal "0", @t.value_of(0) 89 | assert_equal "1", @t.value_of(2) 90 | assert_equal "2", @t.value_of(4) 91 | assert_equal 4, @t.size 92 | end 93 | context 'full last child' do 94 | setup do 95 | 2.times {|n| @t.insert(n*2 + 10, "foo") } 96 | end 97 | should "have full last child" do 98 | assert @t.root.children.last.full?, "Last child of root should be full" 99 | assert_equal 5, @t.size 100 | end 101 | should "be able to add to end" do 102 | @t.insert(100, "YEAH!") 103 | assert_equal "YEAH!", @t.value_of(100) 104 | assert_equal 6, @t.size 105 | end 106 | should "be able to insert many" do 107 | 10.times { |n| @t.insert((n+1) * 100, "YEAH!") } 108 | assert_equal "YEAH!", @t.value_of(100) 109 | assert_equal 15, @t.size 110 | end 111 | end 112 | context 'second split of root' do 113 | setup do 114 | 6.times {|n| @t.insert(n*2 + 10, "foo") } 115 | end 116 | should "work" do 117 | assert_equal 'foo', @t.value_of(10) 118 | assert_equal 9, @t.size 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | --------------------------------------------------------------------------------