Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Depth-First Search (DFS)

Builds on Top-Down DFS, which you haven’t finished yet.

P0MediumBuilt on Top-Down DFS~3 min speedrun

BST Property / Range Validation

Built on
Top-Down DFSThe value passed down is a pair (low, high), and the BST rule picks the single branch to follow.
Unlocks
BST Inorder
The problem

Validate Binary Search Tree (LC 98)(opens leetcode.com)

You're handed the root of a binary tree and must judge whether it is a legitimate binary search tree (BST).

A tree only counts as a valid BST when, at every node:

  • everything in its left subtree is strictly smaller than the node's own key,
  • everything in its right subtree is strictly larger than the node's own key, and
  • both of those subtrees are themselves valid BSTs.
Example 1
Input:root = [2,1,3]
Output:true
1 sits left of 2 and is smaller; 3 sits right of 2 and is larger.
123

BST Bounds

Root ➔ Validate (low, high) Bounds ➔ Deterministic Branch Selection
Spot it when: The tree is a BST and you validate it, search, insert or delete, trim it to a range, rebuild it from its preorder, or look for the closest or next value.
BST Bounds
Python
1from typing import Optional
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9def is_valid_bst(root: Optional[TreeNode]) -> bool:
10 """Validate BST: every node must sit strictly inside the (low, high) window from its
11 ancestors.
12 Time: O(N) | Space: O(H) recursion stack
13 """
14 def validate(node: Optional[TreeNode], low: float, high: float) -> bool:
15 if not node:
16 return True # Empty subtree is a valid BST
17 if not (low < node.val < high):
18 return False # Breaks a bound set by some ancestor, not just the parent
19 
20 # Left subtree: cap the high bound; right subtree: raise the low bound
21 return validate(node.left, low, node.val) and validate(node.right, node.val, high)
22 
23 return validate(root, float('-inf'), float('inf'))
Invariant: Every node that validate accepts satisfies low < node.val < high, where low and high are the tightest bounds set by all its ancestors.

A BST node must fit between bounds set by all its ancestors, not just its parent, so carry the window (low, high) down: going left caps high = node.val, going right raises low = node.val, and each node only checks low < node.val < high, starting from -inf and inf, with a strict comparison so duplicate values fail. Searching, inserting, or looking for the closest or next value walks the same window down one path: the target falls on one side of node.val, so one bound tightens and the other subtree is never visited, and when the walk falls off the tree, low and high are the closest values below and above the target. The trap is checking each node only against its children, left.val < node.val < right.val: in [5, 4, 6, null, null, 3, 7] every parent and child pair passes, but the 3 sits in the root's right subtree and breaks the root's rule.

See it run

Validate a BST with a (low, high) window, step by step

node none · low - · high - · valid not known yet

node 5 · low -inf · high inf · valid not known yet

node 4 · low -inf · high 5 · valid not known yet

node none · low - · high - · valid not known yet

node none · low - · high - · valid not known yet

node 6 · low 5 · high inf · valid not known yet

node 3 · low 5 · high 6 · valid false

node none · low - · high - · valid false

Each circle is a tree node. The calls row shows each active call as the node value and its (low, high) window, with the newest call last.

  • checking the node whose value is compared with low and high
  • accepted a node that passed its window
  • rejected a node outside its window
  • tightens the edge just followed: it sets a new low or a new high for the child

Call validate on the root 5 with low = -inf and high = inf. No ancestor limits it yet.

Check 5 against (-inf, inf). -inf < 5 < inf is true. The left child gets (-inf, 5) and the right child gets (5, inf).

Check 4 against (-inf, 5). -inf < 4 < 5 is true. The left child gets (-inf, 4) and the right child gets (4, 5).

There is no node here. validate returns True for an empty subtree.

There is no node here. validate returns True for an empty subtree.

Check 6 against (5, inf). 5 < 6 < inf is true. The left child gets (5, 6) and the right child gets (6, inf).

