DSA sheet · Trees · DFS traversal pattern

All Nodes at Distance K

This problem asks us to walk outwards from one node, in every direction, for exactly K steps. The tricky part is that a binary tree only lets us walk down (parent → child). Walking up is not built in. The teacher's whole lesson is about one idea: if you can't walk up, remember who your parent is. Once every node knows its parent, the problem becomes a simple level-by-level spread from the target. It is a favourite interview question because it mixes DFS (to build the parent map) and BFS (to spread out K steps) in one solution.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Terms used on this page

given by LeetCode, don't write this in the solution
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

The example used all through this page

            3
          /   \
         5     1
        / \   / \
       6   2 0   8
          / \
         7   4

  target = 5,  k = 2   →   answer = [7, 4, 1]

Why? From 5, two steps down: 5 → 2 → 7 and 5 → 2 → 4. Two steps up and over: 5 → 3 → 1. Nothing else is exactly 2 steps away (6 is only 1 step, 0 and 8 are 3 steps).

Part A · Step 1: the parent map with DFS

LeetCode 863 · All Nodes Distance K in Binary Tree

1The question in simple words

You get the root of a binary tree, a target node inside it, and a number k. Return the values of all nodes that are exactly k steps away from the target. Steps can go to a child or to the parent. Any order is fine.

So the target is like a stone dropped in water: we want the ring of nodes at exactly k steps, spreading down into its own subtree and also up through its parent and then down the other branches.

2What the constraints tell us

3Intuition: the real problem is going up

Going down from 5 is free: node.left and node.right are right there. Going up from 5 to 3 is the problem. A node has no link back to its parent. The arrows in a tree only point downwards.

The teacher says: don't try to flip the tree or change its directions. That's not needed. Instead, do something simpler: walk the tree once, and for every node write down "my parent is ___" in a dictionary. Later, whenever we want to go up from a node, we just look it up and jump to its parent.

Think of it as giving every node a little sticky note with its parent's name. The tree's arrows still point down, but now every node also knows the way up.

4Building the logic from the example

Which data structure stores "node → its parent"?

We want to link one node to one other node: a key and a value. That's exactly what a hash map does. In Python that's a dict: parent[node] = its parent node.

Doubt 1: should the keys be node values (like 5) or the nodes themselves?
→ Store the nodes. In this problem values happen to be unique, so values would also work, but nodes are always safe, and we need the node anyway to read its .left and .right. Python can use a TreeNode object directly as a dictionary key.

How do we fill it? Pass the parent down

We start at the root. The root has no parent, so we store None for it. Then, when we go to the root's left child and right child, we hand them the root as their parent. They store it, and hand themselves down to their own children. And so on.

Doubt 2: in the video, the teacher first writes the parent of 2 as 1. Is that right?
→ No, and she catches it herself during the dry run: 2 hangs under 5, so the parent of 2 is 5. A good reminder to fill the map slowly. A wrong parent sends the BFS to the wrong place later.

The base case

This is a normal DFS, so it needs a stopping point: if the node is None, there's nothing to store. Just return.

The whole of Step 1 if node is None: return
parent[node] = par
build(node.left, node) and build(node.right, node)
First call: build(root, None).
Doubt 3: why store None for the root at all? Couldn't we just skip it?
→ Later, the BFS asks every node "who is your parent?". If the root has no entry, parent[root] would crash with a KeyError. Storing None gives a clean answer: "nobody above me". Then we just check is not None before using it.

5Approach steps

  1. Make an empty dictionary parent.
  2. Call build(root, None).
  3. Inside build(node, par): if node is None, return.
  4. Store parent[node] = par.
  5. Recurse on the left child with node as its parent, then on the right child with node as its parent.

6Code (Python)

Step 1: build the parent map (DFS)
def build_parent(node, par, parent):
    if node is None:              # base case: nothing to store
        return
    parent[node] = par            # sticky note: "my parent is par"
    build_parent(node.left, node, parent)    # I am my left child's parent
    build_parent(node.right, node, parent)   # I am my right child's parent

7Code line by line

linewhat it means
if node is None: returnWe walked past a leaf. There's no node here, so nothing to write down. This stops the recursion.
parent[node] = parWrite down this node's parent. For the root, par is None.
build_parent(node.left, node, parent)Go to the left child, and tell it: "I am your parent".
build_parent(node.right, node, parent)Same for the right child.

8Dry run using the call stack

  1. build(3, None): store 3 → None. Go left.
  2. build(5, 3): store 5 → 3. Go left.
  3. build(6, 5): store 6 → 5. Both children are None, so both calls return at once. 6 is done.
  4. Back in 5, go right: build(2, 5): store 2 → 5. Go left.
  5. build(7, 2): store 7 → 2. Leaf, done. Then build(4, 2): store 4 → 2. Leaf, done.
  6. 2 is done, 5 is done. Back in 3, go right: build(1, 3): store 1 → 3.
  7. build(0, 1): store 0 → 1. build(8, 1): store 8 → 1. Everything is done.
