mirror of
https://github.com/TheAlgorithms/Ruby
synced 2025-02-03 08:46:02 +01:00
Merge pull request #199 from aparibocci/feature/bst
Adding `BinarySearchTree` implementation
This commit is contained in:
commit
64824018d5
2 changed files with 288 additions and 0 deletions
176
data_structures/binary_trees/bst.rb
Normal file
176
data_structures/binary_trees/bst.rb
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
class BinarySearchTreeNode
|
||||||
|
|
||||||
|
attr_reader :key
|
||||||
|
attr_accessor :left
|
||||||
|
attr_accessor :right
|
||||||
|
|
||||||
|
def initialize(key)
|
||||||
|
@key = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# This class represents a binary search tree (not implementing self-balancing) with distinct node keys.
|
||||||
|
# Starting from the root, every node has up to two children (one left and one right child node).
|
||||||
|
#
|
||||||
|
# For the BST property:
|
||||||
|
# - the keys of nodes in the left subtree of a node are strictly less than the key of the node;
|
||||||
|
# - the keys of nodes in the right subtree of a node are strictly greater than the key of the node.
|
||||||
|
#
|
||||||
|
# The main operations of this data structure (insertion, deletion, membership) run - in worst case - in O(n),
|
||||||
|
# where n is the number of nodes in the tree.
|
||||||
|
# The average case for those operations is O(log(n)) due to the structure of the tree.
|
||||||
|
|
||||||
|
class BinarySearchTree
|
||||||
|
|
||||||
|
attr_reader :size
|
||||||
|
attr_accessor :root
|
||||||
|
|
||||||
|
def initialize(keys=[])
|
||||||
|
@size = 0
|
||||||
|
keys.each {|key| insert_key(key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
size == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_key(key)
|
||||||
|
@size += 1
|
||||||
|
if root.nil?
|
||||||
|
@root = BinarySearchTreeNode.new(key)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
parent = root
|
||||||
|
while (key < parent.key && !parent.left.nil? && parent.left.key != key) ||
|
||||||
|
(key > parent.key && !parent.right.nil? && parent.right.key != key)
|
||||||
|
parent = key < parent.key ? parent.left : parent.right
|
||||||
|
end
|
||||||
|
if key < parent.key
|
||||||
|
raise ArgumentError.new("Key #{key} is already present in the BinarySearchTree") unless parent.left.nil?
|
||||||
|
parent.left = BinarySearchTreeNode.new(key)
|
||||||
|
else
|
||||||
|
raise ArgumentError.new("Key #{key} is already present in the BinarySearchTree") unless parent.right.nil?
|
||||||
|
parent.right = BinarySearchTreeNode.new(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def min_key(node=root)
|
||||||
|
return nil if node.nil?
|
||||||
|
min_key_node(node).key
|
||||||
|
end
|
||||||
|
|
||||||
|
def max_key(node=root)
|
||||||
|
return nil if node.nil?
|
||||||
|
max_key_node(node).key
|
||||||
|
end
|
||||||
|
|
||||||
|
def contains_key?(key)
|
||||||
|
!find_node_with_key(key).nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_key(key)
|
||||||
|
parent = find_parent_of_node_with_key(key)
|
||||||
|
if parent.nil?
|
||||||
|
return if root.nil? || root.key != key
|
||||||
|
@size -= 1
|
||||||
|
@root = adjusted_subtree_after_deletion(root.left, root.right)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if key < parent.key
|
||||||
|
node = parent.left
|
||||||
|
parent.left = adjusted_subtree_after_deletion(node.left, node.right)
|
||||||
|
else
|
||||||
|
node = parent.right
|
||||||
|
parent.right = adjusted_subtree_after_deletion(node.left, node.right)
|
||||||
|
end
|
||||||
|
@size -= 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def traverse_preorder(key_consumer, node=root)
|
||||||
|
return if node.nil?
|
||||||
|
key_consumer.call(node.key)
|
||||||
|
traverse_preorder(key_consumer, node.left) unless node.left.nil?
|
||||||
|
traverse_preorder(key_consumer, node.right) unless node.right.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def traverse_inorder(key_consumer, node=root)
|
||||||
|
return if node.nil?
|
||||||
|
traverse_inorder(key_consumer, node.left) unless node.left.nil?
|
||||||
|
key_consumer.call(node.key)
|
||||||
|
traverse_inorder(key_consumer, node.right) unless node.right.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def traverse_postorder(key_consumer, node=root)
|
||||||
|
return if node.nil?
|
||||||
|
traverse_postorder(key_consumer, node.left) unless node.left.nil?
|
||||||
|
traverse_postorder(key_consumer, node.right) unless node.right.nil?
|
||||||
|
key_consumer.call(node.key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_array(visit_traversal=:traverse_preorder)
|
||||||
|
visited = []
|
||||||
|
method(visit_traversal).call(->(key) { visited.append(key) })
|
||||||
|
visited
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def min_key_node(node=root)
|
||||||
|
return nil if node.nil?
|
||||||
|
until node.left.nil?
|
||||||
|
node = node.left
|
||||||
|
end
|
||||||
|
node
|
||||||
|
end
|
||||||
|
|
||||||
|
def max_key_node(node=root)
|
||||||
|
return nil if node.nil?
|
||||||
|
until node.right.nil?
|
||||||
|
node = node.right
|
||||||
|
end
|
||||||
|
node
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_node_with_key(key)
|
||||||
|
node = root
|
||||||
|
until node.nil? || node.key == key
|
||||||
|
node = key < node.key ? node.left : node.right
|
||||||
|
end
|
||||||
|
node
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_parent_of_node_with_key(key)
|
||||||
|
return nil if root.nil? || root.key == key
|
||||||
|
parent = root
|
||||||
|
until parent.nil?
|
||||||
|
if key < parent.key
|
||||||
|
return nil if parent.left.nil?
|
||||||
|
return parent if parent.left.key == key
|
||||||
|
parent = parent.left
|
||||||
|
else
|
||||||
|
return nil if parent.right.nil?
|
||||||
|
return parent if parent.right.key == key
|
||||||
|
parent = parent.right
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def adjusted_subtree_after_deletion(left, right)
|
||||||
|
return right if left.nil?
|
||||||
|
return left if right.nil?
|
||||||
|
if right.left.nil?
|
||||||
|
right.left = left
|
||||||
|
return right
|
||||||
|
end
|
||||||
|
successor_parent = right
|
||||||
|
until successor_parent.left.left.nil?
|
||||||
|
successor_parent = successor_parent.left
|
||||||
|
end
|
||||||
|
successor = successor_parent.left
|
||||||
|
successor_parent.left = successor.right
|
||||||
|
successor.right = right
|
||||||
|
successor.left = left
|
||||||
|
successor
|
||||||
|
end
|
||||||
|
end
|
112
data_structures/binary_trees/bst_test.rb
Normal file
112
data_structures/binary_trees/bst_test.rb
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
require 'minitest/autorun'
|
||||||
|
require_relative 'bst'
|
||||||
|
|
||||||
|
class TestBinarySearchTree < Minitest::Test
|
||||||
|
def test_default_constructor_creates_empty_tree
|
||||||
|
bst = BinarySearchTree.new
|
||||||
|
assert bst.to_array.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_default_constructor_creates_tree_with_given_keys
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
assert bst.to_array == [4, 2, 1, 3, 6]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_exception_when_inserting_key_already_present
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
assert_raises ArgumentError do
|
||||||
|
bst.insert_key(6)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_size_returns_zero_given_empty_tree
|
||||||
|
bst = BinarySearchTree.new
|
||||||
|
assert bst.size == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_empty_returns_number_of_nodes_in_tree
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
assert bst.size == 5
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_empty_returns_true_given_empty_tree
|
||||||
|
bst = BinarySearchTree.new
|
||||||
|
assert bst.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_empty_returns_false_given_non_empty_tree
|
||||||
|
bst = BinarySearchTree.new([1])
|
||||||
|
assert !bst.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_min_key_returns_minimum_key
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
assert bst.min_key == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_max_key_returns_maximum_key
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
assert bst.max_key == 6
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_contains_key_returns_true_if_key_in_tree
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
assert bst.contains_key?(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_contains_key_returns_false_if_key_not_in_tree
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
assert !bst.contains_key?(7)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_delete_key_does_nothing_if_key_not_in_tree
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
bst.delete_key(7)
|
||||||
|
assert bst.to_array == [4, 2, 1, 3, 6]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_delete_key_keeps_bst_property_if_leaf_node
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
bst.delete_key(1)
|
||||||
|
assert bst.to_array == [4, 2, 3, 6]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_delete_key_keeps_bst_property_if_node_with_left_child
|
||||||
|
bst = BinarySearchTree.new([4, 2, 3, 1])
|
||||||
|
bst.delete_key(4)
|
||||||
|
assert bst.to_array == [2, 1, 3]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_delete_key_keeps_bst_property_if_node_with_right_child
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3])
|
||||||
|
bst.delete_key(2)
|
||||||
|
assert bst.to_array == [4, 3, 6]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_delete_key_keeps_bst_property_if_node_with_both_children
|
||||||
|
bst = BinarySearchTree.new([4, 2, 7, 3, 1, 5, 10, 6])
|
||||||
|
bst.delete_key(4)
|
||||||
|
assert bst.to_array == [5, 2, 1, 3, 7, 6, 10]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preorder_traversal_uses_expected_order
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
visited = []
|
||||||
|
bst.traverse_preorder(->(key) { visited.append(key) })
|
||||||
|
assert visited == [4, 2, 1, 3, 6]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_inorder_traversal_uses_expected_order
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
visited = []
|
||||||
|
bst.traverse_inorder(->(key) { visited.append(key) })
|
||||||
|
assert visited == [1, 2, 3, 4, 6]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_postorder_traversal_uses_expected_order
|
||||||
|
bst = BinarySearchTree.new([4, 2, 6, 3, 1])
|
||||||
|
visited = []
|
||||||
|
bst.traverse_postorder(->(key) { visited.append(key) })
|
||||||
|
assert visited == [1, 3, 2, 6, 4]
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue