Facebook Pixel

331. Verify Preorder Serialization of a Binary Tree

Problem Description

You are given a string that represents a preorder traversal serialization of a binary tree. In this serialization:

  • Each non-null node is represented by its integer value
  • Each null node is represented by the character '#'
  • Values are separated by commas

For example, a binary tree with root 9, left child 3, and right child 2 could be serialized as "9,3,4,#,#,1,#,#,2,#,6,#,#" where each '#' marks where a null child would be.

Your task is to determine if the given string is a valid preorder serialization of some binary tree, without actually reconstructing the tree.

The solution uses a stack-based approach. As we process each element from the serialized string:

  1. We push each element onto a stack
  2. Whenever we see a pattern where the top two elements are '#' (null nodes) and the third element from the top is not '#' (a non-null node), this represents a complete subtree
  3. We replace this complete subtree (node with two null children) with a single '#' to represent that entire subtree as processed
  4. We repeat this process until no more reductions can be made

After processing all elements, a valid serialization should reduce to exactly one '#' in the stack, representing the entire tree has been properly formed and collapsed.

The key insight is that in a valid preorder traversal, every non-null node must eventually have exactly two children (which could be null), and the pattern of a node followed by two nulls indicates a leaf node that can be collapsed.

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

Intuition

The key observation is that in a preorder traversal, we visit nodes in the order: root → left subtree → right subtree. When we see a non-null node followed by two '#' symbols, this represents a leaf node (a node with no children). This complete subtree can be thought of as "consumed" or "collapsed" into a single unit.

Think of it like building blocks. When we have a complete subtree (a parent with two null children), we can replace this entire structure with a single '#' to indicate "there was a valid subtree here, and it's now complete."

The pattern recognition works like this:

  • [node, '#', '#'] represents a leaf node with value node and two null children
  • Once we identify this pattern, the entire subtree rooted at node is complete
  • We can replace it with a single '#' to mark this position as a completed subtree

By repeatedly applying this reduction, we're essentially validating the tree from bottom to top. Each reduction confirms that a portion of the tree is structurally valid. If the serialization is correct, all these reductions should eventually collapse the entire tree into a single '#'.

Why does this work? In a valid binary tree:

  • Every non-null node must have exactly 2 children positions (filled with either nodes or nulls)
  • The number of nulls should be exactly one more than the number of non-null nodes
  • The preorder traversal should "consume" all nodes and nulls in a way that forms a complete tree

The stack naturally handles the recursive nature of tree validation - we process nodes in the order they appear, and whenever we can form a complete subtree, we immediately reduce it. If at any point we can't make progress, or if we end with something other than a single '#', the serialization must be invalid.

Solution Approach

The implementation uses a stack to process the serialized string and perform the reduction operations:

  1. Split the input string: First, we split the preorder string by commas to get an array of individual nodes and null markers.

  2. Process each element with a stack: We iterate through each element and push it onto the stack. The stack helps us keep track of the current state of our validation process.

  3. Pattern matching and reduction: After adding each element, we check if the top of the stack matches our reduction pattern:

    • Check if the stack has at least 3 elements
    • Check if the last two elements are both '#' (representing null children)
    • Check if the third-from-last element is NOT '#' (representing a non-null parent node)

    When this pattern [non-null, '#', '#'] is found, it represents a complete leaf node that can be collapsed.

  4. Perform the reduction: When we find the pattern, we:

    • Remove the last 3 elements from the stack (stk = stk[:-3])
    • Push a single '#' back onto the stack to represent the entire collapsed subtree

    This reduction continues in a while loop because collapsing one subtree might create a new pattern that can also be collapsed.

  5. Final validation: After processing all elements, a valid serialization should result in exactly one element in the stack, and that element should be '#'. This single '#' represents the entire tree collapsed into its simplest form.

The algorithm's correctness relies on the property that in a valid preorder serialization:

  • Every complete subtree can be reduced to a single '#'
  • The reduction process will eventually collapse the entire tree if and only if the serialization is valid
  • Any invalid structure will either leave multiple elements in the stack or won't reduce to a single '#'

Time complexity: O(n) where n is the number of nodes, as each element is pushed and popped at most once. Space complexity: O(n) for the stack in the worst case.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a simple example with the serialization "9,3,#,#,2,#,#".

This represents a tree where:

  • Root node is 9
  • Left child is 3 (which has two null children)
  • Right child is 2 (which has two null children)

