Lesson 65 of 83 advanced

DSA III: Trees, Graphs, BFS/DFS, Recursion & Intervals

The hardest patterns — mastered through intuition, not memorization

Open interactive version (quiz + challenge)

Real-world analogy

A tree is a family tree — one root, branches down to leaves, no cycles. A graph is a city road map — nodes are intersections, edges are roads, and there can be loops. BFS explores a graph level by level like a ripple in water. DFS goes as deep as possible first, like exploring a cave — you either use a stack or let recursion do it for you.

What is it?

Trees and graphs are the data structures underlying most complex real-world systems — file systems, network routing, dependency management, social graphs, and Android's view hierarchy. BFS and DFS are the two fundamental algorithms for exploring these structures. Mastering them means recognizing when a problem is really a graph traversal in disguise (even when it does not say 'graph' explicitly — like grid island counting or course scheduling).

Real-world relevance

In FieldBuzz's field officer management system, the organizational hierarchy (manager → area manager → officer) was modeled as a tree. Computing 'total records submitted by all officers under a manager' was a postorder tree traversal — gather results from all children, then combine at the parent. This is the exact same pattern as computing subtree sums in a BST interview problem.

Key points

Code example

// ─── BINARY TREE NODE ───
class TreeNode(var value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

// ─── INORDER TRAVERSAL (O(n)) ───
fun inorder(root: TreeNode?): List<Int> {
    val result = mutableListOf<Int>()
    fun dfs(node: TreeNode?) {
        if (node == null) return
        dfs(node.left)
        result.add(node.value)
        dfs(node.right)
    }
    dfs(root)
    return result
}

// ─── MAX DEPTH (Recursive, O(n)) ───
fun maxDepth(root: TreeNode?): Int {
    if (root == null) return 0
    return 1 + maxOf(maxDepth(root.left), maxDepth(root.right))
}

// ─── LEVEL-ORDER BFS (O(n)) ───
fun levelOrder(root: TreeNode?): List<List<Int>> {
    if (root == null) return emptyList()
    val result = mutableListOf<List<Int>>()
    val queue = ArrayDeque<TreeNode>()
    queue.addLast(root)
    while (queue.isNotEmpty()) {
        val levelSize = queue.size
        val level = mutableListOf<Int>()
        repeat(levelSize) {
            val node = queue.removeFirst()
            level.add(node.value)
            node.left?.let { queue.addLast(it) }
            node.right?.let { queue.addLast(it) }
        }
        result.add(level)
    }
    return result
}

// ─── NUMBER OF ISLANDS (Grid DFS, O(m*n)) ───
fun numIslands(grid: Array<CharArray>): Int {
    var count = 0
    fun dfs(r: Int, c: Int) {
        if (r < 0 || r >= grid.size || c < 0 || c >= grid[0].size) return
        if (grid[r][c] != '1') return
        grid[r][c] = '0' // mark visited by sinking the island
        dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1)
    }
    for (r in grid.indices) {
        for (c in grid[0].indices) {
            if (grid[r][c] == '1') { count++; dfs(r, c) }
        }
    }
    return count
}

// ─── MERGE INTERVALS (O(n log n)) ───
fun merge(intervals: Array<IntArray>): Array<IntArray> {
    intervals.sortBy { it[0] } // sort by start time
    val merged = mutableListOf<IntArray>()
    for (interval in intervals) {
        if (merged.isEmpty() || merged.last()[1] < interval[0]) {
            merged.add(interval) // no overlap
        } else {
            merged.last()[1] = maxOf(merged.last()[1], interval[1]) // merge
        }
    }
    return merged.toTypedArray()
}

Line-by-line walkthrough

  1. 1. inorder uses a nested 'dfs' function — Kotlin allows local function definitions inside other functions, which is clean for recursive helpers that need access to the result list.
  2. 2. maxDepth is the purest recursive tree solution: null returns 0 (base case), non-null returns 1 (this node) plus the maximum of its children's depths. Three lines, perfectly expressive.
  3. 3. levelOrder records queue.size before the inner repeat loop — this is the number of nodes at the current level. As children are added to the queue during the loop, queue.size grows, but the repeat count is already fixed.
  4. 4. node.left?.let { queue.addLast(it) } uses Kotlin's safe-call with let — if left is null, nothing happens. Cleaner than an explicit null check.
  5. 5. numIslands modifies the grid in-place by setting visited '1' cells to '0' — this is the 'sinking island' trick that avoids an O(m*n) visited array. Only acceptable if modifying input is allowed (confirm with interviewer).
  6. 6. The four DFS calls in numIslands explore all 4-directional neighbors — up, down, left, right. The boundary check 'r = grid.size' is the base case that prevents array out-of-bounds.
  7. 7. merge sorts intervals by start time first. Then the merge condition is: if the current interval's start is greater than the last merged interval's end, there is no overlap — add a new interval.
  8. 8. When there is overlap (current.start <= last.end), we take the maximum of the two end times — because one interval might be entirely contained within another (e.g. [1,10] and [2,5] merge to [1,10], not [1,5]).

Spot the bug

fun maxDepth(root: TreeNode?): Int {
    if (root == null) return 0
    val left = maxDepth(root.left)
    val right = maxDepth(root.right)
    return maxOf(left, right)
}
Need a hint?
The function always returns a depth that is 1 less than the correct answer for every tree. Trace through a tree with just one node.
Show answer
Bug: The function returns maxOf(left, right) but forgets to add 1 for the current node itself. A single-node tree should have depth 1: left=0, right=0, result should be 1. But maxOf(0, 0) = 0. A two-level tree with root and one child should have depth 2: the child returns 1 (after fix), root should return 2, but this returns maxOf(1, 0) = 1. The fix: return 1 + maxOf(left, right). The '1 +' accounts for the current node being counted as one level.

Explain like I'm 5

A tree is like a family tree — grandparent at top, parents in the middle, kids at the bottom. BFS explores level by level: grandparent first, then all parents, then all kids. DFS goes deep first: grandparent → parent → kid → back up → other kid. A graph is like a city map where any street can connect to any other. Number of Islands is: count how many land blobs you see when you look at the map from above.

Fun fact

The 'Number of Islands' problem is used in actual production systems — Google Maps' region detection, flood fill in image editors (Paint's bucket tool is DFS on a pixel grid), and network segmentation all use the same connected-component algorithm. When Instagram added the 'flood fill' story background color feature, engineers essentially implemented numIslands on an image pixel graph.

Hands-on challenge

Implement 'Word Ladder': given a beginWord, endWord, and a wordList, find the length of the shortest transformation sequence from beginWord to endWord, where each intermediate word must differ by exactly one letter and exist in the wordList. Example: beginWord='hit', endWord='cog', wordList=['hot','dot','dog','lot','log','cog'] returns 5 (hit->hot->dot->dog->cog). Why is BFS the correct algorithm here? Could DFS give a shorter path? Trace the BFS level by level.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery