Chapter 9: Binary Trees
6 min readChapter 9 Overview: Binary Trees
Binary trees are fundamental hierarchical data structures. Each node has at most two children, referred to as the left child and the right child. They are essential for representing data with a branching structure, such as file systems, expression evaluation, and search trees.
Key Binary Tree Concepts:
- Node: The basic unit of a tree, containing data and pointers to its children.
- Root: The topmost node in a tree.
- Leaf: A node with no children.
- Depth: The length of the path from the root to a node. The root’s depth is 0.
- Height: The length of the longest path from a node to a leaf. The height of a leaf is 0. The height of a tree is the height of its root.
- Full Binary Tree: A tree where every node has either 0 or 2 children.
- Complete Binary Tree: A tree where all levels are completely filled except possibly the last, and the last level has all keys as left as possible.
- Perfect Binary Tree: A tree where all interior nodes have two children and all leaves are at the same level.
Tree Traversal:
The three primary ways to visit all nodes in a tree are:
- In-order Traversal: Left subtree -> Visit Root -> Right subtree. For a Binary Search Tree (BST), this yields the nodes in sorted order.
- Pre-order Traversal: Visit Root -> Left subtree -> Right subtree. Often used to create a copy of the tree.
- Post-order Traversal: Left subtree -> Right subtree -> Visit Root. Often used to delete nodes from the tree.
Section 9.1: Test if a Binary Tree is Height-Balanced
The Problem: A binary tree is height-balanced if, for every node, the difference in height between its left and right subtrees is at most 1. Write a function to determine if a given binary tree is height-balanced.
Algorithm (Recursive Post-order Traversal): A brute-force approach would be to compute the height of the left and right subtrees for every node, which is inefficient (O(n^2)). A better approach integrates the height calculation with the balance check.
Create a recursive helper function, say
check_balance(node). This function will return two pieces of information: whether the subtree atnodeis balanced, and what its height is. A common way to do this is to return a special value (like -1) to signal imbalance.check_balance(node)logic:- Base Case: If
nodeisNone, it’s a balanced subtree of height -1 (or 0, depending on convention). Return(True, -1). - Recursive Step:
- Recursively call
check_balanceon the left child. Getis_left_balancedandleft_height. - If
is_left_balancedisFalse, this subtree is not balanced. Propagate the failure up: return(False, 0). - Recursively call
check_balanceon the right child. Getis_right_balancedandright_height. - If
is_right_balancedisFalse, propagate the failure up: return(False, 0).
- Recursively call
- Check Current Node:
- Check if the current node is balanced:
abs(left_height - right_height) <= 1. - If it is, the height of the current node’s subtree is
1 + max(left_height, right_height). Return(True, height). - If it’s not, return
(False, 0).
- Check if the current node is balanced:
- Base Case: If
Complexity:
- Time: O(n), as each node is visited once.
- Space: O(h), where h is the height of the tree, for the recursion stack. O(log n) for a balanced tree, O(n) for a skewed tree.
Section 9.2: Test if a Binary Tree is a Binary Search Tree (BST)
The Problem: Write a function to check if a given binary tree is a valid Binary Search Tree (BST).
BST Property: For any given node N:
- All values in
N’s left subtree must be less thanN’s value. - All values in
N’s right subtree must be greater thanN’s value. - Both the left and right subtrees must also be binary search trees.
Pitfall: A common mistake is to only check if node.left.data < node.data and node.right.data > node.data. This is not sufficient. The property must hold for all nodes in the subtree, not just the immediate children.
Algorithm (Recursive with Min/Max Range):
The most robust method is to perform a recursive traversal, passing down the valid range of values (min_val, max_val) that a node’s value is allowed to have.
Create a recursive helper function
is_bst_helper(node, min_val, max_val).is_bst_helperlogic:- Base Case: If
nodeisNone, it’s a valid BST. ReturnTrue. - Check Current Node:
- If
node.data <= min_valornode.data >= max_val, the BST property is violated. ReturnFalse.
- If
- Recursive Step:
- Recursively check the left subtree. The new
max_valfor the left subtree is the current node’s value:is_bst_helper(node.left, min_val, node.data). - Recursively check the right subtree. The new
min_valfor the right subtree is the current node’s value:is_bst_helper(node.right, node.data, max_val). - The tree is a BST if both recursive calls return
True.
- Recursively check the left subtree. The new
- Base Case: If
Initial call:
is_bst_helper(root, -infinity, +infinity).
Section 9.4: Find the Lowest Common Ancestor (LCA) in a BST
The Problem: Given a BST and two nodes s and b (smaller and bigger value), find their Lowest Common Ancestor (LCA). The LCA is the deepest node in the tree that has both s and b as descendants.
Algorithm (Exploiting BST Properties): The search for the LCA in a BST is much simpler than in a generic binary tree. We can use the BST property to guide our search from the root.
- Start with
current = root. - Loop as long as
currentis notNone:- If
current.datais greater than boths.dataandb.data, it means both nodes are in the left subtree. The LCA must also be in the left subtree. So, move left:current = current.left. - If
current.datais less than boths.dataandb.data, it means both nodes are in the right subtree. The LCA must also be in the right subtree. So, move right:current = current.right. - Otherwise,
current.datais betweens.dataandb.data(or equal to one of them). This means the path tosandbdiverges atcurrent. Therefore,currentis the LCA. Returncurrent.
- If
Complexity:
- Time: O(h), where h is the height of the tree. In the worst case (a skewed tree), this is O(n). For a balanced tree, it’s O(log n).
- Space: O(1) for the iterative approach.
Section 9.7: Implement an In-order Traversal without Recursion
The Problem: Perform an in-order traversal of a binary tree without using recursion, which implicitly uses the call stack. You must use an explicit stack data structure.
Algorithm (Iterative with a Stack): This approach mimics what the recursion call stack does.
- Initialize an empty
stackand setcurrent = root. - Loop as long as
currentis notNoneor thestackis not empty.- Go Left: If
currentis notNone:- Push
currentonto thestack. - Move to the left child:
current = current.left.
- Push
- Visit and Go Right: If
currentisNone(meaning we’ve gone as far left as possible):- Pop a node from the
stack. This is the node we need to “visit”. Process its data (e.g., add to a result list). - Now, move to the right subtree of the popped node:
current = popped_node.right.
- Pop a node from the
- Go Left: If
- The loop terminates when both the stack is empty and the current node is
None.