DSA: Tree

A Tree is a hierarchical, non-linear data structure used to represent relationships where each element (node) can lead to multiple other elements.

Unlike arrays or linked lists (linear), a tree represents parent–child relationships, which makes it ideal for modeling structure, hierarchy, and fast searching.

Trees appear everywhere: file systems, compilers, databases, UI frameworks, and indexing engines.

At the top of the tree is the root, and every node can have zero or more children.

md
        Root
       /    \
     A        B
    / \        \
   C   D        E

Core Terminology

  • Node: a single element in the tree
  • Edge: connection between two nodes
  • Root: top-most node
  • Parent / Child: relationship between nodes
  • Leaf: node with no children
  • Depth: distance from root
  • Height: longest path to a leaf
  • Subtree: any node and its descendants

Tree Family

This is the full tree of tree-related structures you need for DSA

md
Tree
├── General Tree
│   └── N-ary Tree
│
├── Binary Tree
│   ├── Full Binary Tree
│   ├── Perfect Binary Tree
│   ├── Complete Binary Tree
│   └── Skewed Binary Tree
│
├── Binary Search Tree (BST)
│   ├── AVL Tree
│   ├── Red-Black Tree
│   └── Splay Tree
│
├── Heap
│   ├── Min Heap
│   └── Max Heap
│
├── Trie (Prefix Tree)
│
├── Segment Tree
│
└── B-Trees
    ├── B-Tree
    └── B+ Tree


General Tree

A General Tree allows each node to have any number of children.

md
        A
     /  |  \
    B   C   D
       / \
      E   F

All tree types conceptually start here.

cpp
struct Node {
    int value;
    vector<Node*> children;
};

Complexity

  • Traversal: O(n)
  • Space: O(n)

Use cases

  • File systems
  • Organization charts
  • DOM structure (HTML)

Binary Tree

A Binary Tree restricts each node to at most two children: left and right.

md
      A
     / \
    B   C
   /
  D

cpp
struct Node {
    int value;
    Node* left;
    Node* right;
};

Traversals

Inorder (Left → Root → Right)

cpp
void inorder(Node* root) {
    if (!root) return;
    inorder(root->left);
    cout << root->value << " ";
    inorder(root->right);
}

Preorder (Root → Left → Right)

cpp
void preorder(Node* root) {
    if (!root) return;
    cout << root->value << " ";
    preorder(root->left);
    preorder(root->right);
}

Postorder (Left → Right → Root)

cpp
void postorder(Node* root) {
    if (!root) return;
    postorder(root->left);
    postorder(root->right);
    cout << root->value << " ";
}

Complexity

  • Traversal: O(n)
  • Space (recursion): O(h)

Variants

Full Binary Tree

Every node has 0 or 2 children

md
      A
     / \
    B   C

Perfect Binary Tree

All levels completely filled

md
        A
      /   \
     B     C
    / \   / \
   D   E F   G

Complete Binary Tree

Filled left to right (used by heaps)

md
        A
      /   \
     B     C
    / \   /
   D   E F

Skewed Tree

Degenerates into a linked list

md
A
 \
  B
   \
    C


Binary Search Tree (BST)

A BST maintains order:

  • Left subtree < root
  • Right subtree > root

md
        8
       / \
      3  10
     / \
    1   6

Insert

cpp
Node* insert(Node* root, int value) {
    if (!root) return new Node{value, nullptr, nullptr};

    if (value < root->value)
        root->left = insert(root->left, value);
    else
        root->right = insert(root->right, value);

    return root;
}

cpp
bool search(Node* root, int key) {
    if (!root) return false;
    if (root->value == key) return true;

    if (key < root->value)
        return search(root->left, key);
    else
        return search(root->right, key);
}

Complexity

  • Average: O(log n)
  • Worst (skewed): O(n)

This is where balanced trees come in.

Complexity

OperationAverageWorst
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)

Worst case happens when the tree becomes skewed.


_Why ordinary BSTs are not enough — and how balance saves performance_

A self-balancing tree is a binary search tree (BST) that automatically keeps its height small during insertions and deletions.

The goal is simple:

**Guarantee O(log n) time** for search, insert, and delete — _always_, not just on average.

Self-Balancing Trees

Used to guarantee logarithmic performance.

Every self-balancing tree:

  • Is a Binary Search Tree
  • Tracks structural information
  • Performs rotations when imbalance occurs

Balance is enforced locally, but guarantees global height bounds.

Tree Rotations (The Foundation)

Rotations are constant-time pointer rearrangements that restore balance without breaking BST order.

Right Rotation

Before:

md
        y
       /
      x
     /
    T1

After:

md
        x
         \
          y
         /
        T2

Left Rotation

Before:

md
    x
     \
      y
       \
        T3

After:

md
        y
       /
      x
       \
        T2

Key Rule

Rotations **preserve inorder traversal**, so BST ordering stays intact.

Rotation Cost

  • Time: O(1)
  • Space: O(1)

AVL Tree

  • Strict height balancing
  • Uses rotations

_Strictly balanced, mathematically clean_

An AVL Tree maintains this rule:

