Part III · Trees and Graphs Pattern 8 4 problems

Tree and Graph Depth-First Search

Follow one path to the end, then unwind and try the next. The call stack keeps the path for you. That is why DFS code is so much shorter than the problem sounds.

BFS answers “how far”. DFS answers “which paths” and “what is true of this whole subtree”. It is the workhorse of tree questions. Most tree questions are naturally recursive: the answer for a node combines the answers for its children. In C# there is one extra thing to watch. The call stack is small, about 1 MB, and overflowing it kills the process. This page shows when to switch to an explicit Stack<T>. The Python version of this page is here.

Contents

  1. When to use
  2. Core idea
  3. The three shapes of a DFS
  4. The templates
  5. Common mistakes
  6. Maximum Depth of Binary Tree
  7. Path Sum II
  8. Number of Islands
  9. Clone Graph
  10. Recap

When to use

The trigger. You need to explore whole paths. Or you must decide something about a whole subtree or region. Or a node’s answer is built from its children’s answers. Also, any time the word is “all”: all paths, all islands, all valid arrangements.
Do not use DFS for shortest paths. DFS finds a path, not the shortest one. If the question says “minimum number of steps”, switch to BFS. Reaching for DFS there is a common and costly reflex.

Core idea

Visit a node, then fully explore its first branch before looking at the second. The recursion does the remembering. The chain of active calls is the current path from the root. Returning from a call is exactly the act of backing up one step. BFS needs an explicit Queue<T>. DFS gets its stack for free.
A B E C D F 123 456 call stack while visiting C dfs(C) ← top dfs(B) dfs(A) ← root the stack IS the path A → B → C returning from dfs(C) backtracks one step
Figure 8.1 — Visit order 1 to 6. At any moment the active calls spell out the current root-to-node path.

The three shapes of a DFS

Almost every DFS question is one of these three. Name the shape before you write. It is the fastest way to get the code right.

How to choose. Ask: can a node answer the question using only what its children return? If yes, go bottom-up, and the method returns something. If the node needs to know where it came from, go top-down, and the method takes an extra parameter. If the question is only “how many separate regions”, mark and spread. That method returns void.

The templates

Definitions used on this page
/// <summary>A binary tree node.</summary>
/// <example>new TreeNode(3, new TreeNode(9), new TreeNode(20)) is a root with two leaves.</example>
public class TreeNode(int val = 0, TreeNode? left = null, TreeNode? right = null)
{
    public int Val { get; set; } = val;              // 0 default: payload when none is given
    public TreeNode? Left { get; set; } = left;      // null: no left child
    public TreeNode? Right { get; set; } = right;    // null: no right child
}

/// <summary>An undirected graph node with an adjacency list.</summary>
/// <example>var a = new GraphNode(1); a.Neighbors.Add(new GraphNode(2));</example>
public class GraphNode(int val = 0)
{
    public int Val { get; } = val;                   // 0 default: payload when none is given

    // [] runs once per new node, so every node gets its own fresh list.
    // C# has no shared-mutable-default trap like Python's def f(x=[]).
    public List<GraphNode> Neighbors { get; set; } = [];
}

Both are plain classes, not records. A class compares by reference, so two different nodes are never “equal”. Clone Graph depends on that.

Template A — bottom-up, returns a value
public static class BottomUpTemplate
{
    /// <summary>Combine the children's answers into this node's answer.</summary>
    /// <param name="node">Subtree root, or null for an empty subtree.</param>
    /// <returns>The answer for the whole subtree.</returns>
    /// <example>Summarise(tree 3 / (9, 20 / (15, 7))) returns 3 with the depth Combine.</example>
    public static int Summarise(TreeNode? node)
    {
        // 0: the answer for an empty subtree. Max Depth: an empty tree has depth 0.
        // This base case defines the whole recursion.
        if (node is null) return 0;

        // Trust the calls: each returns the full answer for its subtree.
        int left = Summarise(node.Left);
        int right = Summarise(node.Right);

        return Combine(node.Val, left, right);
    }

