E ExamMaster

Data Structures & Algorithms · Data Structures & Algorithms

Trees and Graphs

Binary trees, binary search trees, traversals and graph representations with BFS and DFS.

Eight concepts on binary trees, BSTs, the four common traversals, graph representations, and BFS/DFS — including unweighted shortest paths and counting connected components (islands). Weighted shortest paths, MSTs and full topological-sort algorithms sit in Graph Algorithms.

  • Data Structures & Algorithms
  • Hard level
  • 8 concepts
  • 5 practice questions

1Binary tree shape

A binary tree gives every node at most two children, conventionally left and right. With n nodes there are exactly n - 1 edges — one parent link into each non-root — so the structure is connected and acyclic by construction. Height h is the longest root-to-leaf path in edges; a complete filling of levels keeps h = \lfloor \log_2 n \rfloor, while a chain of nodes pushes h to n - 1. Unused null child slots are not edges.

Figure. Five-node binary tree. Four parent→child edges; C is a leaf with no children drawn.

What the shape guarantees

  1. At most two childrenEach node may have a left child, a right child, both, or neither — never a third sibling under the same parent.
  2. Tree edge countEvery node except the root has exactly one parent edge, so n nodes imply n - 1 edges.
  3. Height bounds costAny root-to-leaf walk is at most h edges; algorithms that follow child pointers once per level cost O(h).
A binary tree with 7 nodes always has how many edges?
  1. 6 — one fewer than the node count
  2. 7 — one edge per node
  3. 14 — two child pointers reserved per node whether used or not

A tree on n nodes is connected and acyclic, so it has exactly n-1 edges. Unused null child slots are not edges.

2BST order and O(h) search

A binary search tree strengthens the binary tree with an order rule: every key in the left subtree is strictly smaller than the node, and every key in the right subtree is strictly larger. Search, insert and delete then follow one child per level, so each costs O(h). That is O(\log n) when the tree stays balanced and O(n) when it degenerates into a chain — the asymptotic label is height, not a free log-n guarantee.

Figure. Same BST as the worked example. Left subtree of 8 holds {1,3,6}; right holds {10}.

Search for a key

  1. Start at the rootCompare the target with the current key.
  2. Branch onceGo left if the target is smaller, right if larger; stop if equal.
  3. Miss ends at nullA null child means the key is absent; insert would hang a new node there.

Search for 6

BST: root 8 with left 3 and right 10; 3 has left 1 and right 6. Search for key 6.

  • at 8; 6 < 8go left → 3
  • at 3; 6 > 3go right → 6
  • at 6; 6 = 6found
  • comparisons / height used3 steps on a tree of height 2

Pro tip. If the same keys were inserted in sorted order 1,3,6,8,10 the tree would be a right spine and the same search would touch every node — still correct, but O(n).

BST search is O(\log n) only when
  1. Every node stores both children as arrays
  2. The tree's height is \Theta(\log n) (balanced), not when it is skewed
  3. You use DFS instead of following the order rule

The algorithm walks one path of length at most h. Balance makes h = \Theta(\log n); a skewed tree makes h = \Theta(n).

3In-order, pre-order, post-order

Three depth-first walks differ only in when they record the node relative to its subtrees: in-order is left, node, right; pre-order is node, left, right; post-order is left, right, node. In-order of a BST is sorted order — that is the walk for validating a BST or finding the k-th smallest. Pre-order is the natural order to copy or serialise a tree; post-order deletes children before the parent.

Walk the five-key tree from BST order and O(h) search: in-order emits left, node, right (sorted keys); pre-order emits node first; post-order emits the node after both children.

Recursive in-order

  1. Go leftFully traverse the left subtree first.
  2. VisitRecord the current node's key.
  3. Go rightFully traverse the right subtree.
When the node is recorded
WalkOrderTypical use
In-orderleft, node, rightSorted keys in a BST; k-th smallest
Pre-ordernode, left, rightCopy / serialise a tree
Post-orderleft, right, nodeDelete children before the parent

In-order, recursive

def inorder(node, out):
    if not node:
        return
    inorder(node.left, out)
    out.append(node.val)
    inorder(node.right, out)

In-order on the BST

Using the BST from BST order and O(h) search (keys 8, 3, 10, 1, 6), write the in-order sequence.

  • finish left of 8 (subtree 3)emit 1, then 3, then 6
  • visit rootemit 8
  • finish right of 8emit 10
  • full in-order[1, 3, 6, 8, 10]

Pro tip. If in-order is not strictly increasing, the tree is not a BST — that check is cheaper than re-deriving the order rule at every node.

Which walk of a BST yields keys in sorted order?
  1. Level order
  2. In-order (left, node, right)
  3. Pre-order (node, left, right)

