Interactive study notes on Graphs and Trees in Python β€” covering intuition, definitions, traversals, implementations, complexity, and interview tips.

Intuition

Tree = Family Tree

One ancestor (root) β†’ children β†’ grandchildren. No cycles. Every node has exactly one parent (except root).

HierarchicalOne parentAcyclic

Used in: DOM, file systems, org charts, decision trees, BSTs, heaps

Graph = City Road Map

Cities = nodes, roads = edges. Roads can go one-way or both ways. Can have cycles. No strict parent-child rule.

GeneralAny connectionsMay have cycles

Used in: social networks, GPS routing, web crawlers, dependency graphs

Key Mental Model

Tree is a restricted Graph

  • Connected + Acyclic + Directed (parentβ†’child)
  • N nodes β†’ exactly Nβˆ’1 edges
  • Always one path between any two nodes

Graph is the general case

  • Can be connected or disconnected
  • Can have cycles
  • Edges can be weighted
Tree 1 2 3 4 5 Graph A B C D E cycle possible β†Ί

Definitions

Tree Terminology

TermMeaningExample
rootTop node, no parentNode 1 in a tree
leafNode with no childrenBottom-most nodes
heightLongest path from root to leafHeight of tree above = 2
depthDistance from root to a nodeRoot has depth 0
subtreeA node and all its descendantsAny internal node forms one
degreeNumber of children a node hasBinary tree: max degree = 2

Tree Types

Binary Tree

Each node has at most 2 children (left, right)

BST

Left < Node < Right β€” enables O(log n) search

Balanced Tree

Heights of subtrees differ by at most 1 (AVL, Red-Black)

N-ary Tree

Each node can have up to N children

Trie

Tree for string prefixes β€” each edge = one char

Heap

Complete binary tree with heap property (min/max)

Graph Terminology

TermMeaning
vertex / nodeA point in the graph
edgeConnection between two vertices
directed (digraph)Edges have direction (A→B ≠ B→A)
undirectedEdges go both ways (Aβ€”B)
weightedEdges have a cost/weight
degreeNumber of edges connected to a node
pathSequence of nodes connected by edges
cyclePath that starts and ends at same node
connectedEvery node reachable from every other
DAGDirected Acyclic Graph β€” no cycles, ordered

Core Concepts

Graph Representations

Adjacency List (preferred)

graph = {
  'A': ['B', 'C'],
  'B': ['A', 'D'],
  'C': ['A'],
  'D': ['B']
}

Space: O(V+E) β€” ideal for sparse graphs

Adjacency Matrix

matrix = [
  [0, 1, 1, 0],
  [1, 0, 0, 1],
  [1, 0, 0, 0],
  [0, 1, 0, 0],
]

Space: O(VΒ²) β€” good for dense graphs

Tree Node Definition

class TreeNode:
    def __init__(self, val=0):
        self.val = val
        self.left = None
        self.right = None

BST Property β€” Visual Check

8 3 12 1 6 all < 8 all > 8 all < 3 3<x<8

When to use which?

ScenarioUse
Hierarchical data (folders, XML)Tree
Sorted data + fast search/insertBST
Relationship data (social, maps)Graph
Shortest path problemsWeighted Graph + BFS/Dijkstra
Dependency orderingDAG + Topological Sort

Traversals β€” Interactive

Tree Traversals β€” Live Demo

1 2 3 4 5 6 7
Visit order:
Select a traversal above

In-order (L β†’ Root β†’ R)

def inorder(node):
    if not node:
        return
    inorder(node.left)
    print(node.val)
    inorder(node.right)

BST in-order = sorted output

Pre-order (Root β†’ L β†’ R)

def preorder(node):
    if not node:
        return
    print(node.val)
    preorder(node.left)
    preorder(node.right)

Used to copy/serialize a tree

Post-order (L β†’ R β†’ Root)

def postorder(node):
    if not node:
        return
    postorder(node.left)
    postorder(node.right)
    print(node.val)

Used to delete a tree, calc folder sizes

BFS / Level-order

from collections import deque
def bfs(root):
    q = deque([root])
    while q:
        node = q.popleft()
        print(node.val)
        if node.left: q.append(node.left)
        if node.right: q.append(node.right)

Graph Traversals

DFS β€” Recursive

def dfs(graph, node, visited=set()):
    if node in visited:
        return
    visited.add(node)
    print(node)
    for neighbor in graph[node]:
        dfs(graph, neighbor, visited)

BFS β€” Iterative

from collections import deque
def bfs(graph, start):
    visited = set()
    q = deque([start])
    visited.add(start)
    while q:
        node = q.popleft()
        print(node)
        for n in graph[node]:
            if n not in visited:
                visited.add(n)
                q.append(n)

Implementation

Complete BST Implementation

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

    def insert(self, val):
        self.root = self._insert(self.root, val)

    def _insert(self, node, val):
        if not node:
            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)
        return node

    def search(self, val):
        return self._search(self.root, val)

    def _search(self, node, val):
        if not node or node.val == val:
            return node
        if val < node.val:
            return self._search(node.left, val)
        return self._search(node.right, val)