    // Combine(val, left, right): placeholder that builds this node's answer from its
    // children's. Max Depth example: 1 + the deeper side. The 1 counts this node.
    private static int Combine(int val, int left, int right) => 1 + Math.Max(left, right);
}
Template B — top-down with backtracking
public static class TopDownTemplate
{
    /// <summary>Record every root-to-leaf path that qualifies.</summary>
    /// <param name="root">Tree root, or null.</param>
    /// <param name="qualifies">Placeholder test: does this full path count?
    /// Path Sum II example: trail => trail.Sum() == 22.</param>
    /// <returns>Copies of every qualifying path, in traversal order.</returns>
    /// <example>Collect(tree 1 / (2, 3), t => true) returns [[1, 2], [1, 3]].</example>
    public static List<List<int>> Collect(TreeNode? root, Func<List<int>, bool> qualifies)
    {
        var results = new List<List<int>>();
        var trail = new List<int>();          // values from the root down to the current node

        // Invariant: on entry and on exit, trail is the path to node's parent.
        void Walk(TreeNode? node)
        {
            if (node is null) return;         // past a leaf: nothing to record

            trail.Add(node.Val);              // choose

            // Both children null means a leaf, so trail is a full root-to-leaf path.
            if (node.Left is null && node.Right is null)
            {
                if (qualifies(trail)) results.Add([.. trail]);   // COPY: trail keeps changing
            }
            else
            {
                Walk(node.Left);
                Walk(node.Right);
            }

            // Count - 1: index of the last value. Remove it to un-choose: the backtrack.
            trail.RemoveAt(trail.Count - 1);
        }

        Walk(root);
        return results;
    }
}

Choose, recurse, un-choose. Every top-down DFS with a shared mutable trail has this shape. It is the same shape as backtracking. Walk is a local function, so it can read trail and results without passing them in. [.. trail] is a collection expression with a spread, which makes a new list.

1 2 3 4 leaves: 4 and 3 trail after each step (bottom = root) 1 push 1 1 2 push 2 1 2 4 push 4 record 1 2 pop 4 1 pop 2 1 3 push 3 record 1 pop 3 [ ] pop 1
Figure 8.2 — Every push has a matching pop, so the one shared list always equals the path to the current node.

Reading the figure. Each column is the trail list after one step, with the root at the bottom. Amber is a value just pushed. Green is a push that reached a leaf, where a copy is recorded. Blue values are lower on the path. Red labels mark the un-choose step. Notice that the trail grows and shrinks like the call stack in Figure 8.1, and ends empty.

Template C — mark and spread, iterative
public static class FloodTemplate
{
    // Down, up, right, left as (row step, col step). No diagonals.
    private static readonly (int Dr, int Dc)[] Steps = [(1, 0), (-1, 0), (0, 1), (0, -1)];

    /// <summary>Mark the whole connected region containing (r, c). No overflow risk.</summary>
    /// <param name="grid">Rows of '1' (land) and '0' (water). Mutated: land becomes '0'.</param>
    /// <param name="r">Start row. Must be a land cell.</param>
    /// <param name="c">Start column.</param>
    /// <example>Flood([['1','1'],['0','1']], 0, 0) leaves every cell '0'.</example>
    public static void Flood(char[][] grid, int r, int c)
    {
        int rows = grid.Length, cols = grid[0].Length;   // grid[0]: every row has this width
        var stack = new Stack<(int Row, int Col)>();
        stack.Push((r, c));
        grid[r][c] = '0';                    // '0': mark on push so it is never pushed twice

        // Invariant: every cell on the stack is land already marked '0'.
        while (stack.Count > 0)               // > 0: cells still waiting to spread
        {
            var (row, col) = stack.Pop();     // Pop takes the newest: LIFO, so depth-first

            // (dr, dc): one of the four steps around the current cell.
            foreach (var (dr, dc) in Steps)
            {
                int nr = row + dr, nc = col + dc;
                // 0 <= ... < rows/cols: stay on the grid. '1': unvisited land only.
                if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1')
                {
                    grid[nr][nc] = '0';       // '0': sink it so no one pushes it again
                    stack.Push((nr, nc));
                }
            }
        }
    }
}

Stack<T> lives on the heap, so it can hold millions of cells. The call stack cannot. The tuple (int Row, int Col) is a value type, so each push stores two ints and allocates nothing.

start 0 1 0 1 0 0 1 1 0 stack: (0,0) pop (0,0) 0 0 0 0 0 0 1 1 0 stack: (1,0) (0,1) pop (0,1) 0 0 0 0 0 0 1 1 0 stack: (1,0) pop (1,0) 0 0 0 0 0 0 0 1 0 stack: (2,0) pop (2,0) 0 0 0 0 0 0 0 0 0 stack: (2,1) one more pop of (2,1) empties the stack. All five land cells are sunk.
Figure 8.3 — Cells are marked when pushed, so each land cell enters the stack once and the region drains in linear time.