Step-by-step process:

  1. Initial state: Split the string into ['9', '3', '#', '#', '2', '#', '#'] Stack: []

  2. Process '9': Push to stack Stack: ['9']

    • Check pattern: Need 3 elements, only have 1 → no reduction
  3. Process '3': Push to stack Stack: ['9', '3']

    • Check pattern: Need 3 elements, only have 2 → no reduction
  4. Process first '#': Push to stack Stack: ['9', '3', '#']

    • Check pattern: Have 3 elements, but last two aren't both '#' → no reduction
  5. Process second '#': Push to stack Stack: ['9', '3', '#', '#']

    • Check pattern: Last two are '#', third from last '3' is not '#' → MATCH!
    • Reduction: Remove ['3', '#', '#'] and replace with single '#' Stack: ['9', '#']
  6. Process '2': Push to stack Stack: ['9', '#', '2']

    • Check pattern: Last element '2' is not '#' → no reduction
  7. Process third '#': Push to stack Stack: ['9', '#', '2', '#']

    • Check pattern: Last two aren't both '#' → no reduction
  8. Process fourth '#': Push to stack Stack: ['9', '#', '2', '#', '#']

    • Check pattern: Last two are '#', third from last '2' is not '#' → MATCH!
    • Reduction: Remove ['2', '#', '#'] and replace with single '#' Stack: ['9', '#', '#']
    • Check pattern again: Last two are '#', third from last '9' is not '#' → MATCH!
    • Reduction: Remove ['9', '#', '#'] and replace with single '#' Stack: ['#']
  9. Final validation: Stack has exactly one element and it's '#' → Valid serialization!

The key insight is that each reduction represents collapsing a complete subtree. Node 3 with its two null children gets reduced first, then node 2 with its null children, and finally the root node 9 with its two now-collapsed subtrees (represented as '#') gets reduced to a single '#'.

Solution Implementation

1class Solution:
2    def isValidSerialization(self, preorder: str) -> bool:
3        """
4        Verify if a given string is a valid serialization of a binary tree.
5        Uses a stack-based approach to validate the preorder traversal.
6      
7        Args:
8            preorder: A comma-separated string representing preorder traversal
9      
10        Returns:
11            True if valid serialization, False otherwise
12        """
13        # Initialize stack to process nodes
14        stack = []
15      
16        # Split the preorder string by comma and process each node
17        for node in preorder.split(","):
18            # Push current node onto stack
19            stack.append(node)
20          
21            # Collapse pattern: when we see "non-null, #, #" pattern,
22            # it represents a complete subtree that can be replaced with "#"
23            while (len(stack) >= 3 and 
24                   stack[-1] == "#" and 
25                   stack[-2] == "#" and 
26                   stack[-3] != "#"):
27                # Remove the last 3 elements (parent and two null children)
28                stack.pop()
29                stack.pop()
30                stack.pop()
31                # Replace the collapsed subtree with a single "#"
32                stack.append("#")
33      
34        # Valid serialization should result in exactly one "#" in the stack
35        return len(stack) == 1 and stack[0] == "#"
36
1class Solution {
2    public boolean isValidSerialization(String preorder) {
3        // Use a list as a stack to validate the serialization
4        List<String> stack = new ArrayList<>();
5      
6        // Split the preorder string by comma and process each node
7        for (String node : preorder.split(",")) {
8            // Add current node to the stack
9            stack.add(node);
10          
11            // Keep reducing valid subtrees to a single "#"
12            // Pattern: non-null node followed by two "#" represents a complete subtree
13            while (stack.size() >= 3 && 
14                   stack.get(stack.size() - 1).equals("#") &&     // Right child is null
15                   stack.get(stack.size() - 2).equals("#") &&     // Left child is null  
16                   !stack.get(stack.size() - 3).equals("#")) {    // Parent is not null
17              
18                // Remove the complete subtree (parent and two null children)
19                stack.remove(stack.size() - 1);  // Remove right null child
20                stack.remove(stack.size() - 1);  // Remove left null child
21                stack.remove(stack.size() - 1);  // Remove parent node
22              
23                // Replace the subtree with a single "#" (null marker)
24                stack.add("#");
25            }
26        }
27      
28        // Valid serialization should reduce to exactly one "#"
29        return stack.size() == 1 && stack.get(0).equals("#");
30    }
31}
32
1class Solution {
2public:
3    bool isValidSerialization(string preorder) {
4        // Use a stack to simulate the tree construction process
5        vector<string> nodeStack;
6      
7        // Parse the preorder string by comma delimiter
8        stringstream ss(preorder);
9        string node;
10      
11        while (getline(ss, node, ',')) {
12            // Push current node onto the stack
13            nodeStack.push_back(node);
14          
15            // Key observation: In preorder traversal, a non-null node followed by 
16            // two null children forms a complete subtree that can be replaced by a single null
17            // Pattern: [non-null, #, #] -> [#]
18            // This reduces the tree bottom-up until we have a single # representing a valid tree
19            while (nodeStack.size() >= 3 && 
20                   nodeStack[nodeStack.size() - 1] == "#" &&  // Right child is null
21                   nodeStack[nodeStack.size() - 2] == "#" &&  // Left child is null  
22                   nodeStack[nodeStack.size() - 3] != "#") {  // Parent is non-null
23              
24                // Remove the parent and two null children
25                nodeStack.pop_back();  // Remove right null child
26                nodeStack.pop_back();  // Remove left null child  
27                nodeStack.pop_back();  // Remove parent node
28              
29                // Replace the subtree with a single null node
30                nodeStack.push_back("#");
31            }
32        }
33      
34        // A valid serialization should reduce to exactly one null node
35        return nodeStack.size() == 1 && nodeStack[0] == "#";
36    }
37};
38
1/**
2 * Verifies if a given string represents a valid preorder traversal serialization of a binary tree.
3 * Uses '#' to represent null nodes.
4 * 
5 * @param preorder - Comma-separated string representing the preorder traversal
6 * @returns true if the serialization is valid, false otherwise
7 */
8function isValidSerialization(preorder: string): boolean {
9    // Stack to track nodes during validation
10    const nodeStack: string[] = [];
11  
12    // Split the preorder string by comma and process each node
13    const nodes: string[] = preorder.split(',');
14  
15    for (const node of nodes) {
16        // Push current node onto the stack
17        nodeStack.push(node);
18      
19        // Collapse pattern: when we have "number, #, #" at the top of stack,
20        // it represents a complete subtree that can be replaced with a single '#'
21        while (
22            nodeStack.length >= 3 && 
23            nodeStack.at(-1) === '#' &&  // Right child is null
24            nodeStack.at(-2) === '#' &&  // Left child is null
25            nodeStack.at(-3) !== '#'      // Parent is a valid node (not null)
26        ) {
27            // Remove the parent and two null children, replace with single null
28            nodeStack.splice(-3, 3, '#');
29        }
30    }
31  
32    // Valid serialization should result in exactly one '#' representing the entire tree
33    return nodeStack.length === 1 && nodeStack[0] === '#';
34}
35

