BINARY SEARCH TREE › BASIC OPERATIONS
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →The BST ordering tells you where the key can be: smaller keys are in the left subtree and larger keys are in the right subtree. Follow that rule until you find the node or reach a null pointer. Every recursive call returns the root of its subtree after the deletion, so the parent can reconnect its left or right link to that returned root.
Once the target is found, its children determine the repair. A leaf returns null. A node with one child returns that child, allowing the parent to bypass the deleted node. With two children, neither child can simply replace the node without disturbing ordering. Copy the smallest value from the right subtree into the target, then delete that successor from its original position; the successor has no left child, so the difficult case becomes a one-child or leaf case.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(h) | The search descends one path. In the two-child case, finding the successor and deleting it together follows at most the same root-to-leaf depth, so no subtree is scanned more than once along the operation. |
| Space | O(h) extra | The returned root pointer is required output and is not working storage. The recursive calls keep one frame per level on the search and successor-deletion paths, giving O(h) extra space; a degenerate tree makes this O(n), while a balanced tree makes it O(log n). |
#include <vector>
using namespace std;
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) {
return nullptr;
}
if (key < root->val) {
root->left = deleteNode(root->left, key);
} else if (key > root->val) {
root->right = deleteNode(root->right, key);
} else {
if (root->left == nullptr) {
TreeNode* child = root->right;
delete root;
return child;
}
if (root->right == nullptr) {
TreeNode* child = root->left;
delete root;
return child;
}
TreeNode* successor = root->right;
while (successor->left != nullptr) {
successor = successor->left;
}
root->val = successor->val;
root->right = deleteNode(root->right, successor->val);
}
return root;
}
};The key placement is the assignment after each recursive call. Deleting a node can change the root pointer of that subtree: a leaf becomes null, or a one-child node is replaced by its child. Writing root->left = or root->right = stores that changed pointer in the parent. The two-child case deliberately copies only the value, then lets the same method remove the original successor node.
You can perform the same operation with parent and current pointers instead of recursive calls. This is an optimisation for extra space in a deep tree: the search and successor walk still take O(h) time, but the algorithm uses O(1) auxiliary space. The tradeoff is more pointer bookkeeping, especially when the node being removed is the root.
#include <vector>
using namespace std;
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
TreeNode* parent = nullptr;
TreeNode* current = root;
while (current != nullptr && current->val != key) {
parent = current;
if (key < current->val) {
current = current->left;
} else {
current = current->right;
}
}
if (current == nullptr) {
return root;
}
if (current->left != nullptr && current->right != nullptr) {
TreeNode* successorParent = current;
TreeNode* successor = current->right;
while (successor->left != nullptr) {
successorParent = successor;
successor = successor->left;
}
current->val = successor->val;
parent = successorParent;
current = successor;
}
TreeNode* child = current->left != nullptr
? current->left
: current->right;
if (parent == nullptr) {
root = child;
} else if (parent->left == current) {
parent->left = child;
} else {
parent->right = child;
}
delete current;
return root;
}
};