Reading the figure. Violet cells are land not reached yet. Amber cells are on the stack, and they already read '0' because marking happens on push. Red cells were popped and are done. Grey cells are water. Notice that (0,0) is never pushed again when (0,1) looks back at it.

Common mistakes

The problems

1. Maximum Depth of Binary Tree Easy

Problem

Return the number of nodes along the longest path from the root down to the furthest leaf.

Approach

Solution: recursive

public static class MaxDepthOfBinaryTree
{
    /// <summary>Number of nodes on the longest root-to-leaf path.</summary>
    /// <param name="root">Root node, or null.</param>
    /// <returns>0 for an empty tree, 1 for a single node, and so on.</returns>
    /// <example>MaxDepth(tree [3, 9, 20, null, null, 15, 7]) returns 3.</example>
    public static int MaxDepth(TreeNode? root)
    {
        if (root is null) return 0;           // 0: an empty tree has no nodes on any path

        // 1 +: count this node, then add the deeper of the two subtrees.
        return 1 + Math.Max(MaxDepth(root.Left), MaxDepth(root.Right));
    }
}
3 9 20 15 7 returns 1 returns 1 returns 1 returns 2 returns 3 Each call trusts its children, then adds 1. leaf 9: 1 + max(0, 0) = 1 node 20: 1 + max(1, 1) = 2 root 3: 1 + max(1, 2) = 3 An empty child returns 0, so a leaf needs no special case.
Figure 8.4 — Answers flow up. Each node returns one more than its deeper child, and the root returns 3.

Reading the figure. This uses the tree 3, 9, 20, 15, 7 from the BFS page. Blue nodes have finished and returned the purple number. Purple arrows show a child’s answer flowing up to its parent. The green root holds the final answer. The deeper side, through 20, decides the result.

Solution: iterative, if depth is a concern

public static class MaxDepthWithStack
{
    /// <summary>Same answer with an explicit stack of (node, depth) pairs.</summary>
    /// <param name="root">Root node, or null.</param>
    /// <returns>0 for an empty tree, else the node count of the longest path.</returns>
    /// <example>MaxDepth(a left-leaning chain of 100,000 nodes) returns 100000.</example>
    public static int MaxDepth(TreeNode? root)
    {
        if (root is null) return 0;           // 0: an empty tree has depth 0

        int best = 0;                         // 0: no depth seen yet
        var stack = new Stack<(TreeNode Node, int Depth)>();
        stack.Push((root, 1));                // 1: the root alone is a path of one node

        // Invariant: best is the deepest depth among nodes popped so far, and
        // every pair on the stack carries its node's correct depth.
        while (stack.Count > 0)               // > 0: nodes still to visit
        {
            var (node, depth) = stack.Pop();
            best = Math.Max(best, depth);

            // depth + 1: a child sits one level below its parent.
            if (node.Left is not null) stack.Push((node.Left, depth + 1));
            if (node.Right is not null) stack.Push((node.Right, depth + 1));
        }

        return best;
    }
}
TimeO(n)SpaceO(h)htree height

Space is O(log n) on a balanced tree and O(n) on a degenerate one. Say that, not just “O(h)”. It shows you know what h can be.

The variations built on the same three lines

Edge cases to raise

Say this out loud: “The base case returns zero for an empty subtree. That makes a leaf come out as one, so no leaf special case is needed.”

2. Path Sum II Medium

Problem

Return every root-to-leaf path whose node values sum to targetSum. Each path is the list of values along it.

Approach

Solution

public static class PathSumII
{
    /// <summary>All root-to-leaf paths whose values sum to targetSum.</summary>
    /// <param name="root">Root node, or null.</param>
    /// <param name="targetSum">The required total. Values may be negative.</param>
    /// <returns>A list of paths, each a list of values from root to leaf.</returns>
    /// <example>Tree 5 / (4, 8) ... with target 22 returns [[5, 4, 11, 2], [5, 8, 4, 5]].</example>
    public static List<List<int>> PathSum(TreeNode? root, int targetSum)
    {
        var paths = new List<List<int>>();
        var trail = new List<int>();          // values from the root down to the current node

        // remaining: how much the rest of the path still has to add up to.
        // Invariant: on entry and on exit, trail is the path to node's parent.
        void Walk(TreeNode? node, int remaining)
        {
            if (node is null) return;         // past a leaf: no path here

            trail.Add(node.Val);              // choose
            remaining -= node.Val;            // this node pays part of the target

            // Both children null means a leaf. Only full paths may count.
            if (node.Left is null && node.Right is null)
            {
                // == 0: the path used up the target exactly.
                if (remaining == 0) paths.Add([.. trail]);   // copy: trail keeps changing
            }
            else
            {
                Walk(node.Left, remaining);
                Walk(node.Right, remaining);
            }

            // Count - 1: index of the last value. Un-choose, on every exit path.
            trail.RemoveAt(trail.Count - 1);
        }

        Walk(root, targetSum);                // the whole target is still owed at the root
        return paths;
    }
}

Why the leaf test is Left is null && Right is null

A node with one child is not a leaf. If you test only remaining == 0, you report paths that stop halfway down. That is wrong. Suppose you recurse into a null child and test there instead. Then a one-child node whose partial sum matches gets reported twice, once per null. Test for a real leaf, explicitly.

Why [.. trail] and not trail

List<int> is a reference type. trail is one list that changes all through the walk. Adding it stores a reference to that one list. By the time the method returns, every stored reference points at the same, now empty, list. This is the most common bug in every backtracking problem, not just this one.

Walkthrough

Take the tree with root 5, children 4 and 8, and target 22. The recursion drives down 5 → 4 → 11 → 7. That sums to 27, no match. It pops back to 11 and tries 2, which gives 22 and a recorded path. It unwinds to the root, removing each value. Then it explores the 8 branch.

5 4 8 11 13 4 7 2 5 1 sum 27 sum 22 sum 26 sum 22 sum 18 trail when leaf 2 is reached 5 4 11 2 remaining: 22 - 5 - 4 - 11 - 2 = 0 so record a copy, [.. trail] then pop 2, pop 11, pop 4 ... results: [5, 4, 11, 2] [5, 8, 4, 5]
Figure 8.5 — Only leaves are tested. Two of the five root-to-leaf paths use up the target exactly.

Reading the figure. Each leaf shows the sum of its root-to-leaf path. Green leaves and edges sum to 22 and are recorded. Red leaves miss the target. The boxes on the right are trail at the first hit. After that leaf, the walk pops values on the way back up, then tries the 8 branch.

TimeO(n · h)SpaceO(h) working, O(n · h) output

Visiting is O(n). But copying a qualifying path costs O(h) each time. A pathological tree can have O(n) of them.

Edge cases to raise

Say this out loud: “Choose, recurse, un-choose. I add a copy of the trail, because the trail itself changes all the way through the walk.”

3. Number of Islands Medium

Problem

A grid holds '1' for land and '0' for water. An island is a group of land cells joined up, down, left or right. Count the islands. In C# the grid is a char[][], a jagged array, as on LeetCode.

Approach

Solution

public static class NumberOfIslands
{
    // Down, up, right, left as (row step, col step). No diagonals.
    private static readonly (int Dr, int Dc)[] Directions = [(1, 0), (-1, 0), (0, 1), (0, -1)];

    /// <summary>Count connected regions of '1' in a grid, using 4-directional adjacency.</summary>
    /// <param name="grid">Rows of '1' (land) and '0' (water). Mutated: land is sunk
    /// to '0' as it is visited.</param>
    /// <returns>The number of islands.</returns>
    /// <example>NumIslands([['1','1','0'], ['0','1','0'], ['0','0','1']]) returns 2.</example>
    public static int NumIslands(char[][] grid)
    {
        // Length == 0: no rows, or a first row with no columns. Either way no cells.
        if (grid.Length == 0 || grid[0].Length == 0) return 0;   // 0: no cells, no islands

        int rows = grid.Length, cols = grid[0].Length;   // grid[0]: every row has this width
        int islands = 0;                      // 0: none found yet

        // Flood the region containing (startR, startC), iteratively.
        // An explicit Stack<T> avoids the 1 MB call stack, which a large
        // all-land grid would otherwise overflow.
        void Sink(int startR, int startC)
        {
            var stack = new Stack<(int R, int C)>();
            stack.Push((startR, startC));
            grid[startR][startC] = '0';       // '0': sink on push so it is never pushed twice

            // Invariant: every cell on the stack is land already sunk to '0'.
            while (stack.Count > 0)           // > 0: cells still waiting to spread
            {
                var (r, c) = stack.Pop();     // Pop takes the newest: LIFO, so depth-first

                // (dr, dc): one of the four steps around (r, c).
                foreach (var (dr, dc) in Directions)
                {
                    int nr = r + dr, nc = c + dc;
                    // 0 <= ... < rows/cols: stay on the grid. '1': unsunk land only.
                    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1')
                    {
                        grid[nr][nc] = '0';   // mark on push
                        stack.Push((nr, nc));
                    }
                }
            }
        }

        // Scan every cell. Invariant: every island touching a scanned cell is
        // counted and fully sunk, so no '1' left behind belongs to it.
        // r: current row, 0..rows-1. c: current column, 0..cols-1.
        for (int r = 0; r < rows; r++)
        {
            for (int c = 0; c < cols; c++)
            {
                if (grid[r][c] == '1')        // '1': land no flood has reached
                {
                    islands++;                // + 1: a region we have not seen before
                    Sink(r, c);
                }
            }
        }

        return islands;
    }
}

Walkthrough

Take [['1','1','0'],['0','1','0'],['0','0','1']]. The scan hits (0,0) and counts island 1. It sinks (0,0), (0,1) and (1,1). The scan moves on over cells that are now water until (2,2). It counts island 2 and sinks it. The answer is 2.

scan hits (0,0) 1 1 0 0 1 0 0 0 1 islands = 1 island 1 sunk 0 0 0 0 0 0 0 0 1 islands = 1 scan hits (2,2) 0 0 0 0 0 0 0 0 1 islands = 2 island 2 sunk 0 0 0 0 0 0 0 0 0 islands = 2 answer: 2. The counter moves once per region, not once per cell.
Figure 8.6 — Each new land cell the scan finds starts one island. Sinking it stops the scan from counting it again.

Reading the figure. Violet cells are land not visited yet. Amber is the land cell the scan just found, which adds one to the count. Red cells are sunk to '0'. Grey cells are water. Between frame 2 and frame 3 the scan passes over the red cells without counting them.

TimeO(rows × cols)SpaceO(rows × cols) stack worst case

The outer scan looks at every cell once. Each cell is pushed at most once. So the total is linear in the number of cells, despite the nested loops.

Recursive version, and why it is risky

public static class IslandsRecursive
{
    // Down, up, right, left as (row step, col step). No diagonals.
    private static readonly (int Dr, int Dc)[] Steps = [(1, 0), (-1, 0), (0, 1), (0, -1)];

    /// <summary>Elegant, but the call depth equals the region size.</summary>
    /// <param name="grid">The grid. Land in the region becomes '0'.</param>
    /// <param name="r">Row to try. May be off the grid.</param>
    /// <param name="c">Column to try. May be off the grid.</param>
    /// <example>Sink([['1','1']], 0, 0) turns the grid into [['0','0']].</example>
    public static void Sink(char[][] grid, int r, int c)
    {
        // Off the grid (below 0 or past the last index), or not unvisited land: stop.
        if (r < 0 || r >= grid.Length || c < 0 || c >= grid[r].Length || grid[r][c] != '1')
            return;

        grid[r][c] = '0';                     // '0': sink before recursing, or we loop forever

        // (dr, dc): spread to each of the 4 neighbours.
        foreach (var (dr, dc) in Steps) Sink(grid, r + dr, c + dc);
    }
}
A 1000×1000 grid of solid land gives a call chain up to a million frames deep. That is far past 1 MB of stack. In C# this is worse than Python’s RecursionError. StackOverflowException cannot be caught, so the process just ends. Write the recursive version if it is clearer. Then say “in production I would make this iterative because the depth is unbounded.” That sentence is worth real points.

Edge cases to raise

Say this out loud: “The counter goes up once per region, not once per cell. The sink call removes the whole region before the scan moves on.”

4. Clone Graph Medium

Problem

You get a reference to a node in a connected, undirected graph. Return a deep copy of the whole graph. Each node holds a value and a list of neighbours.

The one hard part

A graph has cycles, so a naive recursive copy loops forever. The fix is a dictionary from original node to its clone. The key detail is when you write into it: register the clone before recursing into the neighbours. Then the recursion may come back around a cycle to a node already in progress. It finds the half-built clone and returns it instead of starting again.

This map does three jobs at once. It is the visited set. It is where partly built clones live. And it lets a second edge into the same node find the same clone, not a duplicate.

Solution

