Merge pull request #200 from aparibocci/feature/avl_tree

Adding `AVL Tree` implementation
This commit is contained in:
Stepfen Shawn 2023-04-11 14:50:38 +08:00 committed by GitHub
commit a2730e24c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 413 additions and 0 deletions

View file

@ -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

View file

@ -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