Facebook Pixel

314. Binary Tree Vertical Order Traversal 🔒

Problem Description

You are given the root of a binary tree. Your task is to return the vertical order traversal of the tree's node values, which means traversing the tree column by column from left to right, and within each column, from top to bottom.

To visualize this, imagine drawing vertical lines through the tree. Each vertical line represents a column. Nodes that fall on the same vertical line belong to the same column. The columns are ordered from leftmost to rightmost, and within each column, nodes are ordered from top to bottom (by their depth/level in the tree).

An important rule: if two nodes happen to be in the exact same position (same row and same column), the node that appears on the left side of the tree should come before the node on the right side in the output.

For example, if you have a tree like:

       3
      / \
     9   20
        /  \
       15   7

The vertical order traversal would group nodes by their column positions:

  • Column -1: [9]
  • Column 0: [3, 15]
  • Column 1: [20]
  • Column 2: [7]

So the output would be: [[9], [3, 15], [20], [7]]

The solution uses DFS to traverse the tree while tracking each node's depth (vertical position) and horizontal offset. The root starts at offset 0, left children get offset - 1, and right children get offset + 1. After collecting all nodes with their positions, it sorts them by offset (column) and then by depth (row) to produce the final vertical order traversal.

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 (specifically, a connected acyclic graph with hierarchical structure).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree. Trees are graphs with no cycles, where each node (except the root) has exactly one parent.

DFS

  • We arrive at DFS as the solution pattern.

Conclusion: The flowchart suggests using a DFS (Depth-First Search) approach for traversing the binary tree.

This makes sense for the Binary Tree Vertical Order Traversal problem because:

  1. We need to visit every node in the tree to collect their values
  2. DFS allows us to systematically traverse the tree while tracking important information (depth and horizontal offset)
  3. During the DFS traversal, we can maintain the column position (offset) by decreasing it when going left (-1) and increasing it when going right (+1)
  4. DFS naturally provides us with the depth information as we recursively traverse down the tree
  5. By collecting nodes with their (offset, depth) coordinates during DFS, we can later sort and group them to achieve the vertical order traversal

The DFS pattern is ideal here because it allows us to explore the entire tree structure while maintaining the positional context needed to determine which vertical column each node belongs to.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To solve this problem, we need to think about how to track which vertical column each node belongs to. Imagine standing at the root of the tree and assigning it column position 0. As we traverse the tree, whenever we go left, we move one column to the left (column - 1), and whenever we go right, we move one column to the right (column + 1).

The key insight is that we need to track two pieces of information for each node:

  1. Column position (offset): Which vertical column the node belongs to
  2. Depth (row): How far down the node is from the root

Why do we need depth? Because within the same column, we need to output nodes from top to bottom. If we only tracked column positions, we wouldn't know the correct order within each column.

During a DFS traversal, we can naturally maintain both pieces of information:

  • Start at the root with offset = 0 and depth = 0
  • When moving to a left child: offset - 1, depth + 1
  • When moving to a right child: offset + 1, depth + 1

We can use a dictionary/hashmap where the key is the column offset, and the value is a list of (depth, node_value) pairs. This allows us to group all nodes in the same column together.

After the traversal completes, we have all nodes grouped by column but potentially out of order within each column. We then:

  1. Sort the columns by their offset values (to get left-to-right ordering)
  2. Within each column, sort nodes by their depth (to get top-to-bottom ordering)

The reason DFS works well here is that it allows us to visit every node exactly once while maintaining the relative position information we need. The recursive nature of DFS makes it easy to pass down the updated offset and depth values to child nodes.

Solution Approach

The solution implements a DFS traversal with position tracking, followed by sorting and grouping. Let's walk through the implementation step by step:

1. Data Structure Setup We use a defaultdict(list) to store nodes grouped by their column offset. Each entry in the dictionary maps a column offset to a list of (depth, node_value) tuples.

2. DFS Traversal Function The dfs function takes three parameters:

  • root: The current node being visited
  • depth: The current depth/level in the tree (starting from 0)
  • offset: The horizontal column position (0 for root, negative for left, positive for right)
def dfs(root, depth, offset):
    if root is None:
        return
    d[offset].append((depth, root.val))
    dfs(root.left, depth + 1, offset - 1)
    dfs(root.right, depth + 1, offset + 1)

For each non-null node, we:

  • Store the (depth, node_value) pair in the dictionary at key offset
  • Recursively visit the left child with offset - 1
  • Recursively visit the right child with offset + 1
  • Increment depth by 1 for both children

3. Sorting and Grouping After the DFS traversal completes, we need to organize the collected data:

for _, v in sorted(d.items()):
    v.sort(key=lambda x: x[0])
    ans.append([x[1] for x in v])
  • sorted(d.items()) sorts the dictionary by keys (column offsets), giving us left-to-right column ordering
  • For each column's list of nodes, v.sort(key=lambda x: x[0]) sorts by depth (the first element of each tuple), ensuring top-to-bottom ordering within each column
  • We extract just the node values [x[1] for x in v] and append to our answer

4. Time and Space Complexity

  • Time: O(n log n) where n is the number of nodes. The DFS traversal is O(n), but sorting the columns and nodes within columns takes O(n log n) in the worst case.
  • Space: O(n) to store all nodes in the dictionary and the recursion stack for DFS.

The beauty of this approach is that it separates the traversal logic (collecting nodes with their positions) from the ordering logic (sorting by column and depth), making the solution clean and easy to understand.

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:

      1
     / \
    2   3
   / \
  4   5

Step 1: Initialize data structures

  • Create an empty dictionary d = {} to store nodes by column
  • Start DFS at root with depth=0, offset=0

Step 2: DFS Traversal

Visit node 1 (depth=0, offset=0):

  • Add (0, 1) to d[0]
  • Recurse left to node 2 with depth=1, offset=-1
  • Recurse right to node 3 with depth=1, offset=1

Visit node 2 (depth=1, offset=-1):

  • Add (1, 2) to d[-1]
  • Recurse left to node 4 with depth=2, offset=-2
  • Recurse right to node 5 with depth=2, offset=0

Visit node 4 (depth=2, offset=-2):

  • Add (2, 4) to d[-2]
  • Both children are null, return

Visit node 5 (depth=2, offset=0):

  • Add (2, 5) to d[0]
  • Both children are null, return

Visit node 3 (depth=1, offset=1):

  • Add (1, 3) to d[1]
  • Both children are null, return

Step 3: After DFS, our dictionary looks like:

d = {
    -2: [(2, 4)],
    -1: [(1, 2)],
     0: [(0, 1), (2, 5)],
     1: [(1, 3)]
}

Step 4: Sort and group

  • Sort dictionary by keys (offsets): -2, -1, 0, 1
  • For each column:
    • Column -2: [(2, 4)] → already sorted → extract values: [4]
    • Column -1: [(1, 2)] → already sorted → extract values: [2]
    • Column 0: [(0, 1), (2, 5)] → sort by depth → stays same → extract values: [1, 5]
    • Column 1: [(1, 3)] → already sorted → extract values: [3]

Final Output: [[4], [2], [1, 5], [3]]

