Module 9.3

Trees and Binary Trees in C

Explore hierarchical data structures that power databases, file systems, and search algorithms. Master binary trees, binary search trees (BST), and essential tree traversal techniques including inorder, preorder, and postorder algorithms.

55 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Tree terminology and concepts
  • Binary tree implementation
  • Binary search trees (BST)
  • Tree traversal algorithms
  • Common tree operations
Contents
01

Introduction to Trees

Trees are hierarchical data structures that consist of nodes connected by edges. Unlike linear structures (arrays, linked lists, stacks, queues), trees organize data in a parent-child relationship, making them ideal for representing hierarchies like file systems, organization charts, and decision processes.

Data Structure

Tree

A tree is a non-linear hierarchical data structure consisting of nodes connected by edges. It has a special node called the root with no parent, and every other node has exactly one parent. Nodes with no children are called leaves.

Key property: A tree with N nodes has exactly N-1 edges. There is exactly one path between any two nodes in a tree.

Tree Terminology

// Tree visualization and terminology
//
//                    [A]          ← Root (level 0, depth 0)
//                   / | \
//                 /   |   \
//               [B]  [C]  [D]     ← Level 1, depth 1
//              / \        / \
//            [E] [F]    [G] [H]   ← Level 2, depth 2 (leaves)
//
// Terminology:
// - Root: A (topmost node, no parent)
// - Parent of E, F: B
// - Children of A: B, C, D
// - Siblings: B, C, D (same parent)
// - Leaves: E, F, C, G, H (no children)
// - Internal nodes: A, B, D (have children)
// - Ancestors of E: B, A
// - Descendants of A: B, C, D, E, F, G, H
// - Height of tree: 2 (longest path from root to leaf)
// - Depth of G: 2 (edges from root to G)
// - Degree of A: 3 (number of children)
// - Subtree rooted at B: B, E, F

Tree Terminology Table

Term Definition
Root The topmost node with no parent
Parent A node that has one or more children
Child A node that has a parent
Siblings Nodes that share the same parent
Leaf A node with no children (external node)
Internal Node A node with at least one child
Height Longest path from root to any leaf
Depth Number of edges from root to the node
Degree Number of children a node has
Subtree A tree formed by a node and its descendants

Types of Trees

Binary Tree

Each node has at most 2 children (left and right). Foundation for many advanced tree structures.

Binary Search Tree

Binary tree with ordering: left subtree values < node < right subtree values. Enables fast search.

AVL / Balanced Trees

Self-balancing BST where heights of subtrees differ by at most 1. Guarantees O(log n) operations.

Practice Questions

Task: For the tree shown above (rooted at A), identify: (a) all leaves, (b) height of tree, (c) depth of node G, (d) degree of node D.

Show Solution
// Given tree:
//         A
//       / | \
//      B  C  D
//     / \   / \
//    E   F G   H

// (a) Leaves: E, F, C, G, H (nodes with no children)
// (b) Height: 2 (longest path: A→B→E or A→D→G, etc.)
// (c) Depth of G: 2 (path: A→D→G = 2 edges)
// (d) Degree of D: 2 (D has 2 children: G and H)
02

Binary Trees

A binary tree is a tree where each node has at most two children, referred to as the left child and right child. Binary trees are the foundation for many important data structures including binary search trees, heaps, and expression trees.

Binary Tree Node Structure

// Binary tree node structure
typedef struct TreeNode {
    int data;
    struct TreeNode *left;   // Pointer to left child
    struct TreeNode *right;  // Pointer to right child
} TreeNode;

// Visual representation:
//
//        [data]
//        /    \
//    left      right
//      ↓         ↓
//   [child]   [child]
//     or        or
//    NULL      NULL

Types of Binary Trees

// 1. Full Binary Tree: Every node has 0 or 2 children
//         1
//        / \
//       2   3
//      / \
//     4   5

// 2. Complete Binary Tree: All levels filled except possibly last,
//    which is filled from left to right
//         1
//        / \
//       2   3
//      / \  /
//     4  5 6

// 3. Perfect Binary Tree: All internal nodes have 2 children,
//    all leaves at same level
//         1
//        / \
//       2   3
//      / \ / \
//     4  5 6  7

// 4. Balanced Binary Tree: Height difference between left and right
//    subtrees is at most 1 for every node

// 5. Degenerate (Skewed) Tree: Each parent has only one child
//    (essentially a linked list)
//     1
//      \
//       2
//        \
//         3

Binary Tree Implementation

// binary_tree.h
#ifndef BINARY_TREE_H
#define BINARY_TREE_H

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

// Function prototypes
TreeNode *createNode(int data);
TreeNode *insertLeft(TreeNode *parent, int data);
TreeNode *insertRight(TreeNode *parent, int data);
void freeTree(TreeNode *root);
int countNodes(TreeNode *root);
int treeHeight(TreeNode *root);
bool isLeaf(TreeNode *node);

