Binary Search Trees (BST)

Binary Search Tree (BST)

Read: ~40 min Intermediate 7 practice problems Live C++ runner

1. The BST Invariant (BST-এর মূল নিয়ম)

A binary tree is a Binary Search Tree if for every node:

  • All values in the left subtree are strictly less than this node's value, AND
  • All values in the right subtree are strictly greater than this node's value, AND
  • Both left and right subtrees are themselves BSTs.
একটি binary tree-কে BST বলব, যদি প্রতিটি node-এর জন্য — বাঁ পাশের সকল মান সেই node থেকে ছোট এবং ডান পাশের সকল মান তার চেয়ে বড় হয় (recursive ভাবে দুটি subtree-ও BST)।
Big idea — BST-এর in-order traversal সবসময় sorted output দেয়। এটি invariant — পরীক্ষার জন্য নিজে প্রমাণ করুন।

2. Why a BST?

A sorted array supports binary search in O(log n) but inserting a new element costs O(n) because everything to the right must shift. A BST aims to give us both search and insert in O(log n) — provided the tree stays roughly balanced. Real-world examples include the in-memory indexes of database engines and the std::map / std::set containers in C++.

Sorted array-এ binary search O(log n), কিন্তু insert O(n)। BST আদর্শভাবে দুটোকেই O(log n)-এ আনতে চায় — শর্ত: tree-টি balanced থাকতে হবে। C++-এ std::map এবং Java-র TreeMap এই idea-র production-grade balanced BST।

3. The Hidden Cost — When BSTs Degenerate

What happens if we insert 1, 2, 3, 4, 5 in that order into an empty BST? Each new value is greater than the previous root, so it always goes to the right child. The tree degenerates into a linked list — depth becomes n, and every operation costs O(n).

Balanced BST (insert 3,1,4,2,5) 3 1 4 2 5 height ≈ log n (good) Degenerate BST (insert 1,2,3,4,5) 1 2 3 4 5 Figure 17.1 — same five values, two very different BSTs। Insertion order matters.
Trap — naive BST সাজানো input-এ degenerate হয়। তাই production-এ AVL বা Red-Black tree ব্যবহার হয়। Module 18-এ এই balancing শিখব।

4. Insert & Search — The Easy Half

Both insert and search start at the root and descend. At every step, compare with the current node — if the target is smaller, go left; if larger, go right; if equal, stop (search) or ignore / handle duplicates as you wish (insert).

Insert ও search-এর logic প্রায় একই — root থেকে শুরু, ছোট হলে বাঁয়ে, বড় হলে ডানে। সমান হলে search-এ পেলেন, insert-এ duplicate কীভাবে handle করবেন সেটি আপনার policy।

5. A Full BST — Insert, Search, Delete (3 cases)

Delete is the tricky operation. There are three cases:

  1. Leaf: simply free it.
  2. One child: link parent directly to that child.
  3. Two children: replace the value with the in-order successor (smallest in the right subtree), then delete that successor recursively.
bst.cpp
#include <bits/stdc++.h>
using namespace std;

struct Node {
    int v;
    Node *l, *r;
    Node(int x) : v(x), l(nullptr), r(nullptr) {}
};

Node* insert(Node* root, int x) {
    if (!root) return new Node(x);
    if (x < root->v)      root->l = insert(root->l, x);
    else if (x > root->v) root->r = insert(root->r, x);
    return root; // duplicates ignored
}

bool search(Node* root, int x) {
    while (root) {
        if (x == root->v) return true;
        root = (x < root->v) ? root->l : root->r;
    }
    return false;
}

Node* minNode(Node* r) { while (r->l) r = r->l; return r; }

Node* erase(Node* root, int x) {
    if (!root) return nullptr;
    if (x < root->v) root->l = erase(root->l, x);
    else if (x > root->v) root->r = erase(root->r, x);
    else {
        if (!root->l) { Node* t = root->r; delete root; return t; }
        if (!root->r) { Node* t = root->l; delete root; return t; }
        Node* succ = minNode(root->r);
        root->v = succ->v;
        root->r = erase(root->r, succ->v);
    }
    return root;
}

void inorder(Node* r) {
    if (!r) return;
    inorder(r->l);
    cout << r->v << ' ';
    inorder(r->r);
}

