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
- At most two childrenEach node may have a left child, a right child, both, or neither — never a third sibling under the same parent.
- Tree edge countEvery node except the root has exactly one parent edge, so n nodes imply n - 1 edges.
- 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?
- 6 — one fewer than the node count
- 7 — one edge per node
- 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
- Start at the rootCompare the target with the current key.
- Branch onceGo left if the target is smaller, right if larger; stop if equal.
- 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
- Every node stores both children as arrays
- The tree's height is \Theta(\log n) (balanced), not when it is skewed
- 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
- Go leftFully traverse the left subtree first.
- VisitRecord the current node's key.
- Go rightFully traverse the right subtree.
| Walk | Order | Typical use |
|---|---|---|
| In-order | left, node, right | Sorted keys in a BST; k-th smallest |
| Pre-order | node, left, right | Copy / serialise a tree |
| Post-order | left, right, node | Delete 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?
- Level order
- In-order (left, node, right)
- 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
- Seed the queuePush the root. An empty tree yields an empty list of levels and stops here.
- Freeze the widthAt the start of a pass, set width = length(queue). Those nodes are exactly the current level.
- Drain and expandDequeue width nodes, append each value to this level, and enqueue each existing left then right child.
- 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 outLevels 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?
- The height of the tree so far
- The current length of the queue (the width of this level)
- 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
- Count densityIf E is far below V^2, a list wastes less space and iterates only real edges.
- Need edge tests?Matrix answers "is (u,v) an edge?" in O(1); a list may scan u's neighbor array.
- Default for interviewsBuild a list (or dict of lists) unless the problem states a dense graph or needs constant-time edge queries.
| Property | Adjacency list | Adjacency matrix |
|---|---|---|
| Space | O(V + E) | O(V^2) |
| Iterate neighbors of u | O(\mathrm{deg}(u)) | O(V) |
| Check edge (u,v) | O(\mathrm{deg}(u)) typical | O(1) |
| Best when | Sparse (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
- An adjacency matrix of size 10^5 \times 10^5
- An adjacency list using O(V + E) space
- 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
- SeedSet dist[s] = 0, enqueue s, mark visited.
- ExpandDequeue u; for each unvisited neighbor v set dist[v] = dist[u] + 1, mark visited, enqueue v.
- 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 distDistances 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?
- DFS is asymptotically slower, so its paths are longer
- DFS may dive deep along a long route before it ever explores a short side branch BFS would have taken first
- 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
- MarkMark u visited as you enter.
- RecurseFor each unvisited neighbor v, recurse on v (or push v on an explicit stack).
- 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
- The order vertices are first discovered (preorder)
- The order vertices finish (postorder / finish times), reversed
- 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
- ScanWalk every cell left-to-right, top-to-bottom.
- LaunchOn an unvisited land cell, add 1 to the island count.
- FloodDFS or BFS from that cell, marking every 4-reachable land cell visited (often by flipping '1' to '0').
- 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 islandsTwo 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
- O(RC) — each cell is flooded a constant number of times
- O(2^{RC}) — every subset of land is tried
- 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).
| Operation / structure | Time | Space / note |
|---|---|---|
| BST search / insert / delete | O(h) — \Theta(\log n) if balanced, \Theta(n) if skewed | O(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 graph | O(V + E) | O(V) visited / queue / stack |
| Adjacency list storage | — | O(V + E) |
| Adjacency matrix storage | — | O(V^2) |
| Tree on n nodes | — | exactly 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.
| Question | Prefer | Why |
|---|---|---|
| Shortest hop path (unweighted) | BFS | First reach is minimum hops |
| Group tree nodes by depth | Level-order BFS | Queue width cuts levels |
| Sorted keys in a BST | In-order DFS | Left, node, right respects BST order |
| Cycle / components / topo finish times | DFS | Path structure and finish times |
| Count islands / components | Either | Launches 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