DSA sheet · Trees · BFS pattern

Top View of Binary Tree

This video introduces one new idea that the next few problems (Bottom View, Vertical Order Traversal) are built on: horizontal distance, a "column number" for every node. Once each node has a column number, the top view is simply the first node we meet in each column, going from top to bottom. The teacher solves it twice: first with BFS (a queue), where it is very natural, then with DFS (recursion), where we discover that we also need to carry the level, and she shows a tree that breaks the code if we forget it.

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: horizontal distance

Words we will use

wordmeaning
level (row, depth)How many steps down from the root a node is. The root is on level 0, its children on level 1, their children on level 2, and so on. Level measures up and down.
horizontal distance (HD, also called "vertex", "vertical level" or "column" in the video)How far left or right of the root a node is. The root has HD 0. Every step to a left child subtracts 1. Every step to a right child adds 1. HD measures left and right.
columnAll the nodes that have the same HD. They sit exactly on top of each other when you draw the tree neatly.
top viewStand above the tree and look straight down. In each column you only see the highest node. Everything under it is hidden.

The x-axis picture the teacher uses

Think of the root sitting at 0 on a number line (the x-axis). Moving left takes you to the negative numbers, moving right takes you to the positive numbers.

   left ←   -3   -2   -1    0   +1   +2   +3   → right
                             ↑
                           root

So the rule for children is always the same, no matter how deep we are:

The HD rule root → HD 0
left child → HD of parent − 1
right child → HD of parent + 1

This is the same trick the teacher used in level order traversal with DFS, where every child got "parent's level + 1". There we counted rows. Here we count columns in the same way.

Working out HD on the teacher's tree

Here is the main tree of the video. The number in brackets is the HD of each node.

                  1 (0)
              /          \
         2 (-1)           3 (+1)
         /    \           /    \
     4 (-2)   6 (0)    7 (0)   8 (+2)
         \                        \
         5 (-1)                    9 (+3)
  1. Root 1 → HD 0.
  2. 2 is the left child of 1 → 0 − 1 = −1. 3 is the right child of 1 → 0 + 1 = +1.
  3. 4 is the left child of 2 → −1 − 1 = −2. 6 is the right child of 2 → −1 + 1 = 0.
  4. 7 is the left child of 3 → +1 − 1 = 0. 8 is the right child of 3 → +1 + 1 = +2.
  5. 5 is the right child of 4 → −2 + 1 = −1. 9 is the right child of 8 → +2 + 1 = +3.

Now put every node in a grid: columns are HD, rows are levels. This is what the tree "really" looks like from the side.

level ↓ / HD →−2−10+1+2+3
01
123
246, 78
359

Reading each column from top to bottom:

The top of each column, from left to right, is 4 2 1 3 8 9. That is exactly the expected top view. So the teacher's plan is: give every node its HD, group nodes by HD, and keep only the first (highest) node of each group.

Doubt: 6 is in the left subtree of the root. How can it be in the same column as the root?
→ Because it went left once (−1) and then right once (+1). The two steps cancel out, so it comes back to 0. HD only counts how many lefts and rights you took overall, not which side of the root you started on. 7 does the same thing from the other side (+1, then −1). This is exactly why nodes can hide behind each other.

"Why not just use DFS or BFS and print the corner nodes?"

The teacher first asks whether a normal traversal can solve this on its own.

The two examples from the problem statement

Example 1 → 2 1 3
       1 (0)
      /    \
  2 (-1)   3 (+1)
Example 2 → 40 20 10 30 100
               10 (0)
            /         \
       20 (-1)        30 (+1)
       /    \         /    \
  40 (-2) 60 (0)  90 (0)  100 (+2)

In Example 2, 60 and 90 both land in column 0, under 10. From the top, 10 covers them, so they don't appear in the answer.


Part A · Top View with BFS

GFG · Top View of Binary Tree

1The question in simple words

You get the root of a binary tree. Return the list of node values you would see if you looked at the tree from above, ordered from the leftmost column to the rightmost column. In each column, only the highest node is visible.

If two nodes are in the same column and on the same level (like 6 and 7 above), the one further left counts as "first". (Here they are both hidden anyway, because 1 is higher.)

2What the constraints tell us

3Intuition: why BFS fits perfectly

BFS walks the tree level by level, top to bottom (and left to right inside a level). So the first time BFS reaches a column, it has reached the highest node of that column. Every node that comes later in the same column is lower, so it's hidden.

So we keep a small notebook (a dictionary/map): HD → value. When we pop a node:

4Building the logic from the example

What goes into the queue?

In normal BFS the queue holds just nodes. Now every node also needs its HD, because a node doesn't "know" its own column. Its HD comes from its parent. So the queue holds pairs: (node, hd).

