BST validation
Checking and repairing BSTs: validation with bounds, validation by inorder order, recovering two swapped nodes and building a BST from a preorder sequence.
Learning goals
- Validate a BST with bounds that tighten as the recursion descends
- Validate with a single inorder pass
- Locate and fix two swapped keys in one traversal
- Rebuild a BST from its preorder sequence in linear time
Prerequisites
Checking only that each node is bigger than its left child and smaller than its right child is the classic wrong answer. The animations show why, and then give two correct ways.
Validate BST with bounds
Pass down the allowed open range (lo, hi) for each subtree: the root may be anything; going left, the node's key becomes the new upper bound; going right, it becomes the new lower bound. A node is valid only when lo < key < hi, and both subtrees must be valid with their own ranges.
The “Only parents checked: invalid” preset (tree “5 1 4 # # 3 6”) is the standard trap: each node is correctly ordered relative to its parent, but 3 sits in the right subtree of 5 and is smaller than 5. The range for 3 is (5, 4), so the animation flags it. The “Duplicate key: invalid” preset (2 with two children 2) fails because the strict rule demands distinct keys; use a different inequality if your policy allows duplicates on one side.
1static boolean isValid(TreeNode n, long lo, long hi) { // every key must lie strictly between lo and hi2if (n == null) return true;3if (n.val <= lo || n.val >= hi) return false;4return isValid(n.left, lo, n.val) && isValid(n.right, n.val, hi);5}6// call: isValid(root, Long.MIN_VALUE, Long.MAX_VALUE) (long bounds, so Integer.MIN_VALUE keys work)
Best
O(1)
Average
O(n)
Worst
O(n)
Space
O(h)
Validating a BST with bounds or inorder order, and recovering swapped nodes
Interactive · edit the inputbinary tree · 5 nodes
A BST needs more than 'left < parent < right' at every node: every key in a subtree must stay inside the open range set by all its ancestors. Each node passes its allowed range (lo, hi) down.
1static boolean isValid(TreeNode n, long lo, long hi) { // every key must lie strictly between lo and hi2if (n == null) return true;3if (n.val <= lo || n.val >= hi) return false;4return isValid(n.left, lo, n.val) && isValid(n.right, n.val, hi);5}6static boolean isValidInorder(TreeNode root) {7long prev = Long.MIN_VALUE;8Deque<TreeNode> stack = new ArrayDeque<>();9for (TreeNode n = root; n != null || !stack.isEmpty(); ) {10while (n != null) { stack.push(n); n = n.left; }11n = stack.pop();12if (n.val <= prev) return false; // not strictly increasing13prev = n.val;14n = n.right;15}16return true;17}18static void recover(TreeNode root) { // two nodes were swapped: find them in one inorder pass19TreeNode first = null, second = null, prev = null;20Deque<TreeNode> stack = new ArrayDeque<>();21for (TreeNode n = root; n != null || !stack.isEmpty(); ) {22while (n != null) { stack.push(n); n = n.left; }23n = stack.pop();24if (prev != null && prev.val > n.val) { // a descent25if (first == null) first = prev; // the first descent: the larger key is out of place26second = n; // the last descent: the smaller key is out of place27}28prev = n;29n = n.right;30}31int t = first.val; first.val = second.val; second.val = t;32}
Live state
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | A BST needs more than 'left < parent < right' at every node: every key in a subtree must stay inside the open range set by all its ancestors. Each node passes its allowed range (lo, hi) down. | |
| 2 | L3 | node=5 lo=-inf hi=+inf | 5 lies strictly between -inf and +inf: allowed here. |
| 3 | L3 | node=1 lo=-inf hi=5 | 1 lies strictly between -inf and 5: allowed here. |
| 4 | L3 | node=4 lo=5 hi=+inf | 4 is not strictly between 5 and +inf: it breaks the ordering rule imposed by an ancestor, so the tree is not a BST. |
| 5 | L3 | result=false | false. |
Inorder sortedness
The inorder traversal of a BST is strictly increasing, and the converse holds for validity too: a binary tree is a BST exactly when its inorder sequence is strictly increasing. So validate with one inorder pass that remembers the previous key and fails on current <= previous. The iterative version stops at the first violation without finishing the traversal.
In the animation's “Inorder check, invalid” preset the traversal stops as soon as it sees a key that does not exceed its predecessor. Both validation methods are O(n) in time and O(h) in space; the bounds version carries more state per call, and the inorder version carries one number.
1static boolean isValidInorder(TreeNode root) {2long prev = Long.MIN_VALUE;3Deque<TreeNode> stack = new ArrayDeque<>();4for (TreeNode n = root; n != null || !stack.isEmpty(); ) {5while (n != null) { stack.push(n); n = n.left; }6n = stack.pop();7if (n.val <= prev) return false; // not strictly increasing8prev = n.val;9n = n.right;10}11return true;12}
Best
O(1)
Average
O(n)
Worst
O(n)
Space
O(h)
Validating a BST with bounds or inorder order, and recovering swapped nodes
Interactive · edit the inputbinary tree · 5 nodes
A BST needs more than 'left < parent < right' at every node: every key in a subtree must stay inside the open range set by all its ancestors. Each node passes its allowed range (lo, hi) down.
1static boolean isValid(TreeNode n, long lo, long hi) { // every key must lie strictly between lo and hi2if (n == null) return true;3if (n.val <= lo || n.val >= hi) return false;4return isValid(n.left, lo, n.val) && isValid(n.right, n.val, hi);5}6static boolean isValidInorder(TreeNode root) {7long prev = Long.MIN_VALUE;8Deque<TreeNode> stack = new ArrayDeque<>();9for (TreeNode n = root; n != null || !stack.isEmpty(); ) {10while (n != null) { stack.push(n); n = n.left; }11n = stack.pop();12if (n.val <= prev) return false; // not strictly increasing13prev = n.val;14n = n.right;15}16return true;17}18static void recover(TreeNode root) { // two nodes were swapped: find them in one inorder pass19TreeNode first = null, second = null, prev = null;20Deque<TreeNode> stack = new ArrayDeque<>();21for (TreeNode n = root; n != null || !stack.isEmpty(); ) {22while (n != null) { stack.push(n); n = n.left; }23n = stack.pop();24if (prev != null && prev.val > n.val) { // a descent25if (first == null) first = prev; // the first descent: the larger key is out of place26second = n; // the last descent: the smaller key is out of place27}28prev = n;29n = n.right;30}31int t = first.val; first.val = second.val; second.val = t;32}
Live state
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | A BST needs more than 'left < parent < right' at every node: every key in a subtree must stay inside the open range set by all its ancestors. Each node passes its allowed range (lo, hi) down. | |
| 2 | L3 | node=5 lo=-inf hi=+inf | 5 lies strictly between -inf and +inf: allowed here. |
| 3 | L3 | node=1 lo=-inf hi=5 | 1 lies strictly between -inf and 5: allowed here. |
| 4 | L3 | node=4 lo=5 hi=+inf | 4 is not strictly between 5 and +inf: it breaks the ordering rule imposed by an ancestor, so the tree is not a BST. |
| 5 | L3 | result=false | false. |
Recover swapped nodes
Two nodes of a valid BST had their keys swapped by mistake. In the inorder sequence the damage shows as **descents**, places where a key is followed by a smaller one. If the swapped keys were adjacent in the sequence there is exactly one descent; otherwise there are two. Take the larger key of the first descent and the smaller key of the last descent, and swap their values back.
The animation's “Recover an adjacent swap (one descent)” preset (tree “3 1 4 # # 2”) has the single descent 3 > 2, so it swaps 3 and 2. The “Recover a distant swap (two descents)” preset (“6 3 8 1 7 5 9”) has inorder 1, 3, 7, 6, 5, 8, 9 with descents 7 > 6 and 6 > 5, so the keys to swap back are the 7 from the first and the 5 from the second. The pass is O(n) time and O(h) space.
1static void recover(TreeNode root) {2TreeNode first = null, second = null, prev = null;3Deque<TreeNode> stack = new ArrayDeque<>();4for (TreeNode n = root; n != null || !stack.isEmpty(); ) {5while (n != null) { stack.push(n); n = n.left; }6n = stack.pop();7if (prev != null && prev.val > n.val) { // a descent8if (first == null) first = prev; // the first descent: the larger key is out of place9second = n; // the last descent: the smaller key is out of place10}11prev = n;12n = n.right;13}14int t = first.val; first.val = second.val; second.val = t;15}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Validating a BST with bounds or inorder order, and recovering swapped nodes
Interactive · edit the inputbinary tree · 5 nodes
A BST needs more than 'left < parent < right' at every node: every key in a subtree must stay inside the open range set by all its ancestors. Each node passes its allowed range (lo, hi) down.
1static boolean isValid(TreeNode n, long lo, long hi) { // every key must lie strictly between lo and hi2if (n == null) return true;3if (n.val <= lo || n.val >= hi) return false;4return isValid(n.left, lo, n.val) && isValid(n.right, n.val, hi);5}6static boolean isValidInorder(TreeNode root) {7long prev = Long.MIN_VALUE;8Deque<TreeNode> stack = new ArrayDeque<>();9for (TreeNode n = root; n != null || !stack.isEmpty(); ) {10while (n != null) { stack.push(n); n = n.left; }11n = stack.pop();12if (n.val <= prev) return false; // not strictly increasing13prev = n.val;14n = n.right;15}16return true;17}18static void recover(TreeNode root) { // two nodes were swapped: find them in one inorder pass19TreeNode first = null, second = null, prev = null;20Deque<TreeNode> stack = new ArrayDeque<>();21for (TreeNode n = root; n != null || !stack.isEmpty(); ) {22while (n != null) { stack.push(n); n = n.left; }23n = stack.pop();24if (prev != null && prev.val > n.val) { // a descent25if (first == null) first = prev; // the first descent: the larger key is out of place26second = n; // the last descent: the smaller key is out of place27}28prev = n;29n = n.right;30}31int t = first.val; first.val = second.val; second.val = t;32}
Live state
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | A BST needs more than 'left < parent < right' at every node: every key in a subtree must stay inside the open range set by all its ancestors. Each node passes its allowed range (lo, hi) down. | |
| 2 | L3 | node=5 lo=-inf hi=+inf | 5 lies strictly between -inf and +inf: allowed here. |
| 3 | L3 | node=1 lo=-inf hi=5 | 1 lies strictly between -inf and 5: allowed here. |
| 4 | L3 | node=4 lo=5 hi=+inf | 4 is not strictly between 5 and +inf: it breaks the ordering rule imposed by an ancestor, so the tree is not a BST. |
| 5 | L3 | result=false | false. |
BST from preorder
Any sequence of distinct keys is the preorder of exactly one BST, the one built by inserting them in that order, but inserting one by one costs O(n · h). In linear time: pass down an **upper bound**. The next key becomes a node if it is below the bound; its left subtree takes the following keys below the node's key; its right subtree takes the following keys below the inherited bound. A key above the bound ends the current subtree.
The “From a preorder sequence” preset builds the tree for 8, 5, 1, 7, 10, 12 in one pass over the list, consuming each key once. The animation shows the index into the sequence moving forward and the bound being passed down.
1static int idx;2static TreeNode fromPreorder(int[] pre) {3idx = 0;4return build(pre, Integer.MAX_VALUE);5}6static TreeNode build(int[] pre, int bound) { // keys of this subtree are below bound7if (idx == pre.length || pre[idx] > bound) return null;8TreeNode n = new TreeNode(pre[idx++]);9n.left = build(pre, n.val);10n.right = build(pre, bound);11return n;12}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(h)
Building a BST: balanced from sorted data, by insertion order, from preorder
Interactive · edit the inputsorted keys
Sorted keys can be turned into a balanced BST: make the middle key the root and recurse on each half. The two halves differ in size by at most one.
1static TreeNode sortedToBst(int[] a, int lo, int hi) { // a is sorted and distinct2if (lo > hi) return null;3int mid = (lo + hi) >>> 1; // the middle key becomes the root4TreeNode n = new TreeNode(a[mid]);5n.left = sortedToBst(a, lo, mid - 1);6n.right = sortedToBst(a, mid + 1, hi);7return n;8}9static TreeNode insert(TreeNode n, int key) {10if (n == null) return new TreeNode(key);11if (key < n.val) n.left = insert(n.left, key);12else if (key > n.val) n.right = insert(n.right, key);13return n;14}15static int idx;16static TreeNode fromPreorder(int[] pre, int bound) { // keys of this subtree are below bound17if (idx == pre.length || pre[idx] > bound) return null;18TreeNode n = new TreeNode(pre[idx++]);19n.left = fromPreorder(pre, n.val);20n.right = fromPreorder(pre, bound);21return n;22}
Live state
keys
7
Execution trace (dry run table)Show 17 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | keys=7 | Sorted keys can be turned into a balanced BST: make the middle key the root and recurse on each half. The two halves differ in size by at most one. |
| 2 | L4 | mid=3 segment=[0, 6] | Segment [0, 6] has 7 keys: the middle one, 40, becomes the root of this subtree. |
| 3 | L4 | mid=1 segment=[0, 2] | Segment [0, 2] has 3 keys: the middle one, 20, becomes the root of this subtree. |
| 4 | L4 | mid=0 segment=[0, 0] | Segment [0, 0] has 1 key: the middle one, 10, becomes the root of this subtree. |
| 5 | L2 | segment=[0, -1] | The segment [0, -1] is empty: that child is null. |
| 6 | L2 | segment=[1, 0] | The segment [1, 0] is empty: that child is null. |
| 7 | L4 | mid=2 segment=[2, 2] | Segment [2, 2] has 1 key: the middle one, 30, becomes the root of this subtree. |
| 8 | L2 | segment=[2, 1] | The segment [2, 1] is empty: that child is null. |
| 9 | L2 | segment=[3, 2] | The segment [3, 2] is empty: that child is null. |
| 10 | L4 | mid=5 segment=[4, 6] | Segment [4, 6] has 3 keys: the middle one, 60, becomes the root of this subtree. |
| 11 | L4 | mid=4 segment=[4, 4] | Segment [4, 4] has 1 key: the middle one, 50, becomes the root of this subtree. |
| 12 | L2 | segment=[4, 3] | The segment [4, 3] is empty: that child is null. |
| 13 | L2 | segment=[5, 4] | The segment [5, 4] is empty: that child is null. |
| 14 | L4 | mid=6 segment=[6, 6] | Segment [6, 6] has 1 key: the middle one, 70, becomes the root of this subtree. |
| 15 | L2 | segment=[6, 5] | The segment [6, 5] is empty: that child is null. |
| 16 | L2 | segment=[7, 6] | The segment [7, 6] is empty: that child is null. |
| 17 | L7 | height=2 minimum height=2 | Height 2 for 7 keys: the smallest possible height is 2, so this tree is as short as a binary tree can be. |
Practice ladder
Now try it yourself
Easy
- Validate Binary Search Tree (small inputs)
- Convert Sorted Array to Binary Search Tree
Medium
- Validate Binary Search Tree
- Recover Binary Search Tree
- Construct Binary Search Tree from Preorder Traversal
Advanced
- Largest BST Subtree
- Verify Preorder Sequence in Binary Search Tree
Key takeaways
- Checking a node only against its children is wrong: carry the open range from all ancestors.
- A binary tree is a BST exactly when its inorder sequence is strictly increasing.
- Two swapped keys show up as one or two descents; swap the larger of the first with the smaller of the last.
- Passing an upper bound builds a BST from a preorder sequence in O(n).