stack at step 3
(3, None)(5, 3)(6, 5)
stack at step 5 (deepest)
(3, None)(5, 3)(2, 5)(7, 2)
stack at step 7
(3, None)(1, 3)(8, 1)
node351620874
parentNone33551122

9Complexity & remember

Remember Step 1A tree can't go up, so don't flip it, remember it: one DFS, passing the parent down, fills parent[node]. Root's parent is None.

Part B · Step 2: spreading K steps from the target with BFS

1The question, now that we have parents

Every node now has three possible neighbours: its left child, its right child, and its parent (from the map). The question becomes: starting at the target, take one step at a time to any neighbour, and after exactly k steps, which nodes are we on?

2What the constraints tell us here

3Intuition: rings around the target

Picture the target in the middle. First collect everything 1 step away. From those, collect everything 1 more step away (that's 2 steps from the target). Keep going, ring by ring. When we reach ring k, the nodes in it are the answer.

"Collect all neighbours, then all their neighbours, ring by ring" is exactly BFS with levels. Each ring is one level. A queue holds the current ring.

4Building the conditions from the example

Distance 1 from 5

Down: 5.left = 6 and 5.right = 2. Up: parent[5] = 3. So ring 1 = 6, 2, 3. These go in the queue.

Distance 2: take one more step from every node in ring 1

We take the nodes of ring 1 out one by one and look at their children and parent.

The problem: going back where we came from

If 6 adds its parent 5, then 5 lands in ring 2. That would claim "5 is 2 steps away from 5" (5 → 6 → 5). That's nonsense. We just walked out and walked back in.

Rule: never visit a node twiceKeep a visited set. Put the target in it at the very start. Before adding any neighbour to the queue, check it's not already visited, and mark it visited when you add it.
Doubt 4: why a set and not a list?
→ We ask "have I seen this node?" again and again. A set answers that in O(1). A list would have to scan through every item. (The teacher sometimes says "visited array", but she means this set.)

With the visited set, ring 2 works out like this:

Ring 2 = 7, 4, 1. That's the answer ✓.

The three checks for each neighbour

For the left child, the right child and the parent, the check is the same shape:

  1. It must exist (not None). The root's parent is None, leaves have None children.
  2. It must not be visited yet.
  3. If both pass → mark it visited and push it into the queue.
Doubt 5: in the video, the code does it in one go: "try to add it to the set, and if adding worked, push it". Can I write that in Python?
→ Not the same way. In Java, set.add(x) returns true if x was new and false if it was already there, so one if does both jobs. Python's set.add() returns None every time. So in Python we write it in two steps: if x is not None and x not in visited: then visited.add(x) and queue.append(x). Same logic.

Why process the queue level by level (using its size)?

We need to know where one ring ends and the next begins, so we can count the distance. At the start of each round, the queue holds exactly one ring. We read len(queue) and pop exactly that many. Everything pushed during the round belongs to the next ring. After the round, distance goes up by 1.

Doubt 6: how do I count the distance? Is there more than one way?
→ The teacher mentions two ways. (a) Keep a separate distance counter starting at 0 and add 1 after each ring, stopping when it equals k. (b) Keep reducing k by 1 after each ring and stop when k is 0. Both work. She uses (a), so we do too.

Where exactly do we check "distance == k"?

Only between rings, never in the middle of one. A ring is only complete after its whole round has finished. So the check goes at the top of the while loop, before reading the size. If it matches, we break. The queue then holds exactly the nodes of ring k.

Why not start the BFS from the root?

You might think: "first search the tree from the root until I find the target, then begin". The teacher says that's not needed. LeetCode hands us the target node. From it, we can go down with .left/.right, and up with the parent map. So we can start the BFS directly at the target. The parent map is the only reason we needed the root at all.

The answer at the end

The queue holds nodes, but we must return values. So read .val from each node left in the queue.

5Approach steps (the full solution)

  1. Build the parent map with the DFS from Part A.
  2. Make a queue with the target in it, and a visited set with the target in it. Set distance = 0.
  3. While the queue is not empty: if distance == k, break.
  4. Otherwise, read the size of the queue and pop that many nodes. For each popped node, push its left child, right child and parent, but only if they exist and are not visited (mark them visited when pushing).
  5. After the round, distance += 1.
  6. Return the values of the nodes still in the queue.

6Code (Python)

All Nodes Distance K: DFS for parents + BFS from the target
from collections import deque

class Solution:
    def distanceK(self, root, target, k):
        parent = {}
        self.buildParent(root, None, parent)      # Step 1 (DFS)

        queue = deque([target])                   # Step 2 (BFS) starts at target
        visited = {target}
        distance = 0

        while queue:
            if distance == k:                     # this ring is ring k: stop
                break
            size = len(queue)                     # nodes in the current ring
            for _ in range(size):
                cur = queue.popleft()
                for nxt in (cur.left, cur.right, parent[cur]):
                    if nxt is not None and nxt not in visited:
                        visited.add(nxt)
                        queue.append(nxt)
            distance += 1                         # one more step outwards

        return [node.val for node in queue]

    def buildParent(self, node, par, parent):
        if node is None:
            return
        parent[node] = par
        self.buildParent(node.left, node, parent)
        self.buildParent(node.right, node, parent)

The teacher writes three separate if blocks (left, right, parent). The small loop over (cur.left, cur.right, parent[cur]) is the same three checks, written once. The order (left, right, parent) is kept, so the queue looks exactly like her dry run.

7Code line by line

linewhat it means
parent = {} self.buildParent(root, None, parent)Step 1: every node gets its parent written down. The root gets None.
queue = deque([target]) visited = {target}Start the spread at the target, and mark it seen so nobody walks back into it.
distance = 0The queue currently holds ring 0 (just the target).
if distance == k: breakThe queue holds a complete ring k. Stop spreading. Checked before every round, so k = 0 works too.
size = len(queue)Freeze the size of the current ring, so new pushes don't get mixed into this round.
cur = queue.popleft()Take one node of the current ring.
for nxt in (cur.left, cur.right, parent[cur]):Its three possible neighbours: down-left, down-right, and up.
if nxt is not None and nxt not in visited:It must exist, and we must not have been there before (or we'd walk backwards).
visited.add(nxt) queue.append(nxt)Mark it and put it in the next ring.
distance += 1The whole ring has been expanded: we're one step further out.
return [node.val for node in queue]The queue holds nodes, the answer wants values. If the queue ran empty (k too big), this gives [].

8Dry run: watch the queue

target = 5, k = 2. Yellow = the nodes popped in this round.

start5visited = {5}, distance = 0
round 1distance 0 ≠ 2 → size = 1 → pop 5: left 6 ✓, right 2 ✓, parent 3 ✓ → push all three
623visited = {5, 6, 2, 3}, distance → 1
round 2distance 1 ≠ 2 → size = 3
pop 6: left None, right None, parent 5 → visited → nothing
pop 2: left 7 ✓, right 4 ✓, parent 5 → visited → push 7, 4
pop 3: left 5 → visited, right 1 ✓, parent None → push 1
741distance → 2
checkdistance 2 == k → break
endread values from the queue → [7, 4, 1] ✓

The teacher adds a thought: what if k were 3? Then we'd do one more round from 7, 4, 1. 7 and 4 are leaves whose parent 2 is visited, so they add nothing. 1's left 0 and right 8 are new, and its parent 3 is visited. Ring 3 = [0, 8]. In this case we walked over every node before stopping. That's the worst case for time.

9Complexity & remember

Remember Nodes at Distance K DFS to give every node its parent. Then BFS from the target where each node has 3 neighbours: left, right, parent. Use a visited set so you never walk back. Check distance == k at the top of the loop, then return the values left in the queue.

Part C · Revision page

Step 1: parent mapStep 2: spread from target
traversalDFS (recursion)BFS (queue, level by level)
starts atthe root, with parent Nonethe target (not the root)
storesparent[node] = par in a dictcurrent ring in a deque, seen nodes in a set
neighbours usedleft, rightleft, right, and parent
stops whennode is None (base case)distance == k, or the queue is empty
time / spaceO(n) / O(n) + O(h)O(n) / O(n)
situationwhat happensanswer
k = 0break at the first check[target.val]
k bigger than the treequeue runs empty, loop ends[]
target is the rootparent is None, only children are usedjust the nodes at depth k
target is a deep leafalmost all moves go through the parent mapnodes found by going up then down
If you remember only 5 lines 1. A tree only goes down. To go up, store every node's parent in a dict (one DFS).
2. Now each node has 3 neighbours: left, right, parent.
3. BFS from the target, one ring per round (use the queue's size).
4. Visited set, with the target in it from the start, so you never walk back.
5. Check distance == k at the top of the loop. Return the values in the queue.
Mistakes to avoid ✗ forgetting the visited set (6 would add 5 back as "distance 2")
✗ not putting the target in visited at the start
✗ using parent[cur] without checking it's not None (the root has no parent)
✗ checking distance == k in the middle of a ring instead of between rings
✗ returning nodes instead of their .val
✗ in Python, writing if visited.add(x): like Java (it's always None, so nothing gets pushed)
✗ mixing up a parent while filling the map (the teacher's slip: 2's parent is 5, not 1)
test it yourself (paste under the solution above)
n7, n4 = TreeNode(7), TreeNode(4)
n2 = TreeNode(2, n7, n4)
n5 = TreeNode(5, TreeNode(6), n2)
root = TreeNode(3, n5, TreeNode(1, TreeNode(0), TreeNode(8)))

s = Solution()
print(sorted(s.distanceK(root, n5, 2)))   # [1, 4, 7]
print(sorted(s.distanceK(root, n5, 3)))   # [0, 8]
print(s.distanceK(root, n5, 0))           # [5]
print(s.distanceK(root, n7, 10))          # []

Based on this video: Print All Nodes at Distance K