Graph β€” Build & Traverse

from collections import defaultdict, deque

class Graph:
    def __init__(self):
        self.adj = defaultdict(list)

    def add_edge(self, u, v, directed=False):
        self.adj[u].append(v)
        if not directed:
            self.adj[v].append(u)

    def dfs(self, start):
        visited, stack = set(), [start]
        result = []
        while stack:
            node = stack.pop()
            if node not in visited:
                visited.add(node)
                result.append(node)
                stack.extend(self.adj[node])
        return result

    def bfs(self, start):
        visited, queue = {start}, deque([start])
        result = []
        while queue:
            node = queue.popleft()
            result.append(node)
            for n in self.adj[node]:
                if n not in visited:
                    visited.add(n)
                    queue.append(n)
        return result

# Usage
g = Graph()
g.add_edge('A', 'B')
g.add_edge('A', 'C')
g.add_edge('B', 'D')
print(g.dfs('A'))   # ['A', 'C', 'B', 'D']
print(g.bfs('A'))   # ['A', 'B', 'C', 'D']

Detect Cycle in Undirected Graph

def has_cycle(graph, n):
    visited = [False] * n
    def dfs(node, parent):
        visited[node] = True
        for neighbor in graph[node]:
            if not visited[neighbor]:
                if dfs(neighbor, node):
                    return True
            elif neighbor != parent:
                return True  # back edge = cycle!
        return False
    return any(dfs(i, -1) for i in range(n) if not visited[i])

Complexity & Performance

Tree Operations (BST)

OperationAverageWorst (Unbalanced)
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)
TraversalO(n)O(n)
HeightO(log n)O(n)

Graph Operations

OperationAdj ListAdj Matrix
Add edgeO(1)O(1)
Remove edgeO(E)O(1)
Check edgeO(degree)O(1)
BFS / DFSO(V+E)O(VΒ²)
SpaceO(V+E)O(VΒ²)

Visual: Relative Costs

BST search (balanced)
O(log n)
BST search (skewed)
O(n)
BFS (adj list)
O(V+E)
BFS (adj matrix)
O(VΒ²)
Dijkstra (heap)
O((V+E)logV)

Space Complexity

  • Recursive DFS: O(h) stack β€” h = height. Worst case O(n) for skewed tree
  • BFS queue: O(w) β€” w = max width. Worst case O(n) for complete tree
  • Visited set (graph): always O(V)

Comparison

DFS vs BFS

PropertyDFSBFS
Data structureStack (or recursion)Queue
SpaceO(h) β€” heightO(w) β€” width
Finds shortest path?No (unweighted)Yes (unweighted)
Complete?No (infinite graphs)Yes
Best forDetect cycle, topo sort, mazeShortest path, level-by-level
Tree traversalPre/In/Post-orderLevel-order

Tree Types Comparison

TypeSearchInsertOrdered?Use Case
BSTO(log n)*O(log n)*YesSorted data
AVL TreeO(log n)O(log n)YesFrequent lookups
HeapO(n)O(log n)PartialPriority queue
TrieO(k)O(k)By charAutocomplete
Segment TreeO(log n)O(log n)By indexRange queries

* Worst case O(n) if unbalanced

Graph Representation: When to Use?

Adjacency List β€” Use when:

  • Graph is sparse (few edges)
  • Need to iterate over neighbors
  • Memory is a concern
  • Most real-world graphs

Adjacency Matrix β€” Use when:

  • Graph is dense (many edges)
  • Need O(1) edge lookup
  • Floyd-Warshall algorithm
  • Small number of vertices

Patterns & Use Cases

Pattern: Two-Pass DFS

Run DFS twice to find connected components, bridges, or articulation points (Kosaraju's / Tarjan's)

Pattern: BFS + Distance Array

Maintain dist[] alongside BFS for shortest unweighted paths. Initialize with -1 or infinity.

Pattern: Topological Sort

DAG: use DFS post-order OR Kahn's algorithm (BFS + in-degree). Use for task scheduling, build order.

Pattern: Union-Find

Track connected components efficiently. Use for Kruskal's MST, cycle detection in undirected graphs.

Pattern: Level BFS on Trees

Use a sentinel None or size-based loop to process level by level. Useful for zigzag, average, or right-view problems.

Pattern: Lowest Common Ancestor

Use post-order DFS. Return node when both left and right subtrees find the targets.

Common Interview Patterns

Problem TypeAlgorithmKey Data Structure
Shortest path (unweighted)BFSQueue + visited set
Shortest path (weighted)DijkstraMin-heap (heapq)
Minimum spanning treePrim's / Kruskal'sHeap / Union-Find
Cycle detection (directed)DFS with color (white/gray/black)Visited + recursion stack
Cycle detection (undirected)DFS with parent trackingVisited set
Task orderingTopological SortDeque + in-degree array
Tree diameterDFS twice or single post-orderGlobal max variable

Dijkstra's β€” Clean Template

import heapq

def dijkstra(graph, start):
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    heap = [(0, start)]  # (distance, node)

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return dist