This represents the vertical order traversal from left to right, with nodes in the same 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
9from collections import defaultdict
10
11class Solution:
12    def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
13        """
14        Returns the vertical order traversal of a binary tree.
15        Nodes are grouped by their horizontal distance from root (column).
16        Within each column, nodes are ordered by their depth (row).
17      
18        Args:
19            root: Root node of the binary tree
20          
21        Returns:
22            List of lists containing node values in vertical order
23        """
24      
25        def dfs(node: Optional[TreeNode], depth: int, column: int) -> None:
26            """
27            Depth-first search to traverse the tree and record node positions.
28          
29            Args:
30                node: Current node being processed
31                depth: Current depth (row) in the tree
32                column: Horizontal distance from root (negative for left, positive for right)
33            """
34            if node is None:
35                return
36          
37            # Store the node value along with its depth for later sorting
38            column_values[column].append((depth, node.val))
39          
40            # Traverse left subtree (column decreases by 1)
41            dfs(node.left, depth + 1, column - 1)
42          
43            # Traverse right subtree (column increases by 1)
44            dfs(node.right, depth + 1, column + 1)
45      
46        # Dictionary to store nodes grouped by their column index
47        # Key: column index, Value: list of (depth, node_value) tuples
48        column_values = defaultdict(list)
49      
50        # Start DFS from root at depth 0 and column 0
51        dfs(root, 0, 0)
52      
53        # Build the result list
54        result = []
55      
56        # Sort columns from leftmost to rightmost
57        for column_index in sorted(column_values.keys()):
58            nodes_in_column = column_values[column_index]
59          
60            # Sort nodes within the same column by their depth (top to bottom)
61            nodes_in_column.sort(key=lambda x: x[0])
62          
63            # Extract only the node values (discard depth information)
64            result.append([node_val for depth, node_val in nodes_in_column])
65      
66        return result
67
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    // TreeMap to store nodes grouped by their horizontal position (column)
18    // Key: horizontal position, Value: list of [depth, nodeValue] pairs
19    private TreeMap<Integer, List<int[]>> columnMap = new TreeMap<>();
20  
21    /**
22     * Returns the vertical order traversal of a binary tree.
23     * Nodes are grouped by their horizontal position (column) from left to right,
24     * and within each column, nodes are ordered by their depth (top to bottom).
25     * 
26     * @param root The root of the binary tree
27     * @return List of lists containing node values in vertical order
28     */
29    public List<List<Integer>> verticalOrder(TreeNode root) {
30        // Perform DFS to populate the columnMap with node positions
31        dfs(root, 0, 0);
32      
33        // Build the result list from the columnMap
34        List<List<Integer>> result = new ArrayList<>();
35      
36        // Iterate through columns in sorted order (left to right)
37        for (List<int[]> columnNodes : columnMap.values()) {
38            // Sort nodes within the column by depth (top to bottom)
39            Collections.sort(columnNodes, (a, b) -> a[0] - b[0]);
40          
41            // Extract node values from the sorted column
42            List<Integer> columnValues = new ArrayList<>();
43            for (int[] nodeInfo : columnNodes) {
44                columnValues.add(nodeInfo[1]);
45            }
46          
47            // Add this column to the result
48            result.add(columnValues);
49        }
50      
51        return result;
52    }
53  
54    /**
55     * Depth-first search to traverse the tree and record each node's position.
56     * 
57     * @param root The current node being processed
58     * @param depth The current depth in the tree (0 for root, increases downward)
59     * @param offset The horizontal position (0 for root, decreases left, increases right)
60     */
61    private void dfs(TreeNode root, int depth, int offset) {
62        // Base case: if node is null, return
63        if (root == null) {
64            return;
65        }
66      
67        // Add current node's information to the appropriate column
68        // Store as [depth, nodeValue] to enable sorting by depth later
69        columnMap.computeIfAbsent(offset, k -> new ArrayList<>())
70                 .add(new int[] {depth, root.val});
71      
72        // Recursively process left subtree (column position decreases by 1)
73        dfs(root.left, depth + 1, offset - 1);
74      
75        // Recursively process right subtree (column position increases by 1)
76        dfs(root.right, depth + 1, offset + 1);
77    }
78}
79
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 */
12
13class Solution {
14private:
15    // Type alias for pairs of (depth, node value)
16    using DepthValuePair = pair<int, int>;
17  
18    // Map from column index to vector of (depth, value) pairs
19    // Key: column index (negative for left, positive for right)
20    // Value: vector of pairs containing depth and node value
21    map<int, vector<DepthValuePair>> columnToNodes;
22  
23public:
24    /**
25     * Returns the vertical order traversal of a binary tree
26     * Nodes in the same column are ordered by their depth (top to bottom)
27     * @param root The root of the binary tree
28     * @return 2D vector where each inner vector contains nodes in a vertical column
29     */
30    vector<vector<int>> verticalOrder(TreeNode* root) {
31        // Perform DFS to collect all nodes with their column and depth information
32        traverseTree(root, 0, 0);
33      
34        // Build the result vector
35        vector<vector<int>> result;
36      
37        // Process each column in order (map automatically sorts by key)
38        for (auto& [columnIndex, nodesInColumn] : columnToNodes) {
39            // Sort nodes in the same column by depth (top to bottom)
40            sort(nodesInColumn.begin(), nodesInColumn.end(), 
41                 [](const DepthValuePair& a, const DepthValuePair& b) {
42                     return a.first < b.first;
43                 });
44          
45            // Extract only the values (discard depth information)
46            vector<int> columnValues;
47            for (const auto& depthValuePair : nodesInColumn) {
48                columnValues.push_back(depthValuePair.second);
49            }
50          
51            // Add this column to the result
52            result.push_back(columnValues);
53        }
54      
55        return result;
56    }
57  
58private:
59    /**
60     * DFS traversal to collect nodes with their column and depth information
61     * @param node Current node being processed
62     * @param depth Current depth in the tree (0 for root, increases downward)
63     * @param column Current column index (0 for root, decreases left, increases right)
64     */
65    void traverseTree(TreeNode* node, int depth, int column) {
66        // Base case: null node
67        if (!node) {
68            return;
69        }
70      
71        // Store current node's information: depth and value at this column
72        columnToNodes[column].push_back({depth, node->val});
73      
74        // Recursively traverse left subtree (column - 1, depth + 1)
75        traverseTree(node->left, depth + 1, column - 1);
76      
77        // Recursively traverse right subtree (column + 1, depth + 1)
78        traverseTree(node->right, depth + 1, column + 1);
79    }
80};
81
1/**
2 * Definition for a binary tree node.
3 */
4class TreeNode {
5    val: number;
6    left: TreeNode | null;
7    right: TreeNode | null;
8    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
9        this.val = (val === undefined ? 0 : val);
10        this.left = (left === undefined ? null : left);
11        this.right = (right === undefined ? null : right);
12    }
13}
14
15// Type definition for pairs of (depth, node value)
16type DepthValuePair = [number, number];
17
18// Map from column index to array of (depth, value) pairs
19// Key: column index (negative for left, positive for right)  
20// Value: array of pairs containing depth and node value
21let columnToNodes: Map<number, DepthValuePair[]>;
22
23/**
24 * Returns the vertical order traversal of a binary tree
25 * Nodes in the same column are ordered by their depth (top to bottom)
26 * @param root The root of the binary tree
27 * @return 2D array where each inner array contains nodes in a vertical column
28 */
29function verticalOrder(root: TreeNode | null): number[][] {
30    // Initialize the map for storing column data
31    columnToNodes = new Map<number, DepthValuePair[]>();
32  
33    // Perform DFS to collect all nodes with their column and depth information
34    traverseTree(root, 0, 0);
35  
36    // Build the result array
37    const result: number[][] = [];
38  
39    // Sort column indices to process from leftmost to rightmost
40    const sortedColumns = Array.from(columnToNodes.keys()).sort((a, b) => a - b);
41  
42    // Process each column in order
43    for (const columnIndex of sortedColumns) {
44        const nodesInColumn = columnToNodes.get(columnIndex)!;
45      
46        // Sort nodes in the same column by depth (top to bottom)
47        nodesInColumn.sort((a, b) => a[0] - b[0]);
48      
49        // Extract only the values (discard depth information)
50        const columnValues: number[] = nodesInColumn.map(depthValuePair => depthValuePair[1]);
51      
52        // Add this column to the result
53        result.push(columnValues);
54    }
55  
56    return result;
57}
58
59/**
60 * DFS traversal to collect nodes with their column and depth information
61 * @param node Current node being processed
62 * @param depth Current depth in the tree (0 for root, increases downward)
63 * @param column Current column index (0 for root, decreases left, increases right)
64 */
65function traverseTree(node: TreeNode | null, depth: number, column: number): void {
66    // Base case: null node
67    if (!node) {
68        return;
69    }
70  
71    // Initialize array for this column if it doesn't exist
72    if (!columnToNodes.has(column)) {
73        columnToNodes.set(column, []);
74    }
75  
76    // Store current node's information: depth and value at this column
77    columnToNodes.get(column)!.push([depth, node.val]);
78  
79    // Recursively traverse left subtree (column - 1, depth + 1)
80    traverseTree(node.left, depth + 1, column - 1);
81  
82    // Recursively traverse right subtree (column + 1, depth + 1)
83    traverseTree(node.right, depth + 1, column + 1);
84}
85