Doubt: the teacher creates a Pair class. Do we need that in Python?
→ No. In Java a queue slot holds one object, so she wraps the node and the HD inside a Pair. In Python we just put a tuple (node, hd) in the deque, and unpack it when we pop: node, hd = queue.popleft().

Rule 1: first time we see a column → save it

Pop 1 (HD 0). Column 0 is empty in the notebook → save 0 → 1. Pop 2 (HD −1) → empty → save −1 → 2. Pop 3 (HD +1) → save +1 → 3.

Rule 1if hd not in first: first[hd] = node.val

Rule 2: column already seen → skip (never overwrite)

Later we pop 6 (HD 0). Column 0 already holds 1. Because BFS goes level by level, 1 was popped earlier, so 1 is higher than 6. From the top, 1 hides 6. So we must not overwrite. The same happens for 7 (HD 0) and for 5 (HD −1, hidden under 2).

Doubt: how do I know the node already saved is higher, and not just "saved first"?
→ In BFS, "popped earlier" means "on the same level or higher". That's the whole promise of level order. So in BFS, first seen = top. (In DFS this promise breaks. That is Part B.)

Rule 3: push the children with their new HD

This happens for every popped node, whether or not we saved it. Even a hidden node can have children that stick out, so we must keep exploring below it.

The teacher points out that the push lines sit outside the "not in map" if. If you put them inside, the children of hidden nodes (like 6 and 7) would never be explored.

Last step: read the notebook from left to right

The answer must go from the most negative HD to the most positive HD. Java's TreeMap keeps keys sorted by itself. In Python, a normal dict remembers the order we inserted keys, which is not left-to-right (we inserted 0, −1, +1, −2, …). So at the end we go through sorted(first).