Check 3 against (5, 6). 5 < 3 < 6 is false, so validate returns False.

validate returned False at the rejected node. The and operator stops there, so no later node is checked.

45367
calls
empty
1 / 8
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Checking only left.val < node.val < right.val

    Mistake
    if node.left and node.left.val >= node.val:
        return False
    if node.right and node.right.val <= node.val:
        return False
    Fix
    if not (low < node.val < high):
        return False

    What happens: Every node must fit the bounds set by all its ancestors, not only by its parent. In [5,4,6,null,null,3,7], each parent and child pair is in order. But 3 is in the right subtree of the root 5, so it must be larger than 5. low < node.val < high carries that bound down from the root.

    Example: root = [5,4,6,null,null,3,7]
    Mistake: returns TrueFix: returns False
  2. 2

    Gotcha 2: Allowing equal values with <=

    Mistake
    if not (low <= node.val <= high):
    Fix
    if not (low < node.val < high):

    What happens: In Validate Binary Search Tree (LC 98), a left subtree holds only smaller values, and a right subtree only larger ones. So a value equal to an ancestor breaks the rule. With <=, a duplicate passes the check, and the tree is accepted.

    Example: root = [2,2,2]
    Mistake: returns TrueFix: returns False
  3. 3

    Gotcha 3: Starting the bounds at INT_MIN and INT_MAX

    Mistake
    return validate(root, -2**31, 2**31 - 1)
    Fix
    return validate(root, float('-inf'), float('inf'))

    What happens: Node values in Validate Binary Search Tree (LC 98) can be -231 or 231 - 1. The check is strict, so a node equal to a starting bound fails. -inf and inf are smaller and larger than every possible value.

    Example: root = [2147483647]
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Passing the child's value down as its own bound

    Mistake
    return validate(node.left, low, node.left.val) and validate(node.right, node.val, high)
    Fix
    return validate(node.left, low, node.val) and validate(node.right, node.val, high)

    What happens: The new bound for the left subtree is node.val, the value of the ancestor that sets it. With node.left.val as the bound, the left child is checked against itself. node.left.val < node.left.val is False, so every tree with a left child fails.

    Example: root = [2,1,3]
    Mistake: returns FalseFix: returns True
Not this one if…
  • You need the values in sorted order or by rank, as in Kth Smallest Element in a BST (LC 230), not a check of each node against bounds from its ancestors. See BST Inorder

Deep Dive

Why the window (low, high) is enough

The rule of a BST (binary search tree) is about whole subtrees. Every value in the left subtree of a node is smaller than that node. Every value in the right subtree is larger. So a node must obey every ancestor, not only its parent.

validate(node, low, high) carries the rule down. When the walk goes left from a node, that node's value is a new upper limit, so high = node.val. When the walk goes right, that value is a new lower limit, so low = node.val. The other limit stays as it was. After many steps, low is the value of the nearest ancestor where the walk went right, and high is the value of the nearest ancestor where the walk went left. These are the tightest limits.

Example: the tree [5, 4, 6, null, null, 3, 7] is not a BST. The calls are (5, -inf, inf), then (4, -inf, 5), then (6, 5, inf), then (3, 5, 6). At the last call, 5 < 3 < 6 is false, so validate returns False. The node 7 is never checked. The and in validate(node.left, ...) and validate(node.right, ...) stops at the first False.

The trap: comparing a node only with its parent

A tempting check is left.val < node.val < right.val at every node. It looks at one level only. On [5, 4, 6, null, null, 3, 7], every parent and child pair is in order: 4 < 5 < 6 and 3 < 6 < 7. A parent-only check returns True. The correct answer is False, because 3 is in the right subtree of 5 and must be larger than 5.

Two smaller traps sit in the same line. The first is <= in place of <. For [2, 2, 2], the strict check returns False, because a left value must be smaller and a right value must be larger. With <= it returns True.