The BST invariant plus left-then-node-then-right visits keys from smallest to largest. Level order groups by depth; pre-order emits parents before their left descendants.

4Level-order traversal

Level order visits a binary tree breadth-first: every node at depth d before any node at depth d+1, left to right within a level. The iterative form keeps a queue; the one move that separates levels is recording the queue's length at the start of each pass and draining exactly that many nodes. That is still O(n) time and O(n) queue space in the worst case — a complete level can hold \Theta(n) nodes — and it is the same BFS that finds shortest paths in an unweighted graph.

Figure. Same tree as the worked example. BFS drains depth 0, then depth 1, then depth 2 — never a child before every node on its parent's level.

How it works

  1. Seed the queuePush the root. An empty tree yields an empty list of levels and stops here.
  2. Freeze the widthAt the start of a pass, set width = length(queue). Those nodes are exactly the current level.
  3. Drain and expandDequeue width nodes, append each value to this level, and enqueue each existing left then right child.
  4. Emit the levelAppend the collected level to the answer and loop while the queue still holds the next depth.

Level order, iterative

from collections import deque

def level_order(root):
    if not root:
        return []
    out, q = [], deque([root])
    while q:
        width = len(q)
        level = []
        for _ in range(width):
            node = q.popleft()
            level.append(node.val)
            if node.left:
                q.append(node.left)
            if node.right:
                q.append(node.right)
        out.append(level)
    return out

Levels on a five-node tree

Tree: root 1 with left 2 and right 3; 2 has left 4 and right 5. Return values grouped by level, top to bottom.

  • start: queue[1]
  • width=1; drain 1, enqueue 2,3level [1]; queue [2, 3]
  • width=2; drain 2,3, enqueue 4,5level [2, 3]; queue [4, 5]
  • width=2; drain 4,5 (no children)level [4, 5]; queue []
  • grouped output[[1], [2, 3], [4, 5]]

Pro tip. If you forget to freeze width and instead loop while the queue is non-empty in one flat pass, children join the same pass as their parents and the level boundaries disappear — you still visit every node, but you no longer have level order as a list of lists.

In an iterative level-order walk, what must you capture at the start of each outer iteration?
  1. The height of the tree so far
  2. The current length of the queue (the width of this level)
  3. A stack of right children only

The queue holds exactly the nodes of the current depth when the outer loop begins. Recording that length and dequeuing that many nodes — and only then — keeps each depth in its own batch. Height is not known a priori, and a stack of right children is a DFS habit, not BFS.

5Adjacency list vs matrix

A graph stores vertices and the edges between them. An adjacency list keeps, for each vertex, the neighbors it touches — O(V + E) space and a natural fit for sparse graphs. An adjacency matrix is a V \times V table with a bit (or weight) per ordered pair — O(V^2) space and O(1) edge existence checks, which pays off only when the graph is dense or you query edges constantly. Prefer the list for sparse graphs (most real graphs); reach for the matrix only when density or constant-time edge tests demand it.

Figure. Sparse graphs favour adjacency lists; dense graphs or O(1) edge tests favour a matrix.

Pick a representation

  1. Count densityIf E is far below V^2, a list wastes less space and iterates only real edges.
  2. Need edge tests?Matrix answers "is (u,v) an edge?" in O(1); a list may scan u's neighbor array.
  3. Default for interviewsBuild a list (or dict of lists) unless the problem states a dense graph or needs constant-time edge queries.
List versus matrix
PropertyAdjacency listAdjacency matrix
SpaceO(V + E)O(V^2)
Iterate neighbors of uO(\mathrm{deg}(u))O(V)
Check edge (u,v)O(\mathrm{deg}(u)) typicalO(1)
Best whenSparse (E \ll V^2)Dense, or many edge queries
A road network with 10^5 junctions and 2 \times 10^5 roads should usually be stored as
  1. An adjacency matrix of size 10^5 \times 10^5
  2. An adjacency list using O(V + E) space
  3. Only an edge list with no per-vertex index

The graph is sparse: E is linear in V, not quadratic. A matrix would allocate about 10^{10} cells. A plain edge list without neighbor indexes makes BFS/DFS scan all edges from every start.

6BFS shortest path (unweighted)

Breadth-first search explores a graph level by level with a queue, marking vertices as visited so each is enqueued once. In an unweighted graph (every edge cost 1), the first time BFS reaches a vertex is along a shortest path, so distances are final when dequeued. That is O(V + E) time and O(V) queue/visited space. Shortest path in an unweighted graph is BFS, not DFS — DFS can reach a far vertex down a long path before it ever sees a short one.

Figure. Same graph as the worked example. Node labels show BFS hop distance from A.