Time and Space Complexity

The time complexity is O(n log n), and the space complexity is O(n), where n is the number of nodes in the binary tree.

Time Complexity Analysis:

  • The DFS traversal visits each node exactly once: O(n)
  • After DFS, we sort the dictionary keys using sorted(d.items()). In the worst case, there could be n different horizontal offsets (e.g., a skewed tree), so sorting takes O(n log n)
  • For each vertical column, we sort the nodes by their depth. In the worst case, all n nodes could be in the same column, requiring O(n log n) for that sort
  • Overall: O(n) + O(n log n) + O(n log n) = O(n log n)

Space Complexity Analysis:

  • The dictionary d stores all n nodes with their depth and value information: O(n)
  • The recursion stack depth can be at most O(n) in case of a skewed tree
  • The output list ans contains all n node values: O(n)
  • Overall: O(n) + O(n) + O(n) = O(n)

Note: The reference answer suggests O(n log log n) time complexity, which would be achievable if we used a more efficient data structure like a balanced BST or if the number of distinct columns was bounded by O(log n). However, with the current implementation using Python's built-in sort on potentially n columns and n elements, the time complexity is O(n log n).

Common Pitfalls

Pitfall 1: Using DFS Instead of BFS for Same-Position Node Ordering

The most critical issue with the DFS approach is that it doesn't correctly handle the ordering of nodes that appear at the same position (same row and column). According to the problem statement, when two nodes are at the exact same position, the node on the left side should come before the node on the right side.