The second is the start of the window. If the root call uses -2**31 and 2**31 - 1, the tree [2147483647] returns False, because the check is strict and the value equals the upper start. Node values in Validate Binary Search Tree (LC 98) can reach both of those numbers. Starting from float('-inf') and float('inf') returns True, which is correct.

The same window finds a place in a BST

When you search for a target, only one child can hold it. If target < node.val, go left and set high = node.val. Otherwise go right and set low = node.val. The other subtree is never visited. This is why Search in a Binary Search Tree (LC 700) and Insert into a Binary Search Tree (LC 701) follow one path.

When the walk falls off the tree, the window is still there. low and high are the closest values below and above the target. Example: in [8, 4, 12, 2, 6, 10, 14], a walk for the target 5 goes through 8, 4 and 6, then falls off the left side of 6. The window is (4, 6). The closest smaller value is 4, and the closest larger value is 6.

Insert into a Binary Search Tree (LC 701) places the new node at the place where the walk fell off. Closest Binary Search Tree Value (LC 270) and Inorder Successor in BST (LC 285) read the window at the end.

The cost, and the neighbour card

Each node is checked once with two comparisons, so the time is O(N). The space is O(H), where H is the height of the tree. The space is the recursion stack. A search or an insert visits one path, so it takes O(H) time.

A very deep tree can exceed the recursion limit. In a test run with Python's default limit, a chain of 900 nodes returned True, and a chain of 1,100 nodes raised RecursionError.

Sorted Walk is the neighbour card. It also uses the BST rule, but it reads the values in sorted order, as in Kth Smallest Element in a BST (LC 230). Use BST Bounds when each node must be checked against the limits of its ancestors, or when one path decides the answer. Use Sorted Walk when the answer depends on the order or the rank of the values.

60-Second Pitch

"A BST node must fit between the limits set by all its ancestors, not only its parent. So I pass a window (low, high) down the tree. The root starts with (-inf, inf). Each node checks low < node.val < high, with a strict comparison. The left child gets (low, node.val). The right child gets (node.val, high). A parent-only check fails on [5, 4, 6, null, null, 3, 7], because 3 is smaller than the root 5 but sits in its right subtree. The window (5, 6) catches it. Each node is visited once, so the time is O(N). The space is O(H) for the recursion. Search and insert use the same window on one path, so they take O(H) time."

Common Questions

Why not just check that each node is larger than its left child and smaller than its right child?

That checks one level only. In [5, 4, 6, null, null, 3, 7], every parent and child pair is in order, but 3 is in the right subtree of 5 and is smaller than 5. A node must obey all its ancestors, so the (low, high) window carries their limits down.

Is there another way to validate a BST?

Yes. Walk the tree in order and check that each value is larger than the previous one. For [2, 2, 2] the second value is not larger, so it returns False. This takes O(N) time and O(H) space as well. The window version also lets you stop early on the first bad node and is easy to reuse for search and insert.

Why start with float('-inf') and float('inf') and not the integer limits?

The check is strict. If a node equals the starting limit, it fails. In Validate Binary Search Tree (LC 98), a node can hold 2147483647, so validate(root, -231, 231 - 1) returns False for [2147483647]. Infinity is outside every possible value.

How would you insert a value into a BST with this idea?

Walk down one path. Go left when the value is smaller than node.val, and go right otherwise. When the walk reaches an empty place, put the new node there. This is Insert into a Binary Search Tree (LC 701). It takes O(H) time, because only one child is visited at each step.

What is the space cost, and when does it become a problem?

O(H) for the recursion stack, where H is the height of the tree. A balanced tree has H about log N. A chain has H = N. In Python, a chain of 1,100 nodes raised RecursionError with the default limit, so for a very deep tree I would use an explicit stack or an inorder walk with a loop.

SWE Reasoning

