Facebook Pixel

606. Construct String from Binary Tree

Problem Description

This problem asks you to convert a binary tree into a string representation using a specific format based on preorder traversal.

The conversion rules are:

  1. Each node is represented by its integer value

  2. Children are enclosed in parentheses:

    • If a node has a left child, put the left child's representation in parentheses right after the node's value
    • If a node has a right child, put the right child's representation in parentheses after the left child's parentheses
  3. Empty parentheses () should be omitted, except for one case:

    • When a node has only a right child (no left child), you must include empty parentheses () to represent the missing left child
    • This ensures the string can be uniquely mapped back to the original tree structure

Examples of formatting:

  • A leaf node 5 becomes just "5"
  • A node 1 with only left child 2 becomes "1(2)"
  • A node 1 with only right child 3 becomes "1()(3)" (note the empty parentheses for missing left child)
  • A node 1 with left child 2 and right child 3 becomes "1(2)(3)"

The solution uses a recursive depth-first search (DFS) approach that handles four cases:

  1. If the node is None, return an empty string
  2. If the node is a leaf (no children), return just its value as a string
  3. If the node has only a left child, return value(left_subtree)
  4. If the node has a right child (with or without left child), return value(left_subtree)(right_subtree)

The recursive function builds the string representation by processing each node and its subtrees according to these rules, naturally following a preorder traversal pattern where each node is processed before its children.

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 this is a binary tree problem, a tree is a special type of graph (specifically, a connected acyclic graph). Binary trees consist of nodes connected by edges (parent-child relationships).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree. We have a root node, and each node can have at most two children (left and right).

DFS

  • The flowchart leads us directly to DFS (Depth-First Search) for tree problems.

Conclusion: The flowchart suggests using a DFS approach for this binary tree problem.

This makes perfect sense for our problem because:

  1. We need to traverse the entire tree to build the string representation
  2. The problem requires a preorder traversal (process node before its children), which is naturally implemented using DFS
  3. We need to recursively process each subtree to build their string representations, which aligns perfectly with the recursive nature of DFS
  4. The solution needs to handle each node and its children in a specific order (parent → left child → right child), which DFS handles elegantly through recursion

The DFS pattern allows us to:

  • Visit each node exactly once
  • Process the current node's value first
  • Recursively build the string for left and right subtrees
  • Combine the results according to the parentheses rules specified in the problem
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to convert a tree structure into a string representation, we naturally think about visiting each node and recording its value. The key insight is recognizing that this is fundamentally a traversal problem where we need to preserve the tree's structure in the output string.

Since we want to represent the tree using parentheses to show parent-child relationships, we need to process each node and then its children. This immediately suggests a recursive approach - we can break down the problem into smaller subproblems where each subtree is converted to its string representation independently.

The challenge lies in handling the parentheses correctly. Let's think through the different cases:

  1. A leaf node has no children, so it shouldn't have any parentheses after it - just the node value itself.

  2. A node with only a left child needs parentheses around the left child's representation, but we can omit the empty right parentheses since there's no ambiguity.

  3. A node with only a right child is the tricky case. If we just write node(right), it would be interpreted as a left child! We need node()(right) to clearly indicate the empty left position.

  4. A node with both children needs both sets of parentheses: node(left)(right).

This pattern recognition leads us to a DFS solution where at each node, we:

  • First process the node's value (preorder)
  • Then recursively get the string representation of the left subtree
  • Then recursively get the string representation of the right subtree
  • Combine them based on which children exist

The beauty of this recursive DFS approach is that it naturally handles the nested structure - as we return from deeper recursive calls, the inner parentheses are already properly formed, and we just need to wrap them appropriately at each level.

The empty parentheses rule for the missing left child when there's a right child is crucial for maintaining a one-to-one mapping. Without this rule, 1(2) could mean either "1 with left child 2" or "1 with right child 2", creating ambiguity. The rule ensures each string maps to exactly one tree structure.

Learn more about Tree, Depth-First Search and Binary Tree patterns.

Solution Approach

The solution implements a recursive DFS function that handles the string construction based on the four cases we identified:

