Interview Kickstart has enabled over 21000 engineers to uplevel.
Basic operations on a tree include search, insertion, and deletion. AVL trees can perform all these operations in just O(logN) time complexity, where “N” is the number of nodes in the tree. Therefore, every Software Engineer must be familiar with AVL trees.
Software engineering interview problems involving operations on trees can be solved using AVL trees. This article covers the topic of AVL Trees in detail to help you with your tech interview prep.
Prerequisites: A binary search tree is a prerequisite before learning about AVL Trees. You can read about Binary Search Trees here: BinarySearchTree.
List of topics covered in this article:
AVL tree is a self-balancing Binary Search Tree named after its inventors, Adelson-Velskii and Landis. For each node in an AVL tree, the difference between the heights of the left and right subtrees is either 1, 0, or -1. The Balance Factor of a node refers to the difference between the heights of the left and right subtrees.
AVL tree operations are similar to BST’s, but the key difference is restoring the height balance of the subtrees. Restoring of the height of the tree is done using four types of rotations, namely: Left-Left (LL), Right-Right (RR), Left-Right (LR), and Right-Left (RL).
In a Binary Tree, the balance factor of a node is defined as the difference between the heights of its left subtree and right subtree. For a Binary Tree, let “Node” represent any node in the tree. Then,
BalanceFactor( Node ) = Height( LeftSubtree( Node ) ) - Height( RightSubtree( Node ) ) …...i
We can also represent the BalanceFactor of a node as:
BalanceFactor( Node ) = Height( RightSubtree( Node ) ) - Height( LeftSubtree( Node ) ) .….ii
For the sake of convenience, we will consider the first equation while defining terms in the rest of the article below.
In the case of an AVL tree, the balance factor can comprise of only three possible values: { -1, 0, 1 } that is
BalanceFactor ( Node ) = { -1 , 0 , 1 } . Here Node is any node in the AVL tree.
After inserting a new node or deleting an existing node from an AVL tree, the Balance Factors for all nodes are calculated. If for any node the balance factor is greater than one or less than -1, the tree will rebalance by performing rotations; this is discussed further in later sections.
Let’s look at an example first for a better understanding.
Suppose we have a database that contains the list of all existing metro stations in the country.
Here we can represent our database as an AVL tree. The reason is that the number of search operations required is very high compared to the number of insertions required.
Theoretically, AVL Tree is a self-balancing BST, so the complexity of insertion and lookup takes O (log N) time in both the average and worst case, where “N” is the number of nodes in the tree before the operation.
But the problem with AVL trees is that when we insert a new node, we need to perform rotation to make the tree balanced, which is a costly operation. So for use cases that require more insertions than search, AVL is not the best fit. It’s efficient when we need more search operations than insertions.
AVL tree rotations are required to balance the tree at all levels. There are four types of rotations.
The violation occurs when the balance factor for any node in the tree does not belong to the set {-1,0,1}.
Note: In the diagrams below for LL, RR, LR, and RL rotations, the nodes A, B, C, and D are balanced subtrees.
Order of nodes before rotation:
A < X < B < Y < C
Order of nodes after rotation:
A < X < B < Y < C
Note that after left rotation, the order of invariants remains the same. Here, Left rotation is required because X is imbalanced.
Below is the pseudocode to perform Left rotation on a node in the case of LL :
function rotateLeft(current)
newRoot = current → right
current → right = newRoot → left
newRoot → left = current
updateHeightOfChildren(current)
updateHeightOfChildren(newRoot)
return newRoot
end function
Order of nodes before rotation:
A < X < B < Y < C
Order of nodes after rotation:
A < X < B < Y < C
Note that after Right rotation, the order of invariants remains the same. Here, Right rotation is required because A has become too tall after an insertion.
Below is the pseudocode to perform Right rotation on a node in case of RR :
function rotateRight(current)
newRoot = current → left
current → left = newRoot → right
newRoot → right = current
updateHeightOfChildren(current)
updateHeightOfChildren(newRoot)
return newRoot
end function
Order of nodes before rotation:
A < X < B < Y < C < Z < D
Order of nodes after rotation:
A < X < B < Y < C < Z < D
Note that after Left-Right rotation, the order of invariants remains the same. Here, Left-Right rotation is required because the subtree rooted at Y has become too tall.
Below is the pseudocode to perform Left-Right rotation on a node in case of LR :
function leftRightRotate(current)
current → left = leftRotate(current → left)
return rightRotate(current)
end function
Order of nodes before rotation:
A < X < B < Z < C < Y < D
Order of nodes after rotation:
A < X< B < Z < C < Y < D
Note that after Right-Left rotation, the order of invariants remains the same. Here, Right-Left rotation is required because the subtree rooted at Z has become too tall.
Below is the pseudocode to perform Right-Left rotation on a node in case of RL :
function rightLeftRotate(current)
current → right = rightRotate(current → right)
return leftRotate(current)
end function
For insertion of a new node in an AVL tree, firstly, the proper position of the node needs to be figured out, which is as per the Binary Search Tree rules.
By now, we have inserted our node, and our tree is a BST but is it an AVL tree?. No.
We know that an AVL tree is height-balanced. So we now need to calculate the balance factor of all the nodes, and if some nodes are not balanced, we need to perform the appropriate rotations to keep the tree height balanced.
Note: In this article, we have assumed that every node in the AVL tree will have a unique key. No two nodes can have the same key.
Let us assume we already have “N” nodes in the tree before insertion, and the tree is height-balanced. In this case, we travel the tree’s height in the worst case to insert our new node, which is O(log N). Then, we retrace levels to maintain the balance of the tree if needed, which is also at max O (log N). Thus our complexity of inserting a new node in the worst case is O (log N) time.
1. Recursively find the position of the new node in the AVL tree as per BST rules and insert it.
2. Now, while backtracking the path to the parent node from the newly inserted node, update the height of the current node in the AVL tree.
3. Since the height is updated, we can now compute the balance factor.
4. If the balance factor is greater than 1 or less than -1, we perform rotations (LL, RR, LR, or RL based on the key inserted) and balance the tree.
Let’s understand with an example:
We need to insert the elements below in an AVL tree.
Elements : [ 40, 20 , 10, 25, 30, 22 ]
Searching for a node in an AVL tree is similar to that of a BST. We start from the root node and keep checking the searchKey with the key of the node. If searchKey equals the key of the node we return; if searchKey is greater, we search towards the right subtree. Otherwise, we search in the left subtree.
The time complexity of the search function is based on the height of the tree, which is logN, N being the number of nodes. Hence the worst-case complexity of the search is O(logN).
Code:
public Node search(int key) {
Node temp = root;
while ( temp != null ) {
if ( temp.key == key )
return temp;
else if ( temp.key < key )
temp = temp.right;
else
temp = temp.left;
}
// Key Not found, so return null.
return null;
}
The code below inserts a node in the AVL Tree, balances the AVL tree if anything becomes unbalanced, and finally prints the inorder traversal of the tree.
import java.util.Objects;
class AVLTreeDataStructure {
class AVLTree {
private Node root;
public class Node {
int key;
int height;
Node left , right;
public Node(int key) {
this.key = key;
this.height = 1;
this.left = null;
this.right = null;
}
}
public void insert(int key) {
root = insert(root , key);
}
public void printAVLTree() {
System.out.print("InorderTraversal : ");
inOrderTraversal(root);
System.out.println();
}
/**
* Inserts a new node with the given key inside the AVL tree.
* @param node Root node from where we start our search to find the position of the new node.
* @param key of the new node to be inserted.
* @return Root node of the AVL tree.
*/
private Node insert(Node node , int key) {
if (node == null) {
return new Node(key);
}
if (node.key < key) {
node.right = insert(node.right , key);
} else if (node.key > key) {
node.left = insert(node.left , key);
} else if ( node.key == key ) {
throw new IllegalStateException("Same key already Exists!");
}
node.height = updateNodeHeight(node);
return selfBalanceBasedOnBalanceFactor(node , key);
}
private Node selfBalanceBasedOnBalanceFactor(Node node , int key) {
int balanceFactor = balanceFactor(node);
if (balanceFactor > 1) {
if (key < node.left.key) {
return rotateRight(node);
}
else if (key > node.left.key) {
node.left = rotateLeft(node.left);
return rotateRight(node);
}
} else if (balanceFactor < -1) {
if (key > node.right.key) {
return rotateLeft(node);
} else if (key < node.right.key) {
node.right = rotateRight(node.right);
return rotateLeft(node);
}
}
return node;
}
private Node rotateLeft(Node parent) {
Node rightOfParent = parent.right;
Node rightOfLeftOfParent = parent.right.left;
rightOfParent.left = parent;
parent.right = rightOfLeftOfParent;
updateNodeHeight(parent);
updateNodeHeight(rightOfParent);
return rightOfParent;
}
private Node rotateRight(Node parent) {
Node leftOfParent = parent.left;
Node leftOfRightOfParent = parent.left.right;
leftOfParent.right = parent;
parent.left = leftOfRightOfParent;
updateNodeHeight(parent);
updateNodeHeight(leftOfParent);
return leftOfParent;
}
private int height(Node node) {
return (node == null) ? 0 : node.height;
}
private int updateNodeHeight(Node node) {
return Math.max( height(node.left) , height(node.right) ) + 1;
}
private int balanceFactor(Node node) {
if (node == null)
return 0;
else
return height(node.left) - height(node.right);
}
public Node search(int key) {
Node temp = root;
while ( temp != null ) {
if ( temp.key == key )
return temp;
else if ( temp.key < key )
temp = temp.right;
else
temp = temp.left;
}
// Key Not found, so return null.
return null;
}
private void inOrderTraversal(Node node) {
if ( node == null ) {
return;
}
inOrderTraversal(node.left);
System.out.print(node.key+ " ");
inOrderTraversal(node.right);
}
}
public static void main(String[] args) {
new AVLTreeDataStructure().solve();
}
void solve() {
AVLTree avlTree = new AVLTree();
avlTree.insert(3);
avlTree.insert(2);
avlTree.insert(1);
avlTree.printAVLTree();
System.out.println(Objects.nonNull(avlTree.search(2))?"Found" : "Not Found");
System.out.println(Objects.nonNull(avlTree.search(4))?"Found" : "Not Found");
}
}
InorderTraversal : 1 2 3
Found
Not Found
Let us now look at the time and space complexities associated with AVL Trees.
Consider `N` as the number of nodes of the tree. In the worst and average cases, basic data structure operations like insertion, deletion, and searching take O(logN). In such cases is where the AVL tree outperforms the binary search tree.
The complexities for all operations — insertion, deletion, and search — depend on the tree’s height. The AVL tree is a self-balancing binary search tree, so the tree’s height in the worst case goes O(logN). The AVL tree’s guaranteed height h is O(logN). Balancing the tree rotations is necessary, and it takes O(1) time. So overall complexity in the worst case remains O(logN).
The space complexity of the AVL tree is O(N) in both the worst case and average case.
A red-black tree and an AVL tree both are self-balancing trees. Both of them provide O(logN) for insertion and search operations. An AVL tree is more rigidly (strictly) balanced than a red-black tree.
However, an AVL tree also comes with more rotation costs than a red-black tree. So when insertion of data is costly, a red-black tree is preferred. When lookups of data are high in comparison to insertion, then an AVL tree is preferred.
A binary search tree is not necessarily a balanced tree. So in the case of a skewed tree(left or right), the tree’s height will be O(N). In the worst case, for insert and search operations, it will take O(N).
An AVL tree will always take O(logN) because it’s balanced, and the tree’s height is also guaranteed O(logN).
Strengths:
Weakness:
1. Maintain a dynamic data structure that supports two operations:
2. Implement a data structure that supports insert(X), delete(X), search(X), findMin(), and findMax() in logarithmic time complexity. Here:
Example: Railway databases can take advantage of AVL Trees as new trains are seldom added, whereas many people often search for the available list of trains.
Whether you’re a Coding Engineer gunning for Software Developer or Software Engineer roles, or you’re targeting management positions at top companies, IK offers courses specifically designed for your needs to help you with your technical interview preparation!
If you’re looking for guidance and help with getting started, sign up for our free webinar. As pioneers in the field of technical interview prep, we have trained thousands of Software Engineers to crack the most challenging coding interviews and land jobs at their dream companies, such as Google, Facebook, Apple, Netflix, Amazon, and more!
————
Article contributed by Problem Setters Official
Attend our webinar on
"How to nail your next tech interview" and learn