Data structures in C · about 65 min

Binary search trees

Why this matters

A hash table finds a key in O(1), but it keeps keys in no useful order: you cannot ask it for "all names between Adams and Baker" or "the smallest timestamp after 10:00". A binary search tree keeps keys sorted at all times while still supporting fast insertion, lookup and deletion, typically O(log n). Balanced variants of it sit inside database indexes, file systems, the Linux kernel's scheduler and memory manager (red-black trees), and the ordered maps of C++ and Java. Trees are also the natural home of recursion: almost every tree operation is three lines once you see the recursive shape.

Trees, briefly

A binary tree is made of nodes, each with up to two children, left and right. The top node is the root; nodes without children are leaves. Each child is itself the root of a smaller tree, a subtree, which is why recursive code fits so well.

typedef struct Tree {
    int key;
    struct Tree *left, *right;
} Tree;

Just like a linked list, a tree is a collection of separately allocated nodes connected by pointers, and the whole tree is represented by a pointer to its root (NULL for an empty tree).

The BST property

A binary search tree adds one rule, for every node: all keys in its left subtree are smaller than its key, and all keys in its right subtree are larger.

  inserting 50, 30, 70, 20, 40, 60, 80:

              50                 everything left of 50 is < 50
            /    \               everything right of 50 is > 50
          30      70
         /  \    /  \
        20  40  60  80

To search, start at the root and compare: smaller goes left, larger goes right, equal means found, NULL means absent. Each step discards an entire subtree, so in a well-shaped tree with a million keys a search takes about 20 comparisons (2^20 is about a million). That is binary search, but on a structure you can insert into without shifting an array.

The operations

/* Insert key, ignoring duplicates. Returns the (possibly new) root. */
Tree *insert(Tree *root, int key)
{
    if (root == NULL) {
        Tree *n = malloc(sizeof *n);
        if (n == NULL)
            return NULL;
        n->key = key;
        n->left = n->right = NULL;
        return n;
    }
    if (key < root->key)
        root->left = insert(root->left, key);
    else if (key > root->key)
        root->right = insert(root->right, key);
    return root;
}

int contains(const Tree *t, int key)
{
    while (t != NULL) {
        if (key == t->key)
            return 1;
        t = key < t->key ? t->left : t->right;
    }
    return 0;
}

insert says: inserting into an empty tree produces a one-node tree; otherwise, insert into the correct subtree and store whatever comes back as that child. The assignment root->left = insert(root->left, key) is what links a new node into place: when the recursion reaches the NULL child, it returns the new node, and the caller stores it. Returning the root also lets main handle the first insertion, when root is NULL, with no special case (the same problem the Node ** technique solved for lists).

contains needs no recursion at all: it follows one path down, so a loop is simplest.

Traversals

Visiting every node has three classic recursive orders, named by when the node itself is handled:

void print_inorder(const Tree *t)        /* left, node, right */
{
    if (t == NULL)
        return;
    print_inorder(t->left);
    printf("%d ", t->key);
    print_inorder(t->right);
}
void free_tree(Tree *t)
{
    if (t == NULL)
        return;
    free_tree(t->left);
    free_tree(t->right);
    free(t);
}

int height(const Tree *t)
{
    if (t == NULL)
        return 0;
    int l = height(t->left), r = height(t->right);
    return 1 + (l > r ? l : r);
}

Putting them together:

int main(void)
{
    int keys[] = {50, 30, 70, 20, 40, 60, 80, 30};
    Tree *root = NULL;
    for (int i = 0; i < 8; i++)
        root = insert(root, keys[i]);
    print_inorder(root);
    printf("\nheight %d, contains 60: %d, contains 65: %d\n",
           height(root), contains(root, 60), contains(root, 65));
    free_tree(root);

    root = NULL;
    for (int i = 1; i <= 7; i++)
        root = insert(root, i * 10);
    printf("sorted input -> height %d\n", height(root));
    free_tree(root);
    return 0;
}
20 30 40 50 60 70 80
height 3, contains 60: 1, contains 65: 0
sorted input -> height 7

The duplicate 30 was ignored, and valgrind confirms all nodes were freed.

Recursion and the call stack

Each recursive call gets its own stack frame holding its own t, as you saw in the functions module. The depth of recursion equals the height of the tree. For a balanced tree of a million keys that is about 20 frames, nothing at all. But see the last line of the output above.

The weak spot: degenerate trees

Inserting keys in sorted order builds a tree where every node has only a right child:

  10
    \
     20
       \
        30          height 7 for 7 nodes:
          \         a linked list wearing a tree costume
           ...

Search, insert and height all become O(n), and recursive functions recurse n levels deep: a tree built from a million sorted keys would overflow the stack in the recursive insert. Sorted input is not exotic; it is what you get from timestamps, IDs and already-sorted files.

The real-world fix is a self-balancing tree, which restructures itself with small local "rotations" during insertion so its height stays O(log n) whatever the input order. The common ones are AVL trees and red-black trees; the Linux kernel's rbtree.h and C++'s std::map are red-black trees. They are beyond this lesson, but the BST you wrote is their foundation: they are BSTs with extra bookkeeping.

Deleting, in one paragraph

Removing a key has three cases: a leaf is simply freed (and its parent's pointer set to NULL); a node with one child is replaced by that child; a node with two children takes the key of its in-order successor (the smallest key in its right subtree), and that successor node, which has at most one child, is deleted instead. Try it as an exercise after this lesson.

Common mistakes

Recap

Going further: implement delete_key with the three cases above and test it under valgrind with random insertions and deletions, checking is_bst after every step. Then read the comments at the top of the Linux kernel's lib/rbtree.c for a description of red-black tree rules.

Exercises

Exercise 1 · quiz

Traversal order

Inserting 50, 30, 70, 20, 40, 60, 80 into an empty BST gives this tree:

            50
          /    \
        30      70
       /  \    /  \
      20  40  60  80

A pre-order traversal visits a node, then its left subtree, then its right subtree. What does it print?

Answer
  • 50 30 20 40 70 60 80

Pre-order: node first (50), then all of the left subtree in pre-order (30 20 40), then the right (70 60 80). In-order (left, node, right) gives the sorted sequence 20 30 40 50 60 70 80, which is the reason BSTs exist. Post-order (left, right, node) gives 20 40 30 60 80 70 50, the order to free a tree in, since children go before their parent. The last choice is level order (breadth first), which needs a queue rather than recursion.

Exercise 2 · write code

Sort with a tree

Complete insert (ignore duplicate keys) and free_tree (free every node: children before the parent). main reads integers from stdin, inserts them, and prints them in order on one line separated by single spaces, which sorts them and removes duplicates.

$ echo "5 3 8 1 3 9 5" | ./prog
1 3 5 8 9

The check also runs the program on larger input under valgrind.

File: main.c

Starting point

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

typedef struct Tree {
    int key;
    struct Tree *left, *right;
} Tree;

Tree *insert(Tree *root, int key)
{
    return root;
}

void free_tree(Tree *t)
{
    free(t);
}

static int first = 1;

void print_inorder(const Tree *t)
{
    if (t == NULL)
        return;
    print_inorder(t->left);
    printf(first ? "%d" : " %d", t->key);
    first = 0;
    print_inorder(t->right);
}

int main(void)
{
    Tree *root = NULL;
    int x;
    while (scanf("%d", &x) == 1) {
        Tree *r = insert(root, x);
        if (r == NULL) {
            free_tree(root);
            return 1;
        }
        root = r;
    }
    print_inorder(root);
    printf("\n");
    free_tree(root);
    return 0;
}

Example

input

5 3 8 1 3 9 5

expected output

1 3 5 8 9

Example

input

1 2 3 4 5 6

expected output

1 2 3 4 5 6
Hint 1

Base case: an empty tree (root == NULL) becomes a new single node with both children NULL.

Hint 2

Otherwise recurse into the correct side and store the result back: root->left = insert(root->left, key); then return root.

Hint 3

free_tree is post-order: free_tree(t->left); free_tree(t->right); free(t); with a NULL check first.

Reference solution
#include <stdio.h>
#include <stdlib.h>

typedef struct Tree {
    int key;
    struct Tree *left, *right;
} Tree;

Tree *insert(Tree *root, int key)
{
    if (root == NULL) {
        Tree *n = malloc(sizeof *n);
        if (n == NULL)
            return NULL;
        n->key = key;
        n->left = n->right = NULL;
        return n;
    }
    if (key < root->key)
        root->left = insert(root->left, key);
    else if (key > root->key)
        root->right = insert(root->right, key);
    return root;
}

void free_tree(Tree *t)
{
    if (t == NULL)
        return;
    free_tree(t->left);
    free_tree(t->right);
    free(t);
}

static int first = 1;

void print_inorder(const Tree *t)
{
    if (t == NULL)
        return;
    print_inorder(t->left);
    printf(first ? "%d" : " %d", t->key);
    first = 0;
    print_inorder(t->right);
}

int main(void)
{
    Tree *root = NULL;
    int x;
    while (scanf("%d", &x) == 1) {
        Tree *r = insert(root, x);
        if (r == NULL) {
            free_tree(root);
            return 1;
        }
        root = r;
    }
    print_inorder(root);
    printf("\n");
    free_tree(root);
    return 0;
}

Exercise 3 · write code

Measure the tree

Write two recursive functions:

main inserts the numbers from stdin and prints nodes N height H. Try both a shuffled and a sorted input and compare the heights:

$ echo "50 30 70 20 40 60 80" | ./prog
nodes 7 height 3
$ echo "10 20 30 40 50 60 70" | ./prog
nodes 7 height 7

File: main.c

Starting point

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

typedef struct Tree {
    int key;
    struct Tree *left, *right;
} Tree;

int count_nodes(const Tree *t)
{
    return 0;
}

int height(const Tree *t)
{
    return 0;
}

Tree *insert(Tree *root, int key)
{
    if (root == NULL) {
        Tree *n = malloc(sizeof *n);
        if (n == NULL)
            return NULL;
        n->key = key;
        n->left = n->right = NULL;
        return n;
    }
    if (key < root->key)
        root->left = insert(root->left, key);
    else if (key > root->key)
        root->right = insert(root->right, key);
    return root;
}

void free_tree(Tree *t)
{
    if (t == NULL)
        return;
    free_tree(t->left);
    free_tree(t->right);
    free(t);
}

int main(void)
{
    Tree *root = NULL;
    int x;
    while (scanf("%d", &x) == 1) {
        Tree *r = insert(root, x);
        if (r == NULL) {
            free_tree(root);
            return 1;
        }
        root = r;
    }
    printf("nodes %d height %d\n", count_nodes(root), height(root));
    free_tree(root);
    return 0;
}

Example

input

50 30 70 20 40 60 80

expected output

nodes 7 height 3

Example

input

10 20 30 40 50 60 70

expected output

nodes 7 height 7
Hint 1

Both start with: if (t == NULL) return 0;

Hint 2

A tree's node count is 1 plus the counts of its two subtrees. Its height is 1 plus the larger of the two subtree heights.

Reference solution
#include <stdio.h>
#include <stdlib.h>

typedef struct Tree {
    int key;
    struct Tree *left, *right;
} Tree;

int count_nodes(const Tree *t)
{
    if (t == NULL)
        return 0;
    return 1 + count_nodes(t->left) + count_nodes(t->right);
}

int height(const Tree *t)
{
    if (t == NULL)
        return 0;
    int l = height(t->left), r = height(t->right);
    return 1 + (l > r ? l : r);
}

Tree *insert(Tree *root, int key)
{
    if (root == NULL) {
        Tree *n = malloc(sizeof *n);
        if (n == NULL)
            return NULL;
        n->key = key;
        n->left = n->right = NULL;
        return n;
    }
    if (key < root->key)
        root->left = insert(root->left, key);
    else if (key > root->key)
        root->right = insert(root->right, key);
    return root;
}

void free_tree(Tree *t)
{
    if (t == NULL)
        return;
    free_tree(t->left);
    free_tree(t->right);
    free(t);
}

int main(void)
{
    Tree *root = NULL;
    int x;
    while (scanf("%d", &x) == 1) {
        Tree *r = insert(root, x);
        if (r == NULL) {
            free_tree(root);
            return 1;
        }
        root = r;
    }
    printf("nodes %d height %d\n", count_nodes(root), height(root));
    free_tree(root);
    return 0;
}

Exercise 4 · write code

Is it really a BST?

Write int is_bst(const Tree *t) that returns 1 if the tree satisfies the BST property (every key in a node's left subtree is smaller than the node's key, every key in its right subtree is larger; no duplicates) and 0 otherwise.

Careful: checking only that each node's direct children are on the correct side is not enough. This tree passes that local check but is not a BST, because 60 sits in the left subtree of 50:

        50
       /  \
     30    70
       \
        60

Hint: pass down the range of keys allowed at each node. A helper such as static int in_range(const Tree *t, long lo, long hi) (keys must satisfy lo < key < hi) works; start it with bounds outside the int range, for example LONG_MIN and LONG_MAX (long is 64 bits in the sandbox).

The check builds many trees by hand and calls your function. main builds the two trees above and prints:

good 1
bad 0

File: main.c

Starting point

#include <stdio.h>
#include <limits.h>

typedef struct Tree {
    int key;
    struct Tree *left, *right;
} Tree;

int is_bst(const Tree *t)
{
    if (t == NULL)
        return 1;
    if (t->left != NULL && t->left->key >= t->key)
        return 0;
    if (t->right != NULL && t->right->key <= t->key)
        return 0;
    return is_bst(t->left) && is_bst(t->right);
}

int main(void)
{
    Tree n20 = {20, NULL, NULL}, n40 = {40, NULL, NULL};
    Tree n30 = {30, &n20, &n40}, n70 = {70, NULL, NULL};
    Tree good = {50, &n30, &n70};

    Tree b60 = {60, NULL, NULL};
    Tree b30 = {30, NULL, &b60}, b70 = {70, NULL, NULL};
    Tree bad = {50, &b30, &b70};

    printf("good %d\nbad %d\n", is_bst(&good), is_bst(&bad));
    return 0;
}

Example

expected output

good 1
bad 0
Hint 1

At the root any key is allowed. Going left from a node with key k, every key below must be < k; going right, > k. And the bounds from higher up still apply.

Hint 2

in_range(t->left, lo, t->key) && in_range(t->right, t->key, hi), after checking lo < t->key < hi.

Reference solution
#include <stdio.h>
#include <limits.h>

typedef struct Tree {
    int key;
    struct Tree *left, *right;
} Tree;

static int in_range(const Tree *t, long lo, long hi)
{
    if (t == NULL)
        return 1;
    if (t->key <= lo || t->key >= hi)
        return 0;
    return in_range(t->left, lo, t->key) && in_range(t->right, t->key, hi);
}

int is_bst(const Tree *t)
{
    return in_range(t, LONG_MIN, LONG_MAX);
}

int main(void)
{
    Tree n20 = {20, NULL, NULL}, n40 = {40, NULL, NULL};
    Tree n30 = {30, &n20, &n40}, n70 = {70, NULL, NULL};
    Tree good = {50, &n30, &n70};

    Tree b60 = {60, NULL, NULL};
    Tree b30 = {30, NULL, &b60}, b70 = {70, NULL, NULL};
    Tree bad = {50, &b30, &b70};

    printf("good %d\nbad %d\n", is_bst(&good), is_bst(&bad));
    return 0;
}