Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
A node being larger than its direct left child and smaller than its direct right child is NOT enough — every node must satisfy the invariant against ALL ancestors. The clean answer carries a (low, high) open interval down the recursion: a node's value must lie strictly between them, then recurse left with high = node.value and right with low = node.value. HackerRank uses STRICT inequalities (distinct values 0..10000), so equal values are invalid. O(n) time, O(h) space. The inorder traversal must be strictly increasing is the equivalent check.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you realize the BST property is global — every node constrained by all its ancestors — and can encode that with a tightening range or a strictly-increasing inorder check.
Where candidates slip
Only comparing each node to its immediate children, which passes invalid trees where a deep descendant violates an ancestor's bound. Also using <= when HackerRank requires strict <.
How to stand out
Call out the local-check trap explicitly, present the range recursion, then offer the inorder equivalent and note the strict-inequality requirement for distinct values.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewnode, range (low, high)low < value < high?NoYesExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Brute Force — Validate Each Node Against Subtree Min/Max
1Hint 1Explore hintHide hint
Restate the invariant literally: at every node, the maximum of the left subtree must be < node and the minimum of the right subtree must be > node.
2Hint 2Explore hintHide hint
Write helpers that find the max and min of a subtree, and call them at every node.
3Hint 3Explore hintHide hint
Notice you re-scan subtrees repeatedly — that's the source of the quadratic cost the optimal approach removes.
A · Core idea
The most literal translation of the definition: a node is valid if every value in its left subtree is strictly smaller and every value in its right subtree is strictly larger, and both subtrees are themselves valid BSTs.
How the check works. For each node, compute the maximum value in its left subtree and the minimum value in its right subtree. The node is locally consistent only if leftMax < node.value and node.value < rightMin (strict, per HackerRank). Recurse into both children; the whole tree is a BST only if every node passes.
Why this is correct but slow. Computing the min/max of a subtree means walking that entire subtree. Doing it at every node re-walks overlapping regions over and over. For a skewed tree, the top node scans n−1 descendants, the next scans n−2, and so on — the familiar n²/2 total. It's correct and a fine way to explain the invariant, but it wastes work the bounds approach avoids.
The key realization it leads to. The min/max a node needs are exactly the bounds inherited from its ancestors. Instead of recomputing them bottom-up at every node, we can pass them top-down in O(1) — that's the optimal approach.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
root 3, right child 5 whose left child is 1 (invalid)How to read it:1 sits in 3's right subtree but 1 < 3 — only a subtree-wide (or bounds) check catches it.
falseC · Code with teaching comments
Reference implementation preview// Definition for a binary tree node. class Node { int data; Node left; Node right;Explore the full codeHide the full code
D · Why it works and complexityO(n²) timeO(h) spaceOpen the invariant and proofExplore the proofHide the proof
Stating 'leftMax < node < rightMin at every node' nails the definition, and the redundant subtree scans it forces are precisely what the top-down bounds version eliminates — a clean brute-to-optimal narrative.
ComplexityO(n²) time, O(h) space — At each of n nodes you scan its entire subtree to find min/max — O(n) per node in the worst (skewed) case → O(n²). The recursion stack is O(h).
E · Edge cases and pitfalls5Single node — No subtrees to scan → true.Explore edge casesHide edge cases
Edge cases handled
Single nodeNo subtrees to scan → true.root 2 with left child 2subtreeMax(left)=2 >= 2 → false (strict).
Pitfalls
- Using <= / >= incorrectly: HackerRank is strict, so equal values must fail.
- Re-scanning subtrees at every node — O(n²), too slow at n = 10000 in the worst case.
- Only comparing parent to direct child — misses deep violations entirely.
Approach 2 of 2 · Optimal
Top-Down Bounds — (low, high) Range (Optimal)
1Hint 1Explore hintHide hint
Instead of recomputing each node's allowed min/max from its subtree, can the parent simply tell the child what range it's allowed to be in?
2Hint 2Explore hintHide hint
Pass a (low, high) open interval down. A node is valid only if low < value < high. What range do the children inherit?
3Hint 3Explore hintHide hint
Left child: the value becomes the new HIGH (everything left must be smaller). Right child: the value becomes the new LOW. Use sentinels (±∞) at the root.
A · Core idea
The brute force recomputes, at every node, the min/max that its ancestors already implicitly define. The optimization is to pass those bounds down instead of recomputing them.
The tightening interval. Start at the root with an open interval (−∞, +∞) — the root may be anything. A node is valid iff low < value < high (strict, matching HackerRank's distinct-values rule). Then the children inherit narrowed intervals:
- The left child must be smaller than this node, so its upper bound becomes
node.value: range(low, node.value). - The right child must be larger, so its lower bound becomes
node.value: range(node.value, high).
Because the bounds accumulate down every path, a node is automatically checked against all its ancestors, not just its parent. The earlier counterexample — a 1 buried in the right subtree of 3 — is caught because by the time we reach 1, its inherited low is 3, and 3 < 1 is false.
Why strict matters here. HackerRank's values are distinct, so the comparison is low < value < high. If duplicates were permitted on, say, the right, you'd relax that side to value < high becoming value <= high — but that's not this problem.
Sentinels. Use Long.MIN_VALUE/Long.MAX_VALUE (or nullable bounds) so the root's unbounded interval can't accidentally reject a legitimate 0 or 10000. Since values fit in int, longs as sentinels are safe and simple.
Equivalent: strictly increasing inorder. An inorder traversal of a valid BST yields values in strictly increasing order. Tracking the previous value and checking prev < current at each visit is an equally valid O(n) method — same idea, viewed as a stream.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
root 3, right child 5 whose left child is 1 (invalid)How to read it:Node 1 inherits low = 3, so 3 < 1 fails and the tree is rejected.
falseC · Code with teaching comments
D · Why it works and complexityO(n) timeO(h) spaceOpen the invariant and proofExplore the proofHide the proof
The BST property is global, and the elegant way to enforce a global property locally is to thread context (the bounds) through the recursion. 'Each node narrows the legal interval for its descendants' is the one sentence that turns an O(n²) check into O(n).
ComplexityO(n) time, O(h) space — Each node is visited once and does an O(1) range check → O(n) time. The recursion stack is O(h): O(log n) balanced, O(n) skewed.
E · Edge cases and pitfalls6Single node — −∞ < value < +∞ → true.Explore edge casesHide edge cases
Edge cases handled
Single node−∞ < value < +∞ → true.Strictly increasing right-skewed chainEach node's low rises, all pass → true.Node equal to an ancestorHits node.data <= low or >= high → false (strict).
Pitfalls
- Using int bounds and rejecting legitimate Integer.MIN/MAX values — use long or nullable sentinels.
- Writing < where HackerRank needs strict and accidentally allowing equality (or vice versa).
- Passing the parent's value as a bound to the WRONG child (swapping low/high).
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic boolean checkBST(Node root) { return isBST(root, Long.MIN_VALUE, Long.MAX_VALUE); } private boolean isBST(Node node, long low, long high) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Local parent-child check only
Verifying just node.left < node < node.right passes invalid trees where a deep descendant violates an ancestor. The property is global; bounds must be inherited.
Explore the code comparisonHide the code comparison
Local parent-child check only
Verifying just node.left < node < node.right passes invalid trees where a deep descendant violates an ancestor. The property is global; bounds must be inherited.
Don't write this
boolean isBST(Node n) {
if (n == null) return true;
if (n.left != null && n.left.data >= n.data) return false;
if (n.right != null && n.right.data <= n.data) return false;
return isBST(n.left) && isBST(n.right); // misses deep violations
}Write this instead
boolean isBST(Node n, long low, long high) {
if (n == null) return true;
if (n.data <= low || n.data >= high) return false;
return isBST(n.left, low, n.data) && isBST(n.right, n.data, high);
}2Allowing equality
HackerRank's values are distinct and the rule is strict, so a node equal to a bound must fail. Using < instead of <= on the bound check would wrongly accept duplicates.
Explore the code comparisonHide the code comparison
Allowing equality
HackerRank's values are distinct and the rule is strict, so a node equal to a bound must fail. Using < instead of <= on the bound check would wrongly accept duplicates.
Don't write this
if node.data < low or node.data > high: # allows equality to a bound
return FalseWrite this instead
if not (low < node.data < high): # strict on both sides
return FalseRevise
Carry the pattern into the interview
Pattern to remember
Validate the BST invariant with a tightening (low, high) range
Code pattern to memorize
isBST(node, low, high):
if node is null: return true
if not (low < node.value < high): return false
return isBST(node.left, low, node.value)
and isBST(node.right, node.value, high)Rules to remember
- 1Check each node against an inherited range, not just its immediate children.
- 2Going left tightens the upper bound to the parent's value; going right tightens the lower bound.
- 3HackerRank wants strict inequalities — equal values make it NOT a BST (values are distinct 0..10000).
- 4Equivalent check: an inorder traversal of a BST is strictly increasing.
- 5Comparing only parent-to-direct-child is the classic wrong answer — it misses violations deeper down.
Use this pattern when
- Verifying the BST property holds globally, not just locally
- Any 'is this structure ordered correctly' check that needs ancestor context
- Validating before relying on BST search/insert correctness
Choose another pattern when
- Duplicates are allowed on one side → loosen one bound to ≤ (not HackerRank's case)
- You only need the min/max of a subtree → a simpler aggregation, not full validation
- The tree isn't a search tree at all → a different traversal problem
Interview takeaway
Validating a BST is about propagating bounds: each node narrows the legal interval for its descendants. The local parent-child check is a trap; the range-passing recursion (or a strictly-increasing inorder) is the correct, O(n) answer.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternValidate a global ordering invariant by threading a tightening (low, high) range down the recursion. Going left lowers the upper bound, going right raises the lower bound, so each node is checked against all ancestors in O(n). The strictly-increasing inorder traversal is the equivalent stream-based check; remember HackerRank's strict-inequality, distinct-value rule.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Validate Binary Search TreeThe LeetCode twin — same bounds recursion; watch its duplicate/equality convention.
- Kth Smallest Element in a BSTRelies on the same inorder-is-sorted property a valid BST guarantees.
- Recover Binary Search TreeComing soonTwo nodes are swapped; find them via the inorder sequence's out-of-order pairs.
- Binary Search Tree: InsertionInsertion preserves exactly the invariant this problem validates.