Trees

A hierarchical data structure consisting of nodes connected by edges.

Definition

A tree is a hierarchical data structure made of nodes connected by lines called edges. It starts with a single root node at the top, and each node can have zero or more child nodes beneath it, which creates distinct levels. Trees don't contain any cycles, so there's only one path from the root to any other node. That property makes trees a natural fit for representing hierarchy, like file systems or family trees, and for efficient searching.

Operations

Tree operations behave very differently depending on how the tree is shaped. In a balanced tree, inserting, deleting, and searching usually take O(log n) time, where n is the number of nodes. We get to skip whole subtrees as we descend, which is what makes this fast. In an unbalanced tree, the same operations can degrade to O(n) in the worst case because we may have to walk every node. Here are the common operations on a balanced binary tree:

OperationsTimeSpace
InsertO(n)O(h)
DeleteO(n)O(h)
SearchO(n)O(h)
Get HeightO(n)O(h)
Pre-orderO(n)O(h)
In-orderO(n)O(h)
Post-orderO(n)O(h)
Level-orderO(n)O(w)

The O(log n) complexity in balanced binary trees comes from their structure. At the root, we have access to every node. Each level down halves the number of nodes we still need to consider, from n to n/2. The number of times you can halve n before reaching 1 is literally the definition of log₂(n). Because we quickly focus on a small portion of the tree with each step, many tree operations end up logarithmic.

Tree Nodes

Tree nodes are the building blocks of a tree. A node in a binary tree typically has three parts:

  1. The val holds the data you want to store (a number, a string, etc.).
  2. The left pointer refers to the left child node.
  3. The right pointer refers to the right child node.

If a node has no children, its left and right pointers are set to null. That's enough to wire up the parent-child relationships that make up the tree:

tree_nodes.py

