987. Vertical Order Traversal of a Binary Tree
Problem Description
You are given the root of a binary tree and need to perform a vertical order traversal of the tree.
Each node in the tree has a position defined by coordinates (row, col)
:
- The root node starts at position
(0, 0)
- For any node at position
(row, col)
:- Its left child is at position
(row + 1, col - 1)
- Its right child is at position
(row + 1, col + 1)
- Its left child is at position
The vertical order traversal requires you to:
- Group all nodes by their column index (from leftmost to rightmost column)
- Within each column, order nodes from top to bottom (by row)
- If multiple nodes share the same position
(row, col)
, sort them by their values in ascending order
The output should be a list of lists, where each inner list contains the values of nodes in a single column, ordered according to the rules above.
For example, if you have nodes at:
- Column -1: nodes with values [9, 7]
- Column 0: nodes with values [3, 15]
- Column 1: nodes with values [20]
The vertical order traversal would return: [[9, 7], [3, 15], [20]]
The key challenge is handling nodes that occupy the same position - these must be sorted by their values before being added to the result.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure, with a root node and parent-child relationships.
DFS
- Yes: Since we're dealing with a tree, DFS (Depth-First Search) is the suggested approach.
Conclusion: The flowchart suggests using DFS for this problem.
This makes sense because:
- We need to traverse the entire tree to visit every node
- During traversal, we need to track the position (row, column) of each node
- DFS allows us to recursively explore the tree while maintaining the relative positions
- As we go deeper in the tree (increasing row), we adjust columns left (-1) or right (+1) based on whether we're visiting a left or right child
The DFS approach in the solution:
- Recursively visits each node starting from the root
- Records each node's column (
j
), row (i
), and value in a list - After collecting all nodes with their positions, sorts them by column first, then row, then value
- Groups nodes by column to produce the final vertical order traversal
Intuition
The key insight is that we need to track two pieces of information for each node: its position in 2D space and its value. Since we're asked for a vertical order traversal (column by column), we need to know which column each node belongs to.
Think of the tree as being placed on a 2D grid. The root sits at position (0, 0)
. As we move down the tree, going left means moving one column to the left (col - 1
), and going right means moving one column to the right (col + 1
). The row always increases by 1 as we go deeper.
The natural approach is to traverse the entire tree and record where each node sits. DFS works perfectly here because:
- We can easily pass the current position
(row, col)
as parameters during recursion - When visiting a left child, we update to
(row + 1, col - 1)
- When visiting a right child, we update to
(row + 1, col + 1)
Once we've collected all nodes with their positions, the problem becomes a sorting problem. We need to group nodes by column, and within each column, order them by row (top to bottom). The tricky part is when multiple nodes share the same exact position - in this case, we sort by their values.
The clever trick in the solution is to store each node as a tuple (column, row, value)
. When we sort this list, Python naturally sorts by the first element (column), then by the second element (row) for ties, and finally by the third element (value) for nodes at the same position. This gives us exactly the ordering we need.
After sorting, we simply iterate through the sorted list and group consecutive nodes with the same column value into the same sublist. This produces our final vertical order traversal.
Learn more about Tree, Depth-First Search, Breadth-First Search, Binary Tree and Sorting patterns.
Solution Approach
The solution uses DFS combined with sorting to achieve the vertical order traversal. Let's break down the implementation:
1. DFS Traversal with Position Tracking
We define a recursive function dfs(root, i, j)
where:
root
is the current nodei
represents the row (depth level)j
represents the column
During the DFS traversal:
- For each non-null node, we record its position and value as a tuple
(j, i, root.val)
in thenodes
list - Note the order: column first, then row, then value - this is intentional for sorting later
- Recursively visit the left child with position
(i + 1, j - 1)
- Recursively visit the right child with position
(i + 1, j + 1)
2. Sorting the Collected Nodes
After collecting all nodes with their positions, we call nodes.sort()
. Python's default tuple sorting behavior works perfectly here:
- First sorts by column
j
(leftmost to rightmost) - For nodes in the same column, sorts by row
i
(top to bottom) - For nodes at the exact same position, sorts by value
3. Grouping by Column
We iterate through the sorted nodes
list and group nodes by column:
- Initialize
prev = -2000
(a value outside possible column range) - For each node
(j, _, val)
in the sorted list:- If the column
j
differs fromprev
, start a new group by appending an empty list toans
- Update
prev = j
to track the current column - Append the node's value to the last group in
ans
- If the column
The algorithm has a time complexity of O(n log n)
where n
is the number of nodes, dominated by the sorting step. The space complexity is O(n)
for storing the nodes list.
This approach elegantly handles all the requirements: vertical ordering by column, top-to-bottom ordering within columns, and value-based sorting for nodes at the same position.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider this binary tree:
3 / \ 9 20 / \ 15 7
Step 1: DFS Traversal with Position Tracking
Starting from the root at position (0, 0), we traverse the tree and record each node's position:
- Visit node 3 at (row=0, col=0) → Record: (0, 0, 3)
- Visit left child 9 at (row=1, col=-1) → Record: (-1, 1, 9)
- Visit right child 20 at (row=1, col=1) → Record: (1, 1, 20)
- Visit 20's left child 15 at (row=2, col=0) → Record: (0, 2, 15)
- Visit 20's right child 7 at (row=2, col=2) → Record: (2, 2, 7)
Our nodes
list now contains:
[(0, 0, 3), (-1, 1, 9), (1, 1, 20), (0, 2, 15), (2, 2, 7)]
Step 2: Sorting
Sort the list by (column, row, value):
[(-1, 1, 9), (0, 0, 3), (0, 2, 15), (1, 1, 20), (2, 2, 7)]
Notice how the sorting groups nodes by column first:
- Column -1: [(9)]
- Column 0: [(3), (15)] - ordered by row (0 before 2)
- Column 1: [(20)]
- Column 2: [(7)]
Step 3: Grouping by Column
Iterate through the sorted list and group by column:
- Process (-1, 1, 9): Column -1 is new, create group [9]
- Process (0, 0, 3): Column 0 is new, create group [3]
- Process (0, 2, 15): Column 0 continues, add to current group [3, 15]
- Process (1, 1, 20): Column 1 is new, create group [20]
- Process (2, 2, 7): Column 2 is new, create group [7]
Final Result: [[9], [3, 15], [20], [7]]
This represents the vertical order traversal from left to right, with nodes in each column ordered from top to bottom.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional, List
9
10class Solution:
11 def verticalTraversal(self, root: Optional[TreeNode]) -> List[List[int]]:
12 """
13 Performs vertical traversal of a binary tree.
14 Returns nodes grouped by their vertical column from left to right.
15 Within each column, nodes are ordered by row (top to bottom),
16 and nodes at the same position are ordered by value.
17 """
18
19 def dfs(node: Optional[TreeNode], row: int, col: int) -> None:
20 """
21 Depth-first search to collect all nodes with their positions.
22
23 Args:
24 node: Current tree node
25 row: Current row position (depth in tree)
26 col: Current column position (horizontal distance from root)
27 """
28 if node is None:
29 return
30
31 # Store node information as (column, row, value) for proper sorting
32 node_positions.append((col, row, node.val))
33
34 # Traverse left subtree (row increases, column decreases)
35 dfs(node.left, row + 1, col - 1)
36
37 # Traverse right subtree (row increases, column increases)
38 dfs(node.right, row + 1, col + 1)
39
40 # Collect all nodes with their positions
41 node_positions = []
42 dfs(root, 0, 0)
43
44 # Sort by column first, then row, then value
45 node_positions.sort()
46
47 # Group nodes by column
48 result = []
49 previous_col = -2000 # Initialize with value outside possible column range
50
51 for col, _, val in node_positions:
52 # Start a new column group if column changed
53 if previous_col != col:
54 result.append([])
55 previous_col = col
56
57 # Add node value to current column group
58 result[-1].append(val)
59
60 return result
61
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // List to store node information: [column, row, value]
18 private List<int[]> nodeList = new ArrayList<>();
19
20 /**
21 * Performs vertical traversal of a binary tree.
22 * Nodes are grouped by their vertical column from left to right.
23 * Within each column, nodes are ordered top to bottom.
24 * Nodes at the same position are sorted by their values.
25 *
26 * @param root The root of the binary tree
27 * @return List of lists containing node values grouped by vertical columns
28 */
29 public List<List<Integer>> verticalTraversal(TreeNode root) {
30 // Traverse the tree and collect node information
31 traverseTree(root, 0, 0);
32
33 // Sort nodes by: 1) column (ascending), 2) row (ascending), 3) value (ascending)
34 Collections.sort(nodeList, (nodeA, nodeB) -> {
35 // First, compare by column index
36 if (nodeA[0] != nodeB[0]) {
37 return Integer.compare(nodeA[0], nodeB[0]);
38 }
39 // If same column, compare by row index
40 if (nodeA[1] != nodeB[1]) {
41 return Integer.compare(nodeA[1], nodeB[1]);
42 }
43 // If same position, compare by node value
44 return Integer.compare(nodeA[2], nodeB[2]);
45 });
46
47 // Build the result list by grouping nodes by column
48 List<List<Integer>> result = new ArrayList<>();
49 int previousColumn = Integer.MIN_VALUE;
50
51 for (int[] nodeInfo : nodeList) {
52 int column = nodeInfo[0];
53 int nodeValue = nodeInfo[2];
54
55 // Start a new column group if column index changes
56 if (previousColumn != column) {
57 result.add(new ArrayList<>());
58 previousColumn = column;
59 }
60
61 // Add node value to the current column group
62 result.get(result.size() - 1).add(nodeValue);
63 }
64
65 return result;
66 }
67
68 /**
69 * Depth-first search to traverse the tree and record node positions.
70 *
71 * @param node Current node being processed
72 * @param row Current row index (increases as we go down)
73 * @param column Current column index (decreases left, increases right)
74 */
75 private void traverseTree(TreeNode node, int row, int column) {
76 // Base case: null node
77 if (node == null) {
78 return;
79 }
80
81 // Record current node's position and value
82 nodeList.add(new int[] {column, row, node.val});
83
84 // Traverse left subtree: row increases, column decreases
85 traverseTree(node.left, row + 1, column - 1);
86
87 // Traverse right subtree: row increases, column increases
88 traverseTree(node.right, row + 1, column + 1);
89 }
90}
91
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 vector<vector<int>> verticalTraversal(TreeNode* root) {
15 // Store nodes with their coordinates: (column, row, value)
16 vector<tuple<int, int, int>> nodesWithCoordinates;
17
18 // DFS function to traverse the tree and collect node information
19 // Parameters: current node, row index, column index
20 function<void(TreeNode*, int, int)> dfs = [&](TreeNode* node, int row, int col) {
21 // Base case: if node is null, return
22 if (!node) {
23 return;
24 }
25
26 // Store current node's information (column first for sorting)
27 nodesWithCoordinates.emplace_back(col, row, node->val);
28
29 // Traverse left subtree: row increases by 1, column decreases by 1
30 dfs(node->left, row + 1, col - 1);
31
32 // Traverse right subtree: row increases by 1, column increases by 1
33 dfs(node->right, row + 1, col + 1);
34 };
35
36 // Start DFS from root at position (0, 0)
37 dfs(root, 0, 0);
38
39 // Sort nodes by column first, then by row, then by value
40 // This is automatically handled by tuple comparison
41 sort(nodesWithCoordinates.begin(), nodesWithCoordinates.end());
42
43 // Build the result by grouping nodes with the same column
44 vector<vector<int>> result;
45 int previousColumn = -2000; // Initialize with value outside possible range
46
47 // Process each node in sorted order
48 for (auto [currentColumn, currentRow, nodeValue] : nodesWithCoordinates) {
49 // If we encounter a new column, create a new group
50 if (currentColumn != previousColumn) {
51 previousColumn = currentColumn;
52 result.emplace_back();
53 }
54
55 // Add the node value to the current column group
56 result.back().push_back(nodeValue);
57 }
58
59 return result;
60 }
61};
62
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Performs vertical traversal of a binary tree.
17 * Returns nodes grouped by their vertical column, ordered from left to right.
18 * Within each column, nodes are ordered by row (top to bottom), then by value if same position.
19 *
20 * @param root - The root node of the binary tree
21 * @returns A 2D array where each sub-array contains values from the same vertical column
22 */
23function verticalTraversal(root: TreeNode | null): number[][] {
24 // Store each node's information as [column, row, value]
25 const nodePositions: [number, number, number][] = [];
26
27 /**
28 * Depth-first search to traverse the tree and record node positions
29 * @param node - Current node being visited
30 * @param row - Current row position (depth in tree)
31 * @param column - Current column position (horizontal position)
32 */
33 const traverseTree = (node: TreeNode | null, row: number, column: number): void => {
34 if (!node) {
35 return;
36 }
37
38 // Record current node's position and value
39 nodePositions.push([column, row, node.val]);
40
41 // Traverse left child (column decreases by 1, row increases by 1)
42 traverseTree(node.left, row + 1, column - 1);
43
44 // Traverse right child (column increases by 1, row increases by 1)
45 traverseTree(node.right, row + 1, column + 1);
46 };
47
48 // Start traversal from root at position (0, 0)
49 traverseTree(root, 0, 0);
50
51 // Sort nodes by column first, then by row, then by value
52 nodePositions.sort((a, b) => {
53 if (a[0] !== b[0]) return a[0] - b[0]; // Sort by column
54 if (a[1] !== b[1]) return a[1] - b[1]; // Sort by row
55 return a[2] - b[2]; // Sort by value
56 });
57
58 // Group nodes by column into result array
59 const result: number[][] = [];
60 let previousColumn: number = -2000; // Initialize with value outside possible column range
61
62 for (const [column, _, value] of nodePositions) {
63 // Start a new group when column changes
64 if (column !== previousColumn) {
65 previousColumn = column;
66 result.push([]);
67 }
68
69 // Add value to the current column group
70 result.at(-1)!.push(value);
71 }
72
73 return result;
74}
75
Time and Space Complexity
Time Complexity: O(n × log n)
The algorithm consists of two main phases:
- DFS traversal: The
dfs
function visits each node exactly once, collecting node information with coordinates(j, i, val)
. This takesO(n)
time wheren
is the number of nodes. - Sorting: After collecting all nodes, the
nodes
list containingn
elements is sorted. Python's sort operation has a time complexity ofO(n × log n)
. - Building result: The final loop iterates through the sorted nodes once to construct the answer, taking
O(n)
time.
The overall time complexity is dominated by the sorting step: O(n) + O(n × log n) + O(n) = O(n × log n)
.
Space Complexity: O(n)
The space usage includes:
- Nodes list: Stores information for all
n
nodes as tuples(j, i, val)
, requiringO(n)
space. - Recursion stack: In the worst case (skewed tree), the DFS recursion depth can be
O(n)
. - Answer list: Stores all
n
node values in the final result, usingO(n)
space.
The total space complexity is O(n) + O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Incorrect Handling of Same-Position Nodes
The Problem:
A common mistake is forgetting to sort nodes by their values when they share the exact same position (row, col)
. Developers often assume that nodes at the same position should maintain their traversal order or be added in the order they're discovered during DFS.
Example of Incorrect Implementation:
def verticalTraversal(self, root: Optional[TreeNode]) -> List[List[int]]:
from collections import defaultdict
columns = defaultdict(list)
def dfs(node, row, col):
if not node:
return
# Wrong: Simply appending nodes without considering same-position sorting
columns[col].append((row, node.val))
dfs(node.left, row + 1, col - 1)
dfs(node.right, row + 1, col + 1)
dfs(root, 0, 0)
result = []
for col in sorted(columns.keys()):
# Wrong: Only sorting by row, not handling same-position value sorting
column_nodes = sorted(columns[col], key=lambda x: x[0])
result.append([val for _, val in column_nodes])
return result
Why This Fails:
Consider a tree where two nodes with values 5 and 3 both end up at position (2, 1)
. The incorrect implementation might output them as [5, 3]
based on traversal order, but the correct output should be [3, 5]
(sorted by value).
The Correct Solution: The provided solution avoids this pitfall by:
- Storing all three pieces of information together:
(col, row, node.val)
- Using Python's tuple sorting which naturally handles all three sorting criteria:
- Primary sort by column (left to right)
- Secondary sort by row (top to bottom)
- Tertiary sort by value (ascending order for same position)
# Correct approach - tuple includes all sorting criteria node_positions.append((col, row, node.val)) node_positions.sort() # Sorts by all three fields automatically
Alternative Correct Approach Using Custom Sorting: If you prefer to use a dictionary-based approach, ensure proper sorting:
from collections import defaultdict
columns = defaultdict(list)
def dfs(node, row, col):
if not node:
return
columns[col].append((row, node.val))
dfs(node.left, row + 1, col - 1)
dfs(node.right, row + 1, col + 1)
dfs(root, 0, 0)
result = []
for col in sorted(columns.keys()):
# Correct: Sort by both row AND value
column_nodes = sorted(columns[col], key=lambda x: (x[0], x[1]))
result.append([val for _, val in column_nodes])
Test Case to Verify:
Tree: 1 / \ 2 3 / \ / \ 4 6 5 7 Where nodes 6 and 5 end up at the same position. Expected output should have 5 before 6 in their column.
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!