BST updates
Changing a BST: insertion, the three cases of deletion (leaf, one child, two children), the transplant helper and the intuition behind Hibbard deletion.
Learning goals
- Insert a key into the correct empty position
- Delete a node in each of the three structural cases
- Use transplant to replace one subtree by another
- Explain why copying the successor keeps the ordering rule intact
Prerequisites
Insertion is easy, because new keys always become leaves. Deletion is where the care goes: removing a node from the middle must keep every ordering relation intact.
Insert
To insert a key, search for it. If the search falls off the tree at an empty child, that is exactly where the key belongs: attach a new leaf. A new key always becomes a **leaf**, so no existing node moves and the ordering rule holds automatically. The cost is O(h).
In the “Insert” preset, inserting 35 into the 7-key tree compares it with 50, 30 and 40 and attaches it as the left child of 40; the inorder list [20, 30, 35, 40, 50, 60, 70, 80] stays sorted. The recursive version returns the (possibly new) subtree root so the parent can reattach it.
1static Node insert(Node n, int key) {2if (n == null) return new Node(key); // the empty spot: a new leaf3if (key < n.key) n.left = insert(n.left, key);4else if (key > n.key) n.right = insert(n.right, key); // an equal key is ignored5return n;6}
Best
O(1)
Average
O(log n)
Worst
O(n)
Space
O(h) recursion
Binary search tree: insert, search, delete and in-order traversal
Interactive · edit the inputAn empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger.
1class Bst {2private static class Node { int key; Node left, right; Node(int k) { key = k; } }3private Node root;4void insert(int key) { root = insert(root, key); }5private Node insert(Node n, int key) {6if (n == null) return new Node(key); // found the empty spot7if (key < n.key) n.left = insert(n.left, key);8else if (key > n.key) n.right = insert(n.right, key); // equal keys are ignored9return n;10}11boolean contains(int key) {12Node n = root;13while (n != null) {14if (key == n.key) return true;15n = key < n.key ? n.left : n.right; // discard one whole subtree16}17return false;18}19void delete(int key) { root = delete(root, key); }20private Node delete(Node n, int key) {21if (n == null) return null; // key is not in the tree22if (key < n.key) n.left = delete(n.left, key);23else if (key > n.key) n.right = delete(n.right, key);24else {25if (n.left == null) return n.right; // zero or one child: splice the node out26if (n.right == null) return n.left;27Node s = n.right; while (s.left != null) s = s.left; // two children: in-order successor28n.key = s.key; // copy its key up29n.right = delete(n.right, s.key); // then remove the successor30}31return n;32}33void inorder(Node n, List<Integer> out) {34if (n == null) return;35inorder(n.left, out); out.add(n.key); inorder(n.right, out); // left, node, right: sorted order36}37}
Live state
size
0
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 | An empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger. |
| 2 | L6 | inserted=50 size=1 | Insert 50: the tree is empty, so it becomes the root. |
| 3 | L7 | inserted=30 size=2 | Insert 30: compared with 50, it ends as the left child of 50. |
| 4 | L8 | inserted=70 size=3 | Insert 70: compared with 50, it ends as the right child of 50. |
| 5 | L7 | inserted=20 size=4 | Insert 20: compared with 50 → 30, it ends as the left child of 30. |
| 6 | L8 | inserted=40 size=5 | Insert 40: compared with 50 → 30, it ends as the right child of 30. |
| 7 | L7 | inserted=60 size=6 | Insert 60: compared with 50 → 70, it ends as the left child of 70. |
| 8 | L8 | inserted=80 size=7 | Insert 80: compared with 50 → 70, it ends as the right child of 70. |
| 9 | L12 | key=60 size=7 | Search for 60: start at the root. |
| 10 | L14 | key=60 node=50 steps=1 | 60 > 50: go right; the whole left subtree is ruled out. |
| 11 | L14 | key=60 node=70 steps=2 | 60 < 70: go left; the whole right subtree is ruled out. |
| 12 | L14 | key=60 node=60 steps=3 | 60 == 60: found after 3 comparisons. |
| 13 | L14 | return=true steps=3 | contains(60) = true in 3 steps; the tree has 7 keys, so most of them were never looked at. |
Delete leaf
Deleting a **leaf** is the easy case: cut the link from its parent. Nothing else in the tree depends on it. In the “Delete a leaf” preset, removing 20 leaves the inorder sequence [30, 40, 50, 60, 70, 80], which is still sorted.
1static void removeLeaf(Node parent, Node leaf) { // a leaf: just cut the link2if (parent.left == leaf) parent.left = null;3else parent.right = null;4}
Binary search tree: insert, search, delete and in-order traversal
Interactive · edit the inputAn empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger.
1class Bst {2private static class Node { int key; Node left, right; Node(int k) { key = k; } }3private Node root;4void insert(int key) { root = insert(root, key); }5private Node insert(Node n, int key) {6if (n == null) return new Node(key); // found the empty spot7if (key < n.key) n.left = insert(n.left, key);8else if (key > n.key) n.right = insert(n.right, key); // equal keys are ignored9return n;10}11boolean contains(int key) {12Node n = root;13while (n != null) {14if (key == n.key) return true;15n = key < n.key ? n.left : n.right; // discard one whole subtree16}17return false;18}19void delete(int key) { root = delete(root, key); }20private Node delete(Node n, int key) {21if (n == null) return null; // key is not in the tree22if (key < n.key) n.left = delete(n.left, key);23else if (key > n.key) n.right = delete(n.right, key);24else {25if (n.left == null) return n.right; // zero or one child: splice the node out26if (n.right == null) return n.left;27Node s = n.right; while (s.left != null) s = s.left; // two children: in-order successor28n.key = s.key; // copy its key up29n.right = delete(n.right, s.key); // then remove the successor30}31return n;32}33void inorder(Node n, List<Integer> out) {34if (n == null) return;35inorder(n.left, out); out.add(n.key); inorder(n.right, out); // left, node, right: sorted order36}37}
Live state
size
0
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 | An empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger. |
| 2 | L6 | inserted=50 size=1 | Insert 50: the tree is empty, so it becomes the root. |
| 3 | L7 | inserted=30 size=2 | Insert 30: compared with 50, it ends as the left child of 50. |
| 4 | L8 | inserted=70 size=3 | Insert 70: compared with 50, it ends as the right child of 50. |
| 5 | L7 | inserted=20 size=4 | Insert 20: compared with 50 → 30, it ends as the left child of 30. |
| 6 | L8 | inserted=40 size=5 | Insert 40: compared with 50 → 30, it ends as the right child of 30. |
| 7 | L7 | inserted=60 size=6 | Insert 60: compared with 50 → 70, it ends as the left child of 70. |
| 8 | L8 | inserted=80 size=7 | Insert 80: compared with 50 → 70, it ends as the right child of 70. |
| 9 | L12 | key=60 size=7 | Search for 60: start at the root. |
| 10 | L14 | key=60 node=50 steps=1 | 60 > 50: go right; the whole left subtree is ruled out. |
| 11 | L14 | key=60 node=70 steps=2 | 60 < 70: go left; the whole right subtree is ruled out. |
| 12 | L14 | key=60 node=60 steps=3 | 60 == 60: found after 3 comparisons. |
| 13 | L14 | return=true steps=3 | contains(60) = true in 3 steps; the tree has 7 keys, so most of them were never looked at. |
Delete one-child
A node with **one child** is bypassed: its parent adopts the child, in the place where the node used to hang. The child's whole subtree moves up one level and the ordering still holds, because the subtree sat on the correct side of the removed node's ancestors already.
The “Delete, one child” preset removes 60, which has only the right child 65: the node 70 now has 65 as its left child, and the inorder sequence is [20, 30, 50, 65, 70, 80].
1static void bypass(Node parent, Node node) { // node has exactly one child: the parent adopts it2Node child = node.left != null ? node.left : node.right;3if (parent.left == node) parent.left = child;4else parent.right = child;5}
Binary search tree: insert, search, delete and in-order traversal
Interactive · edit the inputAn empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger.
1class Bst {2private static class Node { int key; Node left, right; Node(int k) { key = k; } }3private Node root;4void insert(int key) { root = insert(root, key); }5private Node insert(Node n, int key) {6if (n == null) return new Node(key); // found the empty spot7if (key < n.key) n.left = insert(n.left, key);8else if (key > n.key) n.right = insert(n.right, key); // equal keys are ignored9return n;10}11boolean contains(int key) {12Node n = root;13while (n != null) {14if (key == n.key) return true;15n = key < n.key ? n.left : n.right; // discard one whole subtree16}17return false;18}19void delete(int key) { root = delete(root, key); }20private Node delete(Node n, int key) {21if (n == null) return null; // key is not in the tree22if (key < n.key) n.left = delete(n.left, key);23else if (key > n.key) n.right = delete(n.right, key);24else {25if (n.left == null) return n.right; // zero or one child: splice the node out26if (n.right == null) return n.left;27Node s = n.right; while (s.left != null) s = s.left; // two children: in-order successor28n.key = s.key; // copy its key up29n.right = delete(n.right, s.key); // then remove the successor30}31return n;32}33void inorder(Node n, List<Integer> out) {34if (n == null) return;35inorder(n.left, out); out.add(n.key); inorder(n.right, out); // left, node, right: sorted order36}37}
Live state
size
0
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 | An empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger. |
| 2 | L6 | inserted=50 size=1 | Insert 50: the tree is empty, so it becomes the root. |
| 3 | L7 | inserted=30 size=2 | Insert 30: compared with 50, it ends as the left child of 50. |
| 4 | L8 | inserted=70 size=3 | Insert 70: compared with 50, it ends as the right child of 50. |
| 5 | L7 | inserted=20 size=4 | Insert 20: compared with 50 → 30, it ends as the left child of 30. |
| 6 | L8 | inserted=40 size=5 | Insert 40: compared with 50 → 30, it ends as the right child of 30. |
| 7 | L7 | inserted=60 size=6 | Insert 60: compared with 50 → 70, it ends as the left child of 70. |
| 8 | L8 | inserted=80 size=7 | Insert 80: compared with 50 → 70, it ends as the right child of 70. |
| 9 | L12 | key=60 size=7 | Search for 60: start at the root. |
| 10 | L14 | key=60 node=50 steps=1 | 60 > 50: go right; the whole left subtree is ruled out. |
| 11 | L14 | key=60 node=70 steps=2 | 60 < 70: go left; the whole right subtree is ruled out. |
| 12 | L14 | key=60 node=60 steps=3 | 60 == 60: found after 3 comparisons. |
| 13 | L14 | return=true steps=3 | contains(60) = true in 3 steps; the tree has 7 keys, so most of them were never looked at. |
Delete two-child
A node with **two children** cannot simply be cut out. Instead, replace its key by the key of its **inorder successor** (the minimum of its right subtree), then delete that successor node from the right subtree. The successor has no left child by definition, so removing it falls into the easy one-child or leaf case. The new key is larger than everything on the left and smaller than everything on the right, so the ordering holds.
In the “Delete, two children” preset, removing 30 copies the successor 40 into its place and then removes the old node 40; the inorder sequence becomes [20, 40, 50, 60, 70, 80]. Using the predecessor (the maximum of the left subtree) works just as well.
1static Node delete(Node n, int key) {2if (n == null) return null; // the key is not in the tree3if (key < n.key) n.left = delete(n.left, key);4else if (key > n.key) n.right = delete(n.right, key);5else {6if (n.left == null) return n.right; // zero or one child: splice the node out7if (n.right == null) return n.left;8Node s = n.right;9while (s.left != null) s = s.left; // two children: the inorder successor10n.key = s.key; // copy its key up11n.right = delete(n.right, s.key); // then remove the successor node12}13return n;14}
Best
O(1)
Average
O(log n)
Worst
O(n)
Space
O(h) recursion
Binary search tree: insert, search, delete and in-order traversal
Interactive · edit the inputAn empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger.
1class Bst {2private static class Node { int key; Node left, right; Node(int k) { key = k; } }3private Node root;4void insert(int key) { root = insert(root, key); }5private Node insert(Node n, int key) {6if (n == null) return new Node(key); // found the empty spot7if (key < n.key) n.left = insert(n.left, key);8else if (key > n.key) n.right = insert(n.right, key); // equal keys are ignored9return n;10}11boolean contains(int key) {12Node n = root;13while (n != null) {14if (key == n.key) return true;15n = key < n.key ? n.left : n.right; // discard one whole subtree16}17return false;18}19void delete(int key) { root = delete(root, key); }20private Node delete(Node n, int key) {21if (n == null) return null; // key is not in the tree22if (key < n.key) n.left = delete(n.left, key);23else if (key > n.key) n.right = delete(n.right, key);24else {25if (n.left == null) return n.right; // zero or one child: splice the node out26if (n.right == null) return n.left;27Node s = n.right; while (s.left != null) s = s.left; // two children: in-order successor28n.key = s.key; // copy its key up29n.right = delete(n.right, s.key); // then remove the successor30}31return n;32}33void inorder(Node n, List<Integer> out) {34if (n == null) return;35inorder(n.left, out); out.add(n.key); inorder(n.right, out); // left, node, right: sorted order36}37}
Live state
size
0
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 | An empty tree. Rule: every key in a node's left subtree is smaller, every key in its right subtree is larger. |
| 2 | L6 | inserted=50 size=1 | Insert 50: the tree is empty, so it becomes the root. |
| 3 | L7 | inserted=30 size=2 | Insert 30: compared with 50, it ends as the left child of 50. |
| 4 | L8 | inserted=70 size=3 | Insert 70: compared with 50, it ends as the right child of 50. |
| 5 | L7 | inserted=20 size=4 | Insert 20: compared with 50 → 30, it ends as the left child of 30. |
| 6 | L8 | inserted=40 size=5 | Insert 40: compared with 50 → 30, it ends as the right child of 30. |
| 7 | L7 | inserted=60 size=6 | Insert 60: compared with 50 → 70, it ends as the left child of 70. |
| 8 | L8 | inserted=80 size=7 | Insert 80: compared with 50 → 70, it ends as the right child of 70. |
| 9 | L12 | key=60 size=7 | Search for 60: start at the root. |
| 10 | L14 | key=60 node=50 steps=1 | 60 > 50: go right; the whole left subtree is ruled out. |
| 11 | L14 | key=60 node=70 steps=2 | 60 < 70: go left; the whole right subtree is ruled out. |
| 12 | L14 | key=60 node=60 steps=3 | 60 == 60: found after 3 comparisons. |
| 13 | L14 | return=true steps=3 | contains(60) = true in 3 steps; the tree has 7 keys, so most of them were never looked at. |
Transplant
**Transplant** is the helper that makes the cases uniform: replace the subtree rooted at u by the subtree rooted at v (which may be null) in u's parent. It only fixes the parent's link, and the child's parent pointer. Deletion then reads as: no left child, transplant the right child; no right child, transplant the left; otherwise find the successor y, move it up (first transplanting its own right child into its old place if it is not z's direct child) and give it z's left subtree.
This version moves the successor **node** instead of copying keys, which matters when other code keeps references to nodes (iterators, caches): the node being deleted is the one that disappears. It needs parent pointers in the nodes.
1static class Node {2int key;3Node left, right, parent;4Node(int key) { this.key = key; }5}6static Node root;78static void transplant(Node u, Node v) { // replace the subtree rooted at u by the one rooted at v9if (u.parent == null) root = v;10else if (u == u.parent.left) u.parent.left = v;11else u.parent.right = v;12if (v != null) v.parent = u.parent;13}1415static void delete(Node z) {16if (z.left == null) transplant(z, z.right);17else if (z.right == null) transplant(z, z.left);18else {19Node y = z.right;20while (y.left != null) y = y.left; // the successor21if (y.parent != z) {22transplant(y, y.right); // y leaves its old place23y.right = z.right;24y.right.parent = y;25}26transplant(z, y); // y takes z's place27y.left = z.left;28y.left.parent = y;29}30}
Best
O(1)
Average
O(log n)
Worst
O(n)
Space
O(1)
Hibbard deletion intuition
The method above is **Hibbard deletion** (1962): a two-child node is replaced by its successor. Why does it keep the invariant? Every key to the left of the node is smaller than the successor (the successor is bigger than the node, which is bigger than the left subtree) and every key to its right, other than the successor itself, is bigger (the successor is the *smallest* of that right subtree). The replaced position is therefore a valid root for both subtrees.
The method is correct but asymmetric: it always draws from the right side. It is known from the literature that after a long sequence of random insertions and deletions done this way, trees tend to become taller than the logarithmic height a random tree has. Alternating between successor and predecessor, or using balanced trees (the next chapter), avoids the drift.
Practice ladder
Now try it yourself
Easy
- Insert into a Binary Search Tree
- Trim a Binary Search Tree
Medium
- Delete Node in a BST
- Convert BST to Greater Tree
Advanced
- Delete Nodes And Return Forest
- Balance a Binary Search Tree
Key takeaways
- A new key always becomes a leaf, found by searching until the tree runs out.
- Deleting a leaf cuts a link; a node with one child is bypassed by its parent.
- A node with two children takes the key of its inorder successor, which is then deleted from the right subtree.
- Transplant replaces one subtree by another and unifies the deletion cases.
- Always drawing from one side can distort the tree over time; balanced trees avoid it.