Command Palette

Search for a command to run...

MTH 4300

Lecture 19

What Is a Tree?

A tree is a collection of nodes connected by edges, with no cycles. Every node except the root has exactly one parent; every node can have zero or more children. Nodes with no children are called leaves.

Key terms:

  • Depth of a node — number of edges from the root to that node
  • Height of a tree — depth of the deepest leaf
  • Subtree — a node together with all of its descendants

A binary tree restricts each node to at most two children, called left and right. Each node stores a value and two pointers.

Binary tree node
struct Node {
    int   value;
    Node* left;
    Node* right;
    Node(int v) : value(v), left(nullptr), right(nullptr) {}
};

You can build a binary tree by allocating nodes and wiring up the pointers manually:

Building a binary tree by hand
Node* root = new Node(5);
root->left  = new Node(2);
root->right = new Node(9);
root->left->left  = new Node(1);
root->left->right = new Node(4);

Unlike a linked list, a binary tree branches in two directions. The shape of the tree — which values end up where — is entirely determined by how you wire the pointers. Later, when we add a rule for where to place each value, that rule becomes the BST invariant.

Traversals

To visit every node in a binary tree, you need an ordering. Four standard orderings exist:

TraversalStrategyVisit orderTypical use
InorderRecursiveleft → node → rightproduces sorted output on a BST
PreorderRecursivenode → left → rightcopy or serialize a tree
PostorderRecursiveleft → right → nodedelete or evaluate (leaves first)
Level orderQueue (BFS)row by row, top to bottomshortest path, level-wise processing

The first three are recursive and follow a depth-first pattern — they go as deep as possible before backtracking. Level order is different: it visits every node at depth 0, then every node at depth 1, and so on. It uses a queue rather than recursion.

Recursive traversals
void inorder(Node* node) {
    if (!node) return;
    inorder(node->left);
    std::cout << node->value << " ";
    inorder(node->right);
}

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

void postorder(Node* node) {
    if (!node) return;
    postorder(node->left);
    postorder(node->right);
    std::cout << node->value << " ";
}
Level order traversal — iterative, uses a queue
#include <queue>

void levelorder(Node* root) {
    if (!root) return;
    std::queue<Node*> q;
    q.push(root);
    while (!q.empty()) {
        Node* cur = q.front();
        q.pop();
        std::cout << cur->value << " ";
        if (cur->left)  q.push(cur->left);
        if (cur->right) q.push(cur->right);
    }
}

The queue enforces the row-by-row order: when you dequeue a node and process it, you immediately enqueue its children. Those children sit behind all other nodes at the same depth, so they are processed only after the current row is exhausted.

Running all four on the tree above:

Traversal trace — tree: 5, left={2,left=1,right=4}, right=9
// Inorder:     1 2 4 5 9   (left subtree, root, right subtree)
// Preorder:    5 2 1 4 9   (root first, then subtrees)
// Postorder:   1 4 2 9 5   (both subtrees before the root)
// Level order: 5 2 9 1 4   (row by row: depth 0 → 1 → 2)

The base case if (!node) return terminates the recursive traversals. Every leaf has left == nullptr and right == nullptr, so both recursive calls hit this base case immediately. Level order handles the same edge case by checking if (!root) return before the queue is even created.

Binary Search Trees

The binary tree above has no rule governing where values go — you decide the shape manually. A binary search tree (BST) adds one invariant:

For every node, every value in its left subtree is strictly less than the node's value, and every value in its right subtree is strictly greater.

This invariant is maintained automatically by insert. Instead of wiring pointers by hand, you call insert and it walks the tree to find the correct position:

The payoff is O(h) search — where h is the tree height — instead of O(n). At each node, you can eliminate the entire left or right subtree from consideration based on a single comparison.

A second payoff: inorder traversal on a BST always produces values in ascending order. That property falls out of the invariant for free.

BST Implementation

BST<T>
#include <queue>

template <typename T>
class BST {
public:
    BST()  : root_(nullptr) {}
    ~BST() { destroy(root_); }

    void insert(const T& value) { root_ = insert(root_, value); }
    bool find(const T& value)   const { return find(root_, value); }
    void remove(const T& value) { root_ = remove(root_, value); }

    void inorder()     const { inorder(root_);   std::cout << "\n"; }
    void preorder()    const { preorder(root_);  std::cout << "\n"; }
    void postorder()   const { postorder(root_); std::cout << "\n"; }
    void levelorder()  const { levelorder(root_); }

private:
    struct Node {
        T     value;
        Node* left;
        Node* right;
        Node(const T& v) : value(v), left(nullptr), right(nullptr) {}
    };

    Node* root_;