Implementation Details:

The main function tree2str defines an inner recursive function dfs that processes each node:

def dfs(root):
    if root is None:
        return ''

First, we handle the base case where the node is None, returning an empty string.

if root.left is None and root.right is None:
    return str(root.val)

For leaf nodes (no left or right child), we simply return the node's value as a string without any parentheses.

if root.right is None:
    return f'{root.val}({dfs(root.left)})'

When a node has only a left child (right child is None), we format the string as value(left_subtree). The recursive call dfs(root.left) builds the complete string representation of the left subtree, which we wrap in parentheses.

return f'{root.val}({dfs(root.left)})({dfs(root.right)})'

For all other cases (when the node has a right child, with or without a left child), we include both sets of parentheses. If the left child is None, dfs(root.left) returns an empty string, giving us the required empty parentheses () for the missing left child.

Algorithm Flow:

  1. Start with the root node
  2. Check if it's None → return empty string
  3. Check if it's a leaf → return just the value
  4. Check if it has only left child → return value(left_subtree)
  5. Otherwise → return value(left_subtree)(right_subtree)

The recursive calls naturally handle the nested structure. For example, with a tree like:

    1
   / \
  2   3
 /
4

The execution would be:

  • Process node 1: needs both children representations
  • Recursively process node 2: has only left child
  • Recursively process node 4: leaf node, returns "4"
  • Node 2 returns "2(4)"
  • Recursively process node 3: leaf node, returns "3"
  • Node 1 returns "1(2(4))(3)"

Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once.

Space Complexity: O(h) where h is the height of the tree, for the recursion call stack. In the worst case (skewed tree), this could be O(n).

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 concrete example to see how the algorithm works:

Given tree:

      1
     / \
    2   3
     \
      4

We'll trace through the recursive calls step by step:

Step 1: Start with dfs(1)

  • Node 1 has both left child (2) and right child (3)
  • Falls into the last case: return f'{1}({dfs(2)})({dfs(3)})'
  • Need to compute dfs(2) and dfs(3)

Step 2: Compute dfs(2) (left subtree of 1)

  • Node 2 has no left child but has right child (4)
  • Falls into the last case: return f'{2}({dfs(None)})({dfs(4)})'
  • Need to compute dfs(None) and dfs(4)

Step 3: Compute dfs(None) (left child of 2)

  • Returns '' (empty string)

Step 4: Compute dfs(4) (right child of 2)

  • Node 4 has no children (leaf node)
  • Falls into the second case: return '4'

Step 5: Back to Step 2, complete dfs(2)

  • dfs(None) returned ''
  • dfs(4) returned '4'
  • Returns f'{2}({''})({4})' = '2()(4)'

Step 6: Compute dfs(3) (right subtree of 1)

  • Node 3 has no children (leaf node)
  • Falls into the second case: return '3'

Step 7: Back to Step 1, complete dfs(1)

  • dfs(2) returned '2()(4)'
  • dfs(3) returned '3'
  • Returns f'{1}({2()(4)})({3})' = '1(2()(4))(3)'

Final Result: "1(2()(4))(3)"

Notice how:

  • Node 2 has empty parentheses () for its missing left child because it has a right child
  • Node 3 is a leaf, so it appears without any parentheses in its own representation
  • Node 4 is also a leaf, appearing without parentheses
  • The parentheses properly nest to show the tree structure

Solution Implementation