Spot it

  • The tree is a binary search tree, and the task is to validate it, search it, insert into it, delete from it, trim it or find the closest value.
  • The rule "left is smaller, right is larger" must hold for a whole subtree, not only for a child.
  • Only one child can hold the target, so one path from the root is enough.
  • The wording says "strictly" or says that values are distinct, so the comparison must be strict.
  • The answer depends on the limits that the ancestors set, which a single number from a child cannot carry.

The invariant, step by step

  1. validate(root, -inf, inf) starts with no limit, so low = -inf and high = inf.
  2. A call receives a node and a window. It accepts the node only if low < node.val < high.
  3. The left child receives (low, node.val). Every value in the left subtree must be smaller than node.val, and still larger than low.
  4. The right child receives (node.val, high). Every value in the right subtree must be larger than node.val, and still smaller than high.
  5. By induction, each node that a call accepts is inside the limits set by all its ancestors, because the window only shrinks as the walk goes down.
  6. An empty subtree returns True. The tree is valid when every call returns True.

Traps

  • Comparing only with the parent. [5, 4, 6, null, null, 3, 7] passes every parent and child pair, but 3 is in the right subtree of 5. Carry (low, high) down.
  • <= in the check. A duplicate value passes, so [2, 2, 2] is accepted. Use <.
  • Starting at INT_MIN and INT_MAX. A node with value 2147483647 fails the strict check. Start at -inf and inf.
  • Passing the child's value as its own bound. validate(node.left, low, node.left.val) compares the child with itself. Pass node.val.
  • Only the left subtree checked. Both calls are needed, joined with and.

Practice

Canonical LeetCode:Validate Binary Search Tree (LC 98)Lowest Common Ancestor of a Binary Search Tree (LC 235)Trim a Binary Search Tree (LC 669)Closest Binary Search Tree Value (LC 270)Inorder Successor in BST (LC 285)Insert into a Binary Search Tree (LC 701)Delete Node in a BST (LC 450)Search in a Binary Search Tree (LC 700)Construct Binary Search Tree from Preorder Traversal (LC 1008)
Theory Context•Depth-First Search (DFS)

Builds on Top-Down DFS, which you haven’t finished yet.

P0MediumBuilt on Top-Down DFS~3 min speedrun

BST Property / Range Validation

Built on
Top-Down DFSThe value passed down is a pair (low, high), and the BST rule picks the single branch to follow.
Unlocks
BST Inorder
The problem

Validate Binary Search Tree (LC 98)(opens leetcode.com)

You're handed the root of a binary tree and must judge whether it is a legitimate binary search tree (BST).

A tree only counts as a valid BST when, at every node:

  • everything in its left subtree is strictly smaller than the node's own key,
  • everything in its right subtree is strictly larger than the node's own key, and
  • both of those subtrees are themselves valid BSTs.
Example 1
Input:root = [2,1,3]
Output:true
1 sits left of 2 and is smaller; 3 sits right of 2 and is larger.
123

BST Bounds

Root ➔ Validate (low, high) Bounds ➔ Deterministic Branch Selection
Spot it when: The tree is a BST and you validate it, search, insert or delete, trim it to a range, rebuild it from its preorder, or look for the closest or next value.
BST Bounds
Python
1from typing import Optional
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9def is_valid_bst(root: Optional[TreeNode]) -> bool:
10 """Validate BST: every node must sit strictly inside the (low, high) window from its
11 ancestors.
12 Time: O(N) | Space: O(H) recursion stack
13 """
14 def validate(node: Optional[TreeNode], low: float, high: float) -> bool:
15 if not node:
16 return True # Empty subtree is a valid BST
17 if not (low < node.val < high):
18 return False # Breaks a bound set by some ancestor, not just the parent
19 
20 # Left subtree: cap the high bound; right subtree: raise the low bound
21 return validate(node.left, low, node.val) and validate(node.right, node.val, high)
22 
23 return validate(root, float('-inf'), float('inf'))
Invariant: Every node that validate accepts satisfies low < node.val < high, where low and high are the tightest bounds set by all its ancestors.