Dry Runs

BFS Dry Run β€” Step by Step

Graph: A-B, A-C, B-D, C-D, D-E    Start: A

BST Insert Dry Run

Insert: 5, 3, 7, 1, 4 into empty BST

InsertActionTree State
5Root is empty β†’ place at root5
33 < 5 β†’ go left β†’ empty β†’ insert5(L:3)
77 > 5 β†’ go right β†’ empty β†’ insert5(L:3, R:7)
11<5β†’L; 1<3β†’L β†’ insert3(L:1); 5's left subtree
44<5β†’L; 4>3β†’R β†’ insert3(L:1, R:4)

In-order result: 1, 3, 4, 5, 7 (sorted!)

Topological Sort Dry Run

Tasks: A→C, B→C, C→D
In-degrees: A=0, B=0, C=2, D=1

Step 1: Queue = [A, B] (in-degree 0)
Step 2: Process A β†’ C's in-degree becomes 1 β†’ order=[A]
Step 3: Process B β†’ C's in-degree becomes 0 β†’ queue=[C] β†’ order=[A,B]
Step 4: Process C β†’ D's in-degree becomes 0 β†’ queue=[D] β†’ order=[A,B,C]
Step 5: Process D β†’ order=[A,B,C,D]

Concept Checks

Test your understanding. Select an answer to reveal the explanation.

Practice Problems

Beginner

1. Maximum Depth of Binary Tree

Return the height of a binary tree.

def maxDepth(root):
    if not root:
        return 0
    return 1 + max(maxDepth(root.left),
                   maxDepth(root.right))

2. Number of Islands (BFS)

Count connected components of '1's in a 2D grid.

def numIslands(grid):
    count = 0
    def bfs(r, c):
        q = deque([(r, c)])
        grid[r][c] = '0'
        while q:
            row, col = q.popleft()
            for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
                nr, nc = row+dr, col+dc
                if 0<=nr
  

Intermediate

3. Lowest Common Ancestor of BST

def lowestCommonAncestor(root, p, q):
    while root:
        if p.val < root.val and q.val < root.val:
            root = root.left
        elif p.val > root.val and q.val > root.val:
            root = root.right
        else:
            return root

4. Course Schedule (Cycle Detection in Directed Graph)

def canFinish(numCourses, prerequisites):
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)
    # 0=unvisited, 1=in-progress, 2=done
    state = [0] * numCourses
    def dfs(node):
        if state[node] == 1: return False  # cycle!
        if state[node] == 2: return True
        state[node] = 1
        for nei in graph[node]:
            if not dfs(nei): return False
        state[node] = 2
        return True
    return all(dfs(i) for i in range(numCourses))

Advanced

5. Serialize & Deserialize Binary Tree

Hint: Use BFS. Encode None as "null". Split on comma. Rebuild using a queue index.

6. Word Ladder (Shortest Transformation)

Hint: Model as unweighted graph. BFS guarantees shortest path. Use wildcard patterns as intermediate keys.

7. Alien Dictionary (Topological Sort)

Hint: Compare adjacent words to build a directed graph of char ordering. Run topo sort. If cycle, return "".

Interview Tips & Common Mistakes

Top Mistakes to Avoid

  • Forgetting the visited set in graph traversal β†’ infinite loop in cyclic graphs
  • Confusing height vs depth β€” height measured from leaves up, depth from root down
  • Assuming BST is always balanced β€” O(log n) is average case, not guaranteed
  • Not handling disconnected graphs β€” loop over all nodes, not just from one source
  • Mutating the graph mid-traversal β€” use a copy if you mark cells in place
  • Stack overflow with deep recursion β€” convert to iterative DFS for production

Key Interview Questions & Approaches

QuestionApproachComplexity
Tree is valid BST?DFS with min/max boundsO(n)
Diameter of binary treePost-order DFS, track max globallyO(n)
All paths root to leafDFS + backtracking with path listO(n)
Clone graphBFS + HashMap (old→new node)O(V+E)
Shortest path in matrixBFS with 4-directional movementO(mΓ—n)
Redundant connectionUnion-FindO(n Ξ±(n))
Critical connectionsTarjan's bridge-finding DFSO(V+E)

Quick Interview Checklist

Before you code

  • Directed or undirected?
  • Weighted edges?
  • Can there be cycles?
  • Connected or disconnected?
  • What's the expected output β€” path, bool, count?

Optimization signals

  • Shortest path β†’ BFS (unweighted), Dijkstra (weighted)
  • All paths β†’ DFS + backtrack
  • Ordering β†’ Topological sort
  • Components β†’ Union-Find or DFS loop
  • Balance needed β†’ AVL or heapq

Python-specific tips

  • Use collections.deque for BFS β€” O(1) popleft vs O(n) for list
  • Use collections.defaultdict(list) for adjacency lists
  • Use heapq (min-heap) for Dijkstra β€” negate values for max-heap
  • Increase recursion limit: sys.setrecursionlimit(10**5) for deep trees
  • Use set() for visited β€” O(1) lookup