Distance BFS from s

  1. SeedSet dist[s] = 0, enqueue s, mark visited.
  2. ExpandDequeue u; for each unvisited neighbor v set dist[v] = dist[u] + 1, mark visited, enqueue v.
  3. StopWhen the queue empties, every reachable vertex has its hop distance; unreachable stay unset.

Unweighted distances

from collections import deque

def bfs_dist(graph, s):
    dist = {s: 0}
    q = deque([s])
    while q:
        u = q.popleft()
        for v in graph[u]:
            if v not in dist:
                dist[v] = dist[u] + 1
                q.append(v)
    return dist

Distances from A

Undirected unweighted graph: edges A–B, A–C, B–D, C–D. Compute hop distance from A to every vertex.

  • startdist A=0; queue [A]
  • pop A; discover B,CB=1, C=1; queue [B, C]
  • pop B; discover DD=2; queue [C, D]
  • pop C; D already setskip; queue [D]
  • pop D; donedist {A:0, B:1, C:1, D:2}

Pro tip. Mark visited (or write dist) when you enqueue, not when you dequeue — otherwise the same vertex can sit in the queue many times and the O(V+E) bound slips.

Coding lab. BFS hops from A runs in the app, with checks on your output.

On an unweighted graph, why can DFS return a longer path to a vertex than BFS?
  1. DFS is asymptotically slower, so its paths are longer
  2. DFS may dive deep along a long route before it ever explores a short side branch BFS would have taken first
  3. DFS cannot visit every vertex

DFS order is depth-first, not breadth-first. The first time it reaches a vertex need not be along a minimum-hop path. BFS expands by hop count, so the first reach is shortest in hops.

7DFS: deepen, then backtrack

Depth-first search pushes as far as possible along one path before backtracking, using the call stack (recursion) or an explicit stack. Like BFS it runs in O(V + E) and marks vertices so each is expanded once. DFS is the usual tool when the question is about structure along a path — cycle detection in directed or undirected graphs, connected components, and (on a DAG) finishing-time order for a topological sort. It is not the tool for unweighted shortest paths.

Figure. DFS deepens then backtracks — right tool for structure along a path (cycles, components, finish-time topo). Unweighted shortest paths stay with BFS.

Recursive DFS from u

  1. MarkMark u visited as you enter.
  2. RecurseFor each unvisited neighbor v, recurse on v (or push v on an explicit stack).
  3. FinishWhen every neighbor is done, u is finished — finish times feed topological order on a DAG.

DFS mark

def dfs(u, graph, seen):
    seen.add(u)
    for v in graph[u]:
        if v not in seen:
            dfs(v, graph, seen)
You need a valid course order from prerequisite edges on a DAG. A standard DFS-based approach uses
  1. The order vertices are first discovered (preorder)
  2. The order vertices finish (postorder / finish times), reversed
  3. BFS distances from an arbitrary source

On a DAG, every edge u \to v has f(u) > f(v)u finishes after v, so the edge points toward an earlier finish time. Reversing finish order (or pushing onto a stack when a vertex finishes) yields a topological order. Discovery order alone is not enough. Full Kahn / DFS topo algorithms are expanded in Graph Algorithms.

8Number of islands

A grid of land ('1') and water ('0') is a graph in disguise: each land cell is a vertex with up to four edges to orthogonal land neighbors. The number of islands is the number of connected components of land. Scan every cell; when you hit an unvisited '1', increment the count and run DFS or BFS to mark the whole component visited. Total work is O(\textit{rows} \times \textit{cols}) — each cell is entered a constant number of times.

Figure. Same 3×3 grid as the worked example. The top-left land blob is one 4-connected component; the bottom-right 1 is a second.

Count components

  1. ScanWalk every cell left-to-right, top-to-bottom.
  2. LaunchOn an unvisited land cell, add 1 to the island count.
  3. FloodDFS or BFS from that cell, marking every 4-reachable land cell visited (often by flipping '1' to '0').
  4. ContinueResume the scan; already-marked land is skipped. Launches equal islands.

Islands via DFS flood

def num_islands(grid):
    if not grid:
        return 0
    R, C = len(grid), len(grid[0])

    def flood(r, c):
        if r < 0 or c < 0 or r >= R or c >= C or grid[r][c] != '1':
            return
        grid[r][c] = '0'
        flood(r + 1, c)
        flood(r - 1, c)
        flood(r, c + 1)
        flood(r, c - 1)

    islands = 0
    for r in range(R):
        for c in range(C):
            if grid[r][c] == '1':
                islands += 1
                flood(r, c)
    return islands

Two islands on a 3×3 grid

