diff --git a/README b/README deleted file mode 100644 index 7ef00b47866b5d73efb30167e59ccecd632372b2..0000000000000000000000000000000000000000 --- a/README +++ /dev/null @@ -1,16 +0,0 @@ -avl_tree - AVL tree and Red-black tree in Ruby -Copyright (C) 2012 Hiroshi Nakamura <nahi@ruby-lang.org> - - -== Author - -Name:: Hiroshi Nakamura -E-mail:: nahi@ruby-lang.org -Project web site:: http://github.com/nahi/avl_tree - - -== License - -This program is copyrighted free software by Hiroshi Nakamura. You can -redistribute it and/or modify it under the same terms of Ruby's license; -either the dual license version in 2003, or any later version. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..449836a56e6addd4f85653c8a73f6cd73b64d3b8 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# AVL tree, Red-black tree in Ruby + +avl_tree - AVL tree, Red-black tree and Lock-free Red black tree in Ruby +Copyright (C) 2014 Hiroshi Nakamura <nahi@ruby-lang.org> + + +## Usage + +You can use AVLTree, RedBlackTree or ConcurrentRedBlackTree just as a +replacement of Hash. + + @points = Hash.new + ... + @points[score] = person + ... + @points.each do |score, person| + ... + end + + -> + + require 'avl_tree' + @points = AVLTree.new + + require 'red_black_tree' + @points = RedBlackTree.new + @points = ConcurrentRedBlackTree.new + +AVLTree and RedBlackTree are faster but not thread-safe. Use ConcurrentRedBlackTree in multi-thread environment. + +## Author + +Name:: Hiroshi Nakamura +E-mail:: nahi@ruby-lang.org +Project web site:: http://github.com/nahi/avl_tree + + +## License + +This program is copyrighted free software by Hiroshi Nakamura. You can +redistribute it and/or modify it under the same terms of Ruby's license; +either the dual license version in 2003, or any later version. diff --git a/avl_tree.gemspec b/avl_tree.gemspec new file mode 100644 index 0000000000000000000000000000000000000000..09394ad5e852ef0fcee3b5bac88b43ff065a2285 --- /dev/null +++ b/avl_tree.gemspec @@ -0,0 +1,32 @@ +######################################################### +# This file has been automatically generated by gem2tgz # +######################################################### +# -*- encoding: utf-8 -*- +# stub: avl_tree 1.2.1 ruby lib + +Gem::Specification.new do |s| + s.name = "avl_tree".freeze + s.version = "1.2.1" + + s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= + s.require_paths = ["lib".freeze] + s.authors = ["Hiroshi Nakamura".freeze] + s.date = "2014-09-28" + s.email = "nahi@ruby-lang.org".freeze + s.files = ["README.md".freeze, "bench/bench.rb".freeze, "bench/bench_element_size.rb".freeze, "bench/bench_thread.rb".freeze, "bench/profile.rb".freeze, "lib/avl_tree.rb".freeze, "lib/red_black_tree.rb".freeze, "test/helper.rb".freeze, "test/test_avl_tree.rb".freeze, "test/test_red_black_tree.rb".freeze, "test/test_red_black_tree_thread.rb".freeze] + s.homepage = "http://github.com/nahi/avl_tree".freeze + s.rubygems_version = "2.5.2.1".freeze + s.summary = "AVL tree, Red black tree and Lock-free Red black tree in Ruby".freeze + + if s.respond_to? :specification_version then + s.specification_version = 4 + + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then + s.add_runtime_dependency(%q<atomic>.freeze, ["~> 1.1"]) + else + s.add_dependency(%q<atomic>.freeze, ["~> 1.1"]) + end + else + s.add_dependency(%q<atomic>.freeze, ["~> 1.1"]) + end +end diff --git a/bench/bench_thread.rb b/bench/bench_thread.rb new file mode 100644 index 0000000000000000000000000000000000000000..c469774449f420b3e986f6ec703b7700ab37665b --- /dev/null +++ b/bench/bench_thread.rb @@ -0,0 +1,39 @@ +require 'benchmark' +require 'red_black_tree' + +Benchmark.bmbm do |bm| + bm.report do + h = ConcurrentRedBlackTree.new + num = 100000 + max = 1000 + threads = [] + # writers + 2.times do + threads << Thread.new { + num.times do + key = rand(max) + h[key] = key + end + } + end + # deleters + 2.times do + threads << Thread.new { + num.times do + key = rand(max) + h.delete(key) + end + } + end + # readers + 2.times do + threads << Thread.new { + num.times do + key = rand(max) + h[key] + end + } + end + threads.each(&:join) + end +end diff --git a/lib/avl_tree.rb b/lib/avl_tree.rb index c9b8af3ff648b8c5b0f4261a0c8a245aa374c265..abd63fc35ac1d51b6889dc4f952cbf1695ea61f6 100644 --- a/lib/avl_tree.rb +++ b/lib/avl_tree.rb @@ -314,6 +314,10 @@ class AVLTree end alias length size + def height + @root.height + end + def each(&block) if block_given? @root.each(&block) diff --git a/lib/red_black_tree.rb b/lib/red_black_tree.rb index ee5e0cea72baa05806279f6858499eb38a9bf56d..c69435f9a59fce9442e61cfa8326731f41b8c887 100644 --- a/lib/red_black_tree.rb +++ b/lib/red_black_tree.rb @@ -1,3 +1,5 @@ +require 'atomic' + class RedBlackTree include Enumerable @@ -7,11 +9,13 @@ class RedBlackTree attr_reader :key, :value, :color attr_reader :left, :right - def initialize(key, value) - @key, @value = key, value - @left = @right = EMPTY + def initialize(key, value, left, right, color = :RED) + @key = key + @value = value + @left = left + @right = right # new node is added as RED - @color = :RED + @color = color end def set_root @@ -67,14 +71,14 @@ class RedBlackTree case key <=> @key when -1 @left = @left.insert(key, value) - if black? and @right.black? and @left.red? and !@left.children_both_black? + if black? and @right.black? and @left.red? and !@left.children_color?(:BLACK) ret = rebalance_for_left_insert end when 0 @value = value when 1 @right = @right.insert(key, value) - if black? and @left.black? and @right.red? and !@right.children_both_black? + if black? and @left.black? and @right.red? and !@right.children_color?(:BLACK) ret = rebalance_for_right_insert end else @@ -108,7 +112,7 @@ class RedBlackTree end when 0 deleted = self - ret, rebalance = delete_self + ret, rebalance = delete_node when 1 deleted, @right, rebalance = @right.delete(key) if rebalance @@ -138,8 +142,8 @@ class RedBlackTree # for debugging def check_height - lh = @left.empty? ? 0 : @left.check_height - rh = @right.empty? ? 0 : @right.check_height + lh = @left.nil? || @left.empty? ? 0 : @left.check_height + rh = @right.nil? || @right.empty? ? 0 : @right.check_height if red? if @left.red? or @right.red? puts dump_tree(STDERR) @@ -156,8 +160,8 @@ class RedBlackTree protected - def children_both_black? - @right.black? and @left.black? + def children_color?(color) + @right.color == @left.color && @right.color == color end def color=(color) @@ -176,15 +180,9 @@ class RedBlackTree @color, other.color = other.color, @color end - def node_flip(other) - @left, other.left = other.left, @left - @right, other.right = other.right, @right - color_flip(other) - end - def delete_min if @left.empty? - [self, *delete_self] + [self, *delete_node] else ret = self deleted, @left, rebalance = @left.delete_min @@ -201,7 +199,7 @@ class RedBlackTree rebalance = false if black? if @right.black? - if @right.children_both_black? + if @right.children_color?(:BLACK) # make whole sub-tree 1 level lower and ask rebalance @right.color = :RED rebalance = true @@ -217,7 +215,7 @@ class RedBlackTree raise 'should not happen' if rebalance end else # red - if @right.children_both_black? + if @right.children_color?(:BLACK) # make right sub-tree 1 level lower color_flip(@right) else @@ -235,7 +233,7 @@ class RedBlackTree rebalance = false if black? if @left.black? - if @left.children_both_black? + if @left.children_color?(:BLACK) @left.color = :RED rebalance = true else @@ -247,7 +245,7 @@ class RedBlackTree raise 'should not happen' if rebalance end else # red - if @left.children_both_black? + if @left.children_color?(:BLACK) color_flip(@left) else ret = balanced_rotate_right @@ -318,7 +316,7 @@ class RedBlackTree # A C a c # def pullup_red - if black? and @left.red? and @right.red? + if black? and children_color?(:RED) @left.color = @right.color = :BLACK self.color = :RED end @@ -346,7 +344,7 @@ class RedBlackTree rotate_left end - def delete_self + def delete_node rebalance = false if @left.empty? and @right.empty? # just remove this node and ask rebalance to the parent @@ -366,8 +364,8 @@ class RedBlackTree end else # pick the minimum node from the right sub-tree and replace self with it - new_root, @right, rebalance = @right.delete_min - new_root.node_flip(self) + deleted, @right, rebalance = @right.delete_min + new_root = Node.new(deleted.key, deleted.value, @left, @right, @color) if rebalance new_root, rebalance = new_root.rebalance_for_right_delete end @@ -403,7 +401,7 @@ class RedBlackTree # returns new_root def insert(key, value) - Node.new(key, value) + Node.new(key, value, self, self) end # returns value @@ -441,53 +439,57 @@ class RedBlackTree @default_proc = block end + def root + @root + end + def empty? - @root == Node::EMPTY + root == Node::EMPTY end def size - @root.size + root.size end alias length size def each(&block) if block_given? - @root.each(&block) + root.each(&block) self else - Enumerator.new(@root) + Enumerator.new(root) end end alias each_pair each def each_key if block_given? - @root.each do |k, v| + root.each do |k, v| yield k end self else - Enumerator.new(@root, :each_key) + Enumerator.new(root, :each_key) end end def each_value if block_given? - @root.each do |k, v| + root.each do |k, v| yield v end self else - Enumerator.new(@root, :each_value) + Enumerator.new(root, :each_value) end end def keys - @root.keys + root.keys end def values - @root.values + root.values end def clear @@ -502,7 +504,7 @@ class RedBlackTree alias insert []= def key?(key) - @root.retrieve(key) != Node::UNDEFINED + root.retrieve(key) != Node::UNDEFINED end alias has_key? key? @@ -525,13 +527,13 @@ class RedBlackTree end def dump_tree(io = '') - @root.dump_tree(io) + root.dump_tree(io) io << $/ io end def dump_sexp - @root.dump_sexp || '' + root.dump_sexp || '' end def to_hash @@ -550,3 +552,297 @@ private end end end + +class ConcurrentRedBlackTree < RedBlackTree + class ConcurrentNode < Node + # direction: ~LEFT == RIGHT, ~RIGHT == LEFT + LEFT = -1 + RIGHT = 0 + + # @Overrides + def insert(key, value) + case key <=> @key + when -1 + dir = LEFT + when 0 + node = new_value(value) + when 1 + dir = RIGHT + else + raise TypeError, "cannot compare #{key} and #{@key} with <=>" + end + if dir + target = child(dir).insert(key, value) + node = new_child(dir, target) + if black? and child(~dir).black? and target.red? and !target.children_color?(:BLACK) + node = node.rebalance_for_insert(dir) + end + end + node.pullup_red + end + + # @Overrides + def retrieve(key) + case key <=> @key + when -1 + @left.retrieve(key) + when 0 + @value + when 1 + @right.retrieve(key) + else + nil + end + end + + # @Overrides + def delete(key) + case key <=> @key + when -1 + dir = LEFT + when 0 + deleted = self + node, rebalance = delete_node + when 1 + dir = RIGHT + else + raise TypeError, "cannot compare #{key} and #{@key} with <=>" + end + if dir + deleted, target, rebalance = child(dir).delete(key) + node = new_child(dir, target) + if rebalance + node, rebalance = node.rebalance_for_delete(dir) + end + end + [deleted, node, rebalance] + end + + protected + + def new_children(dir, node, other, color = @color) + dir == LEFT ? + ConcurrentNode.new(@key, @value, node, other, color) : + ConcurrentNode.new(@key, @value, other, node, color) + end + + def new_child(dir, node, color = @color) + dir == LEFT ? + ConcurrentNode.new(@key, @value, node, @right, color) : + ConcurrentNode.new(@key, @value, @left, node, color) + end + + def new_color(color) + ConcurrentNode.new(@key, @value, @left, @right, color) + end + + def new_value(value) + ConcurrentNode.new(@key, value, @left, @right, @color) + end + + def child(dir) + dir == LEFT ? @left : @right + end + + # @Overrides + def delete_min + if @left.empty? + [self, *delete_node] + else + deleted, left, rebalance = @left.delete_min + node = new_child(LEFT, left) + if rebalance + node, rebalance = node.rebalance_for_delete(LEFT) + end + [deleted, node, rebalance] + end + end + + # rebalance when the left/right sub-tree is 1 level lower than the right/left + def rebalance_for_delete(dir) + target = child(~dir) + rebalance = false + if black? + if target.black? + if target.children_color?(:BLACK) + # make whole sub-tree 1 level lower and ask rebalance + node = new_child(~dir, target.new_color(:RED)) + rebalance = true + else + # move 1 black from the right to the left by single/double rotation + node = balanced_rotate(dir) + end + else + # flip this sub-tree into another type of 3-children node + node = rotate(dir) + # try to rebalance in sub-tree + target, rebalance = node.child(dir).rebalance_for_delete(dir) + raise 'should not happen' if rebalance + node = node.new_children(dir, target, node.child(~dir)) + end + else # red + if target.children_color?(:BLACK) + # make right sub-tree 1 level lower + node = new_child(~dir, target.new_color(@color), target.color) + else + # move 1 black from the right to the left by single/double rotation + node = balanced_rotate(dir) + end + end + [node, rebalance] + end + + # move 1 black from the right/left to the left/right by single/double rotation + def balanced_rotate(dir) + target = child(~dir) + if target.child(dir).red? and target.child(~dir).black? + node = new_child(~dir, target.rotate(~dir)) + else + node = self + end + node = node.rotate(dir) + node.new_children(dir, node.child(dir).new_color(:BLACK), node.child(~dir).new_color(:BLACK)) + end + + # Right single rotation + # (b a (D c E)) where D and E are RED --> (d (B a c) E) + # + # b d + # / \ / \ + # a D -> B E + # / \ / \ + # c E a c + # + # Left single rotation + # (d (B A c) e) where A and B are RED --> (b A (D c e)) + # + # d b + # / \ / \ + # B e -> A D + # / \ / \ + # A c c e + # + def rotate(dir) + new_root = child(~dir) + node = new_child(~dir, new_root.child(dir), new_root.color) + new_root.new_children(dir, node, new_root.child(~dir), @color) + end + + # Pull up red nodes + # (b (A C)) where A and C are RED --> (B (a c)) + # + # b B + # / \ -> / \ + # A C a c + # + # @Overrides + def pullup_red + if black? and @left.red? and @right.red? + new_children(LEFT, @left.new_color(:BLACK), @right.new_color(:BLACK), :RED) + else + self + end + end + + # rebalance when the left/right sub-tree is 1 level higher than the right/left + # move 1 black from the left to the right by single/double rotation + # + # precondition: self is black and @left/@right is red + def rebalance_for_insert(dir) + node = self + if child(dir).child(~dir).red? + node = new_child(dir, child(dir).rotate(dir)) + end + node.rotate(~dir) + end + + private + + # @Overrides + def delete_node + rebalance = false + if @left.empty? and @right.empty? + # just remove this node and ask rebalance to the parent + new_node = EMPTY_CONCURRENT + if black? + rebalance = true + end + elsif @left.empty? or @right.empty? + # pick the single children + new_node = @left.empty? ? @right : @left + if black? + # keep the color black + raise 'should not happen' unless new_node.red? + new_node = new_node.new_color(@color) + else + # just remove the red node + end + else + # pick the minimum node from the right sub-tree and replace self with it + deleted, right, rebalance = @right.delete_min + new_node = deleted.new_children(LEFT, @left, right, @color) + if rebalance + new_node, rebalance = new_node.rebalance_for_delete(RIGHT) + end + end + [new_node, rebalance] + end + + class EmptyConcurrentNode < EmptyNode + # @Overrides + def insert(key, value) + ConcurrentNode.new(key, value, self, self) + end + end + EMPTY_CONCURRENT = ConcurrentNode::EmptyConcurrentNode.new.freeze + end + + def initialize(default = DEFAULT, &block) + super + @root = Atomic.new(ConcurrentNode::EMPTY_CONCURRENT) + end + + def root + @root.get + end + + def empty? + root == ConcurrentNode::EMPTY_CONCURRENT + end + + def clear + @root.set(ConcurrentNode::EMPTY_CONCURRENT) + end + + def []=(key, value) + @root.update { |root| + root = root.insert(key, value) + root.set_root + root.check_height if $DEBUG + root + } + end + alias insert []= + + def [](key) + value = @root.get.retrieve(key) + if value == Node::UNDEFINED + default_value + else + value + end + end + + def delete(key) + deleted = nil + @root.update { |root| + deleted, root, rebalance = root.delete(key) + unless root == ConcurrentNode::EMPTY_CONCURRENT + root.set_root + root.check_height if $DEBUG + end + root + } + deleted.value + end +end diff --git a/metadata.yml b/metadata.yml deleted file mode 100644 index 39358695dd034fb3ca822a16494e8666b2ab7005..0000000000000000000000000000000000000000 --- a/metadata.yml +++ /dev/null @@ -1,59 +0,0 @@ ---- !ruby/object:Gem::Specification -name: avl_tree -version: !ruby/object:Gem::Version - version: 1.1.3 - prerelease: -platform: ruby -authors: -- Hiroshi Nakamura -autorequire: -bindir: bin -cert_chain: [] -date: 2012-05-09 00:00:00.000000000 Z -dependencies: [] -description: -email: nahi@ruby-lang.org -executables: [] -extensions: [] -extra_rdoc_files: [] -files: -- lib/red_black_tree.rb -- lib/avl_tree.rb -- bench/bench_element_size.rb -- bench/profile.rb -- bench/bench.rb -- test/test_red_black_tree.rb -- test/helper.rb -- test/test_avl_tree.rb -- README -homepage: http://github.com/nahi/avl_tree -licenses: [] -post_install_message: -rdoc_options: [] -require_paths: -- lib -required_ruby_version: !ruby/object:Gem::Requirement - none: false - requirements: - - - ! '>=' - - !ruby/object:Gem::Version - version: '0' - segments: - - 0 - hash: -4094052252433988634 -required_rubygems_version: !ruby/object:Gem::Requirement - none: false - requirements: - - - ! '>=' - - !ruby/object:Gem::Version - version: '0' - segments: - - 0 - hash: -4094052252433988634 -requirements: [] -rubyforge_project: -rubygems_version: 1.8.23 -signing_key: -specification_version: 3 -summary: AVL tree and Red-black tree in Ruby -test_files: [] diff --git a/test/test_avl_tree.rb b/test/test_avl_tree.rb index 462768f9948f40ea036f6e5c91ab2347c5d19d3c..93b7f93d91e0fb188665b6a596ebca2120ed5917 100644 --- a/test/test_avl_tree.rb +++ b/test/test_avl_tree.rb @@ -458,6 +458,43 @@ class TestAVLTree < Test::Unit::TestCase assert_equal [], h.values end + def test_height + h = AVLTree.new + assert_equal 0, h.height + h[1] = true + assert_equal 1, h.height + h[2] = true + assert_equal 2, h.height + h[3] = true + assert_equal 2, h.height + h[4] = true + assert_equal 3, h.height + h[5] = true + assert_equal 3, h.height + h[6] = true + assert_equal 3, h.height + h[7] = true + assert_equal 3, h.height + h[8] = true + assert_equal 4, h.height + h.delete(8) + assert_equal 3, h.height + h.delete(7) + assert_equal 3, h.height + h.delete(6) + assert_equal 3, h.height + h.delete(5) + assert_equal 3, h.height + h.delete(4) + assert_equal 2, h.height + h.delete(3) + assert_equal 2, h.height + h.delete(2) + assert_equal 1, h.height + h.delete(1) + assert_equal 0, h.height + end + if RUBY_VERSION >= '1.9.0' # In contrast to RadixTree, AVLTree just uses String#<=> as-is def test_encoding diff --git a/test/test_red_black_tree.rb b/test/test_red_black_tree.rb index 6f60377cdaf73ad12ad87c3d42ea871d5d52bfdc..b18716fca0eaebcab32172458262a324658b1421 100644 --- a/test/test_red_black_tree.rb +++ b/test/test_red_black_tree.rb @@ -1,9 +1,9 @@ # -*- encoding: utf-8 -*- require File.expand_path('./helper', File.dirname(__FILE__)) -class TestRedBlackTree < Test::Unit::TestCase +module RedBlackTreeTest def __test_random - h = RedBlackTree.new + h = create_test_target 10000.times do |idx| key = rand(100) h[key] = key @@ -13,7 +13,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_tree_rotate_RR - h = RedBlackTree.new + h = create_test_target assert_equal '', h.dump_sexp h['a'] = 1 assert_equal 'a', h.dump_sexp @@ -28,7 +28,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_tree_rotate_LL - h = RedBlackTree.new + h = create_test_target h['e'] = 1 h['d'] = 2 assert_equal '(e d)', h.dump_sexp @@ -41,7 +41,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_tree_rotate_RL - h = RedBlackTree.new + h = create_test_target h['b'] = 1 h['a'] = 2 h['g'] = 3 @@ -57,7 +57,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_tree_rotate_LR - h = RedBlackTree.new + h = create_test_target h['g'] = 1 h['b'] = 2 h['h'] = 3 @@ -73,13 +73,13 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_aref_nil - h = RedBlackTree.new + h = create_test_target h['abc'] = 1 assert_equal nil, h['def'] end def test_empty - h = RedBlackTree.new + h = create_test_target h['abc'] = 0 assert_equal nil, h[''] h[''] = 1 @@ -89,13 +89,13 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_aref_single - h = RedBlackTree.new + h = create_test_target h['abc'] = 1 assert_equal 1, h['abc'] end def test_aref_double - h = RedBlackTree.new + h = create_test_target h['abc'] = 1 h['def'] = 2 assert_equal 1, h['abc'] @@ -103,14 +103,14 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_aset_override - h = RedBlackTree.new + h = create_test_target h['abc'] = 1 h['abc'] = 2 assert_equal 2, h['abc'] end def test_split - h = RedBlackTree.new + h = create_test_target h['abcd'] = 1 assert_equal 1, h['abcd'] h['abce'] = 2 @@ -128,7 +128,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_split_and_assign - h = RedBlackTree.new + h = create_test_target h['ab'] = 1 h['a'] = 2 assert_equal 1, h['ab'] @@ -136,7 +136,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_push - h = RedBlackTree.new + h = create_test_target assert_equal 0, h.size h['a'] = 1 assert_equal 1, h['a'] @@ -170,7 +170,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_different_type - h = RedBlackTree.new + h = create_test_target h['a'] = 1 assert_raise(TypeError) do h[3.3] = 2 @@ -179,7 +179,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_leaf - h = RedBlackTree.new + h = create_test_target h['b'] = 1 h['a'] = 2 h['c'] = 3 @@ -189,7 +189,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_leaf_single_rotation - h = RedBlackTree.new + h = create_test_target h['b'] = 1 h['a'] = 2 h['d'] = 3 @@ -201,7 +201,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_leaf_single_rotation_right - h = RedBlackTree.new + h = create_test_target h['d'] = 1 h['e'] = 2 h['b'] = 3 @@ -213,7 +213,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_leaf_double_rotation - h = RedBlackTree.new + h = create_test_target h['b'] = 1 h['a'] = 2 h['e'] = 3 @@ -229,7 +229,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_leaf_double_rotation_right - h = RedBlackTree.new + h = create_test_target h['d'] = 1 h['e'] = 2 h['a'] = 3 @@ -245,7 +245,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_node_right - h = RedBlackTree.new + h = create_test_target h['c'] = 1 h['b'] = 2 h['g'] = 3 @@ -262,7 +262,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_node_left - h = RedBlackTree.new + h = create_test_target h['h'] = 1 h['i'] = 2 h['d'] = 3 @@ -279,7 +279,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_root - h = RedBlackTree.new + h = create_test_target h['b'] = 1 h['a'] = 2 h['c'] = 3 @@ -291,7 +291,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete - h = RedBlackTree.new + h = create_test_target h['a'] = 1 h['ab'] = 2 h['abc'] = 3 @@ -337,7 +337,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_right - h = RedBlackTree.new + h = create_test_target h['f'] = 1 h['e'] = 2 h['d'] = 3 @@ -383,7 +383,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_compaction_middle - h = RedBlackTree.new + h = create_test_target h['a'] = 1 h['abc'] = 2 h['bb'] = 3 @@ -398,7 +398,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_compaction_leaf - h = RedBlackTree.new + h = create_test_target h['a'] = 1 h['abc'] = 2 h['bb'] = 3 @@ -413,7 +413,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_balanced_rotate_left - h = RedBlackTree.new + h = create_test_target h['f'] = 1 h['c'] = 100 h['l'] = 1 @@ -440,7 +440,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_balanced_rotate_right - h = RedBlackTree.new + h = create_test_target h['i'] = 1 h['l'] = 100 h['c'] = 1 @@ -467,7 +467,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_delete_different_type - h = RedBlackTree.new + h = create_test_target h['a'] = 1 h['abc'] = 2 h['bb'] = 3 @@ -478,7 +478,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_each - h = RedBlackTree.new + h = create_test_target s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 } s.each do |k, v| h[k] = v @@ -493,7 +493,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_each_key - h = RedBlackTree.new + h = create_test_target s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 } s.each do |k, v| h[k] = v @@ -508,7 +508,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_each_value - h = RedBlackTree.new + h = create_test_target s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6, 'azzzzz' => 6 } s.each do |k, v| h[k] = v @@ -523,7 +523,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_keys - h = RedBlackTree.new + h = create_test_target s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 } s.each do |k, v| h[k] = v @@ -532,7 +532,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_values - h = RedBlackTree.new + h = create_test_target s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 } s.each do |k, v| h[k] = v @@ -541,14 +541,14 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_to_s - h = RedBlackTree.new + h = create_test_target h[5] = 1 assert_equal 1, h[5] assert_nil h["5"] end def test_key? - h = RedBlackTree.new + h = create_test_target assert !h.key?('a') s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 } s.each do |k, v| @@ -559,22 +559,22 @@ class TestRedBlackTree < Test::Unit::TestCase def test_default assert_raise(ArgumentError) do - RedBlackTree.new('both') { :not_allowed } + create_test_target('both') { :not_allowed } end - h = RedBlackTree.new('abc') + h = create_test_target('abc') assert_equal 'abc', h['foo'] assert_equal 'abc', h['bar'] assert h['baz'].object_id == h['qux'].object_id - h = RedBlackTree.new { [1, 2] } + h = create_test_target { [1, 2] } assert_equal [1, 2], h['foo'] assert_equal [1, 2], h['bar'] assert h['baz'].object_id != h['qux'].object_id end def test_to_hash - h = RedBlackTree.new + h = create_test_target s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 } s.each do |k, v| h[k] = v @@ -583,7 +583,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_clear - h = RedBlackTree.new + h = create_test_target s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 } s.each do |k, v| h[k] = v @@ -595,7 +595,7 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_non_string_keys - h = RedBlackTree.new + h = create_test_target h[1.3] = 'a' h[4.3] = 'b' @@ -603,15 +603,21 @@ class TestRedBlackTree < Test::Unit::TestCase end def test_values_for_empty_tree - h = RedBlackTree.new + h = create_test_target assert_equal [], h.values end + def test_check_height_on_empty_tree + h = create_test_target + + assert_nothing_raised { h.root.check_height } + end + if RUBY_VERSION >= '1.9.0' # In contrast to RadixTree, RedBlackTree just uses String#<=> as-is def test_encoding - h = RedBlackTree.new + h = create_test_target s = { 'ああ' => 1, 'あい' => 2, 'いい' => 3, 'いう瘢雹' => 4, 'あ' => 5, 'あいう瘢雹' => 6 } s.each do |k, v| h[k] = v @@ -627,3 +633,19 @@ class TestRedBlackTree < Test::Unit::TestCase end end end + +class TestRedBlackTree < Test::Unit::TestCase + include RedBlackTreeTest + + def create_test_target(*a, &b) + RedBlackTree.new(*a, &b) + end +end + +class TestConcurrentRedBlackTree < Test::Unit::TestCase + include RedBlackTreeTest + + def create_test_target(*a, &b) + ConcurrentRedBlackTree.new(*a, &b) + end +end diff --git a/test/test_red_black_tree_thread.rb b/test/test_red_black_tree_thread.rb new file mode 100644 index 0000000000000000000000000000000000000000..b0bb04e6a297ef25178d23367098b4a93922c2da --- /dev/null +++ b/test/test_red_black_tree_thread.rb @@ -0,0 +1,39 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('./helper', File.dirname(__FILE__)) + +class TestRedBlackTree < Test::Unit::TestCase + def test_thread + h = ConcurrentRedBlackTree.new + num = 100000 + max = 1000 + threads = [] + # writers + 2.times do + threads << Thread.new { + num.times do + key = rand(max) + h[key] = key + end + } + end + # deleters + 2.times do + threads << Thread.new { + num.times do + key = rand(max) + h.delete(key) + end + } + end + # readers + 2.times do + threads << Thread.new { + num.times do + key = rand(max) + h[key] + end + } + end + threads.each(&:join) + end +end