1from typing import Optional
2
3# Definition for a binary tree node.
4class TreeNode:
5    def __init__(self, val: int = 0, left: Optional['TreeNode'] = None, right: Optional['TreeNode'] = None):
6        self.val = val
7        self.left = left
8        self.right = right
9
10class Solution:
11    def tree2str(self, root: Optional[TreeNode]) -> str:
12        """
13        Constructs a string representation of a binary tree using preorder traversal.
14        Empty parentheses pairs "()" are omitted unless needed to maintain the 
15        one-to-one mapping relationship between the string and the original binary tree.
16      
17        Args:
18            root: The root node of the binary tree
19          
20        Returns:
21            String representation of the binary tree with parentheses
22        """
23      
24        def preorder_to_string(node: Optional[TreeNode]) -> str:
25            """
26            Helper function to recursively build the string representation.
27          
28            Args:
29                node: Current node being processed
30              
31            Returns:
32                String representation of the subtree rooted at this node
33            """
34            # Base case: empty node
35            if node is None:
36                return ''
37          
38            # Leaf node: just return the value as string
39            if node.left is None and node.right is None:
40                return str(node.val)
41          
42            # Node with only left child: include left child in parentheses
43            if node.right is None:
44                return f'{node.val}({preorder_to_string(node.left)})'
45          
46            # Node with both children or only right child: 
47            # include both children (left can be empty)
48            return f'{node.val}({preorder_to_string(node.left)})({preorder_to_string(node.right)})'
49      
50        return preorder_to_string(root)
51
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     * Converts a binary tree to a string representation with parentheses.
19     * The string follows preorder traversal where each node's value is followed
20     * by its left subtree in parentheses, then its right subtree in parentheses.
21     * Empty parentheses are omitted when possible.
22     * 
23     * @param root The root node of the binary tree
24     * @return String representation of the tree
25     */
26    public String tree2str(TreeNode root) {
27        // Base case: empty tree returns empty string
28        if (root == null) {
29            return "";
30        }
31      
32        // Leaf node: return just the node value without parentheses
33        if (root.left == null && root.right == null) {
34            return String.valueOf(root.val);
35        }
36      
37        // Node has only left child: include left subtree in parentheses
38        // Right empty parentheses can be omitted
39        if (root.right == null) {
40            return root.val + "(" + tree2str(root.left) + ")";
41        }
42      
43        // Node has both children or only right child: 
44        // Must include both left and right subtrees in parentheses
45        // (Empty left subtree parentheses cannot be omitted if right exists)
46        return root.val + "(" + tree2str(root.left) + ")(" + tree2str(root.right) + ")";
47    }
48}
49
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     * Converts a binary tree to a string representation with parentheses.
16     * The string follows preorder traversal where each node's value is followed
17     * by its left subtree in parentheses, then right subtree in parentheses.
18     * Empty nodes are omitted, except when needed to distinguish structure.
19     * 
20     * @param root The root node of the binary tree
21     * @return String representation of the tree
22     */
23    string tree2str(TreeNode* root) {
24        // Base case: empty tree returns empty string
25        if (root == nullptr) {
26            return "";
27        }
28      
29        // Leaf node: return just the value without parentheses
30        if (root->left == nullptr && root->right == nullptr) {
31            return to_string(root->val);
32        }
33      
34        // Node with only left child: include left subtree in parentheses
35        // Right subtree parentheses can be omitted when right child is null
36        if (root->right == nullptr) {
37            return to_string(root->val) + "(" + tree2str(root->left) + ")";
38        }
39      
40        // Node with both children or only right child:
41        // Must include both left and right subtrees in parentheses
42        // Even if left is null, we need empty parentheses to maintain structure
43        return to_string(root->val) + 
44               "(" + tree2str(root->left) + ")" + 
45               "(" + tree2str(root->right) + ")";
46    }
47};
48
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 * Converts a binary tree to a string representation with parentheses.
17 * The string representation follows these rules:
18 * - Root value is always present
19 * - Left subtree is wrapped in parentheses (even if empty, unless right subtree is also empty)
20 * - Right subtree is wrapped in parentheses (only if present)
21 * 
22 * @param root - The root node of the binary tree
23 * @returns String representation of the binary tree
24 */
25function tree2str(root: TreeNode | null): string {
26    // Base case: empty tree returns empty string
27    if (root === null) {
28        return '';
29    }
30  
31    // Leaf node: return just the value without parentheses
32    if (root.left === null && root.right === null) {
33        return String(root.val);
34    }
35  
36    // Build the string representation
37    let result: string = String(root.val);
38  
39    // Add left subtree (always include parentheses if not a leaf)
40    const leftSubtree: string = root.left ? tree2str(root.left) : '';
41    result += `(${leftSubtree})`;
42  
43    // Add right subtree only if it exists (with parentheses)
44    if (root.right !== null) {
45        const rightSubtree: string = tree2str(root.right);
46        result += `(${rightSubtree})`;
47    }
48  
49    return result;
50}
51

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node in the tree is visited exactly once. At each node, the operations performed are:

  • Checking if the node is None: O(1)
  • Checking if both children are None: O(1)
  • Checking if the right child is None: O(1)
  • String concatenation and formatting: O(1) for each node's value