A BST node must fit between bounds set by all its ancestors, not just its parent, so carry the window (low, high) down: going left caps high = node.val, going right raises low = node.val, and each node only checks low < node.val < high, starting from -inf and inf, with a strict comparison so duplicate values fail. Searching, inserting, or looking for the closest or next value walks the same window down one path: the target falls on one side of node.val, so one bound tightens and the other subtree is never visited, and when the walk falls off the tree, low and high are the closest values below and above the target. The trap is checking each node only against its children, left.val < node.val < right.val: in [5, 4, 6, null, null, 3, 7] every parent and child pair passes, but the 3 sits in the root's right subtree and breaks the root's rule.

See it run

Validate a BST with a (low, high) window, step by step

node none · low - · high - · valid not known yet

node 5 · low -inf · high inf · valid not known yet

node 4 · low -inf · high 5 · valid not known yet

node none · low - · high - · valid not known yet

node none · low - · high - · valid not known yet

node 6 · low 5 · high inf · valid not known yet

node 3 · low 5 · high 6 · valid false

node none · low - · high - · valid false

Each circle is a tree node. The calls row shows each active call as the node value and its (low, high) window, with the newest call last.

  • checking the node whose value is compared with low and high
  • accepted a node that passed its window
  • rejected a node outside its window
  • tightens the edge just followed: it sets a new low or a new high for the child

Call validate on the root 5 with low = -inf and high = inf. No ancestor limits it yet.

Check 5 against (-inf, inf). -inf < 5 < inf is true. The left child gets (-inf, 5) and the right child gets (5, inf).

Check 4 against (-inf, 5). -inf < 4 < 5 is true. The left child gets (-inf, 4) and the right child gets (4, 5).

There is no node here. validate returns True for an empty subtree.

There is no node here. validate returns True for an empty subtree.

Check 6 against (5, inf). 5 < 6 < inf is true. The left child gets (5, 6) and the right child gets (6, inf).

Check 3 against (5, 6). 5 < 3 < 6 is false, so validate returns False.

validate returned False at the rejected node. The and operator stops there, so no later node is checked.

45367
calls
empty
1 / 8
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Checking only left.val < node.val < right.val

    Mistake
    if node.left and node.left.val >= node.val:
        return False
    if node.right and node.right.val <= node.val:
        return False
    Fix
    if not (low < node.val < high):
        return False

    What happens: Every node must fit the bounds set by all its ancestors, not only by its parent. In [5,4,6,null,null,3,7], each parent and child pair is in order. But 3 is in the right subtree of the root 5, so it must be larger than 5. low < node.val < high carries that bound down from the root.

    Example: root = [5,4,6,null,null,3,7]
    Mistake: returns TrueFix: returns False
  2. 2

    Gotcha 2: Allowing equal values with <=

    Mistake
    if not (low <= node.val <= high):
    Fix
    if not (low < node.val < high):

    What happens: In Validate Binary Search Tree (LC 98), a left subtree holds only smaller values, and a right subtree only larger ones. So a value equal to an ancestor breaks the rule. With <=, a duplicate passes the check, and the tree is accepted.

    Example: root = [2,2,2]
    Mistake: returns TrueFix: returns False
  3. 3

    Gotcha 3: Starting the bounds at INT_MIN and INT_MAX

    Mistake
    return validate(root, -2**31, 2**31 - 1)
    Fix
    return validate(root, float('-inf'), float('inf'))

    What happens: Node values in Validate Binary Search Tree (LC 98) can be -231 or 231 - 1. The check is strict, so a node equal to a starting bound fails. -inf and inf are smaller and larger than every possible value.

    Example: root = [2147483647]
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Passing the child's value down as its own bound

    Mistake
    return validate(node.left, low, node.left.val) and validate(node.right, node.val, high)
    Fix
    return validate(node.left, low, node.val) and validate(node.right, node.val, high)

    What happens: The new bound for the left subtree is node.val, the value of the ancestor that sets it. With node.left.val as the bound, the left child is checked against itself. node.left.val < node.left.val is False, so every tree with a left child fails.

    Example: root = [2,1,3]
    Mistake: returns FalseFix: returns True