Grid rows: ['1','1','0'], ['0','1','0'], ['0','0','1']. Count 4-connected islands.

  • hit (0,0)='1'islands=1; flood marks (0,0),(0,1),(1,1)
  • scan continues; (0,2),(1,0),(1,2) water or markedno launch
  • hit (2,2)='1'islands=2; flood marks (2,2)
  • scan donereturn 2

Pro tip. Diagonal touches do not count under 4-connectivity — here (1,1) and (2,2) share only a corner, so they stay two islands. Changing to 8-connectivity is a different graph.

On an R \times C grid, counting islands with DFS/BFS floods is
  1. O(RC) — each cell is flooded a constant number of times
  2. O(2^{RC}) — every subset of land is tried
  3. O(R + C) — only the border matters

The outer scan visits every cell once; each flood visit marks a cell so it will not be re-expanded. Work is linear in the number of cells.

Notes

  • Binary Search Tree: For every node, all left-subtree keys are smaller and all right-subtree keys larger, giving O(h) search where h is height (O(\log n) if balanced, O(n) if skewed).
  • Tree Traversals: In-order (left, node, right) yields sorted order in a BST; pre-order and post-order are used for copying and deletion respectively.
  • Graph Representations: An adjacency list uses O(V+E) space and suits sparse graphs; an adjacency matrix uses O(V^2) and gives O(1) edge lookups for dense graphs.
  • BFS: Explores level by level using a queue; finds shortest paths in unweighted graphs in O(V+E).
  • DFS: Explores as deep as possible using recursion or a stack; used for cycle detection, topological sort, and connected components in O(V+E).

Formulas

  • BST search/insert/delete: O(h), i.e., O(\log n) balanced, O(n) worst case.
  • BFS and DFS: O(V + E) time, O(V) space.
  • Adjacency list space: O(V + E); adjacency matrix space: O(V^2).
  • Balanced tree height: h = O(\log n); complete binary tree with n nodes has height \lfloor \log_2 n \rfloor.
  • Number of edges in a tree with n nodes: exactly n - 1.

Exam traps & shortcuts

  • Shortest path in an unweighted graph => BFS; explore/detect cycles/topological order => DFS.
  • In-order traversal of a BST outputs keys in sorted order - use it to validate a BST or find the k-th smallest.
  • Choose adjacency list for sparse graphs (most real graphs); matrix only when the graph is dense or you need O(1) edge checks.
  • For 'number of islands' or connected components, run DFS/BFS from each unvisited node and count launches.

Reference tables

Height h governs tree walks; V and E govern graph walks. Balanced BST height is \Theta(\log n); a skewed BST is \Theta(n).

Costs at a glance
Operation / structureTimeSpace / note
BST search / insert / deleteO(h)\Theta(\log n) if balanced, \Theta(n) if skewedO(h) recursion stack; O(1) extra if iterative
Tree traversals (all four)O(n)O(h) recursion or O(n) queue
BFS or DFS on a graphO(V + E)O(V) visited / queue / stack
Adjacency list storageO(V + E)
Adjacency matrix storageO(V^2)
Tree on n nodesexactly n - 1 edges
Complete binary tree height\lfloor \log_2 n \rfloor

Same asymptotic cost; different first-reach order. Weighted shortest paths and MST algorithms are not in this topic.

BFS vs DFS — which question?
QuestionPreferWhy
Shortest hop path (unweighted)BFSFirst reach is minimum hops
Group tree nodes by depthLevel-order BFSQueue width cuts levels
Sorted keys in a BSTIn-order DFSLeft, node, right respects BST order
Cycle / components / topo finish timesDFSPath structure and finish times
Count islands / componentsEitherLaunches from unvisited land; O(RC)

Recap

Trees add child structure and BST order; graphs add arbitrary edges and need a representation. Traversals then answer different questions with the same O(V+E) budget.

Tree edges
n nodes ⇒ exactly n-1 edges; height h bounds root-to-leaf walks.
BST
Left < node < right; search is O(h), not automatically O(\log n).
In-order
BST in-order is sorted — validate or take the k-th smallest.
Level order
Freeze queue length to cut levels; O(n) time and up to O(n) queue.
Store the graph
List for sparse (O(V+E)); matrix for dense or O(1) edge tests.
Unweighted shortest
BFS, not DFS. Mark on enqueue.
Islands
Each DFS/BFS launch from unvisited land is one component; O(RC).

Practise Trees and Graphs

Reading is free and needs no account. Practice, mocks and progress live in the app.

  • 5 exam-style questions on this topic, with explanations
  • A 6-question practice set that ends the chapter
  • Timed mocks scored with the real marking scheme
  • Readiness tracked per topic, kept on your device
Continue with Google — freeNo card, no trial. Works offline once installed.