Lesson 63 of 77 advanced

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

The hardest interview patterns — tree traversal, graph search, recursion thinking, and interval merging

Open interactive version (quiz + challenge)

Real-world analogy

A tree is like a company org chart — the CEO is the root, managers are internal nodes, and individual contributors are leaves. A graph is like a city road network — any intersection can connect to any other. BFS is like spreading rumors at a party — it reaches everyone at the same social distance at the same time. DFS is like following one thread of gossip all the way to the end before backtracking.

What is it?

Trees, graphs, BFS/DFS, recursion, and interval problems are the advanced tier of mobile engineering interviews. They require pattern recognition and the ability to trust recursive thinking — skills that come from understanding the structure, not memorizing solutions.

Real-world relevance

In Tixio's workspace hierarchy (workspace → channels → threads → messages), tree traversal computes unread counts by summing up the tree. In FieldBuzz, finding the shortest route between survey locations on a road network is a graph shortest-path problem. Calendar scheduling in any SaaS app uses merge intervals for conflict detection.

Key points

Code example

// Tree: max depth (recursive)
int maxDepth(TreeNode? node) {
  if (node == null) return 0;
  return 1 + [maxDepth(node.left), maxDepth(node.right)].reduce((a,b) => a>b?a:b);
}

// Tree: level-order traversal (BFS)
import 'dart:collection';
List<List<int>> levelOrder(TreeNode? root) {
  if (root == null) return [];
  final result = <List<int>>[];
  final q = Queue<TreeNode>()..add(root);
  while (q.isNotEmpty) {
    final level = <int>[];
    final size = q.length;
    for (int i = 0; i < size; i++) {
      final node = q.removeFirst();
      level.add(node.val);
      if (node.left != null) q.add(node.left!);
      if (node.right != null) q.add(node.right!);
    }
    result.add(level);
  }
  return result;
}

// Graph: BFS shortest path
int shortestPath(Map<int,List<int>> graph, int src, int dst) {
  final visited = <int>{src};
  final q = Queue<int>()..add(src);
  int steps = 0;
  while (q.isNotEmpty) {
    final size = q.length;
    for (int i = 0; i < size; i++) {
      final node = q.removeFirst();
      if (node == dst) return steps;
      for (final neighbor in graph[node] ?? []) {
        if (visited.add(neighbor)) q.add(neighbor);
      }
    }
    steps++;
  }
  return -1;
}

// Merge intervals
List<List<int>> mergeIntervals(List<List<int>> intervals) {
  intervals.sort((a, b) => a[0].compareTo(b[0]));
  final result = <List<int>>[intervals[0]];
  for (final curr in intervals.skip(1)) {
    if (curr[0] <= result.last[1]) {
      result.last[1] = result.last[1] > curr[1] ? result.last[1] : curr[1];
    } else {
      result.add(curr);
    }
  }
  return result;
}

Line-by-line walkthrough

  1. 1. maxDepth base case: null node contributes 0 to depth
  2. 2. Recursive case: 1 (current node) plus the deeper of left and right subtrees
  3. 3. This single line encodes the entire depth computation — trust the recursion
  4. 4. levelOrder uses BFS with a queue to process nodes level by level
  5. 5. The inner for loop processes exactly one level per outer while iteration
  6. 6. q.length captured before the inner loop — nodes added during iteration are next level
  7. 7. shortestPath BFS increments steps after processing each complete level
  8. 8. visited.add() returns false if element already in set — prevents revisiting
  9. 9. mergeIntervals sorts first so overlapping intervals are guaranteed adjacent
  10. 10. Last interval in result is extended if current interval overlaps it
  11. 11. result.last[1] = max of both ends handles the case where current is fully contained

Spot the bug

// Check if a binary tree is symmetric
bool isSymmetric(TreeNode? root) {
  return isMirror(root, root);
}
bool isMirror(TreeNode? left, TreeNode? right) {
  if (left == null && right == null) return true;
  if (left == null || right == null) return false;
  return left.val == right.val
    && isMirror(left.left, right.left)
    && isMirror(left.right, right.right);
}
Need a hint?
The logic is nearly correct but one recursive call compares the wrong pair of subtrees. A symmetric tree mirrors around its center.
Show answer
Bug: isMirror(left.left, right.left) should be isMirror(left.left, right.right) and isMirror(left.right, right.right) should be isMirror(left.right, right.left). For a mirror check, the left subtree's left child must mirror the right subtree's RIGHT child (not left). The correct calls are: isMirror(left.left, right.right) && isMirror(left.right, right.left).

Explain like I'm 5

A tree is like a family tree — start from grandma (root), visit all kids, then grandkids. BFS visits all the grandma's children before grandchildren (level by level — like rings spreading in water). DFS visits one branch all the way down before backtracking (like following one corridor in a maze to its dead end before trying another).

Fun fact

The concept of graph traversal underpins Google's PageRank algorithm, GPS navigation, social network friend suggestions, and the dependency resolution in Flutter's pub package manager — all of which use variations of BFS or DFS.

Hands-on challenge

Implement a function that counts all messages in a workspace tree where each node has a messageCount and a list of child channels. Use recursion. Then rewrite it iteratively using an explicit stack. Compare the two approaches.

More resources

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