public static class CloneGraph
{
    /// <summary>Deep-copy a connected undirected graph reachable from node.</summary>
    /// <param name="node">Any node of the graph, or null for an empty graph.</param>
    /// <returns>The clone of node, with the whole graph copied.</returns>
    /// <example>Clone(node 1 of cycle 1-2-3-4-1) returns a new 1 linked to new 2 and 4.</example>
    public static GraphNode? Clone(GraphNode? node)
    {
        if (node is null) return null;        // empty graph: nothing to copy

        // original -> its clone. Doubles as the visited set, so cycles stop.
        // ReferenceEqualityComparer: match by identity even if Equals is ever overridden.
        var clones = new Dictionary<GraphNode, GraphNode>(ReferenceEqualityComparer.Instance);

        GraphNode Copy(GraphNode original)
        {
            // TryGetValue: one lookup that also hands back the clone if it exists.
            if (clones.TryGetValue(original, out var existing))
                return existing;              // already built, or being built right now

            var duplicate = new GraphNode(original.Val);
            clones[original] = duplicate;     // REGISTER FIRST, then recurse

            // neighbour: each original neighbour in order. Clone it (or find it), then link.
            foreach (var neighbour in original.Neighbors)
                duplicate.Neighbors.Add(Copy(neighbour));
            return duplicate;
        }

        return Copy(node);
    }
}

Iterative version, BFS flavoured

public static class CloneGraphWithQueue
{
    /// <summary>Same result with an explicit queue, so there is no stack depth limit.</summary>
    /// <param name="node">Any node of the graph, or null for an empty graph.</param>
    /// <returns>The clone of node, with the whole graph copied.</returns>
    /// <example>Clone(a self-looped node) returns a clone whose neighbour is itself.</example>
    public static GraphNode? Clone(GraphNode? node)
    {
        if (node is null) return null;        // empty graph: nothing to copy

        // original -> its clone. Build the start node's clone up front.
        var clones = new Dictionary<GraphNode, GraphNode>(ReferenceEqualityComparer.Instance)
        {
            [node] = new GraphNode(node.Val),
        };
        var queue = new Queue<GraphNode>();   // FIFO is enough: we only take from the front
        queue.Enqueue(node);

        // Invariant: every node in the queue already has a clone in clones.
        // Its clone's neighbour list is filled in when the node is dequeued.
        while (queue.Count > 0)               // > 0: nodes whose links are not copied yet
        {
            var original = queue.Dequeue();

            // neighbour: each original neighbour, in order.
            foreach (var neighbour in original.Neighbors)
            {
                // First sighting: make its clone now and queue it once.
                if (!clones.TryGetValue(neighbour, out var twin))
                {
                    twin = new GraphNode(neighbour.Val);
                    clones[neighbour] = twin;
                    queue.Enqueue(neighbour);
                }

                // Link the clones in the same order as the originals.
                clones[original].Neighbors.Add(twin);
            }
        }

        return clones[node];                  // the clone of the start node
    }
}

C# has no built-in deque. This version does not need one. Queue<T> is a ring buffer with O(1) Enqueue and Dequeue, and that is all BFS uses.

Walkthrough

Take the four-node cycle 1 – 2 – 3 – 4 – 1. Copy(1) registers clone 1, then recurses to 2. That registers clone 2, then goes to 3, then to 4. Node 4’s neighbours are 3 and 1. Both are already in the map, so both return at once. The cycle closes with no endless recursion. The recursion then unwinds, filling in each neighbour list.

1 2 3 4 originals call stack: Copy(1) → 2 → 3 → 4 clones map, in insert order 1. node 1 → clone 1 2. node 2 → clone 2 3. node 3 → clone 3 4. node 4 → clone 4 register first, then recurse clones 1′ 2′ 3′ 4′ 4′ finds 3′ and 1′ in the map dashed links fill in as calls return
Figure 8.7 — Registering each clone before recursing lets the cycle close through the map instead of looping forever.

Reading the figure. Blue nodes are on the call stack, still waiting for their neighbour lists. Amber is the current call, Copy(4). The middle column is clones, filled in the order shown. On the right, 4′ is the clone of node 4. Its green links are done. The dashed links are made as the other calls return.

TimeO(V + E)SpaceO(V)

Each node is created once. Each edge is walked once from each side.

Edge cases to raise

Say this out loud: “I put the clone in the map before I recurse. That stops the cycle. A neighbour that loops back finds the partly built clone instead of starting a new one.”

Recap

The six things to carry forward

Where this goes next

Pattern 9, Top K Elements, changes the tool. It uses a heap, PriorityQueue<TElement, TPriority>, instead of a traversal. The trick is that you almost never need the data sorted, only its extremes.


← 07 — Tree and Graph Breadth-First Search 09 — Top ‘K’ Elements (Heaps) →