Left View of a Binary Tree: DFS and BFS Approaches Explained
#tree
#binary-tree
#b-tree
#treemap
#depth-first-search
#breadth-first-search
There's a specific kind of interview moment that catches a lot of people off guard: the interviewer asks for the left view of a binary tree, you write a solution that looks completely reasonable, run it mentally against the tree on the whiteboard... and it quietly returns the right view instead. Not a crash, not an obviously wrong answer just a plausible-looking list of numbers that happens to be exactly backwards. This article walks through why that mix-up happens so easily and how to build a version that gets it right every time.
Problem Statement
You are given the root of a binary tree. Imagine standing to the left of the tree and looking straight across the left view is the set of nodes you'd actually be able to see from that angle.
More precisely: for every level of the tree, the left view keeps only the node that's furthest to the left at that level. Any other node at that same level, sitting behind it from this angle, is left out.
Given the root of a binary tree, return its left view as a list of values, ordered from the topmost level to the bottommost.
For example, for a tree shaped like this:
10
/ \
20 30
\ \
40 50
/ \
60 70
The left view is:
10, 20, 40, 60
Understanding the Problem First
It's worth pausing on one thing before writing any code: left view groups nodes by level (depth), not by horizontal distance. This is a genuinely common point of confusion, because Top view and Bottom view two closely related problems are built entirely around horizontal distance instead. Left view doesn't care how far left or right a node has drifted; it only cares how deep it is and whether it's the first node encountered at that depth when scanning left to right.
For the tree above:
- Depth
0: just10. - Depth
1:20and30.20is further left, so it wins. - Depth
2:40and50.40is further left, so it wins. - Depth
3:60and70.60is further left, so it wins.
So the problem comes down to: for every depth, find the leftmost node, then read those winners off from the shallowest depth to the deepest. Every approach below is a different way of visiting nodes and deciding whether the one in hand should be allowed to claim its depth.
Setting Up the Tree
We'll use the tree from above throughout this article.
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) {
this.val = val;
}
}
public class SampleTree {
public static TreeNode build() {
TreeNode n10 = new TreeNode(10);
TreeNode n20 = new TreeNode(20);
TreeNode n30 = new TreeNode(30);
TreeNode n40 = new TreeNode(40);
TreeNode n50 = new TreeNode(50);
TreeNode n60 = new TreeNode(60);
TreeNode n70 = new TreeNode(70);
n10.left = n20;
n10.right = n30;
n20.right = n40;
n30.right = n50;
n40.left = n60;
n50.right = n70;
return n10;
}
}
Approach 1: The Naive Attempt (and How It Secretly Becomes Right View)
Here's a first instinct that feels almost obviously correct: run a DFS, always visiting the left child before the right child, track each node's depth as you go and write its value into a map keyed by depth overwriting whatever's already there. Since we're always exploring left before right, surely the map ends up holding the leftmost node at each depth... right?
The flaw is subtle: overwriting unconditionally means the map doesn't actually end up holding the first node visited at each depth it ends up holding the last one. And those are very different things.
Tracing Through It
A preorder DFS (root, then left subtree, then right subtree) visits our tree in this order:
10 → 20 → 40 → 60 → 30 → 50 → 70
Overwriting the map at every visit:
| Visit | Node | Depth | Map after this step |
|---|---|---|---|
| 1 | 10 | 0 | {0: 10} |
| 2 | 20 | 1 | {0: 10, 1: 20} |
| 3 | 40 | 2 | {..., 2: 40} |
| 4 | 60 | 3 | {..., 3: 60} |
| 5 | 30 | 1 | {1: 30, ...} |
| 6 | 50 | 2 | {2: 50, ...} |
| 7 | 70 | 3 | {3: 70, ...} |
Because preorder DFS fully finishes exploring the left subtree before it ever touches the right subtree, every node in the right subtree 30, 50, 70 gets visited after its entire left-side counterpart and ends up overwriting a correct answer with an incorrect one.
The naive output is:
10, 30, 50, 70
Look closely at that list. It isn't a random wrong answer it's the right view of this exact tree. The unconditional-overwrite bug doesn't fail randomly; it consistently produces the mirror-image answer, because "the last node visited at each depth" is precisely the definition of right view, not left view. That's what makes this particular bug so easy to miss in an interview: the output looks completely plausible, it's just answering a different question than the one that was asked.
Approach 2: DFS With "First Occurrence Wins" (Correct)
The fix here turns out to be simpler than you might expect and simpler than the equivalent fix for top view. Instead of tracking each node's depth and comparing it against the current occupant, we only need to ask one question: has this depth already been claimed? If not, this node claims it. If it has, we skip and move on no comparison, no depth field, nothing to overwrite.
This works because preorder DFS always visits a node's left subtree completely before its right subtree, at every level of recursion, all the way down. That means the first node reached at any given depth is guaranteed to be the leftmost one there's no scenario where a right-side branch reaches a depth before the corresponding left-side branch does, since the traversal simply hasn't gotten there yet.
Tracing Through It
Same preorder traversal 10 → 20 → 40 → 60 → 30 → 50 → 70 now only recording a depth the first time it's seen:
| Visit | Node | Depth | Already claimed? | Map after this step |
|---|---|---|---|---|
| 1 | 10 | 0 | no → add | {0: 10} |
| 2 | 20 | 1 | no → add | {..., 1: 20} |
| 3 | 40 | 2 | no → add | {..., 2: 40} |
| 4 | 60 | 3 | no → add | {..., 3: 60} |
| 5 | 30 | 1 | yes → skip | unchanged |
| 6 | 50 | 2 | yes → skip | unchanged |
| 7 | 70 | 3 | yes → skip | unchanged |
Reading the map from the shallowest depth to the deepest:
10, 20, 40, 60
the correct answer.
#include <bits/stdc++.h>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};
void fillLevels(TreeNode* node, int depth, vector<int>& result) {
if (node == nullptr) return;
// The first node we reach at a given depth is always the
// leftmost one, thanks to visiting left before right
if (depth == (int)result.size()) {
result.push_back(node->val);
}
fillLevels(node->left, depth + 1, result);
fillLevels(node->right, depth + 1, result);
}
vector<int> leftView(TreeNode* root) {
vector<int> result;
fillLevels(root, 0, result);
return result;
}
int main() {
TreeNode* root = SampleTree::build();
vector<int> result = leftView(root);
for (int v : result) cout << v << " ";
cout << endl;
// Output: 10 20 40 60
return 0;
}
using System;
using System.Collections.Generic;
public class LeftViewDFS {
public static List<int> LeftView(TreeNode root) {
List<int> result = new List<int>();
FillLevels(root, 0, result);
return result;
}
private static void FillLevels(TreeNode node, int depth, List<int> result) {
if (node == null) return;
// The first node we reach at a given depth is always the
// leftmost one, thanks to visiting left before right
if (depth == result.Count) {
result.Add(node.val);
}
FillLevels(node.left, depth + 1, result);
FillLevels(node.right, depth + 1, result);
}
public static void Main(string[] args) {
TreeNode root = SampleTree.Build();
List<int> result = LeftView(root);
Console.WriteLine(string.Join(", ", result));
// Output: 10, 20, 40, 60
}
}
import java.util.*;
public class LeftViewDFS {
public static List<Integer> leftView(TreeNode root) {
List<Integer> result = new ArrayList<>();
fillLevels(root, 0, result);
return result;
}
private static void fillLevels(TreeNode node, int depth, List<Integer> result) {
if (node == null) return;
// The first node we reach at a given depth is always the
// leftmost one, thanks to visiting left before right
if (depth == result.size()) {
result.add(node.val);
}
fillLevels(node.left, depth + 1, result);
fillLevels(node.right, depth + 1, result);
}
public static void main(String[] args) {
TreeNode root = SampleTree.build();
System.out.println(leftView(root));
// Output: [10, 20, 40, 60]
}
}
def fill_levels(node, depth, result):
if node is None:
return
# The first node we reach at a given depth is always the
# leftmost one, thanks to visiting left before right
if depth == len(result):
result.append(node.val)
fill_levels(node.left, depth + 1, result)
fill_levels(node.right, depth + 1, result)
def left_view(root):
result = []
fill_levels(root, 0, result)
return result
if __name__ == "__main__":
root = SampleTree.build()
result = left_view(root)
print(result)
# Output: [10, 20, 40, 60]
Notice there's no map at all here just a growing List, where depth == result.size() is a cheap way of asking "have we already recorded something at this depth?" It's a small detail, but it's only possible because depth values are sequential non-negative integers, unlike the horizontal distances used in top and bottom view, which can run negative and out of order.
Complexity
| Value | |
|---|---|
| Time | O(n) every node is visited once and the depth check is a constant-time list-size comparison |
| Space | O(h) for the recursion stack, where h is the tree's height |
Approach 3: BFS / Level Order Traversal (Optimal)
BFS makes the whole problem almost self-evident. Since a level order traversal processes one full level at a time, left to right, the leftmost node at any level is simply the first node dequeued during that level's turn. There's no bug to accidentally invert here, because there's no traversal-order subtlety to get wrong in the first place the level boundaries are explicit.
Tracing Through It
Processing the tree level by level:
| Level | Nodes in this level (left to right) | Node taken for left view |
|---|---|---|
| 0 | 10 | 10 |
| 1 | 20, 30 | 20 |
| 2 | 40, 50 | 40 |
| 3 | 60, 70 | 60 |
Final result, read top to bottom:
10, 20, 40, 60
#include <bits/stdc++.h>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};
vector<int> leftView(TreeNode* root) {
vector<int> result;
if (root == nullptr) return result;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int levelSize = q.size();
for (int i = 0; i < levelSize; i++) {
TreeNode* current = q.front();
q.pop();
// The first node processed in this level is the leftmost one
if (i == 0) {
result.push_back(current->val);
}
if (current->left != nullptr) {
q.push(current->left);
}
if (current->right != nullptr) {
q.push(current->right);
}
}
}
return result;
}
int main() {
TreeNode* root = SampleTree::build();
vector<int> result = leftView(root);
for (int v : result) cout << v << " ";
cout << endl;
// Output: 10 20 40 60
return 0;
}
using System;
using System.Collections.Generic;
public class LeftViewBFS {
public static List<int> LeftView(TreeNode root) {
List<int> result = new List<int>();
if (root == null) return result;
Queue<TreeNode> queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0) {
int levelSize = queue.Count;
for (int i = 0; i < levelSize; i++) {
TreeNode current = queue.Dequeue();
// The first node processed in this level is the leftmost one
if (i == 0) {
result.Add(current.val);
}
if (current.left != null) {
queue.Enqueue(current.left);
}
if (current.right != null) {
queue.Enqueue(current.right);
}
}
}
return result;
}
public static void Main(string[] args) {
TreeNode root = SampleTree.Build();
List<int> result = LeftView(root);
Console.WriteLine(string.Join(", ", result));
// Output: 10, 20, 40, 60
}
}
import java.util.*;
public class LeftViewBFS {
public static List<Integer> leftView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
for (int i = 0; i < levelSize; i++) {
TreeNode current = queue.poll();
// The first node processed in this level is the leftmost one
if (i == 0) {
result.add(current.val);
}
if (current.left != null) {
queue.add(current.left);
}
if (current.right != null) {
queue.add(current.right);
}
}
}
return result;
}
public static void main(String[] args) {
TreeNode root = SampleTree.build();
System.out.println(leftView(root));
// Output: [10, 20, 40, 60]
}
}
from collections import deque
def left_view(root):
result = []
if root is None:
return result
queue = deque([root])
while queue:
level_size = len(queue)
for i in range(level_size):
current = queue.popleft()
# The first node processed in this level is the leftmost one
if i == 0:
result.append(current.val)
if current.left is not None:
queue.append(current.left)
if current.right is not None:
queue.append(current.right)
return result
if __name__ == "__main__":
root = SampleTree.build()
result = left_view(root)
print(result)
# Output: [10, 20, 40, 60]
Complexity
| Value | |
|---|---|
| Time | O(n) every node is enqueued and dequeued exactly once |
| Space | O(n) for the queue in the worst case, when the tree's widest level holds close to n / 2 nodes |
Comparing All Approaches
| Approach | Time Complexity | Space Complexity | Core Idea |
|---|---|---|---|
| Naive DFS (buggy) | O(n) | O(h) | Overwrite the map on every visit ends up keeping the last node seen per depth, which is actually the right view |
| DFS with "First Occurrence Wins" | O(n) | O(h) | Record a depth only the first time it's reached safe because preorder always finishes the left subtree before the right |
| BFS / Level Order Optimal | O(n) | O(n) | Take the first node dequeued at each level directly no traversal-order reasoning required |
n = number of nodes, h = height of the tree.
One thing worth calling out if you've also worked through top view or bottom view: those problems need a TreeMap and pay an O(log w) cost per node, because horizontal distance can run negative and isn't visited in any guaranteed order. Left view sidesteps that entirely depth is always a small non-negative integer processed in a predictable order, so both correct solutions here run in clean O(n), with no logarithmic factor at all.
It's also worth being explicit about the trap in Approach 1: it's not that the naive method produces garbage it produces a coherent, correct-looking answer to the wrong problem. That's exactly the kind of bug that survives a quick self-check and only gets caught when someone compares it against the actual expected output.
Key Takeaways
- "Left before right" in your traversal doesn't automatically mean "leftmost node wins." Approach 1 visits left before right too it just doesn't do anything with that ordering, so the overwrite erases the benefit entirely.
- A wrong answer that looks right is more dangerous than one that looks wrong. The naive approach here doesn't fail loudly; it silently computes right view instead, which is easy to miss without a second example to test against.
- Not every "view" problem needs the same toolkit. Top and bottom view need horizontal distance and a sorted map; left view only needs depth and a simple "seen it yet" check recognizing which kind of grouping a problem actually needs saves real implementation effort.
- BFS is often the safer default for level-based problems. When a traversal order subtlety is easy to get backwards in DFS, level order traversal frequently removes the ambiguity by making level boundaries explicit.
Frequently Asked Questions
1. What is the difference between left view and top view of a binary tree?
Left view groups nodes by depth (level) and keeps the leftmost node at each level. Top view groups nodes by horizontal distance from the root and keeps the shallowest node at each horizontal distance. They're built on different groupings and can return different results for the same tree.
2. Why does a naive DFS solution for left view sometimes return the right view instead?
If the implementation overwrites a depth's recorded value on every visit instead of only on the first visit, the last node visited at each depth ends up being kept. In a standard left-before-right preorder traversal, the last node visited at any depth always comes from the rightmost branch which is exactly the definition of right view.
3. Does finding the left view require tracking horizontal distance?
No. Left view only depends on depth and left-to-right position within a level, so it can be solved without any horizontal distance calculation at all unlike top view or bottom view.
4. What is the time complexity of finding the left view of a binary tree?
Both the correct DFS approach and the BFS approach run in O(n) time, where n is the number of nodes. Unlike top and bottom view, no TreeMap or sorting step is needed, since depth values are already sequential.
5. Is BFS or DFS better for solving left view?
Both run in O(n) time. BFS is generally considered more intuitive here because level boundaries are explicit in a queue-based traversal, making it harder to accidentally build the right view instead. The DFS approach is equally correct once you record only the first node seen at each depth.