int main() {
    Node* root = nullptr;
    for (int x : {50, 30, 70, 20, 40, 60, 80})
        root = insert(root, x);

    cout << "Inorder    : "; inorder(root); cout << '\n';
    cout << "Search 40  : " << search(root, 40) << '\n';
    cout << "Search 100 : " << search(root, 100) << '\n';

    root = erase(root, 30); // node with two children
    cout << "After del30: "; inorder(root); cout << '\n';
    return 0;
}
In-order successor-এর বদলে in-order predecessor (left subtree-এর সর্বোচ্চ) ব্যবহার করলেও কাজ হবে — দু'টোই সঠিক, পরীক্ষায় যেকোনো একটি ধারাবাহিকভাবে ব্যবহার করুন।

6. k-th Smallest in a BST (in-order trick)

Because in-order is sorted, the k-th element of in-order is the k-th smallest. We don't have to materialise the entire list — we can stop as soon as the counter reaches k.

kth_smallest.cpp
#include <bits/stdc++.h>
using namespace std;
struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};

N* ins(N* r,int x){
    if(!r) return new N(x);
    if(x<r->v) r->l=ins(r->l,x); else if(x>r->v) r->r=ins(r->r,x);
    return r;
}

int cnt = 0, ans = -1;
void kth(N* r, int k){
    if(!r || ans!=-1) return;
    kth(r->l,k);
    if(++cnt==k){ ans=r->v; return; }
    kth(r->r,k);
}

int main(){
    N* root=nullptr;
    for(int x:{50,30,70,20,40,60,80}) root=ins(root,x);
    kth(root,3);
    cout << "3rd smallest = " << ans;
}

7. Complexity Cheat-Sheet

OperationAverageWorst (degenerate)
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)
In-order traversalO(n)O(n)
Average case-এ height O(log n), কিন্তু worst case-এ O(n)। ICPC বা Codeforces-এ adversarial input দেখলে balanced tree (AVL/RB) বা std::set ব্যবহার করুন — সেগুলো সবসময় O(log n) গ্যারান্টি দেয়।