Time and Space Complexity

The time complexity is O(n) where n is the length of the string preorder. This is because:

  • The split(",") operation takes O(n) time to traverse the entire string
  • The main loop iterates through each element in the split result, which is at most O(n) elements
  • Inside the loop, each element is pushed to the stack once
  • The while loop can pop elements, but each element can only be popped once throughout the entire execution
  • Therefore, the total number of push and pop operations is bounded by O(n)

The space complexity is O(n) where n is the length of the string preorder. This is because:

  • The split(",") operation creates a list that can contain at most O(n) elements (in the worst case, single character nodes separated by commas)
  • The stack stk can grow to at most O(n) elements in the worst case before any reduction occurs
  • Even though the stack size changes during execution due to the collapsing operation (replacing two "#" and a non-"#" node with a single "#"), the maximum space used is still bounded by O(n)

Common Pitfalls

1. Incorrect Pattern Matching Order

A common mistake is checking the pattern elements in the wrong order or using incorrect indices. Some implementations might accidentally check stack[-3] == "#" instead of stack[-3] != "#", which would look for the wrong pattern and fail to properly validate the tree structure.

Wrong approach:

# Incorrect - looking for wrong pattern
while (len(stack) >= 3 and 
       stack[-1] == "#" and 
       stack[-2] == "#" and 
       stack[-3] == "#"):  # Wrong! Should be != "#"

Solution: Always verify that you're checking for a non-null parent node with two null children: the pattern should be [non-null, '#', '#'] at the top of the stack.

2. Not Handling Edge Cases Properly

The algorithm might fail on edge cases like:

  • Empty string ""
  • Single null node "#"
  • String with only commas ",,,"

Solution: These cases are naturally handled by the split and stack approach, but be aware that:

  • "#" is valid (represents an empty tree)
  • "" splits to [""] which won't reduce properly and returns False
  • Multiple consecutive commas create empty strings in the split array

3. Forgetting to Continue Collapsing After Each Reduction

A critical mistake is performing only one reduction after adding each element, rather than continuing to collapse while the pattern exists. This could leave valid trees unrecognized.

Wrong approach:

for node in preorder.split(","):
    stack.append(node)
    # Only checking once - wrong!
    if (len(stack) >= 3 and 
        stack[-1] == "#" and 
        stack[-2] == "#" and 
        stack[-3] != "#"):
        stack = stack[:-3]
        stack.append("#")

Solution: Use a while loop to keep collapsing as long as the pattern exists, since one reduction might expose another collapsible pattern underneath.

4. Inefficient Stack Manipulation

Using stack = stack[:-3] followed by stack.append("#") creates a new list each time, which could be inefficient for large inputs.

More efficient approach:

while (len(stack) >= 3 and 
       stack[-1] == "#" and 
       stack[-2] == "#" and 
       stack[-3] != "#"):
    stack.pop()  # Remove last element
    stack.pop()  # Remove second-to-last
    stack[-1] = "#"  # Replace third-to-last with "#"

This modifies the stack in-place rather than creating new list slices, improving performance for large trees.

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

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

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

Load More