SwadhyAI Notes

All DSA notes

DSA Trees

Binary Search Trees

Brute force finds any product SKU in a catalog of n = 10⁷. The time limit is not amused. You need membership, predecessor, and range listing without rebuilding a sorted array on every insert.

A BST encodes order as shape. The lets each comparison discard an entire subtree. Speed still depends on : short trees win; insertion chains become linked lists.

Algorithm journey

1

Duplicate policy

Reject, count, or equals-to-one-side: pick before coding insert.

2

All left descendants < key < all right descendants.

3

Follow one path

Search/insert/delete examine O(h) nodes along a root-to-null walk.

4

Sorted enumeration and successor/predecessor queries.

What actually happens

Naive idea: keep an unsorted list and scan, or rebuild a sorted array after every write. Correct for tiny n; at 10⁷ keys and frequent inserts, either TLE or Θ(n) reshuffles.

Better idea: store keys so each node splits lower keys from higher keys. Search follows one path because the other branch is provably empty of the target. then streams keys sorted.

The improved design still fails on sorted insertion order (height becomes n − 1), on parent-only validation that misses an ancestor bound, and when delete copies a key but drops the rest of a map payload. Those break the proof or the API, not just style.

Invariant

For every node v (strict unique-key policy): every key in v's left subtree is < v.key, and every key in v's right subtree is > v.key. The same claim holds in every subtree. Validation must use , not child-only checks.

How to choose a BST

1. Need ordered dynamics? Predecessor, range, rank, or sorted iteration besides membership: a BST (or ordered map) fits. Exact-key only: prefer a hash table.

2. Need worst-case log n? A plain BST has no height guarantee. Use AVL, red-black, B-tree, or another balancing scheme.

3. Static data? A sorted array plus binary search often wins on locality if updates are rare.

Quantitative check

Balanced height ≈ log₂(10⁷) ≈ 23 comparisons per lookup. A right spine from sorted inserts costs up to 10⁷ comparisons for one miss, and building that spine by n inserts is Θ(n²). Same ADT, different shapes, different deadlines.

Ordering

global bounds

All descendants, not only children, must fit.

Search work

one path

O(h), with worst-case Θ(h).

Sorted view

inorder

left, node, right emits ordered keys.

The ordering invariant is global

Under a strict unique-key policy, every node v satisfies: all keys reachable throughv.left are less than v.key, and all keys reachable through v.right are greater. Each subtree must satisfy the same rule. Checking only the two immediate children is insufficient: a deep descendant can be on the correct side of its parent but the wrong side of an ancestor.

1

Subtree interval

Each recursive call owns an allowed lower/upper key interval.

2

Acyclic ownership

Each reachable node has one structural parent; no child link creates a cycle.

3

Metadata agreement

Stored size, parent, count, or aggregate fields equal values implied by children.

4

Root reachability

Exactly the logical members are reachable from root; empty means root is null.

Choose a duplicate-key policy before writing insert

“Duplicates go somewhere” is not a complete contract. The choice affects bounds, deletion, rank, rotations, and whatsize means. A set normally rejects equality; a map updates the existing value; a multiset commonly keeps a count in the unique-key node.

Binary search tree duplicate-key policies and consequences
PolicyInsertion ruleConsequence
Reject / updateOne structural node per key; equality is a no-op or updates its value.Strict bounds: every left key < node.key < every right key. This is the cleanest set/map policy.
Store a countOne structural node per distinct key plus a multiplicity field.Ordering remains strict; insert/delete adjust count before changing structure. Works well with rotations.
Equals go left/rightChoose one side consistently and encode an inclusive bound on that side.Search/delete semantics become more subtle, and ordinary rotations can violate the chosen side rule.

Interactive: build a BST

This original visualization shows how inserted values descend through comparisons and become leaves. Try sorted input as well as mixed input and compare the resulting heights.

ℹ️ Ready to build a BST!
Empty BST

Interactive: narrow bounds, delete safely, validate globally

Search for 65 while tracking inherited bounds, remove a node with two children via its inorder successor, then inspect a tree that passes parent-only checks but violates an ancestor bound.

BST invariant laboratory

A comparison narrows a global key interval

Step 1 of 4

Start at 50

bounds (−∞, +∞)
50current2575104060905565

Action

Compare target 65 with the root. Since 65 > 50, only the right subtree can contain it.

Invariant

If 65 exists, it is in the current subtree and satisfies the current bounds.

Observable state

Current node 50; no candidates have been discarded yet.

Keyboard: focus within the demo, then use ← →, Home, or End.

Search and insertion: preserve the candidate-subtree invariant

Search initialization