# Definition of a binary tree node
class TreeNode:
    def __init__(self, val: int = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right
Tree Structure

A tree is made of nodes connected by edges. It starts with a single root node at the top and branches downward into child nodes. This creates parent-child relationships and levels. The main pieces of a tree are:

  1. Root: The topmost node of the tree, with no parent.
  2. Parent: A node with one or more child nodes below it.
  3. Child: A node connected to a parent node above it.
  4. Leaf: A node with no children, found at the bottom of the tree.
  5. Subtree: A section of the tree that itself forms a tree.

Different kinds of trees take different shapes. For example, a node in a general tree can have any number of children, not just two. The shape changes the performance of a tree. Keeping subtrees roughly the same depth (a balanced tree) tends to keep operations fast. The exact tradeoffs depend on which type of tree you're using.

Binary Tree

A binary tree is a hierarchical data structure where each tree node can have up to two children: a left child and a right child. There are two core rules:

Binary Tree Rules

  1. No cycles

    Binary trees can't have any cycles. There's only one path from the root to any node. You can't start at a node, follow some edges, and end up back where you started. This is what keeps the tree a tree, and it's why traversals don't loop forever.

  2. N - 1 Edges

    A binary tree with N nodes has exactly N - 1 edges. Every node except the root has exactly one edge connecting it to its parent. Since N - 1 of the N nodes have a parent, there are N - 1 edges total.

Binary trees are the foundation for more advanced trees like Binary Search Trees (BST) and AVL trees. They show up in expression parsing, data compression, and efficient search and sort algorithms. Binary trees have a few notable variants:

  • Full Binary Tree: Each node has either 0 or 2 children.
  • Complete Binary Tree: All levels are fully filled except possibly the last, which is filled from left to right.
  • Perfect Binary Tree: All parent nodes have two children and all leaves are at the same level.
  • Balanced Binary Tree: The height of the left and right subtrees of every node differs by at most one.

The performance of binary tree operations depends heavily on balance. In a balanced tree, search, insert, and delete typically run in O(log n), where n is the number of nodes. In the worst case (a completely unbalanced tree), those operations degrade to O(n).

Binary Search Tree

A Binary Search Tree (BST) is a binary tree with one extra rule: for every tree node, all values in its left subtree are smaller, and all values in its right subtree are larger than the node's own value. That rule applies to every node, which gives the tree an ordered structure. The order is what makes searching, inserting, and deleting fast.

Binary Search Tree Rules

  1. Ordered Values

    For any node, all values in its left subtree are smaller, and all values in its rightsubtree are larger. This ordering is what makes search fast and what defines the tree's shape.

  2. No Duplicate Values

    BSTs usually don't allow duplicate values. If you do need duplicates, you handle them by storing a count on each node or by picking a consistent rule for where duplicates go (always left, always right, etc.).

Because BSTs are ordered, search, insert, and delete typically run in O(log n) time on average, where n is the number of nodes. BSTs also support in-order traversal, which visits nodes in ascending order. This is handy for sorting and similar tasks:

OperationsTimeSpace
InsertO(h)*O(1)
DeleteO(h)*O(1)
SearchO(h)*O(1)
Get MinimumO(h)*O(1)
Get MaximumO(h)*O(1)
In-orderO(n)O(h)

One thing to keep in mind: these O(log n) times are the average case. In the worst case, when the tree becomes unbalanced or skewed, the operations degrade to O(n), basically a linked list. Self-balancing trees like AVL and Red-Black were built to keep the tree balanced and the operations at O(log n).

Tree Depth & Height

The depth of a node in a binary tree is the number of edges from the root to that node. Put simply, it's how many steps it takes to get from the top of the tree to that node. The root has a depth of 0, its immediate children have a depth of 1, and so on. The depth increases by 1 at each level as you move down the tree.


The height of a binary tree is the number of edges on the longest path from the root to a leaf. It's the longest journey from the top of the tree down to a leaf. A tree with a single node has a height of 0, while an empty tree is usually defined to have a height of -1. The height of the tree is equal to the maximum depth of any node in the tree.

binary_tree_depth.py

# Definition for a binary tree node
class TreeNode:
    def __init__(self, val: int = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        # Base case: If there is no root, just return 0
        if not root:
            return 0
        
        # Setting up DFS on tree
        def dfs(root: Optional[TreeNode]) -> int:
            # Base case: If root doesn't exist, then return 0
            if not root:
                return 0
            
            left = dfs(root.left)
            right = dfs(root.right)

            return max(left, right) + 1
        
        # Call the DFS function
        return dfs(root)

Calculating the depth and height uses a post-order traversal. We start at the bottom of the tree and work upward. For each node, we find the maximum depth of its left and right children, then add 1 to account for the current node. As we move up, the depth at each node grows, and by the time we reach the root we've found the maximum depth (which is the height of the tree).

Binary Tree Diameter

The diameter of a binary tree is the number of nodes on the longest path between any two nodes in the tree. That path may or may not pass through the root. Put another way: it's the longest distance between any two nodes. For a tree with a single node, the diameter is 1. For an empty tree, it's usually defined as 0.

tree_diameter.py

# Definition for a binary tree node
class TreeNode:
    def __init__(self, val: int = 0, left = None, right = None):
        self.val = val
        self.left = left
        self.right = right

class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        # Set up nonlocal variable res to store answer
        res = 0

        # Setting up DFS on binary tree
        def dfs(root: Optional[TreeNode]) -> int:
            # Base case: If there is no root, then return 0
            if not root:
                return 0
            
            # DFS function calls for left and right children
            left = dfs(root.left)
            right = dfs(root.right)

            # If value of left + right children is greater, update res
            nonlocal res
            if (left + right) > res:
                res = (left + right)

            return 1 + max(left, right)
        
        # Call DFS function to store answer in res
        dfs(root)
        return res

To find the diameter, we need to find the two nodes that are farthest apart, so we use a post-order traversal. Our recursivefunction tracks two values at each node: the height of the current subtree and the longest diameter we've seen so far. At each node, the candidate path length is the sum of the left and right subtree heights plus 1 for the current node. This finds the diameter in a single pass and examines all possible paths without revisiting any nodes.

Best Practices

Trees show up everywhere: hierarchical data, search problems, recursive algorithms, and more. Here are a few tips for working with them well:


Always check if the head or root node is empty before you do anything else with your tree. It feels redundant, but it's what avoids edge cases and keeps the code correct(BFS in particular breaks without the base case). It's a small habit that saves you from a lot of subtle bugs.

Algorithms
Practice Problems

Trees are where recursion really clicks. Most of these problems are short to write but force you to think about what each recursive call should return — work through them to internalize the patterns for traversal, BST invariants, path-aggregation, and tree DP: