AVL trees are a self-balancing binary search tree where the difference between heights of left and right subtrees cannot be more than one for all nodes. They were invented by Adelson-Velsky and Landis in 1962. The main advantage of AVL trees over other balanced BSTs like Red-Black Trees is that they are more strictly balanced, which leads to faster lookups.
AVL trees ensure that the height of the tree remains logarithmic with respect to the number of nodes. This property ensures that operations such as search, insert, and delete can be performed in O(log n) time complexity, making AVL trees highly efficient for applications requiring frequent lookups.
An AVL tree is a binary search tree where each node has an additional attribute called balance factor. The balance factor of a node is defined as the height difference between its left and right subtrees:
For an AVL tree to be balanced, the balance factor for every node must be either -1, 0, or +1.
Inserting a new key into an AVL tree is similar to inserting it into a binary search tree. However, after insertion, we need to check if the tree remains balanced by updating the balance factors and performing rotations if necessary.
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
this.height = 1;
}
}
function getHeight(node) {
return node ? node.height : 0;
}
function getBalanceFactor(node) {
return node ? getHeight(node.left) - getHeight(node.right) : 0;
}
function rightRotate(y) {
const x = y.left;
const T2 = x.right;
// Perform rotation
x.right = y;
y.left = T2;
// Update heights
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
function leftRotate(x) {
const y = x.right;
const T2 = y.left;
// Perform rotation
y.left = x;
x.right = T2;
// Update heights
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
return y;
}
function insert(node, key) {
if (!node) return new Node(key);
if (key < node.key) {
node.left = insert(node.left, key);
} else if (key > node.key) {
node.right = insert(node.right, key);
} else {
// Duplicate keys are not allowed in BST
return node;
}
// Update height of this ancestor node
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
// Get the balance factor of this ancestor node to check whether
// this node became unbalanced
const balance = getBalanceFactor(node);
// If this node becomes unbalanced, then there are 4 cases
// Left Left Case
if (balance > 1 && key < node.left.key) {
return rightRotate(node);
}
// Right Right Case
if (balance < -1 && key > node.right.key) {
return leftRotate(node);
}
// Left Right Case
if (balance > 1 && key > node.left.key) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// Right Left Case
if (balance < -1 && key < node.right.key) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
// Return the (unchanged) node pointer
return node;
}
Deleting a node from an AVL tree involves standard BST deletion followed by rotations to maintain balance.
Searching in an AVL tree is identical to searching in a binary search tree, with O(log n) time complexity due to the balanced nature of the tree.
Rotations are used to maintain the balance of the AVL tree after insertions or deletions. There are four types of rotations:
AVL trees are a powerful data structure that ensures efficient operations by maintaining strict balance. Understanding their properties, operations, and rotations is essential for implementing them in real-world applications. By following best practices and adhering to the principles of AVL trees, developers can create highly optimized solutions for scenarios requiring frequent lookups and dynamic updates.
This comprehensive guide provides a deep dive into AVL trees, covering their structure, operations, and best practices. By mastering these concepts, you'll be well-equipped to implement and utilize AVL trees in your software projects.