    Node* insert(Node* node, const T& value) {
        if (!node) return new Node(value);
        if      (value < node->value) node->left  = insert(node->left,  value);
        else if (value > node->value) node->right = insert(node->right, value);
        return node; // duplicate: ignore
    }

    bool find(Node* node, const T& value) const {
        if (!node) return false;
        if (value == node->value) return true;
        if (value <  node->value) return find(node->left,  value);
        return find(node->right, value);
    }

    Node* remove(Node* node, const T& value) {
        if (!node) return nullptr;
        if      (value < node->value) node->left  = remove(node->left,  value);
        else if (value > node->value) node->right = remove(node->right, value);
        else {
            if (!node->left)  { Node* r = node->right; delete node; return r; }
            if (!node->right) { Node* l = node->left;  delete node; return l; }
            // two children: replace with in-order successor, then remove it
            Node* succ = node->right;
            while (succ->left) succ = succ->left;
            node->value = succ->value;
            node->right = remove(node->right, succ->value);
        }
        return node;
    }

    void inorder(Node* node) const {
        if (!node) return;
        inorder(node->left);
        std::cout << node->value << " ";
        inorder(node->right);
    }

    void preorder(Node* node) const {
        if (!node) return;
        std::cout << node->value << " ";
        preorder(node->left);
        preorder(node->right);
    }

    void postorder(Node* node) const {
        if (!node) return;
        postorder(node->left);
        postorder(node->right);
        std::cout << node->value << " ";
    }

    void levelorder(Node* root) const {
        if (!root) return;
        std::queue<Node*> q;
        q.push(root);
        while (!q.empty()) {
            Node* cur = q.front();
            q.pop();
            std::cout << cur->value << " ";
            if (cur->left)  q.push(cur->left);
            if (cur->right) q.push(cur->right);
        }
        std::cout << "\n";
    }

    void destroy(Node* node) {
        if (!node) return;
        destroy(node->left);
        destroy(node->right);
        delete node;
    }
};

Each public method delegates to a private recursive helper that receives the current node and returns the (possibly updated) node pointer. This lets insert and remove reattach subtrees without needing a parent pointer.

Insert and Find

insert walks the BST invariant to find the right slot, allocating a new node when it reaches nullptr. Duplicates fall through to return node with no allocation.

Insert trace — inserting 4 into {5, 2, 7}
// insert(root=5, 4): 4 < 5 → recurse left
//   insert(node=2, 4): 4 > 2 → recurse right
//     insert(nullptr, 4): allocate Node(4), return it
//   node(2)->right = Node(4); return node(2)
// node(5)->left = node(2); return node(5)

find exploits the same invariant to eliminate half the remaining tree at every step — O(h) instead of O(n).

Find trace — searching for 4
// find(root=5, 4): 4 < 5 → go left
//   find(node=2, 4): 4 > 2 → go right
//     find(node=4, 4): 4 == 4 → return true

Remove

Removing a node has three cases depending on how many children it has:

Case 1 — leaf: delete the node and return nullptr to the parent.

Case 2 — one child: delete the node and return its surviving child; it slides up one level.

Case 3 — two children: the node cannot simply be deleted — two subtrees would be left dangling. Instead, find the in-order successor: the leftmost node of the right subtree, which is the smallest value greater than the current node. Copy the successor's value into the current node, then recursively remove the successor from the right subtree (the successor has at most one child, so this reduces to case 1 or 2).

Remove trace — remove 5 from: 5, left={2,right=4}, right={7,left=6}
// find 5 at root — two children
// in-order successor: leftmost of right subtree {7,left=6} → node 6
// copy 6 into root: root is now 6
// remove 6 from right subtree:
//   6 is a leaf at node 7's left → delete, return nullptr
// Tree: 6, left={2,right=4}, right={7}

Always use the in-order successor (leftmost of right subtree) when removing a two-child node. Using an arbitrary child breaks the BST invariant for the remaining subtree.

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

struct Node {
    int   value;
    Node* left;
    Node* right;
    Node(int v) : value(v), left(nullptr), right(nullptr) {}
};

#include <queue>

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

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

void levelorder(Node* root) {
    if (!root) return;
    std::queue<Node*> q;
    q.push(root);
    while (!q.empty()) {
        Node* cur = q.front(); q.pop();
        std::cout << cur->value << " ";
        if (cur->left)  q.push(cur->left);
        if (cur->right) q.push(cur->right);
    }
}

int main() {
    Node* root    = new Node(10);
    root->left    = new Node(5);
    root->right   = new Node(15);
    root->left->left  = new Node(3);
    root->left->right = new Node(7);

    inorder(root);     std::cout << "\n"; // line A
    preorder(root);    std::cout << "\n"; // line B
    levelorder(root);  std::cout << "\n"; // line C

    return 0;
}