The root subtree contains every stored key, so it contains the target if the target exists.

Search maintenance

At key x, target < x excludes the entire right subtree; target > x excludes the entire left subtree.

Search termination

Equality returns the node. A null link proves the narrowed candidate subtree is empty.

Insertion endpoint

The first null link on the search path is the unique leaf position that preserves strict bounds.

Python

python
class Node:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None


def search(root, target):
    current = root
    while current is not None:
        if target == current.key:
            return current
        current = current.left if target < current.key else current.right
    return None


def insert(root, key):
    if root is None:
        return Node(key)
    if key < root.key:
        root.left = insert(root.left, key)
    elif key > root.key:
        root.right = insert(root.right, key)
    # equality is rejected under this set policy
    return root

Java

java
final class Bst {
static final class Node {
    int key;
    Node left, right;

    Node(int key) { this.key = key; }
}

static Node search(Node root, int target) {
    Node current = root;
    while (current != null) {
        if (target == current.key) return current;
        current = target < current.key ? current.left : current.right;
    }
    return null;
}

static Node insert(Node root, int key) {
    if (root == null) return new Node(key);
    if (key < root.key) {
        root.left = insert(root.left, key);
    } else if (key > root.key) {
        root.right = insert(root.right, key);
    }
    // equality is rejected under this set policy
    return root;
}
}

Recursive code uses O(h) call-stack space. The iterative search uses Θ(1) auxiliary space; iterative insertion can do the same if it retains the parent of the null link. Neither form prevents a height-n recursion failure on a chain.

Deletion has three structural cases

No children

return null

Removing a leaf simply clears its parent link. If it was the root, the tree becomes empty.

One child

return the child

Splice the sole child into the removed node's position; every key stays inside the same ancestor bounds.

Two children

successor / predecessor

Use the minimum of the right subtree or maximum of the left, then delete that source node recursively.

The inorder successor is safe because it is greater than every key in the left subtree and no greater than any other key in the right subtree. As the minimum of the right subtree, it has no left child, so its own removal reduces to an easier case. A predecessor-based implementation is the symmetric alternative.

Python

python
def delete(root, key):
    if root is None:
        return None

    if key < root.key:
        root.left = delete(root.left, key)
    elif key > root.key:
        root.right = delete(root.right, key)
    else:
        if root.left is None:
            return root.right
        if root.right is None:
            return root.left

        successor = root.right
        while successor.left is not None:
            successor = successor.left
        root.key = successor.key
        root.right = delete(root.right, successor.key)

    return root

Java

java
final class BstDelete {
static final class Node {
    int key;
    Node left, right;
    Node(int key) { this.key = key; }
}

static Node delete(Node root, int key) {
    if (root == null) return null;

    if (key < root.key) {
        root.left = delete(root.left, key);
    } else if (key > root.key) {
        root.right = delete(root.right, key);
    } else {
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;

        Node successor = root.right;
        while (successor.left != null) {
            successor = successor.left;
        }
        root.key = successor.key;
        root.right = delete(root.right, successor.key);
    }

    return root;
}
}

Payload caution: for a key-value node, move/copy the complete record, not the key alone. With parent pointers, subtree sizes, counts, or aggregates, repair every affected field while unwinding. External references to node objects also make “copy key then delete successor” observably different from transplanting the successor node.

Validation requires inherited bounds

Carry the range allowed by all ancestors. A left edge tightens the upper bound to the parent key; a right edge tightens the lower bound. The strict code below matches the reject-duplicates policy. Inclusive endpoints must change if the chosen policy places equals on a side.

Python

python
def is_valid_bst(root):
    def valid(node, low, high):
        if node is None:
            return True
        if low is not None and node.key <= low:
            return False
        if high is not None and node.key >= high:
            return False
        return (valid(node.left, low, node.key)
                and valid(node.right, node.key, high))

    return valid(root, None, None)

Java

java
final class BstValidation {
static final class Node {
    int key;
    Node left, right;
    Node(int key) { this.key = key; }
}

static boolean isValidBst(Node root) {
    return valid(root, Long.MIN_VALUE, Long.MAX_VALUE);
}

static boolean valid(Node node, long low, long high) {
    if (node == null) return true;
    if (node.key <= low || node.key >= high) return false;
    return valid(node.left, low, node.key)
        && valid(node.right, node.key, high);
}
}

For arbitrary comparable keys, use optional bound objects rather than numeric sentinels. An alternative validator checks that inorder output is strictly increasing, but it must remember the previous key and apply the same duplicate policy.

Complexity is a function of height