For every node,
**|height(left) − height(right)| ≤ 1**

This difference is called the balance factor.

Balance Factor

md
balanceFactor = height(left) - height(right)

Allowed values:

md
-1, 0, +1

Anything else → rebalance needed

AVL Rotations (4 Cases)

LL Case (Left-Left)

md
      z
     /
    y
   /
  x

Right rotation on z

RR Case (Right-Right)

md
  z
   \
    y
     \
      x

Left rotation on z

LR Case (Left-Right)

md
      z
     /
    y
     \
      x

Left rotation on y

Right rotation on z

RL Case (Right-Left)

md
  z
   \
    y
   /
  x

Right rotation on y

Left rotation on z

AVL Insert (Core Logic)

cpp
Node* insert(Node* root, int key) {
    if (!root)
        return new Node(key);

    if (key < root->value)
        root->left = insert(root->left, key);
    else
        root->right = insert(root->right, key);

    updateHeight(root);

    int balance = getBalance(root);

    // LL
    if (balance > 1 && key < root->left->value)
        return rightRotate(root);

    // RR
    if (balance < -1 && key > root->right->value)
        return leftRotate(root);

    // LR
    if (balance > 1 && key > root->left->value) {
        root->left = leftRotate(root->left);
        return rightRotate(root);
    }

    // RL
    if (balance < -1 && key < root->right->value) {
        root->right = rightRotate(root->right);
        return leftRotate(root);
    }

    return root;
}

AVL Complexity

OperationTime
SearchO(log n)
InsertO(log n)
DeleteO(log n)

AVL Characteristics

  • Very strict balance
  • Smaller height
  • More rotations
  • Excellent for read-heavy workloads

Red-Black Tree

_Loosely balanced, industrial-grade_

A Red-Black Tree enforces balance using color rules instead of exact heights.

  • Looser balancing
  • Faster inserts/deletes
  • Used in std::map, std::set

Complexity

All operations:

  • O(log n) time
  • O(n) space

Red-Black Properties

  1. Every node is red or black
  2. Root is black
  3. Red nodes cannot have red children
  4. Every path from root to leaf has the same number of black nodes
  5. Null leaves are considered black

These rules ensure:

Height ≤ **2 × log₂(n)**

Why Colors Work

Instead of tracking exact height:

  • Red nodes act as flexible connectors
  • Black nodes enforce global depth consistency

This reduces rotations.

md
        B
       / \
      R   R
     /     \
    B       B

Black-height consistency is preserved.

Rebalancing Strategy

Red-Black Trees use:

  • Recoloring (cheap)
  • Rotations (only when necessary)

Most fixes do not rotate.


Heap

A Heap is a complete binary tree with a special order.

Min Heap

Parent ≤ children

md
        1
      /   \
     3     5

Max Heap

Parent ≥ children

md
        9
      /   \
     7     4

Used for

  • Priority queues
  • Scheduling
  • Dijkstra’s algorithm

Array Representation

md
Index:  0  1  2  3  4
Value: [1, 3, 5, 7, 9]

  • Parent: (i - 1) / 2
  • Left: 2i + 1
  • Right: 2i + 2

Insert (Push)

cpp
priority_queue<int, vector<int>, greater<int>> minHeap;
minHeap.push(5);
minHeap.push(1);

Complexity

OperationTime
InsertO(log n)
DeleteO(log n)
Get Min/MaxO(1)

Trie (Prefix Tree)

A Trie stores strings character by character.

md
(root)
  |
  c
  |
  a
  |
  t*

cpp
struct TrieNode {
    bool isEnd = false;
    unordered_map<char, TrieNode*> children;
};

Used for

  • Autocomplete
  • Spell checking
  • Prefix search

Search complexity: O(length of word)

Insert

cpp
void insert(TrieNode* root, string word) {
    TrieNode* node = root;
    for (char c : word) {
        if (!node->children[c])
            node->children[c] = new TrieNode();
        node = node->children[c];
    }
    node->isEnd = true;
}

cpp
bool search(TrieNode* root, string word) {
    TrieNode* node = root;
    for (char c : word) {
        if (!node->children[c]) return false;
        node = node->children[c];
    }
    return node->isEnd;
}

Complexity

  • Insert/Search: O(k) where k = word length
  • Space: O(total characters)

Segment Tree

A Segment Tree is a binary tree used for range queries.

Example:

  • Range sum
  • Range minimum / maximum

md
        [0..7]
       /      \
   [0..3]   [4..7]

Query Complexity

  • Build: O(n)
  • Query: O(log n)
  • Update: O(log n)

Used heavily in competitive programming.


B-Trees Family

B-Tree

  • Multi-key nodes
  • Self-balanced
  • Disk-friendly

B+ Tree

  • All data stored in leaves
  • Fast range queries

Used in

  • Databases
  • File systems
  • Indexing engines

How Trees Connect to Hash Tables

StructureOrderSearchUse
Hash TableO(1) avgFast lookup
BSTO(log n)Ordered data
Red-Black TreeO(log n)Balanced maps
TriePrefixO(k)Strings
  • Hash Tables optimize speed
  • Trees optimize structure & ordering