LeetCode 331 - Verify Preorder Serialization of a Binary Tree
The problem gives us a string that represents the preorder serialization of a binary tree. Each value is separated by commas. A normal integer represents a real tree node, while the character '' represents a null pointer.
Difficulty: 🟡 Medium
Topics: String, Stack, Tree, Binary Tree
Solution
Problem Understanding
The problem gives us a string that represents the preorder serialization of a binary tree. Each value is separated by commas. A normal integer represents a real tree node, while the character '#' represents a null pointer.
In preorder traversal, we always visit nodes in this order:
- Current node
- Left subtree
- Right subtree
For example, the serialization:
9,3,4,#,#,1,#,#,2,#,6,#,#
means:
9is the root3is the left child of94is the left child of3#,#means4has no children1becomes the right child of3- and so on
The task is to determine whether the given serialization could represent a valid binary tree, without actually reconstructing the tree.
The constraints are important:
- The input length can be up to
10^4 - Values are already guaranteed to be syntactically valid
- We are explicitly told not to rebuild the tree
These constraints strongly suggest that we should process the string directly in a single pass.
Several edge cases are especially important:
- A single
"#"is a valid tree, representing an empty tree. "1,#"is invalid because the root node still needs one more child."9,#,#,1"is invalid because the tree is already complete before the extra node appears.- Extra nodes after a completed tree must immediately invalidate the serialization.
- Null-only structures must still obey preorder structure rules.
A naive implementation that tries to partially reconstruct trees can become unnecessarily complicated and waste memory.
Approaches
Brute Force Approach
A straightforward approach is to reconstruct the binary tree while reading the preorder traversal.
We could recursively process tokens:
- If the current token is
'#', returnNone - Otherwise create a node
- Recursively build the left subtree
- Recursively build the right subtree
After construction, the serialization is valid only if all tokens were consumed exactly once.
This works because preorder traversal uniquely determines the tree structure when null markers are included.
However, this approach violates the spirit of the problem because we are explicitly told not to reconstruct the tree. It also uses unnecessary memory for node allocation and recursion.
Optimal Approach, Slot Counting
The key observation is that every node in a binary tree occupies one incoming connection slot.
Initially, the root needs one slot.
Then:
- A non-null node consumes one slot and creates two new slots
- A null node consumes one slot and creates zero new slots
This leads to a very elegant validation rule.
We start with one available slot.
For every token:
- Consume one slot
- If slots become negative, the serialization is invalid
- If the token is not
'#', add two new slots
At the end:
- The serialization is valid only if all slots are exactly used up
- Therefore, the final slot count must be zero
This solution avoids tree construction entirely and processes the input in one pass.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Reconstructs the tree recursively |
| Optimal | O(n) | O(1) | Uses slot counting without building the tree |
Algorithm Walkthrough
Optimal Algorithm Using Slot Counting
- Split the input string by commas to obtain all preorder tokens.
Each token represents either a real node or a null node.
2. Initialize a variable called slots to 1.
A binary tree root always requires one available parent slot. 3. Iterate through each token in order.
Before processing a token, consume one slot because every node, including null nodes, must occupy a position in the tree.
4. If slots becomes negative at any point, return False.
This means we encountered more nodes than the tree structure can support.
5. If the current token is not '#', add two new slots.
A real node creates space for a left child and a right child.
6. Continue processing until all tokens are consumed.
7. After processing all tokens, check whether slots == 0.
If zero slots remain, the serialization used exactly the correct number of child positions. 8. Return the result of that check.
Why it works
The algorithm maintains an invariant:
slotsalways represents the number of child positions still available in the partially processed tree.
Every node must occupy exactly one slot. Real nodes then generate two additional child positions.
A valid preorder serialization must consume all available positions exactly. If slots become negative, the traversal attempted to place nodes where no parent position existed. If slots remain positive at the end, some expected children were never provided.
Because preorder traversal processes nodes in the same order the tree structure expands, slot accounting perfectly captures structural validity.
Python Solution
class Solution:
def isValidSerialization(self, preorder: str) -> bool:
slots = 1
for token in preorder.split(","):
slots -= 1
if slots < 0:
return False
if token != "#":
slots += 2
return slots == 0
The implementation directly follows the slot counting algorithm.
We begin with one available slot for the root node. Then we iterate through every token in the preorder serialization.
Each token consumes one slot because every node must occupy a position in the tree. If the slot count becomes negative, we immediately know the structure is invalid because there was no available parent position.
Whenever the token represents a real node rather than '#', we add two new slots for its children.
At the end, we verify that all slots were consumed exactly. A valid tree serialization cannot leave unused child positions.
This implementation is concise because the slot-counting observation completely eliminates the need for stacks, recursion, or tree construction.
Go Solution
package main
import "strings"
func isValidSerialization(preorder string) bool {
slots := 1
tokens := strings.Split(preorder, ",")
for _, token := range tokens {
slots--
if slots < 0 {
return false
}
if token != "#" {
slots += 2
}
}
return slots == 0
}
The Go implementation mirrors the Python solution closely.
The primary difference is that Go uses strings.Split to tokenize the input string. The slot logic remains identical.
Since the problem constraints are small and node values are limited, there are no integer overflow concerns. We also do not need additional data structures because the slot-counting method uses constant extra memory.
Worked Examples
Example 1
Input: "9,3,4,#,#,1,#,#,2,#,6,#,#"
Initial state:
slots = 1
| Token | Slots Before | Consume Slot | Add Slots | Slots After |
|---|---|---|---|---|
| 9 | 1 | 0 | +2 | 2 |
| 3 | 2 | 1 | +2 | 3 |
| 4 | 3 | 2 | +2 | 4 |
| # | 4 | 3 | +0 | 3 |
| # | 3 | 2 | +0 | 2 |
| 1 | 2 | 1 | +2 | 3 |
| # | 3 | 2 | +0 | 2 |
| # | 2 | 1 | +0 | 1 |
| 2 | 1 | 0 | +2 | 2 |
| # | 2 | 1 | +0 | 1 |
| 6 | 1 | 0 | +2 | 2 |
| # | 2 | 1 | +0 | 1 |
| # | 1 | 0 | +0 | 0 |
Final slots equal zero, so the serialization is valid.
Result:
true
Example 2
Input: "1,#"
Initial state:
slots = 1
| Token | Slots Before | Consume Slot | Add Slots | Slots After |
|---|---|---|---|---|
| 1 | 1 | 0 | +2 | 2 |
| # | 2 | 1 | +0 | 1 |
After processing all tokens, one slot remains unused.
This means the root node is still missing one child.
Result:
false
Example 3
Input: "9,#,#,1"
| Token | Slots Before | Consume Slot | Add Slots | Slots After |
|---|---|---|---|---|
| 9 | 1 | 0 | +2 | 2 |
| # | 2 | 1 | +0 | 1 |
| # | 1 | 0 | +0 | 0 |
| 1 | 0 | -1 | +2 | Invalid |
The tree becomes complete after the third token. The extra node 1 has nowhere to attach.
Result:
false
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each token is processed exactly once |
| Space | O(1) | Only a few integer variables are used |
The algorithm scans the serialization exactly once, making the runtime linear in the number of tokens.
The extra space usage is constant because we only maintain the slot counter. No recursion, stack, or tree structure is created.
Test Cases
solution = Solution()
assert solution.isValidSerialization("9,3,4,#,#,1,#,#,2,#,6,#,#") == True
# Valid complex tree
assert solution.isValidSerialization("1,#") == False
# Root missing one child
assert solution.isValidSerialization("9,#,#,1") == False
# Extra node after tree completion
assert solution.isValidSerialization("#") == True
# Empty tree
assert solution.isValidSerialization("1,#,#") == True
# Single valid node
assert solution.isValidSerialization("#,#") == False
# Extra null after empty tree
assert solution.isValidSerialization("1,2,#,#,#") == True
# Valid left-heavy structure
assert solution.isValidSerialization("1,2,#") == False
# Incomplete serialization
assert solution.isValidSerialization("1,#,#,#") == False
# Extra null node
assert solution.isValidSerialization("7,2,#,2,#,#,#") == True
# Valid asymmetric tree
assert solution.isValidSerialization("7,2,#,2,#,#") == False
# Missing final null
assert solution.isValidSerialization("9,#,92,#,#") == True
# Multi-digit values
Test Summary
| Test | Why |
|---|---|
"9,3,4,#,#,1,#,#,2,#,6,#,#" |
Standard valid example |
"1,#" |
Detects incomplete tree |
"9,#,#,1" |
Detects extra nodes after completion |
"#" |
Valid empty tree |
"1,#,#" |
Smallest valid non-empty tree |
"#,#" |
Invalid extra null node |
"1,2,#,#,#" |
Valid left subtree structure |
"1,2,#" |
Missing children |
"1,#,#,#" |
Tree completed too early |
"7,2,#,2,#,#,#" |
Valid asymmetric tree |
"7,2,#,2,#,#" |
Missing final child marker |
"9,#,92,#,#" |
Handles multi-digit values |
Edge Cases
One important edge case is the empty tree serialization:
"#"
This is valid because a null root represents an empty binary tree. A naive implementation might incorrectly reject it because there are no actual node values. The slot-counting approach handles this naturally. We start with one slot, the '#' consumes it, and we finish with exactly zero slots.
Another critical edge case occurs when the tree becomes complete before all tokens are processed:
"9,#,#,1"
After processing "9,#,#", the slot count becomes zero, meaning the entire tree is already finished. The extra "1" attempts to consume a nonexistent slot, causing the count to become negative. The algorithm immediately rejects the serialization.
A third tricky edge case is an incomplete tree:
"1,#"
The root node still requires one more child, but the input ends too early. The final slot count remains positive, indicating unresolved child positions. The implementation correctly returns False because a valid binary tree must consume every slot exactly.
Another subtle case involves consecutive null markers:
"#,#"
The first '#' already completes the entire tree. The second '#' attempts to consume an additional slot, making the slot count negative. This prevents invalid serializations from slipping through.