#endif
// binary_tree.c
#include "binary_tree.h"

// Create a new node
TreeNode *createNode(int data) {
    TreeNode *newNode = (TreeNode *)malloc(sizeof(TreeNode));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// Insert as left child
TreeNode *insertLeft(TreeNode *parent, int data) {
    if (parent == NULL) {
        printf("Parent is NULL!\n");
        return NULL;
    }
    parent->left = createNode(data);
    return parent->left;
}

// Insert as right child
TreeNode *insertRight(TreeNode *parent, int data) {
    if (parent == NULL) {
        printf("Parent is NULL!\n");
        return NULL;
    }
    parent->right = createNode(data);
    return parent->right;
}

// Free entire tree (post-order deletion)
void freeTree(TreeNode *root) {
    if (root == NULL) return;
    
    freeTree(root->left);   // Free left subtree
    freeTree(root->right);  // Free right subtree
    free(root);             // Free current node
}

// Count total nodes
int countNodes(TreeNode *root) {
    if (root == NULL) return 0;
    
    return 1 + countNodes(root->left) + countNodes(root->right);
}

// Calculate height of tree
int treeHeight(TreeNode *root) {
    if (root == NULL) return -1;  // Empty tree has height -1
    
    int leftHeight = treeHeight(root->left);
    int rightHeight = treeHeight(root->right);
    
    return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}

// Check if node is a leaf
bool isLeaf(TreeNode *node) {
    return (node != NULL && node->left == NULL && node->right == NULL);
}

Building a Binary Tree

// main.c - Building and using a binary tree
#include "binary_tree.h"

int main() {
    // Build this tree:
    //        1
    //       / \
    //      2   3
    //     / \   \
    //    4   5   6
    
    TreeNode *root = createNode(1);
    
    TreeNode *node2 = insertLeft(root, 2);
    TreeNode *node3 = insertRight(root, 3);
    
    insertLeft(node2, 4);
    insertRight(node2, 5);
    insertRight(node3, 6);
    
    printf("Number of nodes: %d\n", countNodes(root));  // 6
    printf("Height of tree: %d\n", treeHeight(root));   // 2
    printf("Is root a leaf? %s\n", isLeaf(root) ? "Yes" : "No");  // No
    printf("Is node 4 a leaf? %s\n", isLeaf(node2->left) ? "Yes" : "No");  // Yes
    
    // Clean up
    freeTree(root);
    
    return 0;
}
Practice Questions

Task: Write a function to count the number of leaf nodes in a binary tree.

Show Solution
int countLeaves(TreeNode *root) {
    if (root == NULL) return 0;
    
    // If leaf node
    if (root->left == NULL && root->right == NULL) {
        return 1;
    }
    
    // Count leaves in left and right subtrees
    return countLeaves(root->left) + countLeaves(root->right);
}

// Example:
//        1
//       / \
//      2   3
//     / \   \
//    4   5   6
// countLeaves returns 3 (nodes 4, 5, 6)

Task: Write a function to convert a binary tree to its mirror image (swap left and right children).

Show Solution
void mirrorTree(TreeNode *root) {
    if (root == NULL) return;
    
    // Swap left and right children
    TreeNode *temp = root->left;
    root->left = root->right;
    root->right = temp;
    
    // Recursively mirror subtrees
    mirrorTree(root->left);
    mirrorTree(root->right);
}

// Before:        After (mirror):
//     1              1
//    / \            / \
//   2   3          3   2
//  / \              / \
// 4   5            5   4
03

Binary Search Trees (BST)

A Binary Search Tree (BST) is a binary tree with a special ordering property: for every node, all values in its left subtree are smaller, and all values in its right subtree are larger. This property enables efficient searching, insertion, and deletion with average O(log n) time complexity.

Data Structure

Binary Search Tree

A Binary Search Tree (BST) is a binary tree where for each node: all keys in the left subtree are less than the node's key, and all keys in the right subtree are greater than the node's key.

BST Property: left.data < node.data < right.data holds for every node and its descendants.

BST Visualization

// Valid BST:
//           50
//          /  \
//        30    70
//       / \    / \
//      20 40  60 80
//
// For node 50: left subtree (20,30,40) < 50 < right subtree (60,70,80) ✓
// For node 30: 20 < 30 < 40 ✓
// For node 70: 60 < 70 < 80 ✓

// NOT a valid BST:
//           50
//          /  \
//        30    70
//       / \      \
//      20 60    80   ← 60 is in left subtree of 50 but 60 > 50!

BST Implementation

// bst.h
#ifndef BST_H
#define BST_H

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct BSTNode {
    int data;
    struct BSTNode *left;
    struct BSTNode *right;
} BSTNode;

BSTNode *createBSTNode(int data);
BSTNode *insertBST(BSTNode *root, int data);
BSTNode *searchBST(BSTNode *root, int key);
BSTNode *findMin(BSTNode *root);
BSTNode *findMax(BSTNode *root);
BSTNode *deleteBST(BSTNode *root, int key);
bool isBST(BSTNode *root, int min, int max);

#endif
// bst.c
#include "bst.h"
#include <limits.h>

// Create a new BST node
BSTNode *createBSTNode(int data) {
    BSTNode *newNode = (BSTNode *)malloc(sizeof(BSTNode));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// Insert a value into BST - O(log n) average, O(n) worst
BSTNode *insertBST(BSTNode *root, int data) {
    // Base case: empty tree or reached insertion point
    if (root == NULL) {
        return createBSTNode(data);
    }
    
    // Recursive case: traverse to correct position
    if (data < root->data) {
        root->left = insertBST(root->left, data);
    } else if (data > root->data) {
        root->right = insertBST(root->right, data);
    }
    // If data == root->data, ignore duplicate (or handle as needed)
    
    return root;
}

// Search for a value - O(log n) average, O(n) worst
BSTNode *searchBST(BSTNode *root, int key) {
    // Base cases: empty tree or found
    if (root == NULL || root->data == key) {
        return root;
    }
    
    // Key is smaller, search left subtree
    if (key < root->data) {
        return searchBST(root->left, key);
    }
    
    // Key is larger, search right subtree
    return searchBST(root->right, key);
}

// Find minimum value (leftmost node) - O(log n)
BSTNode *findMin(BSTNode *root) {
    if (root == NULL) return NULL;
    
    while (root->left != NULL) {
        root = root->left;
    }
    return root;
}

// Find maximum value (rightmost node) - O(log n)
BSTNode *findMax(BSTNode *root) {
    if (root == NULL) return NULL;
    
    while (root->right != NULL) {
        root = root->right;
    }
    return root;
}

// Delete a value from BST - O(log n) average
BSTNode *deleteBST(BSTNode *root, int key) {
    if (root == NULL) return NULL;
    
    // Find the node to delete
    if (key < root->data) {
        root->left = deleteBST(root->left, key);
    } else if (key > root->data) {
        root->right = deleteBST(root->right, key);
    } else {
        // Found the node to delete
        
        // Case 1: Leaf node (no children)
        if (root->left == NULL && root->right == NULL) {
            free(root);
            return NULL;
        }
        
        // Case 2: One child
        if (root->left == NULL) {
            BSTNode *temp = root->right;
            free(root);
            return temp;
        }
        if (root->right == NULL) {
            BSTNode *temp = root->left;
            free(root);
            return temp;
        }
        
        // Case 3: Two children
        // Find inorder successor (smallest in right subtree)
        BSTNode *successor = findMin(root->right);
        root->data = successor->data;
        root->right = deleteBST(root->right, successor->data);
    }
    
    return root;
}

// Verify if tree is a valid BST
bool isBST(BSTNode *root, int min, int max) {
    if (root == NULL) return true;
    
    // Check current node
    if (root->data <= min || root->data >= max) {
        return false;
    }
    
    // Check subtrees with updated bounds
    return isBST(root->left, min, root->data) &&
           isBST(root->right, root->data, max);
}

// Wrapper function
bool isValidBST(BSTNode *root) {
    return isBST(root, INT_MIN, INT_MAX);
}

Using the BST

int main() {
    BSTNode *root = NULL;
    
    // Insert values
    root = insertBST(root, 50);
    root = insertBST(root, 30);
    root = insertBST(root, 70);
    root = insertBST(root, 20);
    root = insertBST(root, 40);
    root = insertBST(root, 60);
    root = insertBST(root, 80);
    
    // Tree structure:
    //           50
    //          /  \
    //        30    70
    //       / \    / \
    //      20 40  60 80
    
    // Search
    BSTNode *found = searchBST(root, 40);
    if (found) printf("Found: %d\n", found->data);  // Found: 40
    
    // Find min and max
    printf("Min: %d\n", findMin(root)->data);  // Min: 20
    printf("Max: %d\n", findMax(root)->data);  // Max: 80
    
    // Delete
    root = deleteBST(root, 30);  // Delete node with two children
    // Now tree:
    //           50
    //          /  \
    //        40    70
    //       /     / \
    //      20    60 80
    
    return 0;
}
BST Deletion Cases: Deleting a node requires handling three cases: (1) leaf node - simply remove, (2) one child - replace with child, (3) two children - replace with inorder successor (or predecessor) and delete that node instead.
Practice Questions

Task: Write a function to find the inorder successor of a given node (next larger value).

Show Solution
BSTNode *inorderSuccessor(BSTNode *root, BSTNode *node) {
    // Case 1: Node has right subtree
    // Successor is the minimum in right subtree
    if (node->right != NULL) {
        return findMin(node->right);
    }
    
    // Case 2: No right subtree
    // Successor is the nearest ancestor for which
    // the given node is in its left subtree
    BSTNode *successor = NULL;
    while (root != NULL) {
        if (node->data < root->data) {
            successor = root;  // Potential successor
            root = root->left;
        } else if (node->data > root->data) {
            root = root->right;
        } else {
            break;  // Found the node
        }
    }
    
    return successor;
}

// Example:
//           50
//          /  \
//        30    70
//       / \   
//      20 40  
// Inorder successor of 30 is 40 (min in right subtree)
// Inorder successor of 40 is 50 (ancestor)

Task: Find the kth smallest element in a BST.

Show Solution
// Using inorder traversal (gives sorted order in BST)
int kthSmallest(BSTNode *root, int k, int *count) {
    if (root == NULL) return -1;
    
    // Search in left subtree
    int result = kthSmallest(root->left, k, count);
    if (result != -1) return result;  // Found in left
    
    // Process current node
    (*count)++;
    if (*count == k) return root->data;
    
    // Search in right subtree
    return kthSmallest(root->right, k, count);
}

// Wrapper function
int findKthSmallest(BSTNode *root, int k) {
    int count = 0;
    return kthSmallest(root, k, &count);
}

// Example:
//           50
//          /  \
//        30    70
//       / \   
//      20 40  
// Inorder: 20, 30, 40, 50, 70
// 1st smallest: 20
// 3rd smallest: 40
04

Tree Traversals

Tree traversal is the process of visiting every node in a tree exactly once. The three main depth-first traversals differ in when the root is processed relative to its subtrees. Level-order traversal (BFS) visits nodes level by level.

Traversal Types Overview

// Example tree for all traversals:
//           1
//          / \
//         2   3
//        / \   \
//       4   5   6

// Inorder (Left, Root, Right):    4, 2, 5, 1, 3, 6
// Preorder (Root, Left, Right):   1, 2, 4, 5, 3, 6
// Postorder (Left, Right, Root):  4, 5, 2, 6, 3, 1
// Level-order (BFS):              1, 2, 3, 4, 5, 6

Inorder Traversal (Left → Root → Right)

// Inorder traversal - For BST, gives sorted order!
void inorder(TreeNode *root) {
    if (root == NULL) return;
    
    inorder(root->left);         // Visit left subtree
    printf("%d ", root->data);   // Process root
    inorder(root->right);        // Visit right subtree
}

// Iterative version using stack
void inorderIterative(TreeNode *root) {
    TreeNode *stack[100];
    int top = -1;
    TreeNode *current = root;
    
    while (current != NULL || top >= 0) {
        // Go to leftmost node
        while (current != NULL) {
            stack[++top] = current;
            current = current->left;
        }
        
        // Process and go right
        current = stack[top--];
        printf("%d ", current->data);
        current = current->right;
    }
}

Preorder Traversal (Root → Left → Right)

// Preorder traversal - Useful for copying trees
void preorder(TreeNode *root) {
    if (root == NULL) return;
    
    printf("%d ", root->data);   // Process root first
    preorder(root->left);        // Visit left subtree
    preorder(root->right);       // Visit right subtree
}

// Iterative version using stack
void preorderIterative(TreeNode *root) {
    if (root == NULL) return;
    
    TreeNode *stack[100];
    int top = -1;
    stack[++top] = root;
    
    while (top >= 0) {
        TreeNode *node = stack[top--];
        printf("%d ", node->data);
        
        // Push right first so left is processed first
        if (node->right) stack[++top] = node->right;
        if (node->left) stack[++top] = node->left;
    }
}

Postorder Traversal (Left → Right → Root)

// Postorder traversal - Useful for deleting trees
void postorder(TreeNode *root) {
    if (root == NULL) return;
    
    postorder(root->left);       // Visit left subtree
    postorder(root->right);      // Visit right subtree
    printf("%d ", root->data);   // Process root last
}

// Why postorder for deletion?
// Children must be deleted before parent to avoid memory leaks
void deleteTree(TreeNode *root) {
    if (root == NULL) return;
    
    deleteTree(root->left);   // Delete left subtree first
    deleteTree(root->right);  // Delete right subtree
    printf("Deleting %d\n", root->data);
    free(root);               // Now safe to delete root
}

Level-Order Traversal (BFS)

// Level-order traversal using queue
void levelOrder(TreeNode *root) {
    if (root == NULL) return;
    
    // Simple queue implementation
    TreeNode *queue[100];
    int front = 0, rear = 0;
    
    queue[rear++] = root;
    
    while (front < rear) {
        TreeNode *node = queue[front++];
        printf("%d ", node->data);
        
        // Enqueue children
        if (node->left) queue[rear++] = node->left;
        if (node->right) queue[rear++] = node->right;
    }
}

// Level-order with level separation
void levelOrderWithLevels(TreeNode *root) {
    if (root == NULL) return;
    
    TreeNode *queue[100];
    int front = 0, rear = 0;
    
    queue[rear++] = root;
    int level = 0;
    
    while (front < rear) {
        int levelSize = rear - front;  // Nodes at current level
        printf("Level %d: ", level);
        
        for (int i = 0; i < levelSize; i++) {
            TreeNode *node = queue[front++];
            printf("%d ", node->data);
            
            if (node->left) queue[rear++] = node->left;
            if (node->right) queue[rear++] = node->right;
        }
        printf("\n");
        level++;
    }
}

// Output:
// Level 0: 1
// Level 1: 2 3
// Level 2: 4 5 6

Traversal Comparison

Traversal Order Use Cases
Inorder Left → Root → Right BST sorted output, expression evaluation
Preorder Root → Left → Right Tree copying, prefix expression, serialization
Postorder Left → Right → Root Tree deletion, postfix expression, size calculation
Level-order Level by level Finding shortest path, level-wise operations
Practice Questions

Task: For the tree below, write the output of all four traversals.

//         A
//        / \
//       B   C
//      /   / \
//     D   E   F
Show Solution
// Inorder (L-Root-R):   D, B, A, E, C, F
// Preorder (Root-L-R):  A, B, D, C, E, F
// Postorder (L-R-Root): D, B, E, F, C, A
// Level-order (BFS):    A, B, C, D, E, F

Task: Given Inorder: [D,B,E,A,F,C] and Preorder: [A,B,D,E,C,F], construct the tree.

Show Solution
// Algorithm:
// 1. First element of preorder is always root → A
// 2. Find A in inorder: [D,B,E] | A | [F,C]
//    - Left subtree has elements: D,B,E
//    - Right subtree has elements: F,C
// 3. Next preorder element B is root of left subtree
// 4. Find B in inorder: [D] | B | [E]
// 5. Continue recursively...

// Result:
//         A
//        / \
//       B   C
//      / \  /
//     D   E F

TreeNode *buildTree(int *inorder, int *preorder, 
                    int inStart, int inEnd, int *preIndex) {
    if (inStart > inEnd) return NULL;
    
    TreeNode *node = createNode(preorder[*preIndex]);
    (*preIndex)++;
    
    if (inStart == inEnd) return node;
    
    // Find index of current root in inorder
    int inIndex;
    for (inIndex = inStart; inIndex <= inEnd; inIndex++) {
        if (inorder[inIndex] == node->data) break;
    }
    
    node->left = buildTree(inorder, preorder, inStart, inIndex-1, preIndex);
    node->right = buildTree(inorder, preorder, inIndex+1, inEnd, preIndex);
    
    return node;
}
05

Tree Operations

Beyond basic traversals, trees support many useful operations including finding the lowest common ancestor, checking tree properties, and calculating various metrics.

Lowest Common Ancestor (LCA)

// Find LCA in a Binary Tree
TreeNode *findLCA(TreeNode *root, int n1, int n2) {
    if (root == NULL) return NULL;
    
    // If either n1 or n2 matches root, root is LCA
    if (root->data == n1 || root->data == n2) {
        return root;
    }
    
    // Look for nodes in left and right subtrees
    TreeNode *leftLCA = findLCA(root->left, n1, n2);
    TreeNode *rightLCA = findLCA(root->right, n1, n2);
    
    // If both return non-NULL, root is LCA
    if (leftLCA && rightLCA) return root;
    
    // Otherwise, return non-NULL child
    return leftLCA ? leftLCA : rightLCA;
}

// LCA in BST (more efficient using BST property)
BSTNode *findLCAinBST(BSTNode *root, int n1, int n2) {
    if (root == NULL) return NULL;
    
    // If both n1 and n2 are smaller, LCA is in left subtree
    if (n1 < root->data && n2 < root->data) {
        return findLCAinBST(root->left, n1, n2);
    }
    
    // If both are larger, LCA is in right subtree
    if (n1 > root->data && n2 > root->data) {
        return findLCAinBST(root->right, n1, n2);
    }
    
    // Otherwise, we're at the split point (or one matches)
    return root;
}

Check if Two Trees are Identical

bool areIdentical(TreeNode *root1, TreeNode *root2) {
    // Both empty
    if (root1 == NULL && root2 == NULL) return true;
    
    // One empty, one not
    if (root1 == NULL || root2 == NULL) return false;
    
    // Check current nodes and subtrees
    return (root1->data == root2->data) &&
           areIdentical(root1->left, root2->left) &&
           areIdentical(root1->right, root2->right);
}

Check if Tree is Balanced

// A tree is balanced if height difference of subtrees <= 1
// for every node

int checkBalance(TreeNode *root) {
    if (root == NULL) return 0;
    
    int leftHeight = checkBalance(root->left);
    if (leftHeight == -1) return -1;  // Left subtree unbalanced
    
    int rightHeight = checkBalance(root->right);
    if (rightHeight == -1) return -1;  // Right subtree unbalanced
    
    // Check current node's balance
    if (abs(leftHeight - rightHeight) > 1) {
        return -1;  // Unbalanced
    }
    
    return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}

bool isBalanced(TreeNode *root) {
    return checkBalance(root) != -1;
}

Path Sum Problems

// Check if path with given sum exists (root to leaf)
bool hasPathSum(TreeNode *root, int targetSum) {
    if (root == NULL) return false;
    
    // Subtract current node's value
    targetSum -= root->data;
    
    // If leaf node, check if sum is 0
    if (root->left == NULL && root->right == NULL) {
        return targetSum == 0;
    }
    
    // Check left or right subtree
    return hasPathSum(root->left, targetSum) ||
           hasPathSum(root->right, targetSum);
}

// Print all root-to-leaf paths
void printPaths(TreeNode *root, int path[], int pathLen) {
    if (root == NULL) return;
    
    // Add current node to path
    path[pathLen++] = root->data;
    
    // If leaf, print path
    if (root->left == NULL && root->right == NULL) {
        printf("Path: ");
        for (int i = 0; i < pathLen; i++) {
            printf("%d ", path[i]);
        }
        printf("\n");
    } else {
        // Continue to children
        printPaths(root->left, path, pathLen);
        printPaths(root->right, path, pathLen);
    }
}

// Wrapper
void printAllPaths(TreeNode *root) {
    int path[100];
    printPaths(root, path, 0);
}

Diameter of Tree

// Diameter: longest path between any two nodes
// May or may not pass through root

int diameterHelper(TreeNode *root, int *diameter) {
    if (root == NULL) return 0;
    
    int leftHeight = diameterHelper(root->left, diameter);
    int rightHeight = diameterHelper(root->right, diameter);
    
    // Update diameter if path through this node is longer
    *diameter = (*diameter > leftHeight + rightHeight) ? 
                *diameter : leftHeight + rightHeight;
    
    return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}

int treeDiameter(TreeNode *root) {
    int diameter = 0;
    diameterHelper(root, &diameter);
    return diameter;
}

// Example:
//           1
//          / \
//         2   3
//        / \
//       4   5
// Diameter = 3 (path: 4 → 2 → 1 → 3 or 5 → 2 → 1 → 3)

Time Complexity Summary

Operation Binary Tree BST (Average) BST (Worst)
Search O(n) O(log n) O(n)
Insert O(1)* O(log n) O(n)
Delete O(n) O(log n) O(n)
Traversal O(n) O(n) O(n)
Find Min/Max O(n) O(log n) O(n)

*Binary tree insert at known position. BST worst case occurs with skewed tree (essentially a linked list).

Practice Questions

Task: Find the maximum sum of any root-to-leaf path.

Show Solution
int maxPathSumToLeaf(TreeNode *root) {
    if (root == NULL) return 0;
    
    // If leaf node
    if (root->left == NULL && root->right == NULL) {
        return root->data;
    }
    
    // Get max sum from left and right subtrees
    int leftSum = INT_MIN, rightSum = INT_MIN;
    
    if (root->left) {
        leftSum = maxPathSumToLeaf(root->left);
    }
    if (root->right) {
        rightSum = maxPathSumToLeaf(root->right);
    }
    
    // Return max of left/right plus current
    return root->data + (leftSum > rightSum ? leftSum : rightSum);
}

// Example:
//         10
//        /  \
//       2    10
//      / \     \
//     20  1    -25
// Max path sum: 10 → 2 → 20 = 32

Task: Convert a BST to string (serialize) and back to BST (deserialize).

Show Solution
// Serialize using preorder (root first)
void serialize(BSTNode *root, char *buffer, int *index) {
    if (root == NULL) {
        buffer[(*index)++] = '#';  // Null marker
        buffer[(*index)++] = ',';
        return;
    }
    
    *index += sprintf(buffer + *index, "%d,", root->data);
    serialize(root->left, buffer, index);
    serialize(root->right, buffer, index);
}

// Deserialize
BSTNode *deserializeHelper(char **str) {
    if (**str == '#') {
        (*str) += 2;  // Skip '#,'
        return NULL;
    }
    
    int val = 0;
    while (**str != ',') {
        val = val * 10 + (**str - '0');
        (*str)++;
    }
    (*str)++;  // Skip ','
    
    BSTNode *node = createBSTNode(val);
    node->left = deserializeHelper(str);
    node->right = deserializeHelper(str);
    
    return node;
}

BSTNode *deserialize(char *buffer) {
    return deserializeHelper(&buffer);
}

// Example:
//     5
//    / \
//   3   7
// Serialized: "5,3,#,#,7,#,#,"
06

Tree Balancing

An unbalanced BST can degrade to O(n) operations. Tree balancing techniques ensure the tree maintains a height of O(log n), guaranteeing efficient operations. Self-balancing trees automatically restructure during insertions and deletions.

The Skewed Tree Problem: Inserting sorted data (1,2,3,4,5) into a BST creates a skewed tree (essentially a linked list), causing O(n) search time instead of O(log n).

Balance Factor

// Balance Factor = Height(left subtree) - Height(right subtree)
// For a balanced tree: -1 <= Balance Factor <= 1

int getBalanceFactor(TreeNode *node) {
    if (node == NULL) return 0;
    return treeHeight(node->left) - treeHeight(node->right);
}

// Balanced tree (BF = 0, -1, or 1 for all nodes):
//         50 (BF=0)
//        /  \
//      30    70 (BF=0)
//     /       \
//    20       80

// Unbalanced tree (BF = 2 at root):
//         50 (BF=2)
//        /
//      30 (BF=1)
//     /
//    20 (BF=0)

Tree Rotations

// Rotations are the fundamental operations for balancing
// They restructure the tree while maintaining BST property

// Right Rotation (for left-heavy trees)
//       y                x
//      / \             /   \
//     x   T3   -->    T1    y
//    / \                   / \
//   T1 T2                 T2 T3

TreeNode *rightRotate(TreeNode *y) {
    TreeNode *x = y->left;
    TreeNode *T2 = x->right;
    
    // Perform rotation
    x->right = y;
    y->left = T2;
    
    return x;  // New root of subtree
}

// Left Rotation (for right-heavy trees)
//     x                    y
//    / \                 /   \
//   T1  y     -->       x    T3
//      / \             / \
//     T2 T3           T1 T2

TreeNode *leftRotate(TreeNode *x) {
    TreeNode *y = x->right;
    TreeNode *T2 = y->left;
    
    // Perform rotation
    y->left = x;
    x->right = T2;
    
    return y;  // New root of subtree
}

AVL Tree Insertion (Self-Balancing)

// AVL Tree: Self-balancing BST where balance factor is always -1, 0, or 1

typedef struct AVLNode {
    int data;
    struct AVLNode *left;
    struct AVLNode *right;
    int height;
} AVLNode;

int height(AVLNode *node) {
    return node ? node->height : 0;
}

int max(int a, int b) {
    return (a > b) ? a : b;
}

AVLNode *createAVLNode(int data) {
    AVLNode *node = (AVLNode *)malloc(sizeof(AVLNode));
    node->data = data;
    node->left = node->right = NULL;
    node->height = 1;  // New node starts at height 1
    return node;
}

// AVL Insert with automatic balancing
AVLNode *insertAVL(AVLNode *node, int data) {
    // 1. Standard BST insertion
    if (node == NULL) return createAVLNode(data);
    
    if (data < node->data)
        node->left = insertAVL(node->left, data);
    else if (data > node->data)
        node->right = insertAVL(node->right, data);
    else
        return node;  // Duplicates not allowed
    
    // 2. Update height
    node->height = 1 + max(height(node->left), height(node->right));
    
    // 3. Get balance factor
    int balance = height(node->left) - height(node->right);
    
    // 4. Handle 4 unbalanced cases
    
    // Left Left Case (right rotate)
    if (balance > 1 && data < node->left->data)
        return rightRotate(node);
    
    // Right Right Case (left rotate)
    if (balance < -1 && data > node->right->data)
        return leftRotate(node);
    
    // Left Right Case (left-right rotate)
    if (balance > 1 && data > node->left->data) {
        node->left = leftRotate(node->left);
        return rightRotate(node);
    }
    
    // Right Left Case (right-left rotate)
    if (balance < -1 && data < node->right->data) {
        node->right = rightRotate(node->right);
        return leftRotate(node);
    }
    
    return node;  // Return unchanged node
}

Types of Self-Balancing Trees

Tree Type Balance Condition Use Case
AVL Tree Height difference ≤ 1 Lookup-intensive applications
Red-Black Tree Color-based rules Standard library implementations
B-Tree Multiple keys per node Databases, file systems
Splay Tree Move accessed node to root Caching, frequently accessed data
07

Tree Applications

Trees are fundamental data structures used extensively in computer science. From organizing file systems to powering search engines, trees provide efficient hierarchical organization and fast operations.

1. Expression Trees

// Expression trees represent arithmetic expressions
// Leaves are operands, internal nodes are operators

// Expression: (3 + 5) * (2 - 1)
//           *
//          / \
//         +   -
//        / \ / \
//       3  5 2  1

typedef struct ExprNode {
    char op;        // Operator or '\0' if operand
    int value;      // Value if operand
    struct ExprNode *left;
    struct ExprNode *right;
} ExprNode;

// Evaluate expression tree (postorder)
int evaluate(ExprNode *root) {
    if (root == NULL) return 0;
    
    // Leaf node (operand)
    if (root->left == NULL && root->right == NULL) {
        return root->value;
    }
    
    // Internal node (operator)
    int leftVal = evaluate(root->left);
    int rightVal = evaluate(root->right);
    
    switch (root->op) {
        case '+': return leftVal + rightVal;
        case '-': return leftVal - rightVal;
        case '*': return leftVal * rightVal;
        case '/': return leftVal / rightVal;
    }
    return 0;
}

2. File System Representation

// File systems are tree structures
// Directories are internal nodes, files are leaves

typedef struct FileNode {
    char name[256];
    int isDirectory;           // 1 = directory, 0 = file
    long size;                 // File size in bytes
    struct FileNode *firstChild;  // First child (for directories)
    struct FileNode *nextSibling; // Next sibling
} FileNode;

// Print directory tree structure
void printTree(FileNode *node, int depth) {
    if (node == NULL) return;
    
    // Print indentation
    for (int i = 0; i < depth; i++) printf("  ");
    
    if (node->isDirectory) {
        printf("[DIR] %s/\n", node->name);
        printTree(node->firstChild, depth + 1);
    } else {
        printf("%s (%ld bytes)\n", node->name, node->size);
    }
    
    printTree(node->nextSibling, depth);
}

// Output:
// [DIR] root/
//   [DIR] documents/
//     report.txt (1024 bytes)
//     notes.pdf (2048 bytes)
//   [DIR] images/
//     photo.jpg (50000 bytes)

3. Autocomplete / Trie (Prefix Tree)

// Trie: Tree for efficient prefix-based search
// Used in autocomplete, spell checkers, IP routing

#define ALPHABET_SIZE 26

typedef struct TrieNode {
    struct TrieNode *children[ALPHABET_SIZE];
    bool isEndOfWord;
} TrieNode;

TrieNode *createTrieNode() {
    TrieNode *node = (TrieNode *)malloc(sizeof(TrieNode));
    node->isEndOfWord = false;
    for (int i = 0; i < ALPHABET_SIZE; i++)
        node->children[i] = NULL;
    return node;
}

// Insert word into trie
void insertTrie(TrieNode *root, const char *word) {
    TrieNode *current = root;
    
    while (*word) {
        int index = *word - 'a';
        if (current->children[index] == NULL)
            current->children[index] = createTrieNode();
        current = current->children[index];
        word++;
    }
    current->isEndOfWord = true;
}

// Search for word
bool searchTrie(TrieNode *root, const char *word) {
    TrieNode *current = root;
    
    while (*word) {
        int index = *word - 'a';
        if (current->children[index] == NULL)
            return false;
        current = current->children[index];
        word++;
    }
    return current->isEndOfWord;
}

// Check if prefix exists (for autocomplete)
bool startsWith(TrieNode *root, const char *prefix) {
    TrieNode *current = root;
    
    while (*prefix) {
        int index = *prefix - 'a';
        if (current->children[index] == NULL)
            return false;
        current = current->children[index];
        prefix++;
    }
    return true;  // Prefix found
}

4. Huffman Coding (Data Compression)

// Huffman tree for lossless data compression
// More frequent characters get shorter codes

typedef struct HuffmanNode {
    char ch;           // Character (for leaves)
    int freq;          // Frequency
    struct HuffmanNode *left, *right;
} HuffmanNode;

// Build codes by traversing tree
void buildCodes(HuffmanNode *root, char *code, int depth) {
    if (root == NULL) return;
    
    // Leaf node - print character and its code
    if (root->left == NULL && root->right == NULL) {
        code[depth] = '\0';
        printf("'%c': %s\n", root->ch, code);
        return;
    }
    
    // Left edge = 0, Right edge = 1
    code[depth] = '0';
    buildCodes(root->left, code, depth + 1);
    
    code[depth] = '1';
    buildCodes(root->right, code, depth + 1);
}

// Example output for "aabbbc":
// 'b': 0      (most frequent - shortest code)
// 'a': 10
// 'c': 11

Common Tree Applications Summary

Databases
  • B-trees for indexing
  • Fast query execution
  • Sorted data storage
File Systems
  • Directory hierarchies
  • Path navigation
  • Permission inheritance
Networking
  • IP routing tables
  • DNS hierarchies
  • Network topology
AI / Games
  • Decision trees (ML)
  • Game state trees
  • Minimax algorithms

Key Takeaways

Hierarchical Structure

Trees model parent-child relationships; N nodes = N-1 edges

Binary Tree

Each node has at most 2 children (left and right)

BST Property

left < root < right; enables O(log n) search

Traversal Order

Inorder: sorted; Preorder: copy; Postorder: delete

Recursive Nature

Most tree operations are naturally recursive

Balance Matters

Skewed BST degrades to O(n); balanced trees stay O(log n)

Knowledge Check

Quick Quiz

Test what you have learned about trees and binary trees in C

1 How many edges does a tree with 10 nodes have?
2 Which traversal visits nodes in sorted order for a BST?
3 What is the BST property?
4 Which traversal is best for deleting all nodes in a tree?
5 What is the worst-case time complexity for searching in a BST?
6 In a BST, where is the minimum value always located?
Answer all questions to check your score