255. Verify Preorder Sequence in Binary Search Tree
Problem Description
The challenge is to determine whether a given array of unique integers represents the correct preorder traversal of a Binary Search Tree (BST). Preorder traversal of a BST means visiting the nodes in the order: root, left, right. To qualify as a valid BST traversal, the sequence must reflect the BST's structure, where for any node, all the values in the left subtree are less than the node's value, and all the values in the right subtree are greater.
Intuition
For a preorder traversal of a binary search tree, the order of elements would reflect the root being visited first, then the left subtree, and then the right subtree. In a BST, the left subtree contains nodes that are less than the root, and the right subtree contains nodes greater than the root.
The provided solution uses a stack and a variable that keeps track of the last node value we've visited so far when moving towards the right in the tree. Starting with the first value in the preorder traversal, which would be the root of the BST, we consider the following:
-
BST Property: As we iterate through the
preorder
list, if we encounter a value that is less than thelast
visited node when turning right, we know that the tree's sequential structure is incorrect. -
Preorder Traversal Structure: A stack is used to keep track of the traversal path; when we go down left we push onto the stack, and when we turn right we pop from the stack. The top of the stack always contains the parent node we would have to compare with when going right.
-
Switching from Left to Right: Each time we pop from the stack, we update the
last
value, which symbolizes that all future node values should be larger than this if we are considering the right subtree now. -
Finally, if we complete the iteration without finding any contradictions to the properties above, we return
True
indicating that the sequence can indeed be the preorder traversal of a BST.
Throughout the stack manipulation, we are implicitly maintaining the invariant of the BST - that nodes to the left are smaller than root, and nodes to the right are greater.
Learn more about Stack, Tree, Binary Search Tree, Recursion, Binary Tree and Monotonic Stack patterns.
Solution Approach
The implementation of the solution follows a straightforward algorithm that keeps track of the necessary properties of a BST during the traversal sequence:
-
Initialization: A stack
stk
is created to keep track of ancestors as we traverse the preorder array. A variablelast
is initialized to negative infinity, representing the minimum constraint of the current subtree node values. -
Traversal: We iterate through each value
x
in the preorder array. -
Validity Check: For every
x
, we check ifx < last
. If this condition is met, it means we have encountered a value smaller than the last value after turning right in the tree, which violates the BST property, hence we returnFalse
. -
Updating Stack: While the stack is not empty and the last element (top of the stack) is less than the current value
x
, we are moving from the left subtree to the right subtree. We pop values from the stack as we are effectively traveling up the tree, and update thelast
value. Thelast
value now becomes the right boundary for all the subsequent nodes in the subtree. -
Processing Current Value: After performing the necessary pops (if any), we append the current value
x
to the stack. This push operation represents the traversal down the left subtree. -
Termination: If the entire preorder traversal is processed without returning
False
, it implies that the sequence did not violate any BST properties, and we returnTrue
.
The algorithm uses a stack to model the ancestors in the preorder traversal and a variable last
to ensure that the BST's right subtree property holds at every step. The simplicity of the solution comes from understanding how the preorder traversal works in conjunction with the BST properties.
Here is the highlighted implementation of the approach:
class Solution:
def verifyPreorder(self, preorder: List[int]) -> bool:
stk = [] # [Stack](/problems/stack_intro) for keeping track of the ancestor nodes
last = -inf # Variable to hold the last popped node value as a bound
for x in preorder:
if x < last: # If the current node's value is less than last, it violates the BST rule
return False
while stk and stk[-1] < x:
last = stk.pop() # Update 'last' each time we turn right (encounter a greater value than the top of the stack)
stk.append(x) # Push the current value onto the stack
return True # If all nodes processed without violating BST properties, the sequence is valid
This approach effectively simulates the traversal of a BST while ensuring that both the left and right subtree properties hold throughout the process.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a small example to illustrate the solution approach with the following preorder traversal list [5, 2, 1, 3, 6]
.
-
Initialize the stack
stk
to an empty list, andlast
to negative infinity, which will represent the minimum value of the current subtree's nodes. -
Traverse the preorder list:
a. Begin with the first element
x = 5
. Sincex
is not less thanlast
(which is -inf at this point), we continue and push5
onto the stack. The stack now looks like[5]
.b. Next element
x = 2
. It is not less thanlast
, so we push2
onto the stack. The stack becomes[5, 2]
.c. For element
x = 1
, againx
is not less thanlast
, so we push1
to the stack. The stack is now[5, 2, 1]
.d. Now
x = 3
. It's greater than the top of the stack (which is1
), so we pop1
from the stack, and updatelast
to1
. We continue popping because the top of the stack (2
) is still less than3
, updatelast
to2
, and finally push3
to the stack. Now, the stack is[5, 3]
.e. For the last element
x = 6
, the top of the stack is3
, which is less thanx
, so we pop3
from the stack and the newlast
is now3
. The stack is[5]
and since5
is less thanx
, we pop again and updatelast
to5
. Now the stack is empty, and we push6
to the stack. So, the final stack is[6]
. -
Since we have placed all elements onto the stack according to the rules, and have never encountered an element less than
last
during the process, the iteration completes successfully. -
The preorder is possible for a BST, so we return
True
.
The pattern here shows that the stack is used to maintain a path of ancestors while iterating, and last
serves as a lower bound for the nodes that can come after turning right, ensuring that subsequent nodes are always greater than any node’s value after turning right. Throughout every step, the BST property was maintained properly.
Solution Implementation
1from math import inf # Import 'inf' to use for comparison
2
3class Solution:
4 def verify_preorder(self, preorder: List[int]) -> bool:
5 # Initialize an empty stack to keep track of the nodes
6 stack = []
7
8 # Initialize the last processed value to negative infinity
9 last_processed_value = -inf
10
11 # Iterate over each node value in the preorder sequence
12 for value in preorder:
13 # If the current value is less than the last processed value, the
14 # sequence does not satisfy the binary search tree property
15 if value < last_processed_value:
16 return False
17
18 # While there are values in the stack and the last value
19 # in the stack is less than the current value, update the
20 # last processed value and pop from the stack. This means we are
21 # backtracking to a node which is a parent of the previous nodes
22 while stack and stack[-1] < value:
23 last_processed_value = stack.pop()
24
25 # Push the current value to the stack to represent the path taken
26 stack.append(value)
27
28 # If we have completed the loop without returning False, the sequence
29 # is a valid preorder traversal of a binary search tree
30 return True
31
1class Solution {
2 public boolean verifyPreorder(int[] preorder) {
3 // Stack to keep track of the ancestors in the traversal
4 Deque<Integer> stack = new ArrayDeque<>();
5
6 // The last processed node value from the traversal
7 int lastProcessedValue = Integer.MIN_VALUE;
8
9 // Iterate over each value in the preorder sequence
10 for (int value : preorder) {
11 // If we find a value less than the last processed value, the sequence is not a valid preorder traversal
12 if (value < lastProcessedValue) {
13 return false;
14 }
15
16 // Pop elements from the stack until the current value is greater than the stack's top value.
17 // This ensures ancestors are properly processed for the BST structure.
18 while (!stack.isEmpty() && stack.peek() < value) {
19 // Update the last processed value with the last ancestor for future comparisons
20 lastProcessedValue = stack.pop();
21 }
22
23 // Push the current value to the stack, as it is now the current node being processed
24 stack.push(value);
25 }
26
27 // If the entire sequence is processed without any violations of BST properties, return true
28 return true;
29 }
30}
31
1#include <vector>
2#include <stack>
3#include <climits> // for INT_MIN
4
5class Solution {
6public:
7 // Function to verify if a given vector of integers is a valid preorder traversal of a BST
8 bool verifyPreorder(vector<int>& preorder) {
9 // A stack to hold the nodes
10 stack<int> nodeStack;
11
12 // Variable to store the last value that was popped from the stack
13 int lastPopped = INT_MIN;
14
15 // Iterate over each element in the given preorder vector
16 for (int value : preorder) {
17 // If current value is less than the last popped value, the preorder sequence is invalid
18 if (value < lastPopped) return false;
19
20 // While the stack isn't empty and the top element is less than the current value,
21 // pop from the stack and update the lastPopped value.
22 while (!nodeStack.empty() && nodeStack.top() < value) {
23 lastPopped = nodeStack.top();
24 nodeStack.pop();
25 }
26
27 // Push the current value onto the stack
28 nodeStack.push(value);
29 }
30
31 // If the loop completes without returning false, the preorder sequence is valid
32 return true;
33 }
34};
35
1// Required for the typing generosity of TypeScript
2// Integer array type alias
3type IntArray = number[];
4
5// Stack type alias leveraging Array type for stack operations
6type Stack = number[];
7
8// Initialize a stack to hold the nodes
9let nodeStack: Stack = [];
10
11// Variable to store the last value that was popped from the stack
12let lastPopped: number = Number.MIN_SAFE_INTEGER;
13
14// Function to verify if a given array of integers is a valid preorder traversal of a BST
15function verifyPreorder(preorder: IntArray): boolean {
16 // Reset stack and last popped for each validation run
17 nodeStack = [];
18 lastPopped = Number.MIN_SAFE_INTEGER;
19
20 // Iterate over each element in the given preorder array
21 for (let value of preorder) {
22 // If current value is less than the last popped value, the preorder sequence is invalid
23 if (value < lastPopped) return false;
24
25 // While the stack isn't empty and the top element is less than the current value,
26 // pop from the stack and update the lastPopped value.
27 while (nodeStack.length > 0 && nodeStack[nodeStack.length - 1] < value) {
28 lastPopped = nodeStack.pop()!;
29 }
30
31 // Push the current value onto the stack
32 nodeStack.push(value);
33 }
34
35 // If the loop completes without returning false, the preorder sequence is valid
36 return true;
37}
38
39// Since TypeScript doesn't have IntArray and Stack as inbuilt types, we're creating aliases
40// to enhance readability and maintain a level of abstraction in our code, similar to the original C++ version.
41
Time and Space Complexity
The time complexity of the given code is O(n)
, where n
is the number of nodes in the preorder traversal list. This is because the code iterates through each element of the preorder
list exactly once. During each iteration, both the stack push (stk.append(x)
) and pop (stk.pop()
) operations are executed at most once per element, which are O(1)
operations, therefore not increasing the overall time complexity beyond O(n)
.
The space complexity of the code is O(n)
, due to the stack stk
that is used to store elements. In the worst-case scenario, the stack could store all the elements of the preorder traversal if they appear in strictly increasing order. However, due to the nature of binary search trees, if the input is a valid BST preorder traversal, the space complexity can be reduced on average, but in the worst case, it still remains O(n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
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!