-
Each node must have at most two children. Usually referred to as "left" and "right."
-
All trees must have a "root" node.
-
The order of nodes values must be:
left child < parent < right child
. -
Nodes might need re-ordering after each insert/delete operation to keep the
left ⇐ parent < right
constraint.
The first step is to implement the Binary Tree Node, which can hold 0, 1, or 2 children.
link:../../../src/data-structures/trees/binary-tree-node.js[role=include]
}
}
Does this look familiar to you? It’s almost like the linked list node, but instead of having next
and previous
, it has left
and right
. That guarantees that we have at most two children.
We also added the meta
object to hold some metadata about the node, like duplicity, color (for red-black trees), or any other data needed for future algorithms.
We implemented the node; now, let’s layout other methods that we can implement for a BST:
link:../../../src/data-structures/trees/binary-search-tree.js[role=include]
add(value) { /* ... */ }
find(value) { /* ... */ }
remove(value) { /* ... */ }
getMax() { /* ... */ }
getMin() { /* ... */ }
}
With the methods add
and remove
, we have to guarantee that our tree always has one root element from where we can navigate left or right based on the value we are looking for. Let’s implement those add
method first:
-
If the tree is empty (root element is null), we add the newly created node as root, and that’s it!
-
If the tree has a root, compare the new value with the root. Then we have three possibilities:
-
root == newValue
: we increase the duplicity counter in that case, and done! -
root > newValue
, we search on the left side of the root. -
root < newValue
, we search on the right side of the root.
-
-
Repeat the comparison between the current node and
newValue
, until we find the value or (null) space.
For instance, let’s say that we want to insert the values 19, 21, 10, 2, 18 in a BST:
In the last box of the image above, we start by the root when we insert node 18 (19). Since 18 is less than 19, then we move left. Node 18 is greater than 10, so we move right. There’s an empty spot, and we place it there. Let’s code it up:
link:../../../src/data-structures/trees/binary-search-tree.js[role=include]
-
We are using a helper function
findNodeAndParent
to iterate through the tree, finding a node with the current value “found” and its parent (implementation on the next section). -
We are taking care of duplicates. Instead of inserting duplicates, we are keeping a multiplicity tally. We have to decrease it when removing nodes.
We can implement the find method using the helper findNodeAndParent
as follows:
link:../../../src/data-structures/trees/binary-search-tree.js[role=include]
findNodeAndParent
is a recursive function that goes to the left or right child, depending on the value. However, if the value already exists, it will return it in found
variable.
Deleting a node from a BST has three cases.
-
leaf
-
parent with one child
-
parent with two children/root.
Deleting a leaf is the easiest; we look for their parent and set the child to null.
Node 18, will be hanging around until the garbage collector is run. However, there’s no node referencing to it to no longer be reachable from the tree.
Removing a parent is not as easy since you need to find new parents for its children.
In the example, we removed node 10
from the tree, so its child (node 2) needs a new parent. We made node 19 the new parent for node 2.
Removing a parent of two children is the trickiest of all cases because we need to find new parents. (This sentence might sound tragic out of context 😂)
In the example, we delete the root node 19. This deletion leaves two orphans (node 10 and node 21). There are no more parents because node 19 was the root element. One way to solve this problem is to combine the left subtree (Node 10 and descendants) into the right subtree (node 21). The final result is node 21 is the new root.
What would happen if node 21 had a left child (e.g., node 20)? Well, we would move node 10 and its descendants' bellow node 20.
All the described scenarios removing nodes with zero, one and two children can be sum up on this code:
link:../../../src/data-structures/trees/binary-search-tree.js[role=include]
-
Try to find if the value exists on the tree.
-
If the value doesn’t exist, we are done!
-
Create a new subtree without the value to delete
-
Check the multiplicity (duplicates) and decrement the count if we have multiple nodes with the same value
-
If the
nodeToRemove
was the root, we wouldwould move the removed node’s children as the new root. -
If it was not the root, we will go to the deleted node’s parent and put their children there.
We compute removedNodeChildren
, which is the resulting subtree after combining the deleted node children.
The method to combine subtrees is the following:
link:../../../src/data-structures/trees/binary-search-tree.js[role=include]
Take a look at the code above and the example. You will see how to remove node 30
and combine both children’s subtree, and keeping the BST rules. Also, this method uses a helper to get the left-most node. We can implement it like this:
link:../../../src/data-structures/trees/binary-search-tree.js[role=include]
That’s all we need to remove elements from a BST. Check out the complete BST implementation here.
As we insert and remove nodes from a BST, we could end up like the tree on the left:
The tree on the left is unbalanced. It looks like a Linked List and has the same runtime! Searching for an element would be O(n), yikes! However, on a balanced tree, the search time is O(log n), which is pretty good! That’s why we always want to keep the tree balanced. In further chapters, we will explore how to keep a tree balanced after each insert/delete.