Q2.

Without running the code, draw the BST after all insertions, then predict the output.

#include <iostream>

int main() {
    BST<int> t;
    t.insert(8);
    t.insert(3);
    t.insert(10);
    t.insert(1);
    t.insert(6);
    t.insert(14);
    t.insert(4);
    t.insert(7);

    t.preorder();    // line A
    t.inorder();     // line B
    t.postorder();   // line C
    t.levelorder();  // line D

    return 0;
}

Q3.

Each snippet has a bug related to trees or the BST. Identify what's wrong.

// Snippet A — inorder visits the node before the left subtree
void inorder(Node* node) {
    if (!node) return;
    std::cout << node->value << " "; // printed before left subtree
    inorder(node->left);
    inorder(node->right);
}

// Snippet B — find goes left when value is greater
bool find(Node* node, const T& value) const {
    if (!node) return false;
    if (value == node->value) return true;
    if (value >  node->value) return find(node->left, value); // wrong direction
    return find(node->right, value);
}

// Snippet C — remove two-child case uses the leftmost of the LEFT subtree
Node* remove(Node* node, const T& value) {
    // ... (two-child case)
    Node* succ = node->left;           // wrong subtree
    while (succ->right) succ = succ->right;
    node->value = succ->value;
    node->left  = remove(node->left, succ->value);
    return node;
}

// Snippet D — postorder missing base case
void postorder(Node* node) {
    postorder(node->left);  // crashes when node is nullptr
    postorder(node->right);
    std::cout << node->value << " ";
}

Q4.

Implement insert and find for the BST below.

#include <iostream>

template <typename T>
class BST {
public:
    BST()  : root_(nullptr) {}
    ~BST() { destroy(root_); }

    void insert(const T& value) { root_ = insert(root_, value); }
    bool find(const T& value) const { return find(root_, value); }

    void inorder() const { inorder(root_); std::cout << "\n"; }

private:
    struct Node {
        T     value;
        Node* left;
        Node* right;
        Node(const T& v) : value(v), left(nullptr), right(nullptr) {}
    };

    Node* root_;

    Node* insert(Node* node, const T& value) {
        // TODO: allocate a new node at the correct BST position; ignore duplicates
    }

    bool find(Node* node, const T& value) const {
        // TODO: return true if value exists in the subtree rooted at node
    }

    void inorder(Node* node) const {
        if (!node) return;
        inorder(node->left);
        std::cout << node->value << " ";
        inorder(node->right);
    }

    void destroy(Node* node) {
        if (!node) return;
        destroy(node->left);
        destroy(node->right);
        delete node;
    }
};

int main() {
    BST<int> t;
    int values[] = {5, 3, 7, 1, 4, 6, 8};
    for (int v : values) t.insert(v);

    t.inorder();

    std::cout << t.find(4) << "\n";
    std::cout << t.find(9) << "\n";
    return 0;
}

Q5.

Implement remove for the BST below. Handle all three cases: leaf, one child, two children.

#include <iostream>

template <typename T>
class BST {
public:
    BST()  : root_(nullptr) {}
    ~BST() { destroy(root_); }

    void insert(const T& value) { root_ = insert(root_, value); }
    void remove(const T& value) { root_ = remove(root_, value); }

    void inorder() const { inorder(root_); std::cout << "\n"; }

private:
    struct Node {
        T     value;
        Node* left;
        Node* right;
        Node(const T& v) : value(v), left(nullptr), right(nullptr) {}
    };

    Node* root_;

    Node* insert(Node* node, const T& value) {
        if (!node) return new Node(value);
        if      (value < node->value) node->left  = insert(node->left,  value);
        else if (value > node->value) node->right = insert(node->right, value);
        return node;
    }

    Node* remove(Node* node, const T& value) {
        // TODO:
        // - recurse left/right to find the node
        // - case 1: no children → delete and return nullptr
        // - case 2: one child → delete and return the surviving child
        // - case 3: two children → replace with in-order successor, then
        //           remove the successor from the right subtree
    }

    void inorder(Node* node) const {
        if (!node) return;
        inorder(node->left);
        std::cout << node->value << " ";
        inorder(node->right);
    }

    void destroy(Node* node) {
        if (!node) return;
        destroy(node->left);
        destroy(node->right);
        delete node;
    }
};

int main() {
    BST<int> t;
    for (int v : {8, 3, 10, 1, 6, 14, 4, 7}) t.insert(v);

    t.remove(1);   // leaf
    t.remove(10);  // one child (right=14)
    t.remove(3);   // two children
    t.inorder();
    return 0;
}

Practice Problems