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:
- We push each element onto a stack
- 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 - We replace this complete subtree (node with two null children) with a single
'#'
to represent that entire subtree as processed - 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.
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 valuenode
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:
-
Split the input string: First, we split the
preorder
string by commas to get an array of individual nodes and null markers. -
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.
-
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. -
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.
- Remove the last 3 elements from the stack (
-
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 EvaluatorExample 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:
-
Initial state: Split the string into
['9', '3', '#', '#', '2', '#', '#']
Stack:[]
-
Process '9': Push to stack Stack:
['9']
- Check pattern: Need 3 elements, only have 1 → no reduction
-
Process '3': Push to stack Stack:
['9', '3']
- Check pattern: Need 3 elements, only have 2 → no reduction
-
Process first '#': Push to stack Stack:
['9', '3', '#']
- Check pattern: Have 3 elements, but last two aren't both '#' → no reduction
-
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', '#']
-
Process '2': Push to stack Stack:
['9', '#', '2']
- Check pattern: Last element '2' is not '#' → no reduction
-
Process third '#': Push to stack Stack:
['9', '#', '2', '#']
- Check pattern: Last two aren't both '#' → no reduction
-
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:['#']
-
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 takesO(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 mostO(n)
elements (in the worst case, single character nodes separated by commas) - The stack
stk
can grow to at mostO(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 byO(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.
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
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!