Vasu Patel logoVasu Patel
← Python DSA — Beginner
All chapters in this track
  1. 011. What is DSA & Why It Matters
  2. 022. Python's Built-in Collections
  3. 033. Arrays & Strings
  4. 044. Linked Lists
  5. 055. Stacks & Queues
  6. 066. Hash Maps & Sets
  7. 077. Trees
  8. 088. Heaps & Priority Queues
  9. 099. Graphs
  10. 1010. Sorting & Searching
  11. 1111. Recursion
  12. 1212. Dynamic Programming
  13. 1313. Tries
  14. 1414. DSA in Real Software

Chapter 7 of 14

7. Trees

What Is a Tree?

A tree is a hierarchical data structure where nodes are connected in a parent-child relationship. It's like a family tree — one root at the top, branches below, leaves at the bottom. There are no cycles.

Real-world trees:

  • File system — directories (internal nodes) and files (leaves)
  • HTML/DOM — nested elements
  • JSON/XML — nested data
  • Database indexes — B-trees for fast lookups
  • Company org chart — CEO → VPs → Managers → ICs

7.1 Binary Tree

Each node has at most two children: left and right.

class TreeNode:
    def __init__(self, val: int):
        self.val   = val
        self.left: "TreeNode | None"  = None
        self.right: "TreeNode | None" = None

# Build a simple tree:
#        1
#       / \
#      2   3
#     / \
#    4   5
root        = TreeNode(1)
root.left   = TreeNode(2)
root.right  = TreeNode(3)
root.left.left  = TreeNode(4)
root.left.right = TreeNode(5)

7.2 Tree Traversals

Three ways to visit all nodes, depending on when you process the current node:

# --- Inorder (Left → Node → Right) ---
# For a BST: produces nodes in sorted order
def inorder(root: TreeNode | None) -> list[int]:
    if root is None:
        return []
    return inorder(root.left) + [root.val] + inorder(root.right)

# --- Preorder (Node → Left → Right) ---
# Useful for copying a tree or serializing its structure
def preorder(root: TreeNode | None) -> list[int]:
    if root is None:
        return []
    return [root.val] + preorder(root.left) + preorder(root.right)

# --- Postorder (Left → Right → Node) ---
# Useful for deleting a tree (children before parent)
def postorder(root: TreeNode | None) -> list[int]:
    if root is None:
        return []
    return postorder(root.left) + postorder(root.right) + [root.val]

# --- Level order / BFS (level by level) ---
# Useful when you care about depth (e.g., minimum depth, level-wise operations)
from collections import deque

def level_order(root: TreeNode | None) -> list[list[int]]:
    if not root:
        return []
    result = []
    queue  = deque([root])
    while queue:
        level_size = len(queue)
        level = []
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            if node.left:  queue.append(node.left)
            if node.right: queue.append(node.right)
        result.append(level)
    return result

print(inorder(root))       # [4, 2, 5, 1, 3]
print(preorder(root))      # [1, 2, 4, 5, 3]
print(postorder(root))     # [4, 5, 2, 3, 1]
print(level_order(root))   # [[1], [2, 3], [4, 5]]

7.3 Binary Search Tree (BST)

A BST maintains the BST property: every node's left subtree has smaller values, right subtree has larger values.

This gives O(log⁡n)O(\log n) search, insert, delete — for balanced trees. Worst case (fully skewed) is O(n)O(n).

class BST:
    def __init__(self):
        self.root: TreeNode | None = None

    def insert(self, val: int) -> None:
        self.root = self._insert(self.root, val)

    def _insert(self, node: TreeNode | None, val: int) -> TreeNode:
        if node is None:
            return TreeNode(val)
        if val < node.val:
            node.left = self._insert(node.left, val)
        elif val > node.val:
            node.right = self._insert(node.right, val)
        # val == node.val → no duplicates (or handle as needed)
        return node

    def search(self, val: int) -> bool:
        cur = self.root
        while cur:
            if val == cur.val:
                return True
            cur = cur.left if val < cur.val else cur.right
        return False

# Usage
bst = BST()
for v in [5, 3, 7, 1, 4, 6, 8]:
    bst.insert(v)
print(bst.search(4))   # True
print(bst.search(9))   # False
# Inorder gives sorted output: 1 3 4 5 6 7 8

7.4 Common Tree Algorithms

# --- Height of a tree ---
def height(root: TreeNode | None) -> int:
    if root is None:
        return 0
    return 1 + max(height(root.left), height(root.right))

# --- Count total nodes ---
def count_nodes(root: TreeNode | None) -> int:
    if root is None:
        return 0
    return 1 + count_nodes(root.left) + count_nodes(root.right)

# --- Check if a tree is symmetric (mirror image) ---
def is_symmetric(root: TreeNode | None) -> bool:
    def mirrors(left, right):
        if not left and not right:
            return True
        if not left or not right:
            return False
        return (left.val == right.val
                and mirrors(left.left, right.right)
                and mirrors(left.right, right.left))
    return mirrors(root, root)

# --- Path from root to target value ---
def find_path(root: TreeNode | None, target: int) -> list[int]:
    if root is None:
        return []
    if root.val == target:
        return [root.val]
    for child in [root.left, root.right]:
        path = find_path(child, target)
        if path:
            return [root.val] + path
    return []

print(find_path(root, 4))   # [1, 2, 4]