Since we visit all n nodes exactly once and perform constant time operations at each node, the overall time complexity is O(n), where n is the number of nodes in the binary tree.

Space Complexity: O(n)

The space complexity consists of two components:

  1. Recursion Call Stack: In the worst case (a skewed tree), the recursion depth can be O(n). In the best case (a balanced tree), the recursion depth would be O(log n). Therefore, the call stack space is O(h), where h is the height of the tree.

  2. String Construction: The total space needed to store the output string is O(n) because we need to include all node values in the final string representation. Additionally, due to string immutability in Python, intermediate string concatenations during the recursive calls may create temporary strings, contributing to the space complexity.

The dominant factor is O(n) for the output string storage, making the overall space complexity O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly Handling the Case of Only Right Child

The Pitfall: A common mistake is to treat a node with only a right child the same way as a node with only a left child, which would result in:

# INCORRECT approach
if node.left is None and node.right is not None:
    return f'{node.val}({preorder_to_string(node.right)})'

This would produce "1(3)" for a node with value 1 and only right child 3, which is ambiguous - it looks identical to a node with only a left child! The string cannot be uniquely mapped back to the original tree structure.

Why This Happens: Developers often think symmetrically about left and right children, forgetting that the parentheses position matters for maintaining the one-to-one mapping between string and tree.

The Solution: Always include empty parentheses () when there's no left child but there is a right child:

# CORRECT approach
if node.right is not None:  # Has right child (with or without left)
    return f'{node.val}({preorder_to_string(node.left)})({preorder_to_string(node.right)})'

When node.left is None, preorder_to_string(node.left) returns an empty string, giving us "1()(3)".

2. Unnecessary Empty Parentheses for Leaf Nodes

The Pitfall: Another mistake is adding empty parentheses for nodes that have no children at all:

# INCORRECT - adds unnecessary parentheses
def preorder_to_string(node):
    if node is None:
        return ''
    left_str = f'({preorder_to_string(node.left)})'
    right_str = f'({preorder_to_string(node.right)})'
    return f'{node.val}{left_str}{right_str}'

This would produce "1()()" for a leaf node with value 1, which violates the requirement to omit unnecessary empty parentheses.

The Solution: Explicitly check if a node is a leaf before adding any parentheses:

# CORRECT - handles leaf nodes separately
if node.left is None and node.right is None:
    return str(node.val)  # No parentheses for leaf nodes

3. Edge Case: Empty Tree

The Pitfall: Not handling the case where the root itself is None. While the recursive function handles None nodes correctly, the main function should ensure it works for an empty tree:

# Potential issue if not handled
def tree2str(root):
    # If root is None and we don't handle it...
    return preorder_to_string(root)  # Returns empty string, which might not be expected

The Solution: The current implementation correctly handles this by returning an empty string for None nodes, but it's important to verify this behavior matches the problem requirements. Some variations might require returning "None" or raising an exception for empty trees.

4. String Concatenation Performance

The Pitfall: While not incorrect, using string concatenation in recursive calls can be inefficient for very large trees:

# Less efficient for large trees
return str(node.val) + '(' + preorder_to_string(node.left) + ')' + '(' + preorder_to_string(node.right) + ')'

The Solution: Use f-strings (as shown in the correct solution) or join operations for better performance:

# More efficient
return f'{node.val}({preorder_to_string(node.left)})({preorder_to_string(node.right)})'

These pitfalls highlight the importance of carefully considering the edge cases and the specific requirements for maintaining the unique string-to-tree mapping.

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

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More