Why DFS Fails: With DFS, the traversal order depends on which subtree is visited first. When we traverse left-then-right, we might visit nodes in a different order than their actual left-to-right appearance at the same level.

Consider this tree:

       1
      / \
     2   3
    / \
   4   5

If nodes 4 and 5 somehow ended up in the same column at the same depth (through a more complex tree structure), DFS would visit them in the order determined by the traversal path, not their left-to-right position.

Solution: Use BFS for Correct Level-Order Processing

from collections import deque, defaultdict

class Solution:
    def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []
      
        # Use BFS to ensure left-to-right ordering at same positions
        queue = deque([(root, 0)])  # (node, column)
        column_values = defaultdict(list)
      
        while queue:
            node, column = queue.popleft()
            column_values[column].append(node.val)
          
            # Process left child first, then right child
            # This ensures left-to-right ordering within same level
            if node.left:
                queue.append((node.left, column - 1))
            if node.right:
                queue.append((node.right, column + 1))
      
        # Sort by column index and return values
        result = []
        for column in sorted(column_values.keys()):
            result.append(column_values[column])
      
        return result

Pitfall 2: Unnecessary Depth Tracking and Sorting

The original DFS solution stores depth information and sorts by it within each column. However, if we use BFS, nodes are naturally processed level by level, eliminating the need for:

  • Storing depth information
  • Sorting within columns
  • Complex tuple structures

Why This Matters:

  • The DFS approach has O(n log n) complexity due to sorting
  • BFS approach achieves O(n) complexity for traversal (only sorting columns remains)
  • BFS code is simpler and more intuitive for level-order problems

Pitfall 3: Not Handling Edge Cases

The DFS solution doesn't explicitly handle the empty tree case at the beginning:

# Better approach - handle edge case upfront
if not root:
    return []

This prevents unnecessary dictionary initialization and function calls for null inputs.

Key Takeaway: For vertical order traversal problems where level-order matters (especially when same-position ordering is important), BFS is the more natural and correct choice. It processes nodes level by level from left to right, automatically maintaining the required ordering without additional sorting steps.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

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

Load More