Doubt: doesn't sorting make it slower than O(n)?
→ We only sort the columns, not the nodes. If there are w columns, sorting costs w·log w, and w is never more than n. (Java's TreeMap has the same cost hidden inside it.) If you want strictly O(n), remember the smallest and largest HD while you go, then read range(min_hd, max_hd + 1). Every column in that range is guaranteed to exist, because HD changes by exactly 1 per step.

5Approach steps

  1. Make an empty dictionary first (HD → value) and a queue with (root, 0).
  2. While the queue is not empty, pop (node, hd).
  3. If hd is not in first, save first[hd] = node.val.
  4. Push the left child with hd − 1 and the right child with hd + 1 (only if they exist).
  5. After the loop, return the values of first in increasing order of HD.

6Code (Python)

Top View with BFS
from collections import deque

def topView(root):
    if root is None:                   # safe guard (constraints say n >= 1)
        return []
    first = {}                         # hd -> value of the top node
    queue = deque()
    queue.append((root, 0))            # (node, horizontal distance)

    while queue:
        node, hd = queue.popleft()
        if hd not in first:            # first visit of this column = top
            first[hd] = node.val
        if node.left:                  # children are pushed in ALL cases
            queue.append((node.left, hd - 1))
        if node.right:
            queue.append((node.right, hd + 1))

    return [first[hd] for hd in sorted(first)]

7Code line by line

linewhat it means
first = {}Our notebook. Key = column (HD), value = the node value seen at the top of that column.
queue.append((root, 0))Start BFS with the root, which sits at HD 0.
node, hd = queue.popleft()Take the oldest pair out. We get the node and its column together.
if hd not in first: first[hd] = node.valNobody has claimed this column yet. Since BFS goes top-down, this node is the highest one there.
queue.append((node.left, hd - 1))Left child is one column to the left.
queue.append((node.right, hd + 1))Right child is one column to the right.
[first[hd] for hd in sorted(first)]Read columns from the leftmost (most negative) to the rightmost.

8Dry run: watch the queue and the notebook

On the main tree (4 2 1 3 8 9 expected). Each box shows value:hd. Yellow = popped now.

                  1 (0)
              /          \
         2 (-1)           3 (+1)
         /    \           /    \
     4 (-2)   6 (0)    7 (0)   8 (+2)
         \                        \
         5 (-1)                    9 (+3)
start1:0notebook: { }
pop 10 is new → save 0→1; push 2:−1, 3:+1
2:-13:+1
pop 2−1 is new → save −1→2; push 4:−2, 6:0
3:+14:-26:0
pop 3+1 is new → save +1→3; push 7:0, 8:+2
4:-26:07:08:+2
pop 4−2 is new → save −2→4; push 5:−1
6:07:08:+25:-1
pop 60 already has 1 → skip (6 is hidden under 1)
pop 70 already has 1 → skip
pop 8+2 is new → save +2→8; push 9:+3
5:-19:+3
pop 5−1 already has 2 → skip (5 is hidden under 2)
pop 9+3 is new → save +3→9
endnotebook {0:1, −1:2, +1:3, −2:4, +2:8, +3:9} → sorted by HD → [4, 2, 1, 3, 8, 9] ✓

The teacher catches herself during the dry run: after popping 3, its children 7 and 8 enter the queue before 5, because 5 is one level deeper. The order above follows that fix.

9Complexity & remember

Remember Top View (BFS)Queue of (node, hd). Left = hd − 1, right = hd + 1. Save a column only the first time you see it. Always push children. Answer = values sorted by hd.

Part B · Top View with DFS

1The question in simple words

Same question as Part A. Now we use recursion instead of a queue.

2What the constraints tell us

Doubt (Python only): n can be 10⁵. What if the tree is a long straight line?
→ Then the recursion goes 10⁵ calls deep. Python stops at about 1000 calls by default (RecursionError). For such inputs, either raise the limit with sys.setrecursionlimit(...) or use the BFS version, which has no depth problem. The logic below is still correct.

3Intuition: what's easier, and what breaks

Easier: in BFS we needed pairs because a queue slot holds one thing. In DFS, the function can simply take extra parameters: dfs(node, level, hd). No pair is needed.

What breaks: DFS does not go top to bottom. It dives down the whole left subtree first, then comes back for the right. So "the first node I saw in this column" is not always the highest one. A deep node on the left can claim a column before a higher node on the right gets there.

The fix: remember how high the saved node was. Save [value, level] per column, and if a new node in that column has a smaller level (it's higher), replace the old one.

First try: copy the BFS rule into DFS (no level)

On the main tree, preorder DFS visits 1, 2, 4, 5, 6, 3, 7, 8, 9:

Answer 4 2 1 3 8 9: it works here by luck. So why does the teacher insist on passing the level? She draws a second tree.

4Building the condition from the tricky example

                 1 (0)        level 0
              /      \
         2 (-1)       9 (+1)  level 1
         /    \
     4 (-2)   3 (0)           level 2
                 \
                 5 (+1)       level 3
                    \
                    6 (+2)    level 4

Look at column +1: it holds 9 (level 1) and 5 (level 3). From the top, 9 is visible, 5 is hidden under it. The correct top view is 4 2 1 9 6.

Now follow DFS with the BFS-style rule "save only if the column is new". DFS goes left first, so it visits 1, 2, 4, 3, 5, 6, and only then 9:

  1. 1 → column 0 new → save 1. 2 → column −1 new → save 2. 4 → column −2 new → save 4.
  2. 3 → column 0 already has 1 → skip. 5 → column +1 is new → save 5. 6 → column +2 new → save 6.
  3. Finally the right side: 9 → column +1 already has 5 → skip.

Result 4 2 1 5 6 ✗. The deep 5 grabbed column +1 first only because DFS reached it first, even though 9 is two levels higher.

The fix: compare levels

So for each column we store two numbers: the value and the level it was found on. When a node arrives in a column that's already taken:

With this rule, when 9 (level 1) arrives at column +1, it sees 5 stored with level 3. 1 < 3 → overwrite with 9. Result 4 2 1 9 6 ✓.

The DFS rule column new → save [val, level]
column taken and level < saved_level → overwrite
else → leave it
Doubt: why didn't BFS need the level?
→ BFS visits levels in order, top to bottom. So the first node it meets in a column is always the highest one. In BFS the level is "built in" to the order of the queue. DFS has no such order, so we must carry the level ourselves and compare.
Doubt: why strictly < and not <=?
→ If two nodes share a column and a level, the top view keeps the one further left (the one BFS would meet first). DFS goes left first, so the left one is already saved. Using < means an equal level never replaces it. With <= the right one would win, which is the bottom-view style tie rule (next problem).

5Approach steps

  1. Make an empty dictionary best (HD → [value, level]).
  2. Call dfs(root, level=0, hd=0).
  3. In dfs: if the node is None, return.
  4. If hd is new, save [node.val, level]. Else, if level is smaller than the saved level, overwrite.
  5. Recurse left with (level + 1, hd − 1) and right with (level + 1, hd + 1).
  6. After DFS, return the saved values sorted by HD.

6Code (Python)

Top View with DFS
def topView(root):
    best = {}                          # hd -> [value, level]

    def dfs(node, level, hd):
        if node is None:               # base case: stops the recursion
            return
        if hd not in best:             # column seen for the first time
            best[hd] = [node.val, level]
        elif level < best[hd][1]:      # this node is HIGHER than the saved one
            best[hd] = [node.val, level]
        dfs(node.left, level + 1, hd - 1)
        dfs(node.right, level + 1, hd + 1)

    dfs(root, 0, 0)
    return [best[hd][0] for hd in sorted(best)]

7Code line by line

linewhat it means
best = {}Per column, a small list: index 0 = value, index 1 = level where we found it.
def dfs(node, level, hd):Level and HD travel as parameters, so no pair class is needed.
if node is None: returnBase case. Without it the recursion never stops.
if hd not in best: best[hd] = [node.val, level]First node in this column: nothing to compare with, just save it.
elif level < best[hd][1]: best[hd] = [node.val, level]The column is taken, but this node is higher → it becomes the new top.
dfs(node.left, level + 1, hd - 1)One level down, one column left.
dfs(node.right, level + 1, hd + 1)One level down, one column right.
[best[hd][0] for hd in sorted(best)]Left to right, keep only index 0 (the value). Index 1 was only for comparing.

8Dry run with the call stack

On the tricky tree (1 → 2, 9; 2 → 4, 3; 3 → right 5; 5 → right 6). Each call is written as value(level, hd).

  1. 1(0, 0): column 0 new → best = {0: [1, 0]}. Go left.
  2. 2(1, −1): column −1 new → save [2, 1]. Go left.
  3. 4(2, −2): column −2 new → save [4, 2]. Its children are None → both calls return at once. 4 leaves the stack.
  4. Back in 2, go right: 3(2, 0): column 0 holds [1, 0]. Is 2 < 0? No → keep 1. Left child None. Go right.
  5. 5(3, +1): column +1 new → save [5, 3]. Go right.
  6. 6(4, +2): column +2 new → save [6, 4]. No children → return. Then 5, 3 and 2 return one by one.
  7. Back in 1, go right: 9(1, +1): column +1 holds [5, 3]. Is 1 < 3? Yes → overwrite with [9, 1].
  8. 9 has no children → return. 1 returns. Stack empty.
  9. best = {0:[1,0], −1:[2,1], −2:[4,2], +1:[9,1], +2:[6,4]} → sorted by HD → [4, 2, 1, 9, 6] ✓
deepest point (step 6)
1(0,0)2(1,−1)3(2,0)5(3,+1)6(4,+2)
step 7: the fix happens
1(0,0)9(1,+1) → overwrite 5

The newest call is on top (red). On the main tree the same code gives 4 2 1 3 8 9, exactly like BFS.

9Complexity & remember

Remember Top View (DFS)Pass level and hd as parameters. Save [value, level]. Overwrite only if the new level is strictly smaller. BFS doesn't need the level because its order already goes top-down.

Part C · Revision page

BFS (queue)DFS (recursion)
how HD travelsinside the queue as a tuple (node, hd)as a function parameter
do we need the level?No: the queue already goes top to bottomYes: DFS can reach a deep node first
what we store per columnjust the value[value, level]
when to writeonly if the column is newcolumn new, or level < saved level
base case for Nonenot needed (n ≥ 1), kept as a guardalways needed
timeO(n)O(n)
spaceO(n) queue (last level ≈ n/2)O(height) stack + O(width) map
directionchange in HDchange in level
go to left child− 1+ 1
go to right child+ 1+ 1
If you remember only 5 lines 1. HD: root 0, left child −1, right child +1. Same HD = same column.
2. Top view = the highest node of every column, read from left to right.
3. BFS: first node seen in a column is the top. Never overwrite.
4. DFS: carry the level too; overwrite only when the new level is smaller.
5. Always push/visit children, even of hidden nodes. Sort the columns at the end.
Mistakes to avoid ✗ printing the left view + right view ("corner nodes"): it includes hidden nodes like 5
✗ DFS without the level: a deep left node can steal a column (the 5 vs 9 tree)
✗ putting the child-push lines inside the "column is new" if
✗ forgetting to sort the columns (a Python dict keeps insertion order, not HD order)
✗ using <= in DFS: the tie rule changes
test it yourself (paste under either solution above)
main = TreeNode(1,
         TreeNode(2, TreeNode(4, None, TreeNode(5)), TreeNode(6)),
         TreeNode(3, TreeNode(7), TreeNode(8, None, TreeNode(9))))
tricky = TreeNode(1,
           TreeNode(2, TreeNode(4), TreeNode(3, None, TreeNode(5, None, TreeNode(6)))),
           TreeNode(9))
print(topView(main))      # [4, 2, 1, 3, 8, 9]
print(topView(tricky))    # [4, 2, 1, 9, 6]
print(topView(TreeNode(1, TreeNode(2), TreeNode(3))))   # [2, 1, 3]

Based on this video: Top View of Binary Tree | BFS & DFS