Not this one if…
  • You need the values in sorted order or by rank, as in Kth Smallest Element in a BST (LC 230), not a check of each node against bounds from its ancestors. See BST Inorder

Deep Dive

Why the window (low, high) is enough

The rule of a BST (binary search tree) is about whole subtrees. Every value in the left subtree of a node is smaller than that node. Every value in the right subtree is larger. So a node must obey every ancestor, not only its parent.

validate(node, low, high) carries the rule down. When the walk goes left from a node, that node's value is a new upper limit, so high = node.val. When the walk goes right, that value is a new lower limit, so low = node.val. The other limit stays as it was. After many steps, low is the value of the nearest ancestor where the walk went right, and high is the value of the nearest ancestor where the walk went left. These are the tightest limits.

Example: the tree [5, 4, 6, null, null, 3, 7] is not a BST. The calls are (5, -inf, inf), then (4, -inf, 5), then (6, 5, inf), then (3, 5, 6). At the last call, 5 < 3 < 6 is false, so validate returns False. The node 7 is never checked. The and in validate(node.left, ...) and validate(node.right, ...) stops at the first False.

The trap: comparing a node only with its parent

A tempting check is left.val < node.val < right.val at every node. It looks at one level only. On [5, 4, 6, null, null, 3, 7], every parent and child pair is in order: 4 < 5 < 6 and 3 < 6 < 7. A parent-only check returns True. The correct answer is False, because 3 is in the right subtree of 5 and must be larger than 5.

Two smaller traps sit in the same line. The first is <= in place of <. For [2, 2, 2], the strict check returns False, because a left value must be smaller and a right value must be larger. With <= it returns True.

The second is the start of the window. If the root call uses -2**31 and 2**31 - 1, the tree [2147483647] returns False, because the check is strict and the value equals the upper start. Node values in Validate Binary Search Tree (LC 98) can reach both of those numbers. Starting from float('-inf') and float('inf') returns True, which is correct.

The same window finds a place in a BST

When you search for a target, only one child can hold it. If target < node.val, go left and set high = node.val. Otherwise go right and set low = node.val. The other subtree is never visited. This is why Search in a Binary Search Tree (LC 700) and Insert into a Binary Search Tree (LC 701) follow one path.

When the walk falls off the tree, the window is still there. low and high are the closest values below and above the target. Example: in [8, 4, 12, 2, 6, 10, 14], a walk for the target 5 goes through 8, 4 and 6, then falls off the left side of 6. The window is (4, 6). The closest smaller value is 4, and the closest larger value is 6.

Insert into a Binary Search Tree (LC 701) places the new node at the place where the walk fell off. Closest Binary Search Tree Value (LC 270) and Inorder Successor in BST (LC 285) read the window at the end.

The cost, and the neighbour card

Each node is checked once with two comparisons, so the time is O(N). The space is O(H), where H is the height of the tree. The space is the recursion stack. A search or an insert visits one path, so it takes O(H) time.

A very deep tree can exceed the recursion limit. In a test run with Python's default limit, a chain of 900 nodes returned True, and a chain of 1,100 nodes raised RecursionError.

Sorted Walk is the neighbour card. It also uses the BST rule, but it reads the values in sorted order, as in Kth Smallest Element in a BST (LC 230). Use BST Bounds when each node must be checked against the limits of its ancestors, or when one path decides the answer. Use Sorted Walk when the answer depends on the order or the rank of the values.

60-Second Pitch

