diff --git a/data_structures/binary_trees/bst.rb b/data_structures/binary_trees/bst.rb new file mode 100644 index 0000000..3ec5aab --- /dev/null +++ b/data_structures/binary_trees/bst.rb @@ -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 diff --git a/data_structures/binary_trees/bst_test.rb b/data_structures/binary_trees/bst_test.rb new file mode 100644 index 0000000..5a761dd --- /dev/null +++ b/data_structures/binary_trees/bst_test.rb @@ -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