From 286c0e4dde6b931bd5ab6cec791a7a9e03b543ba Mon Sep 17 00:00:00 2001 From: Amos Paribocci Date: Fri, 17 Feb 2023 23:07:55 +0100 Subject: [PATCH] Adding AVL Tree implementation --- data_structures/binary_trees/avl_tree.rb | 281 ++++++++++++++++++ data_structures/binary_trees/avl_tree_test.rb | 132 ++++++++ 2 files changed, 413 insertions(+) create mode 100644 data_structures/binary_trees/avl_tree.rb create mode 100644 data_structures/binary_trees/avl_tree_test.rb diff --git a/data_structures/binary_trees/avl_tree.rb b/data_structures/binary_trees/avl_tree.rb new file mode 100644 index 0000000..2774e81 --- /dev/null +++ b/data_structures/binary_trees/avl_tree.rb @@ -0,0 +1,281 @@ +class AvlTreeNode + + attr_reader :key + attr_accessor :parent + attr_accessor :left + attr_accessor :right + attr_accessor :height + + def initialize(key, parent=nil) + @key = key + @parent = parent + @height = 1 + end +end + +## +# This class represents an AVL tree (a self-balancing binary search tree) 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. +# +# Due to self-balancing upon key insertion and deletion, the main operations of this data structure +# (insertion, deletion, membership) run - in worst case - in O(log(n)), where n is the number of nodes in the tree. + +class AvlTree + + 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 = AvlTreeNode.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 AvlTree") unless parent.left.nil? + parent.left = AvlTreeNode.new(key, parent) + else + raise ArgumentError.new("Key #{key} is already present in the AvlTree") unless parent.right.nil? + parent.right = AvlTreeNode.new(key, parent) + end + balance(parent) + 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) + root.parent = nil + balance(root.right.nil? ? root : root.right) + return + end + if key < parent.key + node = parent.left + parent.left = adjusted_subtree_after_deletion(node.left, node.right) + unless parent.left.nil? + parent.left.parent = parent + balance(parent.left.right.nil? ? parent.left : parent.left.right) + end + else + node = parent.right + parent.right = adjusted_subtree_after_deletion(node.left, node.right) + unless parent.right.nil? + parent.right.parent = parent + balance(parent.right.right.nil? ? parent.right : parent.right.right) + end + 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 + left.parent = right + 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.parent = successor_parent unless successor.right.nil? + successor.right = right + right.parent = successor + successor.left = left + left.parent = successor + successor + end + + def balance(node) + return if node.nil? + left_height = node.left&.height || 0 + right_height = node.right&.height || 0 + # Assumption: the subtrees rooted at `node.left` and `node.right` are balanced + adjust_height(node) + if right_height - left_height > 1 + # `node` is right-heavy + if !node.right.left.nil? && (node.right.left.height || 0) > (node.right.right&.height || 0) + rotate_right_left(node) + else + rotate_left(node) + end + elsif left_height - right_height > 1 + # `node` is left-heavy + if !node.left.right.nil? && (node.left.right.height || 0) > (node.left.left&.height || 0) + rotate_left_right(node) + else + rotate_right(node) + end + end + + balance(node.parent) + end + + def rotate_left(node) + new_root = node.right + if node == root + @root = new_root + elsif node.parent.left == node + node.parent.left = new_root + else + node.parent.right = new_root + end + new_root.parent = node.parent + if new_root.left.nil? + node.right = nil + new_root.left = node + node.parent = new_root + else + node.right = new_root.left + new_root.left.parent = node + new_root.left = node + node.parent = new_root + end + adjust_height(node) + adjust_height(new_root) + end + + def rotate_right(node) + new_root = node.left + if node == root + @root = new_root + elsif node.parent.left == node + node.parent.left = new_root + else + node.parent.right = new_root + end + new_root.parent = node.parent + if new_root.right.nil? + node.left = nil + new_root.right = node + node.parent = new_root + else + node.left = new_root.right + new_root.right.parent = node + new_root.right = node + node.parent = new_root + end + adjust_height(node) + adjust_height(new_root) + end + + def rotate_right_left(node) + rotate_right(node.right) + rotate_left(node) + end + + def rotate_left_right(node) + rotate_left(node.left) + rotate_right(node) + end + + def adjust_height(node) + node.height = 1 + [node.left&.height || 0, node.right&.height || 0].max + end +end diff --git a/data_structures/binary_trees/avl_tree_test.rb b/data_structures/binary_trees/avl_tree_test.rb new file mode 100644 index 0000000..df577f3 --- /dev/null +++ b/data_structures/binary_trees/avl_tree_test.rb @@ -0,0 +1,132 @@ +require 'minitest/autorun' +require_relative 'avl_tree' + +class TestAvlTree < Minitest::Test + def test_default_constructor_creates_empty_tree + tree = AvlTree.new + assert tree.to_array.empty? + end + + def test_default_constructor_creates_tree_with_given_keys + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + assert tree.to_array == [4, 2, 1, 3, 6, 5, 7] + end + + def test_exception_when_inserting_key_already_present + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert_raises ArgumentError do + tree.insert_key(6) + end + end + + def test_size_returns_zero_given_empty_tree + tree = AvlTree.new + assert tree.size == 0 + end + + def test_empty_returns_number_of_nodes_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.size == 5 + end + + def test_empty_returns_true_given_empty_tree + tree = AvlTree.new + assert tree.empty? + end + + def test_empty_returns_false_given_non_empty_tree + tree = AvlTree.new([1]) + assert !tree.empty? + end + + def test_min_key_returns_minimum_key + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.min_key == 1 + end + + def test_max_key_returns_maximum_key + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.max_key == 6 + end + + def test_contains_key_returns_true_if_key_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.contains_key?(3) + end + + def test_contains_key_returns_false_if_key_not_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert !tree.contains_key?(7) + end + + def test_delete_key_does_nothing_if_key_not_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + tree.delete_key(7) + assert tree.to_array == [4, 2, 1, 3, 6] + end + + def test_delete_key_keeps_avl_property_if_leaf_node + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + tree.delete_key(3) + assert tree.to_array == [4, 2, 1, 6, 5, 7] + end + + def test_delete_key_keeps_avl_property_if_node_with_left_child + tree = AvlTree.new([4, 2, 5, 1]) + tree.delete_key(2) + assert tree.to_array == [4, 1, 5] + end + + def test_delete_key_keeps_avl_property_if_node_with_right_child + tree = AvlTree.new([4, 2, 5, 6]) + tree.delete_key(5) + assert tree.to_array == [4, 2, 6] + end + + def test_delete_key_keeps_avl_property_if_node_with_both_children + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7, 8, 9]) + tree.delete_key(4) + assert tree.to_array == [5, 2, 1, 3, 8, 6, 7, 9] + end + + def test_preorder_traversal_uses_expected_order + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + visited = [] + tree.traverse_preorder(->(key) { visited.append(key) }) + assert visited == [4, 2, 1, 3, 6, 5, 7] + end + + def test_inorder_traversal_uses_expected_order + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + visited = [] + tree.traverse_inorder(->(key) { visited.append(key) }) + assert visited == [1, 2, 3, 4, 5, 6, 7] + end + + def test_postorder_traversal_uses_expected_order + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + visited = [] + tree.traverse_postorder(->(key) { visited.append(key) }) + assert visited == [1, 3, 2, 5, 7, 6, 4] + end + + def test_left_rotation + tree = AvlTree.new([1, 2, 3]) + assert tree.to_array == [2, 1, 3] + end + + def test_right_rotation + tree = AvlTree.new([3, 2, 1]) + assert tree.to_array == [2, 1, 3] + end + + def test_right_left_rotation + tree = AvlTree.new([1, 3, 2]) + assert tree.to_array == [2, 1, 3] + end + + def test_left_right_rotation + tree = AvlTree.new([3, 1, 2]) + assert tree.to_array == [2, 1, 3] + end +end