Binary Tree Problems Must For Interviews and Competitive Coding
Binary Tree Problems Must For Interviews and Competitive Coding
iq.OPENGENUS.org
About this book
This book “Binary Tree Problems ” is carefully crafted to present you the
knowledge and practice (around the data structure, Binary Tree ) needed to
ace Coding Interviews and Competitive Coding Contests.
The book takes you through the fundamentals of Binary Tree, presents how
to implement it in a good and secure way, make you practice key problems,
present variants like Threaded Binary Tree, Binary Space Partitioning Tree,
Skewed Binary Tree, AVL Tree, Treap and much more.
The content covered is deep and is not covered by any other standard book.
Chapter
#
1 Binary Tree
2 Properties of Binary Tree
3 Implementation of Binary Tree
4 Implementation of Binary Tree with no NULL
5 Intuitive View of a Binary Tree
6 Traversing a Binary Tree (Preorder, Postorder, Inorder)
Convert Inorder+Preorder to Binary Tree (+ other
7 combinations)
8 Find height or depth of a Binary Tree
9 Find Level of each node from root node
1
0 Diameter of a Binary Tree
1
2 Check if a Binary Tree is Balanced by Height
1
3 Find number of Universal Value subtrees in a Binary Tree
1
4 Counting subtrees where nodes sum to a specific value
1 Find if a given Binary Tree is a Sub-Tree of another
5 Binary Tree
1 Check if a Binary Tree has duplicate values
6
1 Find nodes which are at a distance k from root in a Binary
7 Tree
1
8 Finding nodes at distance K from a given node
1
9 Find ancestors of a given node in a binary tree
2
0 Largest Independent Set in Binary Tree
2
1 Copy a binary tree where each node has a random pointer
2
2 Serialization and Deserialization of Binary Tree
2
3 0-1 Encoding of Binary Tree
2
4 ZigZag Traversal of Binary Tree
2
5 Check if 2 Binary Trees are isomorphic
2
6 Convert Binary Tree to Circular Doubly Linked list
Binary Tree
A binary tree is a data structure in which each element links to at most two
other elements. It is represented by a starting element from which we can
reach the other elements by following the links.
Each element is known as a Node and the starting node of a Binary Tree is
known as Root.
In a Binary Tree, every node has at most 2 children that is the left child (left
sub-tree) and the right child (right sub-tree). Node of a binary tree will look
as follows:
You can see that a node consists of three components:
Complete Binary Tree : A Binary Tree is complete Binary Tree if all levels
are completely filled except the last level and last level should have all
nodes as left as possible.
Perfect Binary Tree : A Perfect Binary Tree is a binary tree in which all
interior nodes have two children, and all leaves have the same depth that is
same level.
Full Binary Tree : A Full Binary Tree is a tree in which every node other
than the leaves has two children.
Balanced Binary Tree : A Balanced Binary Tree is a binary tree in which
height of the tree is O(log N) where N is the number of nodes.
Basic Operations
A Binary Tree supports the following basic operations:
Insertion: To insert new element into the Binary Tree
Deletion: To delete a specific element from a Binary Tree
Search/ Traversal: To go through all elements in a Binary Tree
Insertion operation
Suppose a binary tree is given with few allocated values. As given tree is
binary tree, we can insert element wherever we want. Only one property of
binary tree should be maintained, and property is that every node can have
maximum two children.
We need to insert the new element wherever we find the node whose left or
right child is null.
Given (Original Tree) :
Delete data 3.
Deletion steps :
By this way, deletion is done in binary trees.
Visualize the structure of a Balanced Binary Tree. The first level (0) will
have 1 node (known as Root). The second level (1) will have 2 nodes. The
third level will have 4 nodes which will be children of nodes at second
level.
Hence, at level (i), there will be 2(i) nodes.
Assume we number the nodes starting from 1 to N in order (top to bottom
and left to right). A sample graph will look like this:
Height can be N in the worst case where each node has at most 1
child node.
The minimum height will be “log N”. This is the case of a perfectly
balanced Binary Tree.
The ideal case is to maintain the height to be close to “log N”.
Having a height of N, decomposes a Binary Tree to an Array and
there are no differences.
A random Binary Tree achieves the height of “log N” on average.
There are conditions or modifications of Binary Tree where the
height is maintained to be “log N” at all times.
The height directly impacts the performance of operations like
Insert and Delete so it is preferred to keep it as low as possible.
For a Binary Tree of height H, the maximum number of nodes will be 2 H+1
+ 1 and the minimum number of nodes will be H . You can estimate the
significant difference between the two cases.
One is linear and the other is exponential.
For a Complete Binary Tree of height H, the number of nodes will be from
2 H + 2 to 2 H+1 + 1 .
Insight :
With this, you have a strong idea of the bounds/ limitations of various
aspects of a Binary Tree. This will help you in analyzing various algorithms
and arrive at the time and space complexity easily.
For linked list, we keep the information of address of the head node. We can
access all other nodes from head node using links. In this case, we know the
address of the root node, which is must, without this we wot be able to
access the tree using links.
To declare the Binary tree, we will first need to create a pointer to Node that
will store address of root node by : Node root . Our tree does not have any
data yet, so let us assign our root node as NULL.
If we have the data to be set along with the pointer/ link to Left child node
and Right child node, then we can create a new empty node and set the
attributes accordingly:
Node newNode = new Node() ; // Creates a new empty node newNode -> Key
= key ;
newNode -> Left = Left_child ;
newNode -> Right = Right_child ;
This will create a Balanced Binary Tree. There are other approaches to
insert a new node in a Binary Tree which comes from specific requirements
of the problem such as:
Set new node as the root and move the previous root node
accordingly
Place the new node as the left most or the right most leaf node.
This is efficiently as we need not traverse the entire tree and can be
done in time proportional to the height of the tree.
We will follow our approach of inserting the new node at the first empty
position. The pseudocode of our insertion operation will be:
Node InsertNode(Node root, int data)
{
// If the tree is empty, create a new node as Root if ( root == NULL )
{
root = NewNode( data );
return root ;
}
// Find the first empty place // where node can be inserted // Queue
of Nodes to keep track of nodes to be processed queue < Node > q ;
q .push( root );
// If left node is not NULL, put it in queue if ( temp -> left != NULL )
q .push( temp -> left );
// If left node is NULL, empty space have been found else {
temp -> left = NewNode( data );
return root ;
}
// If right node is not NULL, put it in queue if ( temp -> right != NULL )
q .push( temp -> right );
// If right node is NULL, empty space have been found else {
temp -> right = NewNode( data );
return root ;
}
}
}
Go through the pseudocode and you will get the complete idea.
We used a NewNode() function to create a new node with a given value
which can be implemented as follows:
You can easily expand the Insert function to perform the traversal of a
Binary Tree or search for a specific element. Try it. We will present this part
following the deletion operation.
Deletion Operation
There are multiple approaches to delete a node from a Binary Tree. Some of
the common approaches are:
Delete the concerned node and place a leaf node (mainly, the
rightmost left node) in its place.
Delete the concerned node, place the right or left child node in its
place and delete the original placed node accordingly.
We will take the second approach. Both approaches are equivalent in terms
of performance, but the second approach is preferred as it maintains the
balanced nature of the original Binary Tree.
Consider the following Binary Tree:
There are 3 possible cases for removing the node from the tree which are as
follows:
Removing the leaf node is the easiest case of the three. We just
have to remove the node which does not have any child. In our tree,
leaf nodes are- 8, 12, 17, 25
If the removed node has one child (either left or right). After
deleting the node we just have to point the address of parent node
to the child node of the deleted one. for example, in above tree,
nodes 88 and 3 have a single node. If we want to remove node 88
then we have to point parent node of 53 to 31 and make right node
of 31 as 53.
Removing the node with 2 children. If our node has 2 children then
first find out the successor or predecessor of that node, then replace
that node with successor or predecessor node. For eg if want to
removes node 12, then first anyone of the successor or predecessor,
let us find predecessor to 12, which is 7 then replace 12 with 7.
Now, 7 has 15 and 3 as its children. As we can choose anyone of
the successor or predecessor, we could have 2 binary tree
representations after deletion of a node. For this case we first have
to define predecessor and successor of a node.
// The node have only one child at right else if ( node -> Left == NULL
&& node -> Right != NULL )
{
// The only child will be connected to the parent of node directly node
-> Right -> Parent = node -> Parent ;
// Bypass node node = node -> Right ;
}
// The node have only one child at left else if ( node -> Left != NULL
&& node -> Right == NULL )
{
// The only child will be connected to the parent of node directly node
-> Left -> Parent = node -> Parent ;
// Bypass node node = node -> Left ;
}
// Replace node's key with right child node's key node -> Key = rightKey
;
// Delete the original right child node node -> Right = Remove( node ->
Right , rightKey );
}
}
Note that from the point of structure of the Binary Tree, only one leaf node
is deleted so the balanced nature of the tree is maintained.
The search() function finds the node to be deleted and returns a link to that
node. We can search the node using a search utility which will be similar to
our insertion function.
For our insertion function, we took an iterative approach. For our search
function, we will take a recursive approach with the same basic idea so that
you get the idea clearly and get flexible with both approaches.
// Search right side of Binary Tree Node right = Search( node -> right , key
)
With this, you have the implementation idea of the three basic operations of
a Binary Tree:
Insertion
Deletion
Search
Insight :
The details we have covered is the most common implementation strategy.
The main disadvantage is the use of NULL. This is because NULL is
considered as a special case and for each algorithm, we need to consider
NULL along with other conditions. Hence, NULL is an edge case.
NULL condition has been the cause of several runtime bugs in production
system.
Use of NULL
NULL is the null pointer literal used in C and sometimes even in C++. It
defines a null pointer value. In the BST, they are used to indicate the
absence of a node.
The NULL values are a useful implementation feature but in reality, it is
advised to avoid using NULL in designing software as it increases the
number of edge cases and needs to be handled separately. The prominent
reason for this is due to the fact that NULL represents both the value 0 and
a null pointer literal. Thus, the usage is not type safe.
Placeholder nodes
In our implementation, the placeholder nodes (instead of NULL) indicate
the absence of a node. Since we will be using nodes themselves to indicate
absence of nodes, we need a way to differentiate them.
We could ideally assume that their key values are negative infinity or
positive infinity but we cannot really use infinity, so we use the value of
macros INT_MIN and INT_MAX defined in header <climits> for the left
and right nodes respectively. Similar constants are available in all
Programming Languages.
The reason of using INT_MIN and INT_MAX values is because we assume
that they are small enough and large enough respectively to not be used in
other nodes.
BST nodes
The node contains an integer key and two pointers left and right. This is
similar to a Conventional binary tree.
Node {
int key ;
Node left ;
Node right ;
// to determine a place to insert the node, traverse left if ( key < root ->
key )
{
root -> left = insertNode( root -> left , key );
}
else { // or traverse right
root -> right = insertNode( root -> right , key );
}
return root ;
}
Deleting a node
Deleting a node has several cases, depending on the type of node we want
to delete. For convenience, let us assume the node to be deleted is N.
N is a leaf node (i.e., has no child nodes) - In this case we just delete the
node.
node to delete has one child node - In this case we cannot directly delete
the node. So, we first promote its child node to its place here if the node to
be deleted is a left child of the parent, then we connect the left pointer of the
parent (of the deleted node) to the single child. Otherwise, if the node to be
deleted is a right child of the parent, then we connect the right pointer of the
parent (of the deleted node) to single child.
node to delete has two child nodes - In this case the node N to be deleted
has two sub-trees. here we need to find the minimum node in the right sub-
tree of node N and then replace it with N. In the code we use
minValueNode(root->right) to find the minimum value in the right sub-tree.
// case: node with both children // get successor and then delete the
node node * temp = minValueNode( root -> right );
// Copy the inorder successor's content to this node root -> key = temp -
> key ;
// Delete the inorder successor root -> right = deleteNode( root -> right ,
temp -> key );
}
return root ;
}
Inorder traversal
To print the tree, we use the inorder traversal in this implementation.
Inorder traversal always produces the keys in a sorted order. The reason for
this is due to the order of visits. For any root, the left sub-tree of the root is
visited first then the root is visited and then finally the right sub-tree is
visited.
In the function given below we first check if the pointer root is pointing to a
placeholder node. If it is then, it means that the sub-tree is empty, in which
case it returns, else it recursively traverses the left sub-tree and then prints
the root and then recursively traverses the right subtree.
The pseudocode is as follows:
With this, you have the complete idea of using Placeholder nodes to avoid
use of NULL. You can use the same idea of placeholder nodes in other
variants of Binary Tree.
Insight :
Implementing Data Structures without the use of NULL is a standard
practice for producing high quality code but is not commonly known. You
know this technique now and this puts you at an advantage along with top
0.1% of Programmers.
You may try it apply this idea for the problems we will explore and make
yourself comfortable with this technique.
Inorder traversal
Preorder traversal
Postorder traversal
We will learn about inorder, preorder and postorder and their use. These
three types of traversals generally used in different types of binary tree.
In summary:
Inorder: left, root, right
Preorder: root, left, right
Postorder: left, right, root
Inorder Traversal
In Inorder traversal we traverse from left-root-right.
In this traversal. left subtree visited first then the root and later the right
subtree.
Remember that every node may represent a subtree itself and hence, be the
root of that subtree. Every node needs to follow the rules.
Algorithm
def Inorder(root):
if root :
Inorder ( root . left )
print ( root . key , end =" " )
Inorder ( root . right )
Preorder Traversal
In Preorder traversal we traverse from root-left-right.
In this traversal root visited first then the left subtree and later the right
subtree. Remember that every node may represent a subtree itself and needs
to follow the rules.
Algorithm of preorder traversal
Start with root node 30. Print 30 as it is the root and recursively traverse the
left subtree.
Next node is 20. Now 20 has a subtree so print 20 as it is the root of the
current subtree and traverse to left subtree of 20 .
Next node is 15 and 15 has a subtree so print 15 and traverse to left subtree
of 15.
5 is next node and 5 have no subtree so print 5 and traverse to right subtree
of 15.
Next node is 18 and 18 have no child so print 18 as it is the root of a
subtree with only one node and traverse to right subtree of 20.
25 is right subtree of 20. 25 has no child so print 25 and start traverse to
right subtree of 30.
Next node is 40. Node 40 have subtree so print 40 and then traverse to left
subtree of 40.
Next node is 35. 35 have no subtree so print 35 and then traverse to right
subtree of 40.
Next node is 50. 50 has a subtree so print 50 and traverse to left subtree of
50.
Next node is 45. 45 have no subtree so print 45 and then print 60 (right
subtree) of 50.
Our final output is {30 , 20 , 15 , 5 , 18 , 25 , 40 , 35 , 50 , 45 , 60}
def Preorder(root) :
if root :
print ( root . key , end =" " )
Preorder ( root . left )
Preorder ( root . right )
Postorder Traversal
In Preorder traversal, we traverse from left-right-root.
In this traversal, left subtree visited first then the right subtree and later the
root.
Remember that every node may represent a subtree itself and must follow
the rules as with the previous two traversal techniques.
Algorithm of postorder traversal
We start from 30, and following Post-order traversal, we first visit the left
subtree 20. 20 is also traversed post-order.
15 is left subtree of 20. 15 is also traversed post order.
5 is left subtree of 15. 5 have no subtree so print 5 and traverse to right
subtree of 15 .
18 is right subtree of 15. 18 have no subtree so print 18 and then print 15.
Post order traversal for 15 is finished.
Next move to right subtree of 20.
25 is right subtree of 20. 25 has no subtree so print 25 and then print 20.
Post order traversal for 20 is finished.
Next visit the right subtree of 30 which is 40. 40 is also traversed post-order
(40 has a subtree).
35 is left subtree of 40. 35 have no more sub-tree so print 35 and traverse to
right subtree of 40.
50 is right subtree of 40. 50 should also traversed post order.
45 is left subtree of 50. 45 have no more sub-tree so print 45 and then print
60 which is right subtree of 50.
Next print 50. Post order traversal for 50 is finished.
Now print 40 and post order traversal for 40 is finished.
Print 30. Post order traversal for 30 is finished.
Our final output is {5 , 18 , 15 , 25 , 20 , 35 , 45 , 60 , 50 , 40 , 30}
def Postorder(root) :
i f root :
Postorde r ( roo t . lef t )
Postorde r ( roo t . righ t )
print ( roo t . ke y , en d =" " )
Summary
Time complexity for all three traversals (Inorder, Preorder, Postorder) is
O(N) so it depends on the problem which traversal should be chosen. N is
the number of nodes/ elements in the Binary Tree.
In summary:
Insight :
The traversal techniques we explored are standard techniques that are used
widely. A reasonable traversal technique shall take O(N) time complexity
by the space complexity may vary from O(1) to O(N). This does not restrict
you to other combinations of traversals which we explore in further
chapters.
Can you figure out other types of traversal at this point?
We can form non-balanced tree by having the left most element (1 as the
root of the entire Binary Tree) as root node of concerned sub-tree:
Now, for the same tree we can consider node 4 as root node.
It would look something like this:
Since both the above trees generate the same in-order traversal but different
pre-order traversal, there is no guarantee for a single, unique binary tree to
be formed from the Inorder traversal. Hence, we need additional traversal
information for the Binary Tree to be unique.
If we consider the leftmost node as the root node, then we get a right
skewed Binary Tree.
If we consider the rightmost node as the root node, then we get a left
skewed Binary Tree.
The problem exists from every subtree of the Binary Tree. We have no
information about the root node.
Root
Leftmost node
Rightmost node
Preorder and Postorder traversal give us information about the root node.
Inorder traversal give us information about the leftmost and rightmost node.
Hence, if we take any two traversals: one being Inorder and the order from
Preorder or Postorder, we can create a unique Binary Tree.
Note: We cannot create a Unique Binary Tree from the combination of
Preorder and Postorder traversal as we have the information of the root
only.
Hence, combinations from which we can create a unique Binary Tree are:
Inorder + Postorder
Inorder + Preorder
Inorder
Postorder
Preorder
Preorder + Postorder
Pick the first element from the preorder and then increment the
preorder index variable for next element in next recursive call.
Create new tree node with data as picked element.
Find that element index in Inorder traversal.
Call function again for elements before index of that element and
build the left subtree of node.
Call the function for elements after that element index for right
subtree.
Return the root node.
Now we have the helper function to help us find the index value in array.
We will assume the value is present in the inorder traversal.
int helper( char arr[], int start, int end, int value)
{
int i ;
for ( i = start ; i <= end ; i ++ )
{
if ( arr [ i ] == value )
return i ;
}
}
Now we define the function maketree, this will be our recursive function to
construct the binary tree of size length from Inorder traversal and preorder
traversal.
First, we pick the current node from Preorder traversal using the
preIndex and increment preIndex
If that node has no children, then we will return
Else we find the index of this node in Inorder traversal
Using the index in the Inorder traversal, we will construct the left
and right subtree.
Following is the implementation of maketree function:
node * maketree( int in[], int pre[], int istart, int iend)
{
static int preIndex = 0 ;
if ( istart > iend )
return NULL ;
node * finNode = newNode( pre [ preIndex ++ ]);
if ( istart == iend )
return finNode ;
int iIndex = helper( in , istart , iend , finNode -> data );
finNode -> left = maketree( in , pre , istart , iIndex - 1 );
finNode -> right = maketree( in , pre , iIndex + 1 , iend );
return finNode ;
}
Time complexity: O(N) Note : The time complexity is same to get the
traversal and to generate the Binary Tree from the traversal.
Construction of the binary tree from Inorder and Postorder Traversal
Example:
Inorder Traversal: [4, 2, 5, 1, 3]
Postorder Traversal: [4, 5, 2, 3, 1]
Now from the postorder traversal we can see the right most element, node 1
is the root node. From Inorder traversal we can see that node 3 is on right of
root node 1, and others on left. In similar fashion, we end up with tree:
For the construction of the binary tree from the inorder traversal and
postorder traversal, we can use the same concept as used in above approach
(inorder + preorder) to find our tree.
By using both recursion and hashmap, we will be able to re-construct our
tree from the traversals.
Insight :
The time complexity to do a traversal is same as the time complexity to
recreate the Binary Tree from a given traversal or a set of traversals. This
brings in the idea that even if we do not have any information about the
Binary Tree, we can store the entire information in at most 2 traversals (or
linear arrangement of the elements).
See Binary Tree is a 2D data structure and the information can be stored in
2 1D data structures (array or list). Think over this point.
Steps to find height of binary tree Following are the steps to compute
the height of a binary tree:
Pseudocode
Following is the pseudocode of the algorithm:
int height(Node root) // return the height of tree {
if ( root == null)
return - 1 ;
else
{
int left = height( root . left );
int right = height( root . right );
Complexity
Time complexity : O(N) where N is the number of nodes in the Binary
Tree.
This can be solved using a standard graph traversal technique like Breadth
First Search and Depth First Search as well. Modification of inorder,
preorder and postorder traversals make the process of calculating the height
of Binary Tree.
Insight :
This is a standard problem but is used as a sub-routine in solve some of the
challenging Binary Tree problems. This is a standard definition and
approaches to find defined values are important.
Can you imagine how height of a node may be useful?
Root: The node from which we start our breadth first search
traversal is called the root or source node.
Level: The level of a node is defined by 1 + the number of
connections between the node and the root.
Leaf Node: Any node whose left and right child are null is called
the leaf node of the graph.
Hence, level of a node is the length of the path from the root node to that
node. As the length of path from root node to root node is 0, level of root
node is 0.
Algorithm
Explanation
This algorithm follows breadth first search algorithm at the core.
After the graph has been created, we will set the level of the root node in
the level vector as 0 and then apply the breadth first search.
In this algorithm we will pop the first element of the queue and set the level
of all of connected nodes as 1 more than the element popped from the
queue and simultaneously push the connected nodes in the queue.
Example
The graph shown below is a 3-level graph (level 0 , 1 and 2) and we can
find the level of each node in the graph using Breadth First Search .
We will also create a level vector which stores the level of each node .
In the process, first we will set the level of node 0 ( root node ) as 0 and
enqueue it in the queue.
Now , we will pop the first element of the queue ( which is 0 initially ) and
push all the neighboring nodes ( 2 and 3 ) in the queue and set level of all
the neighboring nodes 1 more than the node element we just popped from
the queue.
Now we have node 1 , 2 and 3 in the queue , now we will repeat the same
process explained in the above step , first we will deque the first element (
node 1 ) and set the level of neighboring nodes (node 4 and 5) to more than
the first node which we just dequeued.
We will repeat the same process with nodes 2 and 3.
At last, we will have 4 , 5 , 6 and 7 nodes in the queue so will just pop each
element because there are no neighboring elements to these elements which
are unvisited.
Now, we will just print the level vector which describes the level of each
node.
Node x = q .front() ;
q .pop() ;
// popping the first element and setting up // the level of all the
connected // nodes to the current node for ( node in [x.left_child, x.right_child]
)
{
// setting up the level of node to 1
// more than the current level
q .push( node ) ;
level [ node ] = level [ x ] + 1 ;
}
}
return level ;
Complexity Analysis
Insight :
The technique we explored is also known as Level Order traversal and is a
traversal technique alternative to Inorder, Preorder and Postorder traversal.
Find the level of a node is the direct application of this technique but it goes
beyond this as we will see in further problems.
In the first example, the diameter is 6, and for the second one, diameter is 5.
There can be more than one longest path, but the diameter will always be
maximum.
This problem can be solved with various methods: Approach 1: Use
recursion
We use recursion to calculate the height of a subtree and the diameter. So,
we make a recursive function that is diameter(node) We can consider that
the diameter up to any given node will be the sum of the height of its left
and right subtrees and 1.
Hence,
Diameter = Left subtree height + Right subtree height + 1.
Time complexity : O(N 2 ) where there are N nodes in the Binary Tree.
The time complexity is O(N2 ) as for each node, we are calculating the
height of the concerned tree separately and using the result to compute the
diameter recursively.
Space Complexity: O(log N) , if a balanced tree, O(N) otherwise. Space
complexity is due to recursion.
Pseudocode:
// get the height of left and right sub trees int lheight = height( root . left
);
int rheight = height( root . right );
// get the diameter of left and right subtrees int ldiameter = diameter(
root . left );
int rdiameter = diameter( root . right );
Time Complexity
The above approach would work fine but time complexity for the above
approach will be O(N2 ) as we run DFS for each node. This is quite high.
Better Approach
We can find the diameter of any tree using only 2 DFS run. How ?
Let us proof this fact that on running DFS we always reach the end point of
diameter of the tree
Proof by contradiction
We are considering x as the root of the tree Assume that on running DFS
first time from x and finding the farthest node we reach on node y .
Let us say y is not the end point of the diameter of the tree and v-p-y is the
actual diameter of the tree In that case according to the above image we can
write the equation ( note that if root of the tree lies on the diameter of the
tree then distance xp is considered as 0 ) xp + py < xp + pv
and
xp + py < xp + pu
Example
Suppose we want to find the diameter of the graph shown in the above
image.
Let us take node 4 as our starting node (we can choose any node) Now on
running the depth first search for the first time, we will be able to find the
one end of the diameter which will be 7 for this example.
Now on running DFS from 7 and finding the farthest node, we will reach 6
via path 7-3-2-4-6.
4. The count of nodes along the path is 5 which is the diameter of the tree
in this example.
(Note - we could have also reached 6 via path 7-3-2-4-5 which would also
have been the diameter of the tree as both count as farthest node with same
value of distances ) 5. In this way, we can find the diameter of any tree
using only two DFS run .
Pseudocode:
void DFS( vector < int > graph[] , int node , int d )
{
// marking the node as visited visited [ node ] = 1 ;
// applying the dfs for the second time as this // will give the
diameter of the tree DFS( graph , maxNode , 1 ) ;
Insight :
Using this approach, we can find the two farthest points in a Binary Tree.
Consider the situation where we represent games in a Tournament as a
Binary Tree. Finding the diameter in this case will give us two players who
if face each other needs the maximum number of games to be conducted.
If the Binary Tree is balanced, there can be multiple diameters. Think on
this carefully.
Each level “I” has 2 I nodes except the last level where there
can be less nodes
The idea is same as the above point. If all levels are completely filled, then
it is a complete Binary Tree and hence, it is balanced. If there are a few
nodes missing in the last level, still then it is balanced as the maximum
height difference is 1.
Example :
Considering the binary tree below:
1. For the root node, the height of its left subtree is 2 (from node 1 to node
2 and node 2 to node 4). Similarly, the height of the right subtree is 1 (from
node 1 to node 3). So, absolute difference in the height of left and right
subtree is 1.
Hence, satisfying the condition that absolute difference of the height of left
and right subtree is smaller than or equal to 1.
2. Similarly, for the node 2 the height of its left subtree is 1 (node 2 to node
4) and height of right subtree is also 1 (node 2 to node 5). Hence, the
absolute difference of their heights is 0.
Hence, satisfying the condition that absolute difference of the height of left
and right subtree is smaller than or equal to 1.
3. For node 4, node 5 and node 3 the absolute difference in their left and
right subtrees is 0 since they do not have any children nodes.
Hence, satisfying the condition that absolute difference of the height of left
and right subtree is smaller than or equal to 1.
This way all nodes satisfy the required condition. Hence, our binary tree is
balanced.
Insight :
This is an important concept as you will see that a Binary Tree reaches its
full potential only when it is balanced by height.
There are techniques to ensure that our Binary Tree is balanced at all times
and such trees are known as Self Balancing Binary Tree which will be
explore later in this book.
The binary tree given below is not universal. It has subtrees which have
universal values. Leaves of a binary tree are always universal as they do
not point to any node instead point to NULL.
How many subtrees having universal values are present?
We count all the leaves due to the above-mentioned reason and then
subtrees with same values and non-null nodes also.
3 4
/\ an d /
3 3 4
Explanation
When is the subtree considered to be univalue subtree?
When the subtree's both left and right child nodes’ values are equal
to root's value.
When they are leaves that is left and right nodes are null.
When the left or right node is null, but the existing right or left
node has value equal to the root's value.
When the subtree does not satisfy these three conditions , then it is
not a univalue subtree.
Efficient Algorithm
Check if root is NULL. If true, then return 0 as no roots are
present.
Count Number of left and right subtrees by recursive calls.
Check if right or left subtrees returned are univalued. If no, then
return false.
Check if left or right nodes exist .
If right or left node's value is not equal to root's value, then return
false and do not count.
If any of the eliminating cases are not satisfied then it is a univalue
subtree, so increment the count.
Finally, after checking on every node return the count.
Pseudocode
inpu t : 1
/\ 3 3
/\ 3 3
1 ----root /
3 ---subroot
/\
33 ---both leaves
After counting in the 2 leaves(3,3) , they are checked if they are not equal
to subtree's root data .As both are equal to subroot's data so count is
incremented.
1 ---root
\
3 ---leaf
3 being leaf is counted as univalued but it is not equal to the root's value ,so
its returned false. Now, as we have reached the root, we return the count of
univalued subtrees which is 4.
Time Complexity:
This code follows a linear time complexity O(N), where N means the
number of nodes of the tree.
Space Complexity:
Auxiliary space used is O(h) for the recursion call stack , where h is the
height of the binary tree as we are recursively travelling till h height.
Insight :
This may seem to be just a practice problem, but this has some key
applications in real problems. For example, in specific systems, we want to
compress the Binary Tree to reduce duplicates and such univalued sub-
trees can be compressed to a single node.
Can you think of such a situation?
Let us say we need to find count of subtrees whose nodes sum to 11. In the
example subtree, the whole left subtree's nodes sum up to 11 and the leaf
itself is 11.So the total number of subtrees whose nodes added up to 11
were 2.
Brute Force approach:
The first approach that comes to mind when we see this question is to go to
the root and check if the sum of root + leftnode + rightnode is equal to sum,
else move to the next right or left node and repeat the same. Also check if
entire tree's sum is equal to the given value.
This approach will have the time complexity of O(N 2 ) as you check for
each and every node.
So, let us examine few methods through which we can count subtrees
whose sum would be equal to say, x.
1.BFS method:
In BFS or level wise traversal , the idea is to go through each node in level
wise fashion and find the subtrees of each node whose sum equals the
given value and count all such subtrees.
Algorithm:
Pseudocode:
INT subtreeSum(ROOT) {
CHECK : ( ROOT = NULL )
RETURN 0
RETURN ROOT [ VALUE ] + subtreeSum( ROOT -> LEFT ) + subtreeSum(
ROOT -> RIGHT )
}
INT checkSubtreeSumX(ROOT,LEVEL,X,REFERENCE(count))
{
CHECK( ROOT == NULL ) : RETURN 0
CHECK : ( LEVEL = 1 && subtreeSum( ROOT ) = X )
count ++ ;
if ( LEVEL > 1 )
{
checkSubtreeSumX( ROOT -> RIGHT , LEVEL -1 , X , count );
checkSubtreeSumX( ROOT -> RIGHT , LEVEL -1 , X , count );
}
2. Iterative method:
We will be now using post order traversal iteratively, to find the subtrees
having the total nodes sum equal to the value specified.
We will be using two stacks to traverse in postorder and then use the stack
having elements in postorder fashion to find the count of the subtrees
whose sum of all its nodes is equal to the given value, X.
Algorithm:
X=Specified value
count=Subtrees count
Store all nodes in a stack in postorder fashion.
Select the top and check if its left and right nodes exists.
If yes then check if the node's value is equal to X.
If the value=X then increment the count.
If No then:
Store the left, right nodes in a stack by calling postorder function
and derive the sum by adding all the top elements and popping
them up. Check if their sum=X. If yes, then increment the count.
Pop the top and continue same steps with the next node until the
stack is empty.
Pseudocode
stacks:
Similar arrangements happen until the s1 is empty.
The process to count the subtrees whose sum=x here x=1: So, whenever the
node present in the stack points to NULL and has no right and left children
that is if they are leaves, we just need to check if its value is equal to 1.
If yes, then we increment the count.
What if the node is not NULL and points to left or right or both?
We store all the nodes of that node in a stack in a postorder way. Later we
will be adding all the nodes values to the sum and checking if its equal to 1.
The tree is:
pop(2) sum=2+6
pop(6) sum=8+3=11
pop(3)
3. Recursive method:
Algorithm
We will be now using post order traversal recursively, to find the subtrees
having the total nodes sum equal to the value specified.
INT helper(ROOT,TSUM,(REFERENCE)COUNT) {
CHECK :( ROOT = NULL )
RETURN 0
LEFTSUBTREE : = helper( ROOT -> LEFT , TSUM , COUNT )
RIGHTSUBTREE : = helper( ROOT -> RIGHT , TSUM , COUNT )
CHECK TSUM = LEFTSUBTREE + RIGHTSUBTREE + ROOT [ VALUE ]
IF YES : COUNT : = COUNT + 1
RETURN SUM
}
INT countSubtrees( ROOT , TSUM )
{
COUNT :=0
CHECK :( ROOT = NULL )
RETURN 0
LEFTSUBTREE : = helper( ROOT -> LEFT , TSUM , COUNT )
RIGHTSUBTREE : = helper( ROOT -> RIGHT , TSUM , COUNT )
CHECK TSUM = LEFTSUBTREE + RIGHTSUBTREE + ROOT [ VALUE ]
IF YES : COUNT : = COUNT + 1
RETURN COUNT
}
There are different approaches that can be followed to solve this problem.
We will take two approaches:
Brute force (using recursion)
Efficient approach (using pre-order and in-order traversal)
Approach 1: Recursion
In this approach, for every node at the “target” tree, we check if the nodes
of the “source” tree are present in the same structure. Hence, for every
possible sub-tree in target tree, we match every node of source tree.
Let us assume that there are N nodes in “target” tree and M nodes in
“source” tree.
Algorithm:
Within the function "subtree", Step 1: If the 'Source' tree is null then return
1
Step 2: If the 'Target' tree is null then return 0
Step 3: If 'Target' and 'Source' are identical, then return 1
Step 4: Call function "subtree" by passing arguments- left node of the
'target' tree and the root node of 'source' tree and call function "subtree" by
passing arguments - right node of 'target' tree and root node of 'source' tree.
If any of them executes to true, return true.
Explanation :
In the above algorithm, we start by creating a function "subtree". In this
function, we provide first base condition to return 1 if the 'Source' tree is
null. Since, any null tree is a sub-tree of all trees.
Then, we give another base condition if a 'Target' tree is null then no tree
can be its sub-tree. Hence, we return 0.
Then, we define third base condition. If the 'Target' tree and 'Source' tree
are identical then, we return 1. We check this condition by calling function
"identical". This function we check if nodes passed as arguments are both
null then return 1.
Else if both are not null then, return true if all of the below 3 conditions are
true: Data of node of "Target" and "Source" tree are equal.
Data of left node of "Target" and "Source" tree are equal (via recursion)
Data of right node of "Target" and "Source" tree are equal (via recursion)
Else, we return false if one node is null and one isn't.
Tree 'Target':
In the function "subtree", neither 'Source' nor 'Target' null. Hence, we check
third condition if the two trees are identical. We call function "identical".
Now, we do not enter in its condition 1 since both are not NULL.
Following the condition two, we check if 'Source' and 'Target' nodes are
equal. Now this condition is not true for the root node of 'Target' and
'Source' hence we return false. Now the control comes back to "subtree"
function.
Now, we come to the last condition of subtree and recursively call the
subtree function again by passing left node and right node of 'Target'
respectively along with root node of 'Source'.
This way upon, recursion we will return true when the right subtree of the
'Target' is executed, since it exactly matches 'Source'.
Let us have a look at the code:
// if both are not null then return true if all // three mentioned
conditions are true else if ( a != NULL && b != NULL )
{
return ( ( a -> data == b -> data ) &&
(identical( a -> left , b -> left )) &&
(identical( a -> right , b -> right )));
}
//if one node is null and one is not null then return false else return (
false );
}
if (identical( a , b )){
return 1 ;
}
/* Recursively call subtree function with arguments :
left node of 'Target' and root node of 'Source' AND
right node of 'Target' and root node of 'Source'.
If any of them returns true then the answer returned is true */
The Algorithm is :
Step 1: Find out the preorder and inorder traversals of 'Target' and
save them in arrays.
Step 2: Find out the preorder and inorder traversals of 'Source' and
save them in arrays.
Step 3: Check if inorder traversal of ‘Source’ is a sub-array of
inorder traversal of ‘Target’
Step 4: Check if preorder traversal of ‘Source’ is a sub-array of
preorder traversal of ‘Target’
Step 5: If step 3 and 4 holds true, then ‘Source’ is a sub-tree of
‘Target’.
if ( root == NULL )
{
arr [ i ++ ] = ' ' ;
return ;
}
/*Else, we recursively call the Inorder Function till all nodes on the
left-subtree are traversed*/
Inorder( root -> left , arr , i );
// Finally, we traverse the right sub-tree Inorder( root -> right , arr , i );
}
Then, we define the Pre-order Traversal :
// Then, we traverse the left sub-tree Recursively Preorder( root -> left
, arr , i );
Following is the main function that uses in-order and pre-order traversals:
if ( Source == NULL )
return true ;
if ( Target == NULL )
return false ;
/*Now, initialize the variables m and n by 0
as they represent the array indexes of 'Target'
and 'Source' Traversal arrays */
int i = 0 , j = 0 ;
//Now, call the function Inorder for target and source trees Inorder(
Target , inTarget , i );
Inorder( Source , inSource , j );
i=0,j=0;
char preTarget [ 100 ], preSource [ 100 ];
This method takes time complexity of O(N) and Space complexity of O(N).
Note :
If you analyze carefully, in our second approach, we reduced the Binary
Tree to a list of numbers and reduced the problem to check if a list exists
within another list. This is important as it brings in two key ideas:
Insight :
Checking if two data points or Data Structures are similar is a key
operation in any field for example, finding similarity between two DNA to
find if two animals can from the same source or crossed each other at some
point.
Similarly, the way you use Binary Tree determines the use of this approach.
Inorder traversal
Preorder traversal
Postorder traversal
For illustration, we will be using Preorder traversal, a traversal in which
root is visited first then the left subtree and later the right subtree. While
traversing, we store the value of the nodes in an Array and then, it can be
check if Array contains any duplicate elements.
Note: We are using Array List since the number of nodes in the tree are
unknown (we do not require to specify size for an Array List during its
initialization), an array can also be used if number of nodes is known
beforehand or we can traverse the tree once to find out total number of
nodes so that we are able to specify size of array that will store the data
values.
Steps:
6
/ \
10 9
/ \/ \
12 6 5 4
For example, in the given tree above, we traverse using preorder traversal.
So, when we start, we check if root that is 6 is present in HashMap already.
As it is not, we put it in the HashMap. Next, we go to 10 according to
preorder and check if it is present, if not we put in the HashMap, and so on.
When we come to the 6 which is the right child of 10 ,we check if it is
present in the map already and we find that it is, as we already have put 6
in the map because 6 was the root element and therefore, we have found a
duplicate and we return true. Suppose you have a tree which has no
duplicates then we keep putting elements in the map and finally when we
have traversed the whole tree, false is returned.
Pseudocode:
Time Complexity: O(N) In worst case, we will be traversing the whole tree
and algorithm would take O(N) time as tree has n nodes.
Space Complexity: O(N) In worst case that is when no duplicates are
present, we will be storing all the nodes data value in the Map and would
require O(N) space as tree has a total of n nodes.
Insight :
This is simple problem but involve some key ideas that can help formulate
efficient solutions to challenging problems. An extension of the problem is
to find if there are duplicate sub-trees in a Binary Tree. This can be solved
using the insights from the previous two problems we explored.
Think deeply about this extended problem.
Find nodes which are at a distance k
from root in a Binary Tree We are
given the root of a tree, and an integer
K. We need to print all the nodes
which are at a distance K from the
root. Distance is the number of edges
in the path from the source node (Root
node in our case) to the destination
node.
Walkthrough
Let us walk through the procedure with our given example.
Initially, the root of the tree is 1 and k = 2. Since k is not equal to 0, we will
recursively call the tree with its left child as the root and k-1. Hence, the
function printNodes( root->left, 1 ) will be called.
Our new root will then be 2 and k =1. Again, k is not 0, hence we will call
the function printNodes( root->left, 0 ).
Now, our root is 4 and this time, k=0. So, we will print the data in the given
root, as this node will be at a distance of k=2 from the original root.
Similarly, the recursive function will be called in the right subtrees until we
get a NULL value, or until k=0.
The following graph depicts the recursion route. (The function name
printNodes is abbreviated to pN)
Pseudocode:
if ( k == 0 )
{
print( root -> data ) ;
return ;
}
else {
printNodes( root -> left , k - 1 ) ;
printNodes( root -> right , k - 1 ) ;
}
}
if ( K == 0 ){
return { target -> val };
}
// call function Graph to make graph unordered_map < int , vector <
int >> graph ;
Graph( root , graph );
while ( ! q .empty()){
pair < int , int > curr = q .front();
q .pop();
visited .insert( curr . first );
return result ;
}
The time complexity of this approach is O(N) which is optimal, but the
issue is that we need to convert the Binary Tree to an undirected graph
which is an overhead.
Percolate Distance:
In this method, we traverse the tree and check if the current node is the
'Target Node'. When we get them, we perform the pre-order traversal to
find all neighboring nodes at k distance.
In this method, the striking difference is that we consider the target node as
the root node.
Consider an example, suppose we have a root node from which our target
node is at distance 3, in right branch. Then, any node at distance k-3 in left
branch will be returned.
Complexity Analysis:
Pseudocode:
void percolate(root, k)
{
if ( root == NULL || k < 0 )
return ;
if ( k == 0 )
{
print( root -> data );
return ;
}
percolate( root -> left , k -1 );
percolate( root -> right , k -1 );
Insight :
In terms of performance and space, both algorithms are equivalent, but the
second approach is important as we are able to avoid modifying the input
data (that is the Binary Tree).
In real systems, it is important that the input data is not modified even if
there is no such requirements. When needed, a copy of the input is used.
Recursive solution
Iterative solution
Input the binary tree and the key_node whose ancestors are to be
printed.
Traverse all the nodes of the tree and perform recursive post order
traversal.
Until the key_node is found, traverse the left and right sub trees
recursively.
Once the key_node is reached, return the data of the nodes in the
path.
2. Iterative solution
To solve this problem using recursion, we need to store the parent node of
all the nodes in the tree explicitly (using a map in C++ or a dictionary in
Python). Then we would traverse the binary tree in an iterative preorder
fashion.
An iterative preorder traversal is similar to the regular preorder traversal:
start at the root node and traverse the left subtree followed by traversing
the right subtree. Set the parent pointer of each node and print the
ancestors of the given node using the explicit map that is used to store all
the nodes in the binary tree.
Input the binary tree and the key_node whose ancestors are to be
printed.
Traverse all the nodes of the tree and perform iterative post order
traversal.
Stop the traversal when the desired key_node is reached.
Store the ancestors of the nodes while traversing in a stack.
Once we reach the key_node, print the stack.
// search node in left subtree bool left = printAncestors( root -> left ,
node );
}
// Iterative function to set parent nodes for all nodes // of the binary
tree in a given map. The function is // similar to the iterative preorder
traversal void setParent(Node root, unordered_map < int , int > &
parent)
{
// create an empty stack and push the root node stack < Node > stack;
stack.push( root );
// push its left child into the stack and set // its parent on the map if (
curr -> left )
{
parent [ curr -> left -> data ] = curr -> data ;
stack.push( curr -> left );
}
}
}
// set parent nodes for all nodes of the binary tree setParent( root ,
parent );
Insight : This problem may seem simple at first sight, but this brings in a
key idea of using the current node to find properties of nodes that come
before it. This is distinct from a simple traversal that we keep going deeper
into the Binary Tree.
This understanding will help in solving some key problems of Binary Tree.
As there are N nodes, there will be 2N subsets and for each subset, we need
to do one traversal O(N) to check if it is independent. Hence, the time
complexity of this approach will be O(N x 2 N ).
This might seem easy to eliminate the N factor and bring the time
complexity to O(2N ) but going beyond it might look difficult.
In fact, this problem has an inherent structure within it and using a
Dynamic Programming approach, we can solve it efficiently.
The idea is simple, there are two possibilities for every node X:
It should be noted that the above function computes the same subproblems
again and again. Since same suproblems are called again, this problem has
Overlapping Subprolems property. So LISS problem has both properties of
a Dynamic programming problem. Like other typical Dynamic
Programming (DP) problems, recomputations of same subproblems can be
avoided by storing the solutions to subproblems and solving problems in
bottom-up manner.
data
left child
right child
liss (an extra field)
size(root->right->right)
if ( root -> left == NULL && root -> right == NULL )
return ( root -> liss = 1 );
Explanation
The idea is to maintain two list of alternative nodes that is grandchild
nodes of nodes. First go to the left most node of the tree and make the liss
field of that node is equal to one.
Then go to the right most node and the liss field of that node equal to one
and liss_excl = 2.
Now liss_incl = 1 and now go to another leaf node that is 8 and make the
liss of that equal to one.
Then, go the right leaf node that is 7 and make liss of that equal to one and
liss_incl=3.
Then, go the parent and make its liss equal to liss_excl and liss_incl.
Now, assign the value of liss_incl to the parent.
Now go the right of the root and assign its liss equal to one and liss_incl =
5 and assign root node liss equal to liss_incl.
Hence, the answer is 5.
Insight :
Observe that we identified a subset with a specific property with just one
traversal and avoided going through all subsets. This is important as it
brings in the idea that with just one traversal, we can extract a lot of
information which can solve seemingly challenging problems.
Augmenting a data structure is yet another key idea where we need to tune
a given structure to solve the problem efficiently.
Think about these ideas deeply.
key(value)
left pointer
right pointer
random pointer
Random pointer can point to any random node of the binary tree or it can
point to null.
Example of a random pointer Binary tree:
The first time we will traverse it using a standard traversal technique like
inorder or preorder and use only the left and right pointers. This way we
will copy the entire tree with every random pointer being null.
The idea of the second traversal is to fill up the random pointers. In the
second traversal, we will go to each node using standard traversal
techniques and copy the random pointer. For each random pointer, we need
to find which node it corresponds to in the cloned Binary Tree which itself
takes O(N) time.
At this point, we have copied the entire tree with 2 traversals.
The steps are:
update random pointers from the original binary tree into the map
copynode.random = map[treenode].random
// Copy function: calls 2 functions // one to copy left and right child
nodes // other to copy random nodes Node copytree(Node root) {
if ( root == null)
return null;
// Create the copy node Node copynode = new Node( treenode . data ); //
Add entry in the Hash Map m .put( treenode , copynode );
copynode . random
= m .get( treenode . random );
copyrandompointer( treenode . left , copynode . left , m );
copyrandompointer( treenode . right , copynode . right , m );
}
Code Explanation:
Original tree
First, we create map to store mappings from node in the original tree to the
corresponding node in the cloned tree.
Following this, we call the copyleftrightpointer() function which is a
simple traversal like inorder traversal and does two things:
Copies left and right child pointers of all nodes and creates the
initial cloned tree
Stores the mapping for each node in the map
Following this, we call copyrandompointer() function which is same as
copyleftrightpointer() with some differences like:
Insight :
There are other approaches to tackle this problem of which one is to
modify the original Binary Tree (or create a copy with the modified
structure) and following this, the clone of the original Binary Tree can be
created using just one traversal.
If you notice carefully, we are still doing 2 traversals as one traversal is
needed to modify the original Binary Tree. In terms of space, we do not get
any advantage as well as we need to store the modified Binary Tree.
Think about this deeply.
In the above code snippet, we declare an empty string t. Then using for
loop, we add the length of string to "l" and push that value to string "t".
Then, we add delimiter or character (here "-") and the string to temp.
Note: There are other elegant ways to serialize and deserialize an array and
the approach we present is just for giving you the overall idea. It works
equally well as other approaches.
follows:
If the given tree is Binary Search Tree, then we can use preorder and
postorder traversals to store it. For a general Binary Tree, we can use either
of preorder or postorder traversal along with inorder traversal.
In case of Binary Tree is complete then level order traversal will be
sufficient as all levels of the tree will be completely filled.
If given tree is Full Binary Tree, then every node will have either 0 or 2
children. Hence, preorder traversal will do along with storing a bit with
every node to show if its internal or leaf node.
For special cases, we can use a simple approach like converting a Binary
Tree to an array using preorder traversal and append the element with
space as a delimiter. This will give you a clear idea of the overall process.
We will use preorder traversal to present the idea and mark Null pointers:
For Serialization:
For deserialization:
In above approaches, we used the preorder traversal and marked the null as
"#" after converting the tree to single string.
Recursive Approach:
Serialization Function
We can implement the serialize function as follows:
string emp = "X" ;
void serializeRecursive(node * root, ostringstream & os)
{
if ( root == nullptr )
{
os << emp << " " ;
return ;
}
os << root -> val ;
os << " " ;
serialize( root -> left , os );
serialize( root -> right , os );
}
Explanation :
Firstly, we initialize a string emp with "X".
Then, we have two functions, serialize and serializeRecursive.
The serialize function calls the serializeRecursive function iteratively.
Inside the serialize function, we create an output stream "oss".
Then we call the serializeRecursive function with root and oss as
arguments.
Inside serializeRecursive function: The base condition is if the root is null,
the output string will emp, having value "X" to denote null node.
If the node is not null, then we insert the value of root into output string
and empty string for the space between two consecutive node values.
Then, we recursively call the serializeRecurive function for traversing all
the nodes of the left subtree.
Similarly, then we recursively call the serializeRecurive function for
traversing all the nodes of the right subtree.
Once, the whole recurisve process is over, we return back to our Serialize
function. Now, we return the output string "oss" as a string.
Deserialization Function
We can implement the deserialise function as follows:
Insight :
This may seem to be a simple problem, but this brings in a key idea. Any
data structure how complex the structure is can be represented as a single
dimensional data like an array or just an element and can be stored in a
file.
This allows you to implement a hash function that can generate a hash for
a Binary Tree. The idea is to create a serialized version and create a hash
out of it.
0-1 Encoding of Binary Tree
Succinct encoding is an approach to convert a Binary Tree to a string or
list of numbers with the lowest possible space and maintain the structural
information. Based on the encoded string, we can recreate the structure of
the original Binary Tree.
For example, if the Binary Tree is:
There can be order encodings and this is one of the possible techniques.
We will present the approach and then, you will be able to understand it
effectively.
Encoding approach
To implement this encoding, we can first traverse the nodes of the tree in
preorder traversal. This should output the encoding of "1" for internal node
and "0" for leaf nodes. In case of the tree nodes containing values, we can
store them in an array using preorder traversal.
Example:
For the below given tree,
The preorder traversal will be: 1 4 6 5 2 3
If we consider NULL nodes, preorder traversal will be: 1 4 6 NULL
NULL 5 NULL NULL 2 NULL 3 NULL NULL
Encoding will be: 1 1 1 0 0 1 0 0 1 0 1 0 0
Note: We changed the nodes to 1 and NULL nodes to 0.
We will check if the node is null. Since the root node is not null, then we
append value of binary tree to data array and the value 1 as encoded value.
Then we go to left subtree.
So, after root node we check node 4. Since it is not null, hence we put 1 in
encoding.
Then we check its left subtree. Node 6 is not null hence we put 1 in
encoding. Then the left subtree of 6 is null hence we put 0. Then we check
the right subtree of 6, which is null hence we again put 0.
Then we go back to node 4 and check right subtree of node 4. Node 5 is
not null hence we insert 1. Then the left and right subtree of 5 is null hence
we put 0. Then we return to node 4 then to node 1.
The left subtree of node 1 is traversed then we traverse the right subtree of
node 1. Here the right subtree of node 1 is node 2. Hence, we insert 1. for
the left subtree of node 2 we insert 0 and for the right subtree we have
node 3.
For the node 3 we have both right and left subtree as null hence we insert
0. We kept on inserting the values of nodes in data array continuously.
else {
append 1 to s;
append n.data to data; encoding(n.left, s, data); encoding(n.right, s, data); }
void encoding(node * root, list < bool > & s, list < int > data)
{
if ( root == 0 )
{
s .push_back( 0 );
return ;
}
else {
s .push_back( 1 );
data .push_back( root -> val );
encoding( root -> left , s , data );
encoding( root -> right , s , data );
}
}
Example :
We have the following encoding given to us: 1 1 1 0 0 1 0 0 1 0 1 0 0
For the decoding, since the first value is 1, hence we create new node.
The first element of data array be put in the value of the node. Hence, we
obtain our root node with value 1. We follow this fashion for left nodes
and then for right nodes. Then in the end we return node N and our tree is
obtained. Else in the case of null node, we would return null.
Then, after the decoding we will get back our binary tree as below:
else {
return null
}
node * decoding(list < bool > & s, list < int > & data)
{
if ( s .size() == 0 )
return NULL ;
else {
bool b = s .front();
s .pop_front();
if ( b == 1 )
{
int val = data .front();
data .pop_front();
node * root = newnode( val );
root -> left = decoding( s , data );
root -> right = decoding( s , data );
return root ;
}
return NULL ;
}
}
With the help of above functions, we can easily do encoding and decoding
of the binary tree. We can use recursion for encoding and decoding of the
tree as explained in above approaches.
Try these:
Insight :
This is important is cases when we need to reduce the storage of our
Binary Tree to facilitate an intermediate process like transfer of data or
make system memory available for a highly critical and memory heavy
process.
Approach 1: We can use two stacks and swap the values of these
stacks at each level.
Approach 2: We can use deque to solve problem. Depending
upon the even or odd level we will push or pop.
Approach 3: We can also use recursion. It will print the spiral
binary tree.
Approach 4: Last approach will use iteration. We will use two
stacks for printing left to right stack.
Approach 1
The problem can be solved using two stacks.
They could be current and next . There would be another variable required
to keep track of current level order. We will print values of the nodes after
popping from the left to right. Then we push the nodes left child, then its
right child to the stack next. The stack is a LIFO structure, when we pop
the nodes from the next, it will be reversed.
When current order is right to left then the nodes get pushed with right
child first and then their left child. At the end of each level, we swap these
stacks.
Algorithm :
Approach 2
This approach uses a deque to solve problem. Deque is double ended
Queue (FIFO: First In First Out) data structure.
Depending upon the even or odd level we will push or pop.
We start the solution from level 1.
We start a loop till the queue is empty, we will pop the values from it if the
level is even.
When level is odd then if the temp is left then we push value in q, and push
value of node in v.
If temp is right, then we push the right node of temp in q and its value in v.
We will keep incrementing the level till the loop stops.
At the end, we return v.
Approach 3:
In this approach we use recursion. It will print the binary tree in a spiral
way.
function level
function height
function zig zag
We will use level function to print nodes at various levels in tree. Function
height will be used to check height of the tree and function zigzag we will
print the zigzag traversal result.
Function height:
In this function we will check the height of a tree.
We start by checking if the node is null. If so, then we return 0.
Else we compute the height of each subtree. In recursive fashion, we
iterate the call left and right subtree.
Then we check if left height is greater or right subtree height. We return
the greater height + 1.
Function zigzag:
In this function, we will print the zigzag traversal result. We initialize the
height of tree using height function defined above.
Then we initialize boolean “ltr” with false.
Then we have for loop from 1 till height of tree. Here we call the Level
function defined above in each iteration.
We also revert “ltr” to traverse next level in opposite order.
Approach 4
This is the iterative approach. We will use two stacks for printing left to
right stack.
We use two stacks one for left to right and other for right to left.
In each iteration, we have nodes of one level in one of the stacks.
We will print nodes, and then push nodes of next level in the other stack.
For the above tree, we check if the root is null or not. Since in the above
tree it is not the case, we initialize s1 and s2 stack. Then we push the root
to s1 stack.
Now, we start iteration till s1 or s2 is not empty. Then, we have another
while loop within it. It will iterate till s1 is not empty. Then we have temp
that will store the top node of s1. Which right now will be the root node.
Then we pop the root node from the s1 stack and print the data of root
node. If the right of the root node is not null, then s2 will store the value
temp. If the left of node is not null then we will store its value in s2.
This will store values for right to left.
Then we will enter another loop where we start by initializing value of top
of s2 in temp. Then we pop top value of s2 and print its data. If the left of
temp node is not null, then we will store its value in s1. Then if the right of
temp is not null then we will store its value in right of temp.
This will store values for left to right.
Therefore, we can opt for any of the above approaches to find the zig zag
manner of the tree. We can use stacks with complexity O(N), or we can use
the deque approach, or the recursion approach or the iterative approach.
Each of them will give us zig zag binary tree from the given binary tree.
Insight :
This is an important problem as it reminds us that we can do traversal in
any way we want. Each traversal comes with its own properties and hence,
using the right traversal technique is the key to solve a problem efficiently.
Can you think of other ways we can traverse a Binary Tree?
Approach 1
We need to traverse both trees first. Let current internal nodes of two trees
being traversed be n1 and n2.
To be isomorphic we have following conditions:
Data of n1 is equivalent to n2
One of following is true for children of n1 and n2:
Left child of n1 is isomorphic to left child of n2.
Right child of n1 is isomorphic to right child of n2
Left child of n1 is isomorphic to right child of n2
Right child of n1 is isomorphic to left child of n2
Consider the following two Binary Trees:
For the above tree, we start by checking if roots of both trees are null.
Since roots of both trees are not null, we check the condition if the value of
both root nodes of both trees is equal. Since the value of both root nodes is
same, we return the iterative call of function Isomorphic.
First, we will check the condition if left child of both trees is isomorphic
and the right child of nodes of both trees are isomorphic. If any of these
conditions holds true, then the result will be false since they have &&
operator.
Second, in "or" operator we called the function again for left and right
child of both trees. Using && operator to this condition, we check for right
and left child nodes.
Now, the left and right child node of first tree is 2 and 3, respectively. The
left and right child node of second tree is 3 and 2, respectively.
For the first recursive call, we have left child node of first and second tree.
Since both of them have different values. Hence this will result in false.
For the second recursive call we have right child node of first and second
tree. They both again have different values. Hence it returns false.
For the third recursive call, we have left and right child nodes of first and
second tree, respectively. It returns true.
For the fourth recursive call, we have right and left child nodes of first and
second tree, respectively. It returns true.
Hence the overall result will be true. In similar fashion we will keep
traversing whole tree and find out if it is isomorphic or not.
Function Isomorphic:
Approach 2
We will traverse the trees iteratively using level order traversal and store
that in a queue. The conditions to be checked are:
If the key is not found, then first tree does not have node found in
other tree does not have node at given level.
If key is found but value is negative, then second tree has more
nodes with same value as first one.
If size of the map is not zero, then it means there are some keys
left. Hence the first tree has node that does not match any node in
other tree.
Function Isomorphic:
In second approach, we are traversing the trees iteratively using level order
traversal and store that in a queue. Then we will check the mentioned
conditions.
Hence, the time complexity is O(N + M) with space complexity of
O(N+M).
N and M are number of nodes in the two Binary Trees, respectively.
Insight :
This might seem to be a standard problem, but this has applications in
other fields like Chemistry. Many may associate the word “Isomorphic” to
Chemistry and hence, the idea we explored is a fundamental concept in
designing Algorithms for Computational Chemistry.
Can you use Binary Tree in other ideas like representing DNA and support
features like Mutation? Try this.
convert function
dll function
In convert function we will use inorder traversal to convert the binary tree.
In next function, dll we will form the doubly linked list by using convert
function.
Function convert:
In this we first check if the nodes are null or not.
Then we convert the left subtree and link it to the root. Then we will
convert the left subtree.
Then we will find the inorder predecessor. After this loop, left points to the
inorder predecessor.
Make root as next of predecessor and predecessor as previous of root.
Then, we convert right subtree and link it to the root.
Then, we will convert the right subtree.
Then, we will find the inorder successor. After the loop, right will point to
the inorder successor.
Make root as previous of successor.
Make successor as next of root.
Function dll:
First, we check for the base case. If the root is null, then we return root.
Else, then we call convert function above. It returns the root node of
converted DLL.
We will need pointer to left most node as it will be the head for the DLL.
Now we start a while loop to return left most node.
In the end, we return root.
Approach 2:
In this approach, we will traverse the tree in inorder fashion. We will keep
track of each node we visit. We will keep track of the DLL head and tail
pointers, insert each visited node at end of DLL using tail ptr. Finally, we
return the head of the list.
Approach 3:
In this approach have two types of pointers:
Fixed Left pointers : We will fix the left pointer. We will change
left pointers to point to previous nodes in DLL. We will perform
inorder traversal of the tree. In inorder traversal, we will keep
track of previous visited nodes. We will change left pointer to
previous node.
Fixed right pointers : We will use the left pointers fixed as above.
Starting from rightmost node in Binary Tree, it is the last node in
DLL. Since left pointers are changed to point to previous node in
DLL. Hence, we can traverse the complete DLL using these
pointers. The traversal is from last to first node. While traversing
we will keep track of previously visited node. We will change
right pointer to previous node.
We have four main functions:
Function prev:
In this function, we change left pointers to work as previous pointers.
The function is performing inorder traversal.
Then it updates left pointer using previously visited node.
Function next:
In this function, we change right pointers to act as next pointers in
converted DLL
We find the right most node in binary tree or last node in DLL
We start from the rightmost node, traverse using left pointers.
While traversing we change right pointer of nodes The leftmost node is the
head of the linked list. We will return it.
Function fin:
In this function, we convert the binary tree to DLL.
We first set previous pointer.
Then we set next pointer and return head of DLL.
Then we traverse the DLL from left to right.
Insight :
If you observe carefully, all approaches are fundamentally same. This
brings in the idea that some approaches are same but still performance and
analysis may difference because the way we implement is important.
This does not mean that all approaches are same. There are distinct
approaches and this results in different classes of problems which comes
under Complexity Theory.
Introduction to Skewed Binary Tree
A binary tree can be called a skewed binary tree if all nodes have one child
or no child at all. They can be of two types:
The above code snippet creates a new node, that will be the root for our
tree and initialize it with value 5. Then, it creates its left node with value 8
and further its left node with value 10.
Case 2 : Right skewed binary tree In case of right skewed binary tree,
we will insert values only in right nodes and leave all left nodes as null.
The above code snippet creates a new node, that will be the root for our
tree and initialize it with value 5. Then, it creates its right node with value
8 and further its right node with value 10.
Note : A skewed Binary Tree is what we want to avoid in real problems as
it reduces to an array and takes algorithms to their worst case. Still, we will
analyze this structure and understand it so that we can handle this better.
Some specific problems require the use of Skewed Binary Tree for
efficiency.
Algorithmic Steps:
For each node N, check the following:
If both left and right child nodes are present, it is not a valid
skewed tree.
If the node has only one left child node, then we check its left
child node
If the node has only one right child node, then we check its right
child node
If there are no child nodes, it is a valid node of a skewed tree.
The pseudocode is as follows:
//Now, we check if the node has two children, if yes then we return
false if ( root -> left && root -> right )
return false ;
//If the node has only one left child node then we check its left child
node //else we check its right root node if ( root -> left )
return skewed( root -> left );
return skewed( root -> right );
}
Explanation:
Consider the above binary tree. Firstly, the function will check if the root is
null or if both left and right nodes are null. In above binary tree, initially
this condition is false.
Now, it checks if the right and the left nodes are not null. If so, it will
return false since skewed binary tree can only have one child either on left
or right. In above binary tree it is not the case.
Now, we come for the last condition. If the left node is not null, then it
returns recursively the left node of the root. Else, in similar recursive
fashion it returns the right node of root.
This way in above example, the leaf node will satisfy first condition of null
left and null right node and hence return true. Hence, it is a skewed binary
tree.
If the tree would not have been skewed, then the function would have
returned false.
Time Complexity
Insight :
By examining each node irrespective of the overall structure, we can
identify if a Binary Tree is skewed. Hence, specific types depending on the
structure of a Binary Tree does not depend on checking the overall
structure. This is an important property.
Hence, we can convert binary tree into two types of skewed binary trees
provided we have Binary Search Trees:
Get the inorder traversal of the tree (that is the left most element in
the Binary Tree)
Insert new nodes as right child in the new Skewed Tree
This may be hard to realize at first. Think over the steps and you will
realize how this is same as doing a simple inorder traversal.
// Now we will Recurse for the left or right subtree on the basis of
the order skewed( leftNode );
}
This may be hard to realize at first. Think over the steps and you will
realize how this is same as doing a simple inorder traversal. If you
understood the first case, then this is the same with the order reversed.
// Now we will Recurse for the left or right subtree on the basis of
the order skewed( rightNode );
}
Both the functions for Increasing and Decreasing order can be merged
together using a control variable K where:
// Now we will Recurse for the left or right subtree on the basis of
the order if ( k ) skewed( leftNode , k );
else skewed( rightNode , k );
}
Explanation:
In above tree, the function will start by checking if root is null. If so, then
it will return. This is not the case with our example.
If k is 1, that means we need to follow increasing order. Hence, we will
recursively call the right child of root and pass k as argument to keep the
track if we need to find increasing or decreasing order of skewed binary
tree.
Then, we make a new node for the final skewed tree. We initialize the left
node with left child of root and right with right child of node.
If the root of the skewed binary tree is not defined, then we define the head
node and right node of skewed tree as root. The left node is defined null
since it is a skewed binary tree, and it can only have one child.
If head node is already defined then the right child of previous node is
given value of our root and left child is null since in skewed tree, one node
can have only one child. Previous node is given value of root.
At the end we recurse according to our value of k that defines if the order
will be increasing or decreasing.
If k is 1 then we recursively call function and pass left node as argument
else we will pass right node if k = 0.
Time Complexity: O(N) as we traverse through the tree only once.
Space Complexity: O(N) in a recursive implementation due to function
call stack.
Insight :
This problem is important as it illustrates how we can generate a new
structure just by going through the traversal the first time. At first, it seems
that the problem requires information of the entire traversal, but it is not
true as evident by our algorithm.
Can you think of other information that can be generated while going
through the traversal?
rightThread
leftThread
Both new attributes are of type Boolean.
Following is the node structure in a Single threaded binary tree and Double
threaded binary tree:
// single threaded
class Node {
int data ;
Node left ;
Node right ;
bool rightThread ;
}
// double threaded
class Node
{
int data ;
Node left ;
Node right ;
bool leftThread ;
bool rightThread ;
}
For example:
Let us say for some node right pointer is pointing to some node and
righThread is set to true, this means that it is pointing to a child node but if
in the same case, if rightThread is set to false this means that it is pointing
to a parent node (and not child node).
The time required for finding inorder predecessor or successor for a given
node in Threaded Binary Tree is O(1) provided we are on that node.
Case 1
When new node is inserted in an empty threaded binary search tree, we set
the new node’s left and right pointers as null pointers. This step is same in
both binary as well as threaded binary search tree.
root= tm p ;
tmp - > left = NULL ;
tmp - > right = NULL ;
Case 2
When new node is inserted in binary search tree as left child of some
already existing node, then node we perform two operations in parent and
two operations in child for example -
For Child
For newly inserted child node, we set the new node’s left child node point
to left of parent (that is Inorder predecessor of the parent node) and it's
right child node point to parent.
For Parent
For parent of the child node inserted, we set it's lthread to true indicating
left child exist and also setting it's left child node as temp as child node is
the new inorder predecessor for the parent node and also it's the left child
of the parent.
Case 3
When new node is inserted as right child of some node, we perform two
operations on the child and two operations on parent similar to what we
have done in case 2 -
For Child
Let us say the new node inserted is child node and the node to which it's
inserted is parent node, then we can say that the parent of child inserted is
now it's inorder predecessor and the inorder successor of parent is now the
inorder succesor for the child node.
So, the left and right threads of the new node will be –
if ( par == NULL )
{
root = tmp ;
tmp -> left = NULL ;
tmp -> right = NULL ;
}
else if ( ikey < ( par -> info ))
{
tmp -> left = par -> left ;
tmp -> right = par ;
par -> lthread = false ;
par -> left = tmp ;
}
else {
tmp -> left = par ;
tmp -> right = par -> right ;
par -> rthread = false ;
par -> right = tmp ;
}
return root ; }
We need to keep repeating the above algorithm until we find the required
node , if we are unable to locate the node and reach null we can return -1
indicating that element does not exist in the tree.
after deletion left thread of the parent node is made to point to left
thread of the child
and parent's leftThread is set to false indicating parent is pointing
to some inorder predecessor.
// If Node to be deleted is left // of its parent else if ( ptr == par -> left
){
par -> lthread = true ;
par -> left = ptr -> left ;
}
else {
par -> rthread = true ;
par -> right = ptr -> right ;
}
Case 2: node to be deleted has only 1 child left or right Before deleting
such node first it's inorder predecessor and inorder successor is found
out:
s= inSucc( ptr );
p = inPred( ptr );
then,
If Node to be deleted has left subtree, then after deletion right thread of its
inorder predecessor should point to its inorder successor.
p -> left = s ;
If Node to be deleted has right subtree, then after deletion left thread of its
inorder successor should point to its inorder predecessor .
s -> left = p ;
free( ptr );
return root ;
}
return root ;
}
Insight :
The key idea in understanding the operations is that how changing the
structure of a Binary Tree impacts the 3 basic operations. Threaded Binary
Tree is a variant of Binary Tree as an example. In real problems, we may
need to develop other variants and tune the 3 basic operations accordingly.
This approach is easy to follow but the problem with this approach is it
use extra memory and also, it is traversing the tree two times which makes
approach 1 quite heavy on space and time.
Approach 2
In this approach, we will take a recursive approach. The Algorithmic steps
are as follows:
1. Do the reverse inorder traversal which means visit right child first
then parent followed by left child.
2. In each recursive call, pass the node which you have visited
before visiting current node.
3. In each recursive call whenever you encounter a node whose right
pointer is set to NULL and previous visited node is set to not
NULL then, make the right pointer of node points to previously
visited node and mark the value rightThread as true.
Whenever making a new recursive call to right subtree, do not change the
previous visited node and when making a recursive call to left subtree,
pass the actual previous visited node.
Remember that when our right pointer of the current node is pointing to
it's children node, bool rightThread it set to true and if it is pointing to
some of it's ancestor, then rightThread is set to false.
This approach is better than the previous approach as this uses less time
and much less constant space.
Let us look at an example to see how we can use this algorithm to convert
a binary tree to threaded binary tree.
Consider the following tree as an example:
Step 1 :-
In the given tree, we will traverse the tree in reverse inorder traversal
which means we will first visit the right subtree then root then followed by
the left subtree.
Step 2 :-
As we will follow this algorithm recursively , so first we will visit the
rightmost leaf node 20 , since there is no need which we have visited prior
to this node we will make it's right pointer point to NULL and bool
variable as false.
Step 3 :-
Now we will move to root which is node 15 in the given case and since we
have already visited node 20 prior to node 15 so we will mark the right
pointer of current node (15) to 20 and make bool but currently we are not
on the leaf node so we will also make rightThread bool variable as true
(indicating it's pointing to it's child node).
Step 4 :-
We will again repeat the step three on node 12 but this is a leaf node
whose right pointer is pointing to it's ancestor so we will set rightThread
bool variable as false.
Step 5 :-
We will just keep repeating the steps 2, 3 and 4 until whole tree is
traversed just keeping one thing mind - whenever we make a new
recursive call to right subtree, do not change the previous visited node and
when we make a recursive call to left subtree then pass the actual previous
visited node .
Step 6 :-
At the end we will have the whole binary tree converted to threaded binary
tree.
The time and space complexity required for the above conversion is:
class BSTtoThreadedBST {
public :
BSTtoThreadedBST(){
Insight:
We explored how we can convert a simple Binary Tree to one of its
variants, Single Threaded Binary Tree. This is an important path as for
many problems, it is better to change the structure than to work on the
original structure.
It is called a search tree because it can be used to search for the presence
of a number in O(logN) time in contrast to O(N) time for a simple Binary
Tree or array list.
The properties that separate a binary search tree from a regular binary tree
are:
BSTnode node
{
int data;
BSTnode left_node;
BSTnode right_node;
}
If value is less than the root, we can say for sure that the value is not in the
right subtree; we need to only search in the left subtree and if the value is
above root, we can say for sure that the value is not in the left subtree; we
need to only search in the right subtree.
Algorithm
If root== NULL
return NULL ;
If number==root->data
return root->data;
If number<root->data
return search(root->left)
If number>root->data
return search(root->right)
// Key is greater than root's key if ( root -> key < key )
return search( root -> right , key );
// Key is smaller than root's key return search( root -> left , key );
}
Algorithm
If node == NULL
return createNode(data)
if (data<node->data)
node->left=insert(node->left,data); else if (data > node->data)
node->right = insert(node->right,data); return node;
struct node
{
int key ;
struct node * left, * right;
};
Example
Illustration to insert 2 in above tree: Start from root which in this case is 8.
Now Check if the value that is 2 is less than root that is 8 then go to left of
the 8.
Now current node is 3 compare the value that is 2 with current node if the
value is less than 3 go to left side of 3.
Now current node is 1 compare the value that is 2 with current node if the
value is less than 1 go to left side of 1 else go to the right.
Now if the node is equal to null therefore create new node and insert it.
3. Deletion in a binary search tree
We must delete a node from a binary search tree in such a way, that the
property of binary search tree does not violate. There are three situations
of deleting a node from binary search tree:
In the above figure, we are deleting the node 85, since the node is a leaf
node, therefore the node will be replaced with NULL and allocated space
will be freed.
2. The node to be deleted has only one child In this case, replace the
node with its child and delete the child node, which now contains the
values to be deleted. Simply replace it with the NULL and free the
allocated space.
Example:
In the above figure, the node 12 is to be deleted. It has only one child. The
node will be replaced with its child node and the replaced node 12 (which
is now leaf node) will simply be deleted.
Example:
In the above figure, the node 50 is to be deleted which is the root node of
the tree. The in-order traversal of the tree given below.
6, 25, 30, 50, 52, 60, 70, 75.
Replace 50 with its in-order successor 52. Now, 50 will be moved to the
leaf of the tree, which will simply be deleted.
Complexity
Searching
Insertion
Deletion
Advantages
We can always keep the cost of insert(), delete(), lookup() to O(logN)
where N is the number of nodes in the tree - so the benefit really is that
lookups can be done in logarithmic time which matters a lot when N is
large.
To give you an idea, if N = 1,048,576 (that is more than 1M), the logN is
just 20. Hence, in a Balanced Binary Search Tree with 1M element, we
can search an element just by comparing 20 elements.
We can have ordering of keys stored in the tree. Any time we need to
traverse the increasing (or decreasing) order of keys, we just need to do
the in-order (and reverse in-order) traversal on the tree.
We can implement order statistics with binary search tree - Nth smallest,
Nth largest element. This is because it is possible to look at the data
structure as a sorted array.
We can also do range queries-find keys between N and M (N<=M).
BST can also be used in the design of memory allocators to speed up the
search of free blocks (chunks of memory) and to implement best fit
algorithms where we are interested in finding the smallest free chunk with
size greater than or equal to size specified in an allocation request.
Applications
A Self-Balancing Binary Search Tree is used to maintain sorted stream of
data. For example, suppose we are getting online orders placed and we
want to maintain the live data (in RAM) in sorted order of prices.
For example, suppose we wish to know number of items purchased at cost
below a given cost at any moment. Or we wish to know number of items
purchased at higher cost than given cost.
Insight :
Binary Tree has a structure and if we are able to distribute the data
according to a certain pattern, then we can make use of the structure to
gain advantage. This is the fundamental idea of Binary Search Tree.
As we move forward in this book, you will see another variant based on
this idea: Binary Space Partitioning Tree.
#include <bits/stdc++.h>
using namespace std;
int main() {
vector < int > arr ;
int arr1 [ 8 ] = { 2 , 9 , 6 , 5 , 7 , 1 , 8 , 4 };
for ( auto x : arr1 ) arr .push_back( x );
sort( arr .begin(), arr .end());
//Further code...
}
As the array is now sorted, we can implement the tree data structure to
store the values of the array elements.
Implementation of tree:
For the implementation of tree, we need a Node class which defines a tree
node to store data and pointers to left and right subtree.
So, we can create a Node class by the following syntax:
class Node {
public :
int data ;
Node * left ;
Node * right ;
}
After the creation of Node class, we can implement the tree functions
which are inserting nodes and traversing them.
Firstly, we will need function to make an empty tree node in the memory,
For implementation, we will take vector as it's input and then find the mid
element of the list, we will take this element as root and find the mid
elements of left and right subtree recursively to create a balanced tree.
Step 2: Find the mid element of the array and insert into the tree.
//If starting index goes beyond end index then return NULL
if ( start > end )
return NULL ;
Step 3 & 4: Find the mid element of left and right tree and insert into the
array. Repeat until no element is left.
//Return the root element which is the middle element of the list
return root ;
}
Conversion from sorted array to BST takes N steps as the function has to
go over every element once, hence the time complexity is O(N).
So, the sorted array becomes,
Algorithm (iterative version): 1. Take a sorted array and take a stack.
2. Each tuple keeps track of the child's parent and the side of the parent
that the child will become.
3. We only push child tuples to the stack after their parents are created, the
process will create the children until we reach the base case, whereby that
branch has exhausted its corresponding chunk of the original elements.
struct T {
int low_idx ;
int high_idx ;
Node node ;
Tree( int low , int high , Node _node ) {
low_idx = low high_idx = high
node = _node
}
}
Step 2: Then we will create a function sortedArrayToBST.
if ( n == 0 )
return NULL ;
Step 4: Create stack and push the node with middle element of array.
Step 5: Pop the top node and assign the left and right child to it.
Step 6: If the lower index of tmp element is less than mid we will pick
middle element and push that into the tree.
Step 7: If the higher index of tmp element is greater than the mid we will
pick middle element of high_idx and mid then push that into the tree.
Conversion from sorted array to BST takes n steps as the function has to
go over every element once, hence the time complexity is O(n) So, the
sorted array becomes,
Now, you can implement various traversals such as inorder, preorder and
postorder. For this example we will demonstrate preorder traversal
method.
In the preorder traversal method, first the root is traversed then the left
subtree and then the right subtree.
We will create a function for this purpose,
//Return the root element which is the middle //element of the list
return root ;
}
Insight :
This is important as you should realize that we are converting one data
structure (array) to another data structure (Binary Search Tree) and are
preserving the properties of the original data structure (that is: sorted
order).
Following this, you will realize that at the end, it is the restrictions that
define a data structure, and all other properties are interchangeable .
Minimum number of swaps to
convert a binary tree to binary search
tree We understand that a Binary
Tree is an unrestricted version of
Binary Search Tree. It is the order of
elements that differentiate both
variants. Swapping is a fundamental
operation where two elements are
interchanged.
The problem is to find the minimum number of swaps to convert a Binary
Tree to a Binary Search Tree.
The graph will now contain many non-intersecting cycles. Now a cycle
with 2 nodes will only require 1 swap to reach the correct ordering.
Hence,
summation of i = 1 to k: ans = Σ i = (cycle_size – 1) where k is the
number of cycles Approach :
Create an array of pairs where first element is an array element and second
element is position of first element.
Sort the array by array element values to get right position of every
element as second element of pair.
To keep track of visited elements, initialize all elements as not visited or
false.
Traverse array elements and find out the number of nodes in this cycle and
add in ans and Update answer by adding current cycle.
// find out the number of node in // this cycle and add in ans int
cycle_size = 0 ; int j = i ;
while ( ! vis [ j ])
{
vis [ j ] = true ;
j = arrPos [ j ]. second ;
cycle_size ++ ;
}
return ans ;
}
Now by combing above two methods, we will get the minimum number
of swaps to convert the binary tree into binary search tree Approach:
Do the Inorder Traversal of the binary tree and store the elements of tree
in an array.
Then find the minimum number of swaps require to make the array sorted
which is made from above process and store it in result and give as
output.
Insights :
In the previous problem (converting a sorted array to Binary Search Tree),
we understood that different Data Structures are the same with different
restrictions.
Our current problem brings up a key point that this interchange between
different Data Structures come at a cost .
Traverse the node from root to left recursively until left is NULL.
The node whose left is NULL is the node with minimum value.
Approach for finding maximum element:
Explanation:
For Finding Minimum value in Binary search tree.
start from root that is 8.
Time Complexity:
O(N) Worst case happens for left skewed trees in finding the
minimum value.
O(N) Worst case happens for right skewed trees in finding the
maximum value.
O(1) Best case happens for left skewed trees in finding the
maximum value.
O(1) Best case happens for right skewed trees in finding the
minimum value.
In the balanced Binary Search tree, the time complexity is O(log
N)
N is the number of elements in a Binary Search tree Space complexity:
O(1) as we need not store any information while finding the minimum or
maximum element.
Insight :
This problem illustrates the power of Binary Search Tree.
We are able to find the minimum element in O(logN) time instead of O(N)
time because the restriction in the structure of a Binary Search Tree has
already done the extra work (O(N) – O(logN)) in other previous
operations which we do not observe.
Hence, restrictions in a Data Structure make it suitable for specific
problems.
Implementation
Following is the implementation of the above algorithm
return root ;
}
Explanation:
First of all, we will do inorder traversal and store the elements in an array.
First go to the left of the root but it is null therefore go to the root of the
tree and store it in an array.
Then go to the right of the root go to the 2.left check if left child of the 2
is null the store 2 in the array.
Then go to the right of the 2 and check if the left child of 3 is null the
store the 3 in array.
Then go to the right of 3 and check if the left child of 4 is null then store 5
in the array
Then go to the right of 4 and check if the left child of 5 is null then store 5
in array. Now check if the right child of 5 is null then return the array.
Now we will build the balanced binary search tree from the sorted array
we obtained through the above process.
First of all, find the middle of the array that is 3 and store it as root of the
new tree.
Then go to the left of the 3 and build the left subtree for that find again the
middle of the left sub array of 3 that is 2 and store as the left child of 3.
Then go the left sub array of the 2 and again find the middle of the array
and store it as the left child of 2.
Now start>end return to root that is 3 of the tree. Now our Balanced
Binary Search Tree is ready.
Time Complexity:
The Inorder Traversal of Binary search tree is in O(N) time complexity.
To form Balanced Binary tree from Sorted array, it takes O(N) time to
complete.
Following is the recurrence relation for buildTreeUtil():
T(n) = 2 T(n/ 2 ) + C
T(n) --> Time taken for an array of size n
C --> Constant
Insight :
This is a key step as Binary Search Tree reaches its full potential only if it
is balanced. To make it reach this, extra work is needed if this was not in
consideration initially.
As we will see beyond, if we consider this initially, no extra load is
necessary, and this leads to “Self-Balancing Binary Tree ”
The idea simple do inorder traversal and store it an array. We know by the
property of the binary search tree, inorder traversal gives element of a
binary search tree in sorted form.
Explanation:
First of all, we go to the left most element of the tree and store it in a
vector.
Then check the right children if it is not existing, then go to parent and
then again return to parent of parent because it is visited and store it in
vector.
Then go to the right child of present node and check if left child does not
exist the store the present node in vector.
The right child of the present node and check if left child exist then go
there otherwise store the present element in the vector.
Then check it right child exist then go there if not then, go to the root and
now all the nodes are visited therefore return to the main function.
Now required element is the kth one in the array therefore return it and it is
required output.
Time Complexity:
Inorder Traversal of Binary search Tree takes O(N) time and fetching the
Kth element from vector requires O(1) time.
Therefore, all over time complexity of this algorithm is O(N) .
Algorithm:
start :
if K = root.leftElement + 1
root node is the K th node.
goto stop
else if K > root.leftElements
K = K - (root.leftElements + 1)
root = root.right goto start
else root = root.left goto start
stop :
if ( root )
{
/* A crawling pointer */
node * pTraverse = root ;
/* Go to k-th smallest */
while ( pTraverse )
{
if ( ( pTraverse -> lCount + 1 ) == k )
{
ret = pTraverse -> data ;
break ;
}
else if ( k > pTraverse -> lCount )
{
/* There are less nodes on left subtree Go to right subtree */
k = k - ( pTraverse -> lCount + 1 );
pTraverse = pTraverse -> right ;
}
else {
/* The node is on left subtree */
pTraverse = pTraverse -> left ;
}
}
}
return ret ;
}
Explanation:
Start with the root compare the lcount with k if k is equal to lcount then
break if k<lcount then go the left otherwise go right. Subtract lCount+1
from k
In our example, k is smaller then, lcount therefore we come to the left
child now again compare lcount of the current node and k. Subtract
lCount + 1 from k
As we can see than lcount of the current node is less than k therefore, we
go right child now.
Now we can see that lcount of current node is equal to k therefore, return
the value of that node which is our output.
Insight :
A BST is built to find the smallest or the largest element, but the structure
is such that we can find the kth smallest element as well. This is not true in
other data structures that we may develop.
Can you find the first k smallest elements? by extending our approach.
We will answer in the next problem.
Naive Approach
This is a simple approach this takes O(n) time and O(1) extra space.
Algorithm:
Explanation :
First of all, go to the left most element and add to the variable. In this
case, it is res and increment the counter by 1. So, now res = 49 and count
= 1. Now, check if count is equal to three. If no, then go another element.
Now current node is 51 add it to the res and increase counter by 1 now res
= 100 and count = 2, check if count is not equal to k, then go to the
another element.
Now current node is 52 add it to the res and increase counter by 1 now res
= 152 and count = 3 check if count if equal to k yes then return res and it
is required output.
Efficient Approach
Here we use augmented tree data structure to solve this problem
efficiently in O(h) time [ h is height of Binary Search Tree ] .
node * left ;
node * right ;
};
/* Iterative insertion
Recursion is least preferred unless we gain something
*/
node * insert(node * root, int data)
{
node * node_t = newNode( data );
/* A crawling pointer */
node * pTraverse = root ;
node * currentParent = root ;
return root ;
}
Find Kth smallest element [ temp_sum store sum of all element less than equal to K ]
ksmallestElementSumRec(root, K, temp_sum)
>lcount+ 1 , temp_sum)
ELSE
// Goto left sun-tree ksmallestElementSumRec( root->left, K, temp_sum)
Explanation :
First of all, go to the root and compare k with lcount + 1 if it is equal then
temp_sum = 208 and return otherwise if k < lCount then go the left child
of the root.
Now compare the lCount + 1 of current node with k if it is equal then
temp_sum = 100 and return if k>lCount the go to right and temp_sum =
100 and k = 1.
Insight :
This problem shows that a Data Structure can go beyond its power if it
stores extra information. You can imagine that if we store all information,
a specific Data Structure can be taken to the edge. This brings in
significant space overhead, so Data Structure are tuned according to the
problem at hand.
You can imagine this as different species of the same Data Structure.
Different Self Balancing Binary Trees
A self-balancing binary tree is a
Binary tree that automatically keeps
its height small in the face of
arbitrary insertions and deletions on
the tree. The height is usually
maintained in the order of log n
(optimal) so that all operations
performed on that tree take O(log n)
time on an average. Let us look at the
most widely used self-balancing
binary trees, their complexities and
their use cases. The various self-
balancing binary search trees are:
2-3 tree
Red-Black Tree
AVL tree
B tree
AA tree
Scapegoat Tree
Splay tree
Treap
Weight Balanced trees
1. 2-3 Tree
A 2-3 tree is a self-balancing binary tree data structure where each node
in the tree has either:
You can see from the above tree that it satisfies all the rules mentioned
above.
Time Complexity in Big O notation: The time complexity for average and
worst case is the same for a 2-3 tree i.e.
Space - O(n)
Search - O(log n)
Insert - O(log n)
Delete - O(log n)
2. Red-Black Tree
A Red-Black tree is another self-balancing binary search tree. Each node
stores an extra bit which represents the color which is used to ensure that
the tree remains balanced during the insertion and deletion operations.
Every node has the following rules in addition to that imposed by a binary
search tree:
Time Complexity in Big O notation: The time complexity for average and
worst case is the same for a red-black tree i.e.
Space - O(n)
Search - O(log n)
Insert - O(log n)
Delete - O(log n)
3. AVL Tree
AVL tree is a type of self-balancing binary search tree where the
difference between heights of the left and right subtrees cannot be more
than one for all nodes in the tree. This is called the Balance Factor and is
defined to be: BalanceFactor(node) = Height(RightSubTree(node)) -
Height(LeftSubTree(node)) which is an integer from the set {-1, 0, 1} if it
is an AVL tree.
Time Complexity in Big O notation: The time complexity for an AVL tree
is as shown below
Space - O(n)
Search - O(log n)
Insert - O(log n)
Delete - O(log n)
4. B-tree
A B-Tree is a type of self-balancing binary search tree which generalizes
the binary search tree, allowing for nodes with more than 2 children.
A B-tree of order m is a tree which satisfies the following properties:
Space - O(n)
Search - O(log n)
Insert - O(log n)
Delete - O(log n)
Applications of B-Trees:
Well suited for storage systems that read and write relatively
large block of data.
Used in databases and file systems.
5. AA Tree
Unlike in red-black trees, red nodes on an AA tree can only be added as a
right sub-child that is no red node can be a left sub-child. The following
five invariants hold for AA trees:
Space - O(n)
Search - O(log n)
Insert - O(log n)
Delete - O(log n)
Time Complexity in Big O notation (Worst Case) : The time complexity
for a Scapegoat tree for worst case is as shown below
Space - O(n)
Search - O(log n)
Insert - amortized O(log n)
Delete - amortized O(log n)
7. Splay tree
A splay tree is a self-balancing binary search tree with the additional
property that recently accessed elements are quick to access again. All
normal operations on a binary tree are combined with one basic operation
called splaying. Splaying of the tree for a certain element rearranges the
tree so that the element is placed at the root of the tree.
For example, when you perform a standard binary search for an element
in question, and then use the tree rotations in a specific order such that
this element is now placed as the root. We could also make use of a top-
down algorithm to combine the search and the reorganization into a single
phase.
Splaying depends on three different factors when we try to access an
element called x:
Based on this, we have three different types of operations: Zig : This step
is done when p is the root.
Zig-Zig : This step is done when p is not the root and x and p are either
both right children or are both left children.
Zag-Zig : This is done when p is not the root and x is a right child and p
is a left child or vice versa.
Space - O(n)
Search - amortized O(log n)
Insert - amortized O(log n)
Delete - amortized O(log n)
8. Treap
Treap is a data structure that combines both a binary tree and a binary
heap, but it does not guarantee to have a height of O(log n) . The concept
is to use randomization and binary heap property to maintain balance with
high probability.
Every node of a treap contains 2 values:
The above example is a treap with alphabetic key and numeric max heap
order.
Treap support the following basic operations:
To search for a given key value, apply a standard binary search
algorithm in a binary search tree, ignoring the priorities.
To insert a new key x into the treap, generate a random priority y
for x. Binary search for x in the tree, and create a new node at the
leaf position where the binary search determines a node for x
should exist. Then, as long as x is not the root of the tree and has
a larger priority number than its parent z, perform a tree rotation
that reverses the parent-child relation between x and z.
To delete a node x from the treap, if x is a leaf of the tree, simply
remove it. If x has a single child z, remove x from the tree and
make z be the child of the parent of x (or make z the root of the
tree if x had no parent). Finally, if x has two children, swap its
position in the tree with the position of its immediate successor z
in the sorted order, resulting in one of the previous cases. In this
final case, the swap may violate the heap-ordering property for z,
so additional rotations may be needed to be performed to restore
this property.
Space - O(n)
Search - O(log n)
Insert - O(log n)
Delete - O(log n)
Space - O(n)
Search - O(n)
Insert - O(n)
Delete - O(n)
9. Weight Balanced trees
Weight-balanced trees are binary search trees, which can be used to
implement finite sets and finite maps. Although other balanced binary
search trees such as AVL trees and red-black trees use height of subtrees
for balancing, the balance of WBTs is based on the sizes of the subtrees
below each node.
The size of a tree is the number of associations that it contains. Weight-
balanced binary trees are balanced to keep the sizes of the subtrees of
each node within a constant factor of each other.
This ensures logarithmic times for single-path operations (like lookup and
insertion). A weight-balanced tree takes space that is proportional to the
number of associations in the tree.
At this point, you must have the idea that there are several types of Self-
balancing Binary Search Trees and each has an unique approach to keep
the tree balanced.
Insight
Self-balancing BSTs are flexible data structures, in that it is easy to
extend them to efficiently record additional information or perform new
operations. For example, one can record the number of nodes in each
subtree having a certain property, allowing one to count the number of
nodes in a certain key range with that property in O(log n) time. These
extensions can be used, for example, to optimize database queries or other
list-processing algorithms.
We will cover AVL Tree and Splay Tree in depth so that you have a clear
idea of a couple of ways we can make sure that our Binary Tree is
balanced. These two tree data structures give you a strong foundation and
are similar to other common data structures we listed in terms of usage.
AVL Tree
An AVL Tree (Adelson-Velsky and Landis tree) is a self-balancing
binary search tree such that for every internal node of the tree, the heights
of the children of node can differ by at most 1. If the difference in the
height of left and right sub-trees is more than 1, the tree is balanced
using rotation techniques .
The credit of AVL Tree goes to Georgy Adelson-Velsky and Evgenii
Landis .
struct Node
{
int _data ;
Node * left ;
Node * right ;
int _height ;
};
In Left Rotation, every node moves one position to left from the current
position.
Notice that node N3 takes the place of node N1. N1 takes the place of
node N2 and becomes the left child of node N3. Node N2 becomes the
left child of node N1. Node N4, which was the left child of node N3,
becomes the right child of node N1.
In Right Rotation, every node moves one position to right from the
current position.
Consider the following example:
Notice that node N3 takes the place of node N5. N1 takes the original
place of node N3 and becomes the left child of node N3. Node N6, which
was the right child of node N3, becomes the left child of node N5.
Get the balance factor (left subtree height – right subtree height)
of the current node.
If balance factor is greater than 1, then the current node is
unbalanced and we are either in Left case or left Right case. To
check whether it is left case or not, compare the newly inserted
key with the key in left subtree root.
If balance factor is less than -1, then the current node is
unbalanced and we are either in Right case or Right Left case. To
check whether it is Right case or not, compare the newly inserted
key with the key in right subtree root.
Only one case occurs because tree was balanced before insert
After the appropriate single or double rotation: the smallest
unbalanced subtree has the same height as before the insertion
So, all ancestors are now balanced
Example:
This is an example of building an AVL Tree by inserting 4,5,6,7,16 and
15 sequentially to an, initially, balanced binary search tree with 3 nodes
(1,2,3)):
Complexity
Insight :
With AVL Tree, you got an idea how the structure of nodes are modified
if we want to keep our Binary Tree balanced. Notice that we added the
restriction of keeping our Binary Tree balanced but it did not impact the
complexity performance.
Can you imagine (based on the problems we solved) for which
restrictions, fundamental operations face a setback or overload?
Splay Tree
Splay tree is a Self-adjusting Binary Tree with additional property that
recently accessed elements as kept near the top and hence, are quick to
access next time. After performing operations, the tree gets adjusted/
modified and this modification of tree is called Splaying .
Why Splaying?
The frequently accessed elements move closer to root so that they can be
accessed quickly. Having frequently used nodes near the root is useful for
implementing cache and garbage collection as the access time is reduced
significantly for real-time performance.
Splaying
Whenever a node is accessed, a splaying operation is performed on that
node. This is a sequence of operations done on the node which
sequentially brings up that node closer to root and eventually makes it as
root of that tree. Now, if we want to access that same node from the tree,
then the time complexity will be O(1) . This is what we mean by
frequently accessed elements are easily accessible in less time.
We will explain the exact steps further into this chapter.
The Image does not include rotation step. This is just simple insertion
operation for a BST.
Following is the pseudocode for insert operation:
class Node:
def __init__(self, data):
self . data = data
self . parent = None
self . left = None
self . right = None
while x != None :
y=x
if node . data < x . data :
x = x . left
else :
x = x . right
Splaying Operation
Let the node to be accessed be x and parent of that node be p. Let
grandparent of x be g. (parent of node p) Depends upon 3 factors:
Zig-Zag step (right and left rotation) Performed if parent node is not
the root node and x is right child of its parent and that parent node is
left child of its parent node.
If x is root node, then there is no need to perform Splay operation.
Example:
Pseudocode
Enter in while loop if parent of node x is not None.
Check if grandparent of node is None. This means our node is at 2nd
level. If it is at 2nd level, then again check if its right child or left child. If
its left child performs right rotation or if its right child, then perform left
rotation.
Condition to check if both the parent and grandparent of node x exists and
are left children of their parent nodes.
(x == x.parent.left and x.parent == x.parent.parent.left) If true, then
perform right rotation on grandparent first then parent. This is Zig
Zig condition.
Condition to check if both the parent and grandparent of node x exists and
are right children of their parent nodes.
(x == x.parent.right and x.parent == x.parent.parent.right) ZagZag
condition.
Check x == x.parent.right and x.parent == x.parent.parent.left this
conditions checks if the parent node is right child and this parent node is
left child of "its" parent node. First perform left rotation on parent then
right rotation on grandparent. (Zig Zag condition) Opposite case will be
for Zag Zig condition. Perform right rotation on parent node. then left
rotation on grandparent.
Right rotation
Observing the rotation, we can see that right child of x is now left child of
x and y becomes root node.
In right rotation, reverse of the left rotation happens.
Pseudocode:
Now there could be 2 conditions possible, which are if the key is found
and if it is not found.
We will traverse the tree till the key is found, and when it is not found, we
will eventually reach to the end that is leaf node. So, now perform Splay
operation and return that key is not found.
Pseudocode
1. Search node to be deleted.
2. Return something if key is not found.
3. Perform splay operation on that key.
4. Unlink that key node from its parent and its children, causing the tree
to split into 2 subtrees.
5. Call Join function.
While traversing, if the key is found in the tree, then perform Splaying
which makes the node as the root of tree as shown in the figure. Now, the
node to be deleted is root of the tree, so split the tree by unlinking the
right and left child of x. Now, we will have 2 sub trees.
Join the 2 trees using join operation, we will define join operation after
deletion operation just to continue with the flow.
Now let us define join operation Note that even though the right
subtree is empty we have to perform splay operation on the
maximum element in left subtree Join operation is done by finding
the maximum value in left subtree, then splaying it.
Pseudocode
1. If left subtree is empty then return right subtree as final tree. else
perform 3rd step.
2. If right subtree is empty then still perform 3rd step.
3. Find maximum value node from the left subtree and perform splaying
on that maximum node to bring it at the top.
4. Make the root of right subtree as the right child of root of the left
subtree.
def __join(self, s, t): #s is left subtree's root, t is right subtree's root
if s == None : #if left subtree is empty return t
#return right subtree as it is.
if t == None : #if right subtree is empty x = self . maximum ( s )
self . __splay ( x )
return x
x = self . maximum ( s )
self . __splay ( x )
#pointing right child of x to t x . right = t
#making parent of right child as x t . parent = x
return x
If left subtree is empty, then we will simply return right subtree as it is.
Else if right subtree is empty then we will find the latest predecessor of
our deleting node key by calling maximum function on left subtree which
returns the greatest element from the left subtree. Then perform splaying
operation on that element.
Else if both sub-trees are not empty, then find last predecessor of the node
to be deleted or find the maximum value node in the left subtree, then
splay it to bring that largest node to the top. Then link the root of right
subtree as the right child of left subtree's root.
To find the greatest element in a Binary search tree, we will use its
fundamental property that the right subtree of a node contains only nodes
with keys greater than the node’s key.
Using recursion, we will traverse towards the most rightward key of that
tree. We first check if right child of a node is empty or not. If it is not
empty, then traverse its right child again and again till our condition of
node.right != None returns False.
Similarly for minimum function visit the most left node of the tree as the
left subtree of a node contains only nodes with keys lesser than the node’s
key.
Pseudocode:
1. If left child of node is empty, return that node.
2. Else if left child exists then call the left child of that node till we reach
leaf node that is node.left == None.
Search Operation
Pseudocode
1. Check if node to be find is none or key is equal to root node, then
return that node as found.
2. If key to be found is less than node traverse left child recursively till
we find the target key and reach leaf node.
3. If key is greater than node then traverse its right child till it is found
and up to the leaf node.
Inorder Traversal
Inorder (Left, Root, Right) : 4 2 5 1 3
Pseudocode:
We are recursively traversing the left children of tree until we reach the
leaf node then explore its right sibling then its parent node and its right
sibling. Again, the parent node of current node is processed recursively.
Then, we reach at the root node. We will now explore to the deepest of
right subtree and print the leaf nodes values first then their parent
repeatedly.
y = x . parent
whil e y ! = Non e an d x == y . lef t :
x = y
y = y . parent
return y
With this, you have the complete knowledge of Splay Tree, a self-
balancing Binary Tree (just like AVL Tree, Red Black Tree and others)
but with an additional property.
Insight :
Splay Tree is important as it shows that we can impose multiple
restrictions like self-balancing and recently accessed near the top and
maintain the performance of simpler Binary Tree.
Think of what other features you want in your Self-balancing Binary
Tree.
Example 1
The root partitioning line is drawn along D, this splits the geometry in
two sets as described in the tree.
Example 2
The entire space is referred by the root node.
This is split by selecting a partition hyperplane.
These two sub-planes, referred to as front and back contain more nodes,
and hence shall be subdivided to get more sub-planes.
This process needs to be recursively repeated in every subspace created to
finally render the complete binary tree where each leaf node contain
distinct circles.
There we reach our Binary Space Partitioned Tree.
Time Complexity
You need to answer this question to get the time complexity.
How to bound the number of recursive calls?
Recursive calls give rise to new recursive calls (splitting), the expected
number is bounded by expected number of fragments. The time
complexity can be pretty fine to pretty catastrophic, depending on the
space being mapped.
The time consumed for building the tree, can be compromised for quicker
rendering of the tree.
Traversal
If the current node is a leaf node then:
render the polygons at the current node.
Else if the viewing location V is in front of the current node then: 1.
Render the child BSP tree containing polygons behind the current node
2. Render the polygons at the current node
3. Render the child BSP tree containing polygons in front of the current
node Else if the viewing location V is behind the current node then: 1.
Render the child BSP tree containing polygons in front of the current
node
2. Render the polygons at the current node
3. Render the child BSP tree containing polygons behind the current node
Else if the viewing location V must be exactly on the plane associated
with the current node, then: 1. Render the child BSP tree containing
polygons in front of the current node 2. Render the child BSP tree
containing polygons behind the current node Time Complexity
A BSP Tree is traversed in linear time that is O(N) and renders the
polygon in a far to near ordering, suitable for the painter's algorithm.
This ensures fast rendering that is the primary motive behind using
Binary Space Partitioning trees in real life situations even though
generation might be costlier.
C++ Implementation
This is based on the assumption that Object exposes its position, and the
Node class is responsible for, and can write on object's position.
In addition, Objects are added in the first node having room to hold it. In
case all nodes are full, we create two empty children (if not done yet),
each one representing half part of the parent node.
To remove an object from the tree, we have to find it. We will search it in
each node recursively, using its position to take the right branch at each
step. When found, we just remove it and return.
Retrieving an object from the tree is really fast when it comes to BSP
Trees and the best part is, the more the depth of the tree, greater will be
the efficiency in terms of data retrieval. All we have to do is to test all
object in all the nodes in the interval. If a node is not in the interval, we
can dismiss the entire branch in our search Following is the sample
implementation in C++ to give you the complete idea:
class Object {
int pos ;
public: int position() const ;
int & position();
};
class Node {
static const unsigned int depth_max = 32 ;
static const unsigned int max_objects = 32 ;
// Get all object in requisite range void getObject( int posMin, int
posMax, std::list < Object *> & );
};
// Public constructor, to create root node Node ::Node( int min , int max
)
: depth( 0 ), min( min ), max( max ), center(( min + max ) / 2 ), objects(),
children(nullptr)
{}
Node :: ~ Node()
{
delete[] children ;
}
// remove from the old partition and add to the new one else if (
newPos <= center && obj -> position() > center )
{
children [ 1 ] -> delObject( obj );
children [ 0 ] -> addObject( newPos , obj );
}
else if ( newPos > center && obj -> position() <= center )
{
children [ 0 ] -> delObject( obj );
children [ 1 ] -> addObject( newPos , obj );
}
}
// object is now in the right place, so update it position obj ->
position() = newPos ;
}
void Node ::getObject( int posMin , int posMax , std::list < Object *> &
list)
{
// get all wanted objects in this node for ( auto it = objects .begin(); it !=
objects .end(); it ++ )
if ( it -> position() >= posMin && it -> position() <= posMax )
list.push_back( * it );
Applications
Since its inception, Binary Space Partition Trees have been found to be of
immense use in the following:
Computer Graphics
Back face Culling
Collision detection
Ray Tracing
Game engines using BSP trees include the Doom (id Tech 1),
Quake (id Tech 2 variant), GoldSrc and Source engines.
Do not forget it brought you the game that revolutionized video games!
By now you must have understood the significance, utility, concept, and
application of Binary Space Partition Trees.
Insight :
Based on our insights, you may be driven to the thought that Binary Tree
are used to represent a 2-dimensional space. The main idea of a Binary
Tree is to represent an N-dimensional space by considering an attribute
(or a set) that results in 2 options at each step.
Binary Heap
Heap is a binary tree with two special properties:
A min heap is a heap where every single parent node, including the root,
is less than or equal to the value of its children nodes.
The most important property of a min heap is that the node with the
smallest, or minimum value, will always be the root node.
Max Heap
A max heap is effectively the converse of a min heap; in this format,
every parent node, including the root, is greater than or equal to the value
of its children nodes.
The important property of a max heap is that the node with the largest, or
maximum value will always be at the root node.
Implementation
Bubble-up Operation
Steps:
Sink-Down Operation
Steps:
Take out the element from the root.( it will be minimum in case
of Min-Heap and maximum in case of Max-Heap).
Take out the last element from the last level from the heap and
replace the root with the element.
Perform Sink-Down.
All delete operation must perform Sink-Down Operation ( also known as
bubble-down, percolate-down, sift-down, trickle down, heapify-down,
cascade-down).
Pseudocode:
HEAP-EXTRACT-MIN ( A )
if heap-size[A] < 1
then error ‘‘ heap underflow ’’
min < - A[1]
A[1] < - A[heap-size[A] ]
heap-size[A] < - heap-size[A] - 1
MIN-HEAPIFY ( A ,1)
return min
Delete Operation
Steps:
Applications
The heap data structure has many applications:
Insight :
Binary Heap is important when compared to other variants of Binary Tree
because this illustrates how we can represent a Binary Tree using an
array.
Hence, array and linked list are two fundamental data structures and
Binary Tree builds over it by removing the linear nature and bringing in
the branching nature.
Treap
A Treap is a height balanced binary tree.
It is used to store a sequence in a tree, which allows for various
applications like searching. A Cartesian tree (a Binary Search Tree
variant) in case of a sorted sequence, would be basically a linked list,
making tree virtually useless. (Think of this with BST )
Treap is used to solve such cases by using random priority for each node.
Thus, treap is a balanced binary tree with heap properties.
struct Node
{
int key , priority ;
Node * left , * right ;
Node( int x )
{
key = x ;
priority = rand() % 10000 ;
left = right = NULL ;
}
};
Tree rotation operation:
In the above image, A, B, C are subtrees, while P and Q are two nodes
that are target of rotation. The left image shows right rotation of node P
while image on right shows right rotation of node Q. These two
operations are inverse of each other.
Following are the implementations of the left rotate and right rotate
operations:
Node * rotateLeft(Node * p)
{
Node * q = p -> right ;
return q ;
}
Node * rotateRight(Node * q)
{
Node * p = q -> left ;
return p ;
}
Search
Insert
Delete
Inorder traversal of a treap would give the sorted sequence.
Algorithm
Insert operation
Insert operation is done for the whole sequence to build the tree.
1. Create new node with the given key value x and random priority value.
2. Start from root and perform search for x by using key values to
perform a binary search. When a leaf node is reached, insert new node at
the leaf. This is basically a binary search tree insertion.
3. Rotate up to make sure heap property is satisfied with respect to
priority values.
The image above shows insert operation for key 15. Green values in
nodes are keys and orange values are priorities. Priority of 8 is assigned
to node with key 15 randomly.
Following is the implementation of Insert operation:
}
}
else {
// Run insert for left sub-tree sub_tree -> left = insert( sub_tree -> left ,
key );
Search operation
Search operation is same as search in binary search tree since rotation
maintains BST property.
1. Start from root to search for a key x.
2. Compare key of the node. If key is equal to x, return the node.
3. If key is less than x go to right child and repeat step 2.
4. If key is greater than x go to left child and repeat step 2.
5. If key is not equal to x and node has no children, then x is not present
in tree.
Delete operation
1. Search for the node to be deleted.
2. If the node is at leaf, delete it directly.
3. Otherwise, set the node to lowest priority and perform rotations until
heap property of tree is satisfied.
4. Delete the node when it is at leaf.
For step 3, if tree is a max heap, set node's priority to negative infinity
and if tree is a min heap, set priority to infinity.
Applications
Insight :
Treap is an important variant of Binary Tree as it illustrates how
randomness can be incorporated in a Data Structure and still manage to
make it useful in an efficient way. In fact, several Data Structures that are
used in practice are probabilistic in nature.
Can you think of other ways that we can make Binary Tree work
randomly in a specific direction?
As excel sheet can be of a vast size with empty cells, using an array or
linked list will result in significant wastage of space. Additionally,
operations will depend on the total size supported by our structure.
Each cell is represented by x and y coordinates (x, y).
The idea is “Each cell will be a node of Binary Tree”.
If cell X is on the left side of cell Y, then X will be the left child node of
Y or X will be in the left sub-tree of Y.
Inserting a new node will require you to check the correct position in the
Binary Tree and will be similar to inserting element in Binary Search
Tree. The operation will depend on the total number of elements.
It is advised to use a self-balancing Binary Search Tree.
When a memory is requested, we will traverse our tree to find the node
which has the smallest memory available such that it satisfies the
requested memory.
On allocation, we will update the node and its attributes if some memory
of the chunk is left. After updating, we need to move it to its correct
position which should not take more than O(logM) time where M is the
number of chunks.
If no memory is left, the node is deleted, and its child nodes are adjusted
accordingly.
Problem 3: Cache
This may not be the best solution in general but for specific caches, this
approach may work better than other alternatives.
For this problem of designing a cache, there are other viable options like
Linked Lists, Priority Queue, Priority Heap and much more.
You shall consider all types and then, choose a specific approach for your
problem at hand.
As you must have noted, there are several other variants of Binary Tree
(as listed in the applications) which we have not covered as they are
advanced topics, but we have presented the core ideas in the problems we
have covered.
Hence, if you just read the basic idea of a particular variant, you can
easily figure out the details of different operations. Try this will
“Scapegoat Tree ”.
The general idea is: We will not keep doing small operations to keep the
Binary Tree self-balanced as with AA Tree and Splay Tree.
instead, if we notice that something goes wrong, then we will find the
node who is responsible for it (“scapegoat”) and completely, rebuild the
sub-tree.
Work on this idea independently.
With this, you have a strong idea of Binary Tree. You can confidently ace
every Problem related to Binary Tree at Coding Interviews of Top
Companies and swim through Hard Competitive Coding Problems.
As a next step, you may randomly pick a problem from this book, read
the problem statement and dive into designing your own solution and
implement it in a Programming Language of your choice.
You may need to revise the concepts present in this book again in two
months to strengthen your practice.
Remember, we are here to help you. If you have any doubts in a problem,
you can contact us by email (team@opengenus.org ) anytime.
Best of Luck.
If you want more practice, feel free to join our Internship Program:
internship.OPENGENUS.org
Aditya Chatterjee
Srishti Guleria
Ue Kiao
OPENGENUS