655. Print Binary Tree
Problem Description
This problem asks you to create a visual representation of a binary tree as a 2D string matrix following specific formatting rules.
Given a binary tree, you need to construct a matrix where:
-
Matrix dimensions:
- The number of rows
m
equalsheight + 1
, whereheight
is the height of the tree - The number of columns
n
equals2^(height+1) - 1
- The number of rows
-
Node placement rules:
- The root node goes in the middle of the first row at position
res[0][(n-1)/2]
- For any node placed at position
res[r][c]
:- Its left child goes at
res[r+1][c - 2^(height-r-1)]
- Its right child goes at
res[r+1][c + 2^(height-r-1)]
- Its left child goes at
- Node values should be converted to strings when placed in the matrix
- Empty positions should contain empty strings
""
- The root node goes in the middle of the first row at position
The solution uses two main steps:
-
Calculate tree height: A recursive function finds the maximum depth of the tree. The height is defined as the number of edges in the longest path from root to leaf.
-
Fill the matrix: Using DFS traversal starting from the root, each node is placed at its calculated position. The formula
2^(height-r-1)
determines the horizontal offset for child nodes, which decreases as you go deeper in the tree (largerr
values).
The key insight is that the offset for placing children nodes depends on both the tree's total height and the current row number. This ensures proper spacing that creates a balanced visual representation where each level's nodes are evenly distributed across the available columns.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: While not explicitly a graph problem, a binary tree is a special type of graph (specifically, a connected acyclic graph).
Is it a tree?
- Yes: The problem explicitly deals with a binary tree structure where we need to traverse and process each node.
DFS
- Yes: This is our conclusion. The problem requires visiting each node in the tree to place it in the correct position in the output matrix.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem.
This makes sense because:
- We need to traverse the entire tree structure to place each node in the matrix
- DFS allows us to recursively process each node along with its position information (row and column)
- The parent-child relationship in the tree naturally maps to the recursive nature of DFS, where we process a node and then recursively process its children with updated position parameters
- The formula for calculating child positions (
c - 2^(height-r-1)
for left,c + 2^(height-r-1)
for right) requires maintaining the current depth/row, which DFS handles elegantly through its recursive call stack
Intuition
The key insight is to think about how we would draw a binary tree on paper. We naturally place the root at the top center, then position children nodes below their parents with appropriate spacing to avoid overlaps.
To formalize this intuitive drawing process, we need to determine:
- How large should our canvas (matrix) be?
- Where exactly should each node be placed?
For the matrix size, we observe that:
- Each level of the tree needs its own row, so we need
height + 1
rows - The width must accommodate the maximum possible spread of nodes. In a complete binary tree of height
h
, the bottom level can have up to2^h
nodes, and we need spaces between them. This leads to2^(h+1) - 1
columns.
For node positioning, imagine dividing the available horizontal space recursively:
- The root takes the middle position
- Each child gets half of its parent's "territory"
- The offset from parent to child decreases by half at each level
This halving pattern gives us the formula 2^(height-r-1)
for the offset at row r
. At the top (row 0), children are placed far apart with offset 2^height
. As we go deeper, the offset decreases: 2^(height-1)
, 2^(height-2)
, and so on, until leaves have an offset of 2^0 = 1
.
The DFS approach naturally fits because:
- We process each node once (place its value in the matrix)
- We need to pass down contextual information (current position) to children
- The recursive structure mirrors the tree structure itself
By calculating the tree height first, we can determine both the matrix dimensions and the correct offsets for each level, then use DFS to systematically fill in each node's value at its calculated position.
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The implementation consists of two main phases: calculating the tree height and filling the matrix using DFS.
Phase 1: Calculate Tree Height
The height
function recursively computes the tree's height:
- Base case: If the node is
None
, return-1
(this ensures a single node has height 0) - Recursive case: Return
1 + max(height(left), height(right))
This gives us the maximum depth from root to any leaf, which we need for determining matrix dimensions and calculating offsets.
Phase 2: Matrix Construction and DFS Traversal
Once we have the height h
, we:
- Calculate matrix dimensions:
- Rows:
m = h + 1
- Columns:
n = 2^(h+1) - 1
- Rows:
- Initialize an
m × n
matrix filled with empty strings""
- Start DFS from the root at position
(0, (n-1)//2)
The dfs
function places nodes in the matrix:
def dfs(root, r, c):
if root is None:
return
ans[r][c] = str(root.val) # Place current node's value
dfs(root.left, r + 1, c - 2^(h - r - 1)) # Place left child
dfs(root.right, r + 1, c + 2^(h - r - 1)) # Place right child
Key Implementation Details:
-
Position Calculation: The offset
2^(h - r - 1)
determines how far left or right to place children from their parent- At row 0: offset =
2^h
(maximum spacing) - At row 1: offset =
2^(h-1)
(half the previous) - This pattern continues, halving at each level
- At row 0: offset =
-
String Conversion: Node values are converted to strings using
str(root.val)
before placement -
Matrix Initialization: Using list comprehension
[[""] * n for _ in range(m)]
creates the empty matrix efficiently
The algorithm visits each node exactly once, making it O(n) in time complexity where n is the number of nodes. The space complexity is O(m × n) for the output matrix, which is O(2^h × h) in terms of tree height.
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:
1 / \ 2 3 \ 4
Step 1: Calculate Tree Height
Starting from the root (1):
- height(1) = 1 + max(height(2), height(3))
- height(2) = 1 + max(height(None), height(4))
- height(4) = 1 + max(height(None), height(None)) = 1 + max(-1, -1) = 0
- So height(2) = 1 + max(-1, 0) = 1
- height(3) = 1 + max(height(None), height(None)) = 1 + max(-1, -1) = 0
- So height(1) = 1 + max(1, 0) = 2
- height(2) = 1 + max(height(None), height(4))
The tree height is 2.
Step 2: Calculate Matrix Dimensions
- Rows: m = height + 1 = 2 + 1 = 3 rows
- Columns: n = 2^(height+1) - 1 = 2^3 - 1 = 7 columns
Initialize a 3×7 matrix filled with empty strings:
[["", "", "", "", "", "", ""], ["", "", "", "", "", "", ""], ["", "", "", "", "", "", ""]]
Step 3: DFS Traversal to Fill Matrix
Start DFS from root at position (0, (7-1)/2) = (0, 3):
-
Place root (1) at row 0, col 3:
- ans[0][3] = "1"
- Calculate offset: 2^(2-0-1) = 2^1 = 2
- Process left child (2) at position (1, 3-2) = (1, 1)
- Process right child (3) at position (1, 3+2) = (1, 5)
-
Place node 2 at row 1, col 1:
- ans[1][1] = "2"
- Calculate offset: 2^(2-1-1) = 2^0 = 1
- Left child is None (skip)
- Process right child (4) at position (2, 1+1) = (2, 2)
-
Place node 3 at row 1, col 5:
- ans[1][5] = "3"
- Both children are None (skip)
-
Place node 4 at row 2, col 2:
- ans[2][2] = "4"
- Both children are None (skip)
Final Result:
[["", "", "", "1", "", "", ""], ["", "2", "", "", "", "3", ""], ["", "", "4", "", "", "", ""]]
The matrix shows:
- Root "1" centered in row 0
- "2" and "3" in row 1, positioned with offset 2 from their parent
- "4" in row 2, positioned with offset 1 from its parent "2"
Each level's offset halves as we go deeper: starting at 2^1=2 for level 1, then 2^0=1 for level 2, ensuring proper spacing throughout the tree visualization.
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 printTree(self, root: Optional[TreeNode]) -> List[List[str]]:
12 """
13 Constructs a 2D representation of a binary tree.
14 Each node is placed at a specific position based on its level and subtree position.
15 """
16
17 def get_tree_height(node: Optional[TreeNode]) -> int:
18 """
19 Calculate the height of the binary tree.
20 Height is defined as the number of edges in the longest path from root to leaf.
21 Returns -1 for empty tree, 0 for single node, etc.
22 """
23 if node is None:
24 return -1
25
26 left_height = get_tree_height(node.left)
27 right_height = get_tree_height(node.right)
28
29 return 1 + max(left_height, right_height)
30
31 def fill_matrix(node: Optional[TreeNode], row: int, col: int) -> None:
32 """
33 Recursively fill the result matrix with node values.
34
35 Args:
36 node: Current tree node being processed
37 row: Current row index in the result matrix
38 col: Current column index in the result matrix
39 """
40 if node is None:
41 return
42
43 # Place current node value at the specified position
44 result[row][col] = str(node.val)
45
46 # Calculate offset for child nodes based on current depth
47 # The offset decreases as we go deeper in the tree
48 offset = 2 ** (tree_height - row - 1)
49
50 # Recursively process left and right subtrees
51 # Left child goes to the left (col - offset)
52 fill_matrix(node.left, row + 1, col - offset)
53 # Right child goes to the right (col + offset)
54 fill_matrix(node.right, row + 1, col + offset)
55
56 # Calculate tree dimensions
57 tree_height = get_tree_height(root)
58
59 # Matrix dimensions based on tree height
60 # Rows: height + 1 (since height is 0-indexed)
61 # Columns: 2^(height+1) - 1 (full binary tree width)
62 num_rows = tree_height + 1
63 num_cols = 2 ** (tree_height + 1) - 1
64
65 # Initialize result matrix with empty strings
66 result = [[""] * num_cols for _ in range(num_rows)]
67
68 # Start filling from root at the center of the first row
69 center_col = (num_cols - 1) // 2
70 fill_matrix(root, 0, center_col)
71
72 return result
73
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 /**
18 * Prints a binary tree in a 2D matrix format
19 * @param root The root of the binary tree
20 * @return A 2D list representing the tree structure
21 */
22 public List<List<String>> printTree(TreeNode root) {
23 // Calculate the height of the tree
24 int treeHeight = height(root);
25
26 // Calculate matrix dimensions
27 // Rows: height + 1 (0-indexed height means h+1 rows)
28 // Columns: 2^(h+1) - 1 (formula for perfect binary tree width)
29 int rows = treeHeight + 1;
30 int columns = (1 << (treeHeight + 1)) - 1;
31
32 // Initialize the result matrix with empty strings
33 String[][] resultMatrix = new String[rows][columns];
34 for (int i = 0; i < rows; i++) {
35 Arrays.fill(resultMatrix[i], "");
36 }
37
38 // Populate the matrix using DFS traversal
39 // Start from root at row 0, center column
40 populateMatrix(root, resultMatrix, treeHeight, 0, (columns - 1) / 2);
41
42 // Convert 2D array to List<List<String>>
43 List<List<String>> result = new ArrayList<>();
44 for (String[] row : resultMatrix) {
45 result.add(Arrays.asList(row));
46 }
47
48 return result;
49 }
50
51 /**
52 * Recursively populates the matrix with tree node values
53 * @param node Current tree node
54 * @param matrix The result matrix to populate
55 * @param treeHeight Total height of the tree
56 * @param row Current row index
57 * @param column Current column index
58 */
59 private void populateMatrix(TreeNode node, String[][] matrix, int treeHeight, int row, int column) {
60 // Base case: null node
61 if (node == null) {
62 return;
63 }
64
65 // Place current node value at the specified position
66 matrix[row][column] = String.valueOf(node.val);
67
68 // Calculate offset for child nodes
69 // Offset decreases as we go deeper: 2^(h-r-1)
70 int childOffset = 1 << (treeHeight - row - 1);
71
72 // Recursively process left child (column - offset)
73 populateMatrix(node.left, matrix, treeHeight, row + 1, column - childOffset);
74
75 // Recursively process right child (column + offset)
76 populateMatrix(node.right, matrix, treeHeight, row + 1, column + childOffset);
77 }
78
79 /**
80 * Calculates the height of the binary tree
81 * @param node The root node of the tree/subtree
82 * @return Height of the tree (-1 for null tree, 0 for single node)
83 */
84 private int height(TreeNode node) {
85 // Base case: null node has height -1
86 if (node == null) {
87 return -1;
88 }
89
90 // Height is 1 + maximum height of subtrees
91 return 1 + Math.max(height(node.left), height(node.right));
92 }
93}
94
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 /**
15 * Prints a binary tree in a 2D matrix format
16 * Each node is placed at a specific position based on its level and horizontal position
17 * @param root: Root node of the binary tree
18 * @return: 2D matrix representation of the tree
19 */
20 vector<vector<string>> printTree(TreeNode* root) {
21 // Calculate the height of the tree (0-indexed)
22 int treeHeight = height(root);
23
24 // Matrix dimensions: rows = height + 1, cols = 2^(height+1) - 1
25 int numRows = treeHeight + 1;
26 int numCols = (1 << (treeHeight + 1)) - 1;
27
28 // Initialize result matrix with empty strings
29 vector<vector<string>> result(numRows, vector<string>(numCols, ""));
30
31 // Start DFS traversal from root at position (0, middle column)
32 int startRow = 0;
33 int startCol = (numCols - 1) / 2;
34 dfs(root, result, treeHeight, startRow, startCol);
35
36 return result;
37 }
38
39private:
40 /**
41 * Performs depth-first traversal to place nodes in the matrix
42 * @param node: Current node being processed
43 * @param result: Reference to the result matrix
44 * @param treeHeight: Total height of the tree
45 * @param row: Current row position in the matrix
46 * @param col: Current column position in the matrix
47 */
48 void dfs(TreeNode* node, vector<vector<string>>& result, int treeHeight, int row, int col) {
49 // Base case: null node
50 if (!node) {
51 return;
52 }
53
54 // Place current node value at position (row, col)
55 result[row][col] = to_string(node->val);
56
57 // Calculate offset for child nodes based on current level
58 // Offset = 2^(treeHeight - row - 1)
59 int childOffset = pow(2, treeHeight - row - 1);
60
61 // Recursively process left child (column - offset)
62 dfs(node->left, result, treeHeight, row + 1, col - childOffset);
63
64 // Recursively process right child (column + offset)
65 dfs(node->right, result, treeHeight, row + 1, col + childOffset);
66 }
67
68 /**
69 * Calculates the height of the binary tree
70 * @param node: Root node of the tree/subtree
71 * @return: Height of the tree (0-indexed), returns -1 for null node
72 */
73 int height(TreeNode* node) {
74 // Base case: null node has height -1
75 if (!node) {
76 return -1;
77 }
78
79 // Height is 1 + maximum height of left and right subtrees
80 return 1 + max(height(node->left), height(node->right));
81 }
82};
83
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 * Calculates the height of a binary tree
17 * @param node - Current tree node
18 * @param currentHeight - Current height level
19 * @returns The maximum height of the tree
20 */
21function getTreeHeight(node: TreeNode | null, currentHeight: number): number {
22 // Base case: if node is null, return the previous height
23 if (node === null) {
24 return currentHeight - 1;
25 }
26
27 // Recursively find the maximum height between left and right subtrees
28 return Math.max(
29 getTreeHeight(node.left, currentHeight + 1),
30 getTreeHeight(node.right, currentHeight + 1)
31 );
32}
33
34/**
35 * Performs depth-first search to fill the result matrix with tree values
36 * @param node - Current tree node
37 * @param row - Current row index in the result matrix
38 * @param col - Current column index in the result matrix
39 * @param result - The result matrix to fill
40 * @param treeHeight - The height of the tree
41 */
42function fillMatrix(
43 node: TreeNode | null,
44 row: number,
45 col: number,
46 result: string[][],
47 treeHeight: number
48): void {
49 // Base case: if node is null, do nothing
50 if (node === null) {
51 return;
52 }
53
54 // Place current node's value in the matrix
55 result[row][col] = String(node.val);
56
57 // Calculate the offset for child nodes based on current row
58 const childOffset: number = Math.pow(2, treeHeight - row - 1);
59
60 // Recursively fill left subtree (column - offset)
61 fillMatrix(node.left, row + 1, col - childOffset, result, treeHeight);
62
63 // Recursively fill right subtree (column + offset)
64 fillMatrix(node.right, row + 1, col + childOffset, result, treeHeight);
65}
66
67/**
68 * Prints a binary tree in a 2D string array format
69 * @param root - The root node of the binary tree
70 * @returns A 2D string array representation of the tree
71 */
72function printTree(root: TreeNode | null): string[][] {
73 // Calculate the height of the tree
74 const treeHeight: number = getTreeHeight(root, 0);
75
76 // Calculate matrix dimensions
77 const rows: number = treeHeight + 1;
78 const cols: number = Math.pow(2, treeHeight + 1) - 1;
79
80 // Initialize result matrix with empty strings
81 const result: string[][] = Array.from(
82 { length: rows },
83 () => new Array<string>(cols).fill('')
84 );
85
86 // Start DFS from root at the middle column of the first row
87 const middleCol: number = Math.floor((cols - 1) / 2);
88 fillMatrix(root, 0, middleCol, result, treeHeight);
89
90 return result;
91}
92
Time and Space Complexity
Time Complexity: O(n * 2^h)
where n
is the number of nodes in the tree and h
is the height of the tree.
- The
height
function visits every node once to calculate the height, takingO(n)
time. - The
dfs
function visits each node in the tree exactly once, performingO(1)
work at each node (assigning a value to the matrix), takingO(n)
time. - Creating the answer matrix with dimensions
(h+1) × (2^(h+1) - 1)
and initializing all cells with empty strings takesO(h * 2^h)
time. - Since in the worst case (a complete binary tree),
n = 2^(h+1) - 1
, the matrix initialization dominates, giving usO(n * 2^h)
time complexity. - In a balanced tree,
h = O(log n)
, so this becomesO(n^2)
. - In a skewed tree,
h = O(n)
, so this becomesO(n * 2^n)
.
Space Complexity: O(h * 2^h)
or equivalently O(m * n)
where m
and n
are the dimensions of the output matrix.
- The output matrix
ans
has dimensions(h+1) × (2^(h+1) - 1)
, requiringO(h * 2^h)
space. - The recursion stack depth for both
height
anddfs
functions is at mostO(h)
. - Since the output matrix space dominates, the overall space complexity is
O(h * 2^h)
. - In terms of the number of nodes, for a balanced tree where
h = O(log n)
, this becomesO(n log n)
. - For a skewed tree where
h = O(n)
, this becomesO(n * 2^n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Height Calculation
A common mistake is defining the height function incorrectly, particularly the base case. Some implementations might return 0
for a None
node, which would make a single-node tree have height 1 instead of 0.
Incorrect:
def get_tree_height(node):
if node is None:
return 0 # Wrong! This makes a leaf node have height 1
return 1 + max(get_tree_height(node.left), get_tree_height(node.right))
Correct:
def get_tree_height(node):
if node is None:
return -1 # Correct! This makes a leaf node have height 0
return 1 + max(get_tree_height(node.left), get_tree_height(node.right))
2. Off-by-One Error in Offset Calculation
The offset formula 2^(height - row - 1)
is crucial. A common error is forgetting the -1
or miscalculating the exponent, leading to incorrect child placement.
Incorrect:
offset = 2 ** (tree_height - row) # Missing the -1 # or offset = 2 ** (row) # Completely wrong formula
Correct:
offset = 2 ** (tree_height - row - 1)
3. Integer Division vs Floor Division
When calculating the center column for the root, using regular division instead of floor division can cause issues in Python 3.
Incorrect:
center_col = (num_cols - 1) / 2 # Returns float in Python 3
result[0][center_col] = str(root.val) # TypeError: list indices must be integers
Correct:
center_col = (num_cols - 1) // 2 # Integer division
4. Matrix Initialization with Reference Issues
Creating the matrix incorrectly can lead to all rows referencing the same list object.
Incorrect:
result = [[""] * num_cols] * num_rows # All rows reference the same list! # Modifying result[0][0] would affect all rows
Correct:
result = [[""] * num_cols for _ in range(num_rows)] # Each row is a separate list
5. Forgetting to Convert Node Values to Strings
The problem requires string values in the matrix, but it's easy to forget the conversion.
Incorrect:
result[row][col] = node.val # Mixing integers and strings in the result
Correct:
result[row][col] = str(node.val)
6. Not Handling Edge Cases
Failing to consider special cases like empty trees or single nodes.
Solution: Always test with:
- Empty tree (though most problems guarantee at least one node)
- Single node tree (height = 0)
- Skewed trees (all nodes on one side)
- Complete binary trees
What are the most two important steps in writing a depth first search function? (Select 2)
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!