"A BST node must fit between the limits set by all its ancestors, not only its parent. So I pass a window (low, high) down the tree. The root starts with (-inf, inf). Each node checks low < node.val < high, with a strict comparison. The left child gets (low, node.val). The right child gets (node.val, high). A parent-only check fails on [5, 4, 6, null, null, 3, 7], because 3 is smaller than the root 5 but sits in its right subtree. The window (5, 6) catches it. Each node is visited once, so the time is O(N). The space is O(H) for the recursion. Search and insert use the same window on one path, so they take O(H) time."

Common Questions

Why not just check that each node is larger than its left child and smaller than its right child?

That checks one level only. In [5, 4, 6, null, null, 3, 7], every parent and child pair is in order, but 3 is in the right subtree of 5 and is smaller than 5. A node must obey all its ancestors, so the (low, high) window carries their limits down.

Is there another way to validate a BST?

Yes. Walk the tree in order and check that each value is larger than the previous one. For [2, 2, 2] the second value is not larger, so it returns False. This takes O(N) time and O(H) space as well. The window version also lets you stop early on the first bad node and is easy to reuse for search and insert.

Why start with float('-inf') and float('inf') and not the integer limits?

The check is strict. If a node equals the starting limit, it fails. In Validate Binary Search Tree (LC 98), a node can hold 2147483647, so validate(root, -231, 231 - 1) returns False for [2147483647]. Infinity is outside every possible value.

How would you insert a value into a BST with this idea?

Walk down one path. Go left when the value is smaller than node.val, and go right otherwise. When the walk reaches an empty place, put the new node there. This is Insert into a Binary Search Tree (LC 701). It takes O(H) time, because only one child is visited at each step.

What is the space cost, and when does it become a problem?

O(H) for the recursion stack, where H is the height of the tree. A balanced tree has H about log N. A chain has H = N. In Python, a chain of 1,100 nodes raised RecursionError with the default limit, so for a very deep tree I would use an explicit stack or an inorder walk with a loop.

SWE Reasoning

Spot it

  • The tree is a binary search tree, and the task is to validate it, search it, insert into it, delete from it, trim it or find the closest value.
  • The rule "left is smaller, right is larger" must hold for a whole subtree, not only for a child.
  • Only one child can hold the target, so one path from the root is enough.
  • The wording says "strictly" or says that values are distinct, so the comparison must be strict.
  • The answer depends on the limits that the ancestors set, which a single number from a child cannot carry.

The invariant, step by step

  1. validate(root, -inf, inf) starts with no limit, so low = -inf and high = inf.
  2. A call receives a node and a window. It accepts the node only if low < node.val < high.
  3. The left child receives (low, node.val). Every value in the left subtree must be smaller than node.val, and still larger than low.
  4. The right child receives (node.val, high). Every value in the right subtree must be larger than node.val, and still smaller than high.
  5. By induction, each node that a call accepts is inside the limits set by all its ancestors, because the window only shrinks as the walk goes down.
  6. An empty subtree returns True. The tree is valid when every call returns True.

Traps

  • Comparing only with the parent. [5, 4, 6, null, null, 3, 7] passes every parent and child pair, but 3 is in the right subtree of 5. Carry (low, high) down.
  • <= in the check. A duplicate value passes, so [2, 2, 2] is accepted. Use <.
  • Starting at INT_MIN and INT_MAX. A node with value 2147483647 fails the strict check. Start at -inf and inf.
  • Passing the child's value as its own bound. validate(node.left, low, node.left.val) compares the child with itself. Pass node.val.
  • Only the left subtree checked. Both calls are needed, joined with and.

Practice

Canonical LeetCode:Validate Binary Search Tree (LC 98)Lowest Common Ancestor of a Binary Search Tree (LC 235)Trim a Binary Search Tree (LC 669)Closest Binary Search Tree Value (LC 270)Inorder Successor in BST (LC 285)Insert into a Binary Search Tree (LC 701)Delete Node in a BST (LC 450)Search in a Binary Search Tree (LC 700)Construct Binary Search Tree from Preorder Traversal (LC 1008)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•