Facebook Pixel

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)

The vertical order traversal requires you to:

  1. Group all nodes by their column index (from leftmost to rightmost column)
  2. Within each column, order nodes from top to bottom (by row)
  3. 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:

  1. We need to traverse the entire tree to visit every node
  2. During traversal, we need to track the position (row, column) of each node
  3. DFS allows us to recursively explore the tree while maintaining the relative positions
  4. 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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. We can easily pass the current position (row, col) as parameters during recursion
  2. When visiting a left child, we update to (row + 1, col - 1)
  3. 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 node
  • i 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 the nodes 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 from prev, start a new group by appending an empty list to ans
    • Update prev = j to track the current column
    • Append the node's value to the last group in ans

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 Evaluator

Example 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:

  1. DFS traversal: The dfs function visits each node exactly once, collecting node information with coordinates (j, i, val). This takes O(n) time where n is the number of nodes.
  2. Sorting: After collecting all nodes, the nodes list containing n elements is sorted. Python's sort operation has a time complexity of O(n × log n).
  3. 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:

  1. Nodes list: Stores information for all n nodes as tuples (j, i, val), requiring O(n) space.
  2. Recursion stack: In the worst case (skewed tree), the DFS recursion depth can be O(n).
  3. Answer list: Stores all n node values in the final result, using O(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:

  1. Storing all three pieces of information together: (col, row, node.val)
  2. 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.
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More