8. Practice Problems

  1. Validate whether a given binary tree is a BST (use min-max bound technique).
    একটি tree BST কিনা পরীক্ষা করুন (min-max bound পদ্ধতি)।
    ✨ Show Answer
    validate.cpp
    #include <bits/stdc++.h>
    using namespace std;
    struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};
    bool valid(N* r, long lo, long hi){
        if(!r) return true;
        if(r->v <= lo || r->v >= hi) return false;
        return valid(r->l, lo, r->v) && valid(r->r, r->v, hi);
    }
    int main(){
        N* r=new N(5); r->l=new N(3); r->r=new N(8);
        r->l->l=new N(1); r->l->r=new N(4);
        cout << (valid(r, LONG_MIN, LONG_MAX) ? "BST" : "NOT BST");
    }
  2. Find the Lowest Common Ancestor (LCA) of two values in a BST.
    BST-তে দুটি মানের LCA বের করুন।
    ✨ Show Answer
    lca.cpp
    #include <bits/stdc++.h>
    using namespace std;
    struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};
    N* ins(N* r,int x){if(!r)return new N(x); if(x<r->v) r->l=ins(r->l,x); else if(x>r->v) r->r=ins(r->r,x); return r;}
    N* lca(N* r,int a,int b){
        while(r){
            if(a<r->v && b<r->v) r=r->l;
            else if(a>r->v && b>r->v) r=r->r;
            else return r;
        }
        return nullptr;
    }
    int main(){
        N* root=nullptr;
        for(int x:{50,30,70,20,40,60,80}) root=ins(root,x);
        cout << "LCA(20,40)=" << lca(root,20,40)->v << '\n';
        cout << "LCA(20,80)=" << lca(root,20,80)->v;
    }
  3. Find floor(x) and ceil(x) in a BST (largest ≤ x and smallest ≥ x).
    BST-তে x-এর floor ও ceil বের করুন।
    ✨ Show Answer
    floor_ceil.cpp
    #include <bits/stdc++.h>
    using namespace std;
    struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};
    N* ins(N* r,int x){if(!r)return new N(x); if(x<r->v) r->l=ins(r->l,x); else if(x>r->v) r->r=ins(r->r,x); return r;}
    int floorBST(N* r,int x){
        int ans=INT_MIN;
        while(r){ if(r->v==x) return x; if(r->v<x){ans=r->v; r=r->r;} else r=r->l; }
        return ans;
    }
    int ceilBST(N* r,int x){
        int ans=INT_MAX;
        while(r){ if(r->v==x) return x; if(r->v>x){ans=r->v; r=r->l;} else r=r->r; }
        return ans;
    }
    int main(){
        N* root=nullptr;
        for(int x:{10,20,30,40,50}) root=ins(root,x);
        cout << "floor(25)=" << floorBST(root,25) << " ceil(25)=" << ceilBST(root,25);
    }
  4. Range sum in a BST: sum all values x with L ≤ x ≤ R.
    BST-তে [L, R] range-এর সব মানের যোগফল।
    ✨ Show Answer
    range_sum.cpp
    #include <bits/stdc++.h>
    using namespace std;
    struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};
    N* ins(N* r,int x){if(!r)return new N(x); if(x<r->v) r->l=ins(r->l,x); else if(x>r->v) r->r=ins(r->r,x); return r;}
    int rs(N* r,int L,int R){
        if(!r) return 0;
        if(r->v<L) return rs(r->r,L,R);
        if(r->v>R) return rs(r->l,L,R);
        return r->v + rs(r->l,L,R) + rs(r->r,L,R);
    }
    int main(){
        N* root=nullptr;
        for(int x:{10,5,15,3,7,18}) root=ins(root,x);
        cout << "sum[7..15] = " << rs(root,7,15);
    }
  5. Convert a sorted array into a balanced BST.
    Sorted array থেকে balanced BST তৈরি করুন।
    ✨ Show Answer
    sorted_to_bst.cpp
    #include <bits/stdc++.h>
    using namespace std;
    struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};
    N* build(vector<int>& a, int lo, int hi){
        if(lo>hi) return nullptr;
        int m=(lo+hi)/2;
        N* n=new N(a[m]);
        n->l=build(a,lo,m-1);
        n->r=build(a,m+1,hi);
        return n;
    }
    void in(N* r){if(!r)return; in(r->l); cout<<r->v<<' '; in(r->r);}
    int h(N* r){return r? 1+max(h(r->l),h(r->r)):0;}
    int main(){
        vector<int> a={1,2,3,4,5,6,7};
        N* root=build(a,0,(int)a.size()-1);
        cout << "In-order: "; in(root); cout << "\nHeight: " << h(root);
    }
  6. Check if two BSTs contain identical sets of values (using in-order).
    দুটি BST-তে একই set আছে কিনা পরীক্ষা করুন (in-order মিলিয়ে)।
    ✨ Show Answer
    identical_bst.cpp
    #include <bits/stdc++.h>
    using namespace std;
    struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};
    N* ins(N* r,int x){if(!r)return new N(x); if(x<r->v) r->l=ins(r->l,x); else if(x>r->v) r->r=ins(r->r,x); return r;}
    void collect(N* r, vector<int>& v){ if(!r)return; collect(r->l,v); v.push_back(r->v); collect(r->r,v); }
    int main(){
        N* a=nullptr; for(int x:{5,3,8,1}) a=ins(a,x);
        N* b=nullptr; for(int x:{8,3,1,5}) b=ins(b,x);
        vector<int> va,vb; collect(a,va); collect(b,vb);
        cout << (va==vb ? "Identical" : "Different");
    }
  7. Recover a BST in which exactly two nodes have been swapped.
    একটি BST-তে দুটি node ভুলে swap হয়ে গেছে — সেগুলো খুঁজে ঠিক করুন।
    ✨ Show Answer
    recover.cpp
    #include <bits/stdc++.h>
    using namespace std;
    struct N{int v;N*l,*r;N(int x):v(x),l(nullptr),r(nullptr){}};
    N *first=nullptr,*second=nullptr,*prev=nullptr;
    void in(N* r){
        if(!r) return;
        in(r->l);
        if(prev && prev->v > r->v){
            if(!first) first=prev;
            second=r;
        }
        prev=r;
        in(r->r);
    }
    int main(){
        // Correct BST: 1 2 3 4 5 ; here we swap 2 and 4 in-place
        N* root=new N(3);
        root->l=new N(4); root->r=new N(5);
        root->l->l=new N(1); root->l->r=new N(2);
        in(root);
        if(first && second) swap(first->v, second->v);
        // verify
        prev=nullptr; first=second=nullptr; in(root);
        cout << (first==nullptr ? "Recovered OK" : "Still broken");
    }

Summary — Module 17

A BST keeps a single invariant — left < node < right — and that single rule unlocks search, insert, and delete in average O(log n). The in-order traversal of a BST is always sorted, which gives us elegant solutions for k-th smallest, range sum, validation, and recovery. The catch: naive BSTs degenerate on sorted input. Module 18 fixes that with self-balancing trees.

BST একটি invariant ধরে রাখে — left < node < right। এর in-order সবসময় sorted। কিন্তু sorted insert-এ tree degenerate হয়ে O(n) হয়ে যায়। তাই production-এ AVL/Red-Black এর মতো self-balancing tree ব্যবহার হয় — সেটিই পরের module।

Next Module → Self-Balancing BSTs: AVL & Red-Black — যেখানে height সর্বদা O(log n)।