Let height h be the number of edges on the longest root-to-leaf path (empty tree −1, leaf 0). A path operation examines at most h + 1 nodes, so it is O(h), with worst-case Θ(h) when the path reaches the deepest level. A balanced tree has h = Θ(log n); an insertion chain has h = n − 1.

Binary search tree operation costs as a function of height
OperationHeight formBalancedChainWhy
Search / containsO(h) · worst Θ(h)O(log n) · worst Θ(log n)O(n) · worst Θ(n)At most one root-to-leaf path.
InsertO(h) · worst Θ(h)O(log n) · worst Θ(log n)O(n) · worst Θ(n)Plus metadata updates on that path.
DeleteO(h) · worst Θ(h)O(log n) · worst Θ(log n)O(n) · worst Θ(n)Successor/predecessor also lies on one downward path.
Min / maxO(h) · worst Θ(h)O(log n) · worst Θ(log n)O(n) · worst Θ(n)Follow left or right links respectively.
Full inorder traversalΘ(n)Θ(n)Θ(n)Every node must be visited.
Range [lo, hi], k outputsO(h + k)O(log n + k)O(n)Prune subtrees outside the range.
Rank / select with subtree sizesO(h) · worst Θ(h)O(log n) · worst Θ(log n)O(n) · worst Θ(n)Requires correct size metadata.

Degeneracy: a BST does not balance itself

Favorable shape

When each subtree holds a comparable share of the keys, height is Θ(log n). Searching then discards a large region per comparison. Random insertion order of distinct keys yields logarithmic expected height, but only under that randomness assumption.

Insertion chain

Inserting already sorted distinct keys into a plain BST makes every node a right child. The height becomes n − 1, one search becomes Θ(n), and building the tree by n such insertions takes Θ(n²).

Need worst-case bounds?

Use AVL, red-black, B-tree, treap with stated randomness, or another balancing scheme.

Keys already sorted?

Build a balanced tree from the middle recursively in Θ(n), rather than repeated plain insertion.

Static membership only?

A sorted array may be smaller and more cache-friendly while retaining binary search.

Ordered queries are the reason to choose a BST

Floor / ceiling

O(h)

Track the best ancestor while descending toward the target.

Successor

O(h)

Minimum of the right subtree, or first ancestor reached from its left side.

Range output

O(h + k)

Skip left when key < lo and skip right when key > hi; emit inorder.

Rank / select

O(h)

Store subtree size so one comparison also reveals how many smaller keys lie left.

Language-neutral pseudocode: order-statistic select

Terminal
# select(node, r): key with zero-based rank r
while node is not null:
leftSize = size(node.left)
if r < leftSize:
node = node.left
else if r == leftSize:
return node
else:
r = r - leftSize - 1
node = node.right
return NOT_FOUND
 
# Mutation obligation:
# node.size = 1 + size(node.left) + size(node.right)

Common failure modes

Checking only children

Validate every node against bounds inherited from all ancestors.

Mismatched duplicate behavior

Make insert, search, delete, validation, traversal, and rotations share one explicit policy.

Losing the returned root

Insert/delete can replace a subtree root; assign the function's return value to the parent link and top-level root.

Deleting key but not payload

Move the complete entry or transplant the successor node; preserve map semantics and external-handle expectations.

Claiming O(log n) unconditionally

State O(h), with worst-case Θ(h). A plain BST has no worst-case logarithmic-height guarantee.

Stale augmentation

Recompute size, height, sums, or parent links after every structural change on the affected path.

Simple rule of thumb

Choose a BST when you need a dynamic ordered set or map: predecessor/successor, sorted iteration, rank, or range queries in addition to membership. State the duplicate policy and height guarantee. If only exact-key lookup matters, a hash table is usually simpler; if the data is static, a sorted array often has better locality; if worst-case logarithmic updates matter, choose a self-balancing BST rather than hoping a plain BST stays short.

Exercises

  1. Build a tree that passes every parent–child comparison but violates the global . Which catches it?
  2. Insert 1..10⁵ in order into a plain BST, then search for 10⁵. Estimate comparisons. How would you rebuild a height-balanced tree from the same keys in Θ(n)?
  3. Delete a two-child node using its . Why is the successor guaranteed to have no left child, and what breaks if you copy only the key of a key–value node?

Primary sources

  • Cormen et al., Introduction to Algorithms (CLRS): binary search trees (search, insert, delete, successor).
  • Sedgewick & Wayne, Algorithms: BST symbol tables and the cost of ordered inserts.
  • Guibas & Sedgewick: red-black trees (how balancing restores logarithmic height when a plain BST fails).

Takeaway

A BST is order encoded as shape under a global search-tree invariant. Path operations cost O(h); balance is optional machinery you must add when worst-case log n matters.