255. Verify Preorder Sequence in Binary Search Tree 🔒
Problem Description
You are given an array of unique integers called preorder
. Your task is to determine if this array represents a valid preorder traversal sequence of a binary search tree (BST).
In a preorder traversal, we visit nodes in this order:
- Visit the root node first
- Recursively visit the left subtree
- Recursively visit the right subtree
A binary search tree has the property that for any node:
- All values in its left subtree are less than the node's value
- All values in its right subtree are greater than the node's value
The solution uses a stack-based approach to validate the sequence. It maintains a stack and a variable last
that represents the lower bound for subsequent elements. As we iterate through the array:
- If we encounter a value less than
last
, it means we're trying to add a node that violates the BST property (going to the right subtree but finding a smaller value), so we returnFalse
- While the stack is not empty and the top element is less than the current value
x
, we pop from the stack and updatelast
. This simulates moving from a left subtree to a right subtree - We push the current element onto the stack
The algorithm effectively tracks the constraints imposed by the BST structure during a preorder traversal. The stack helps maintain the ancestors we haven't fully processed, and last
ensures we don't violate the BST property when moving to right subtrees.
For example, with preorder = [5, 2, 1, 3, 6]
, this represents a valid BST where 5 is the root, 2 is its left child (with 1 and 3 as its children), and 6 is the right child of 5. The function would return True
for this input.
Intuition
When we perform a preorder traversal on a BST, we get a specific pattern: we visit the root first, then all nodes in the left subtree (which are smaller than root), followed by all nodes in the right subtree (which are larger than root).
The key insight is that once we've moved to the right subtree of any node, we should never encounter a value smaller than that node again. This is because in preorder traversal, after visiting a node and its entire left subtree, when we move to the right subtree, all subsequent values must be greater than that node.
Think of it this way: as we traverse the preorder sequence, we're essentially "descending" down the tree. When we see smaller values, we're going left. When we see a larger value, we might be going right. The tricky part is figuring out which ancestor's right subtree we're entering.
This is where the stack comes in. The stack keeps track of the "path" from root to our current position - essentially all ancestors we haven't finished processing. When we encounter a value larger than some elements in our stack, it means we're done with those nodes' left subtrees and are moving to a right subtree.
The moment we pop a value from the stack (because current element is larger), that popped value becomes our new lower bound (last
). Why? Because we're now in its right subtree, and everything that follows must be greater than it.
For example, consider [5, 2, 6, 1]
. After processing 5, 2, and 6, our last
would be 5 (since 6 > 5, we popped 5 and moved to its right subtree). Now when we see 1, which is less than 5, we know this can't be a valid BST preorder sequence - we can't have a value less than 5 appearing after we've already moved to 5's right subtree.
The algorithm elegantly captures this constraint: use a stack to track ancestors, and maintain the most recent "left-behind" ancestor as the minimum bound for all future values.
Solution Approach
The solution uses a stack-based simulation of the preorder traversal validation. Here's how the implementation works:
Data Structures Used:
stk
: A stack to keep track of ancestor nodes we haven't fully processedlast
: A variable initialized to negative infinity that maintains the lower bound for subsequent values
Algorithm Steps:
-
Initialize the stack and lower bound:
stk = [] last = -inf
The stack starts empty, and
last
is set to negative infinity since initially there's no lower bound constraint. -
Process each element in the preorder array:
for x in preorder:
-
Check BST violation:
if x < last: return False
If the current value
x
is less thanlast
, we've violated the BST property. This means we're trying to insert a value that's smaller than a node whose right subtree we've already entered. -
Update the stack and lower bound:
while stk and stk[-1] < x: last = stk.pop()
While the stack is not empty and its top element is less than the current value
x
, we pop elements. This simulates moving from a left subtree to a right subtree. The last popped value becomes our new lower boundlast
because we're entering its right subtree. -
Push current element to stack:
stk.append(x)
Add the current element to the stack as it becomes part of the current path.
-
Return the result:
return True
If we process all elements without finding a violation, the sequence is valid.
Example Walkthrough:
For preorder = [5, 2, 1, 3, 6]
:
- Process 5:
stk = [5]
,last = -inf
- Process 2: 2 > -inf ✓, 2 < 5 (no pop),
stk = [5, 2]
- Process 1: 1 > -inf ✓, 1 < 2 (no pop),
stk = [5, 2, 1]
- Process 3: 3 > -inf ✓, 3 > 1 (pop 1), 3 > 2 (pop 2),
last = 2
,stk = [5, 3]
- Process 6: 6 > 2 ✓, 6 > 3 (pop 3), 6 > 5 (pop 5),
last = 5
,stk = [6]
- All elements processed successfully → return
True
Time Complexity: O(n)
where n is the length of the preorder array. Each element is pushed and popped at most once.
Space Complexity: O(n)
in the worst case when the tree is completely left-skewed, causing all elements to be in the stack.
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 small example with preorder = [8, 5, 1, 7, 10, 12]
to see how the algorithm validates this sequence.
Initial State:
stk = []
(empty stack)last = -infinity
(no lower bound yet)
Step 1: Process 8
- Check: 8 > -infinity ✓ (no violation)
- Stack top check: stack is empty, no popping needed
- Push 8:
stk = [8]
- State: We're at the root
Step 2: Process 5
- Check: 5 > -infinity ✓ (no violation)
- Stack top check: 5 < 8, no popping needed
- Push 5:
stk = [8, 5]
- State: We went left from 8
Step 3: Process 1
- Check: 1 > -infinity ✓ (no violation)
- Stack top check: 1 < 5, no popping needed
- Push 1:
stk = [8, 5, 1]
- State: We went left from 5
Step 4: Process 7
- Check: 7 > -infinity ✓ (no violation)
- Stack top check: 7 > 1, so pop 1 →
last = 1
- Continue: 7 > 5, so pop 5 →
last = 5
- Continue: 7 < 8, stop popping
- Push 7:
stk = [8, 7]
- State: We're now in the right subtree of 5 (all future values must be > 5)
Step 5: Process 10
- Check: 10 > 5 ✓ (no violation)
- Stack top check: 10 > 7, so pop 7 →
last = 7
- Continue: 10 > 8, so pop 8 →
last = 8
- Push 10:
stk = [10]
- State: We're now in the right subtree of 8 (all future values must be > 8)
Step 6: Process 12
- Check: 12 > 8 ✓ (no violation)
- Stack top check: 12 > 10, so pop 10 →
last = 10
- Push 12:
stk = [12]
- State: We're in the right subtree of 10
Result: All elements processed successfully → return True
The corresponding BST would look like:
8 / \ 5 10 / \ \ 1 7 12
Why this works: The algorithm tracks when we transition from a left subtree to a right subtree. When we pop elements from the stack (because we found a larger value), we're essentially saying "we're done with this node's left subtree and moving to its right." The last popped value becomes our minimum bound because everything in a right subtree must be greater than its parent node.
Solution Implementation
1from typing import List
2from math import inf
3
4class Solution:
5 def verifyPreorder(self, preorder: List[int]) -> bool:
6 """
7 Verify if the given sequence is a valid preorder traversal of a BST.
8
9 Algorithm:
10 - Use a stack to track the path from root to current node
11 - Keep track of the minimum allowed value (lower bound)
12 - When we encounter a value greater than stack top, we're moving to the right subtree
13 - Pop all smaller values from stack and update the lower bound
14
15 Args:
16 preorder: List of integers representing potential preorder traversal
17
18 Returns:
19 bool: True if valid BST preorder, False otherwise
20 """
21 # Stack to maintain the ancestors of current node
22 stack = []
23
24 # Lower bound for the next element (initially negative infinity)
25 min_value = -inf
26
27 # Process each value in the preorder sequence
28 for current_value in preorder:
29 # If current value violates BST property (less than minimum allowed)
30 if current_value < min_value:
31 return False
32
33 # Pop all values smaller than current (we're moving to right subtree)
34 # The last popped value becomes the new lower bound
35 while stack and stack[-1] < current_value:
36 min_value = stack.pop()
37
38 # Add current value to stack (potential parent for future nodes)
39 stack.append(current_value)
40
41 # If we processed all values without violation, it's valid
42 return True
43
1class Solution {
2 public boolean verifyPreorder(int[] preorder) {
3 // Stack to maintain a decreasing sequence of values
4 // Represents the right boundary of the current subtree
5 Deque<Integer> stack = new ArrayDeque<>();
6
7 // Tracks the lower bound for subsequent values
8 // Initially set to minimum value as there's no restriction
9 int lowerBound = Integer.MIN_VALUE;
10
11 // Iterate through each value in the preorder traversal
12 for (int currentValue : preorder) {
13 // If current value is less than the lower bound,
14 // it violates BST property (should be in left subtree but appears in right)
15 if (currentValue < lowerBound) {
16 return false;
17 }
18
19 // Pop all values smaller than current value from stack
20 // These represent completed left subtrees, and current value is in right subtree
21 // The last popped value becomes the new lower bound
22 while (!stack.isEmpty() && stack.peek() < currentValue) {
23 lowerBound = stack.poll();
24 }
25
26 // Push current value onto stack
27 // Maintains decreasing order for potential left subtree values
28 stack.push(currentValue);
29 }
30
31 // If we've processed all values without violation, it's a valid preorder
32 return true;
33 }
34}
35
1class Solution {
2public:
3 bool verifyPreorder(vector<int>& preorder) {
4 // Stack to maintain potential parent nodes
5 stack<int> nodeStack;
6
7 // Tracks the lower bound (last popped parent when moving to right subtree)
8 int lowerBound = INT_MIN;
9
10 // Process each value in the preorder traversal
11 for (int currentValue : preorder) {
12 // If current value is less than lower bound, it violates BST property
13 // (right subtree values must be greater than parent)
14 if (currentValue < lowerBound) {
15 return false;
16 }
17
18 // Pop all nodes smaller than current value
19 // These are left subtree nodes, and current value is in right subtree
20 while (!nodeStack.empty() && nodeStack.top() < currentValue) {
21 lowerBound = nodeStack.top(); // Update lower bound to last popped parent
22 nodeStack.pop();
23 }
24
25 // Push current value to stack as potential parent for future nodes
26 nodeStack.push(currentValue);
27 }
28
29 return true;
30 }
31};
32
1function verifyPreorder(preorder: number[]): boolean {
2 // Stack to maintain potential parent nodes
3 const nodeStack: number[] = [];
4
5 // Tracks the lower bound (last popped parent when moving to right subtree)
6 let lowerBound: number = Number.MIN_SAFE_INTEGER;
7
8 // Process each value in the preorder traversal
9 for (const currentValue of preorder) {
10 // If current value is less than lower bound, it violates BST property
11 // (right subtree values must be greater than parent)
12 if (currentValue < lowerBound) {
13 return false;
14 }
15
16 // Pop all nodes smaller than current value
17 // These are left subtree nodes, and current value is in right subtree
18 while (nodeStack.length > 0 && nodeStack[nodeStack.length - 1] < currentValue) {
19 lowerBound = nodeStack[nodeStack.length - 1]; // Update lower bound to last popped parent
20 nodeStack.pop();
21 }
22
23 // Push current value to stack as potential parent for future nodes
24 nodeStack.push(currentValue);
25 }
26
27 return true;
28}
29
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the preorder array.
The algorithm iterates through each element in the preorder array exactly once. For each element, we perform:
- A comparison with
last
:O(1)
- A while loop that pops elements from the stack
- An append operation to the stack:
O(1)
Although there's a nested while loop, each element can be pushed to and popped from the stack at most once throughout the entire execution. Therefore, the total number of operations across all iterations is bounded by 2n
(each element is pushed once and popped at most once), resulting in O(n)
time complexity.
Space Complexity: O(n)
in the worst case.
The space is used by the stack stk
. In the worst case, when the preorder traversal represents a skewed tree (all nodes only have left children or form a strictly decreasing sequence), all elements would be pushed onto the stack before any popping occurs, requiring O(n)
space. In the best case of a skewed tree with only right children, the stack would maintain at most one element at a time, giving O(1)
space, but we consider worst-case complexity.
Common Pitfalls
1. Forgetting to Update the Lower Bound Correctly
A common mistake is not properly tracking the lower bound (last
or min_value
) when transitioning from a left subtree to a right subtree. Some implementations might forget to update this value or update it incorrectly.
Incorrect Implementation:
# WRONG: Not updating the lower bound while stack and stack[-1] < current_value: stack.pop() # Just popping without updating min_value
Correct Implementation:
# CORRECT: Update min_value with the last popped element while stack and stack[-1] < current_value: min_value = stack.pop()
The last popped value represents the parent node whose right subtree we're entering, so it becomes the new lower bound.
2. Using Wrong Initial Value for Lower Bound
Another pitfall is initializing the lower bound with 0 or some arbitrary value instead of negative infinity.
Incorrect Implementation:
# WRONG: May fail for negative values in the tree min_value = 0 # What if the BST contains negative values?
Correct Implementation:
# CORRECT: Use negative infinity to handle all possible values min_value = -inf
3. Incorrect Comparison in the While Loop
Using <=
instead of <
in the while loop condition can lead to incorrect results when dealing with the BST property.
Incorrect Implementation:
# WRONG: Equal values shouldn't be popped while stack and stack[-1] <= current_value: min_value = stack.pop()
Correct Implementation:
# CORRECT: Only pop strictly smaller values while stack and stack[-1] < current_value: min_value = stack.pop()
4. Not Handling Edge Cases
Forgetting to handle edge cases like empty arrays or single-element arrays.
Better Implementation with Edge Case Handling:
def verifyPreorder(self, preorder: List[int]) -> bool:
# Handle edge cases
if not preorder or len(preorder) <= 1:
return True
stack = []
min_value = -inf
for current_value in preorder:
if current_value < min_value:
return False
while stack and stack[-1] < current_value:
min_value = stack.pop()
stack.append(current_value)
return True
5. Misunderstanding the Algorithm's Purpose
Some might think the stack needs to be empty at the end for validation, but this is incorrect. The stack can contain elements at the end (representing the rightmost path in the tree).
Wrong Assumption:
# WRONG: Stack doesn't need to be empty
return len(stack) == 0
Correct Understanding:
# CORRECT: Just return True if all elements processed without violation return True
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
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 Search Tree Intro Definition A Binary Search Tree or BST is a rooted binary tree with the value of each internal node being greater than all the values in the respective node's left subtree and less than the ones in its right subtree An empty tree is a BST since it satisfies the
Want a Structured Path to Master System Design Too? Don’t Miss This!