2331. Evaluate Boolean Binary Tree
Problem Description
You are given the root of a full binary tree that represents a boolean expression. The tree has specific properties:
Node Values:
- Leaf nodes contain either
0
or1
, where:0
representsFalse
1
representsTrue
- Non-leaf nodes contain either
2
or3
, where:2
represents the booleanOR
operation3
represents the booleanAND
operation
Evaluation Rules:
- If a node is a leaf node, its evaluation is simply its value (
True
for1
,False
for0
) - If a node is a non-leaf node, you must:
- Evaluate both of its children
- Apply the boolean operation specified by the node's value to the children's results
Your task is to evaluate the entire tree starting from the root and return the final boolean result.
Important Definitions:
- A full binary tree means every node has exactly 0 or 2 children (no node has just 1 child)
- A leaf node is a node with no children
For example, if you have a tree where the root has value 2
(OR), its left child is a leaf with value 1
(True), and its right child is a leaf with value 0
(False), the evaluation would be: True OR False = True
.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a binary tree, which is a special type of graph with nodes (TreeNode objects) and edges (parent-child relationships).
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure where each node has at most two children and there's a root node.
DFS
- We arrive at DFS: Since we're dealing with a tree structure, the flowchart directly leads us to use Depth-First Search (DFS).
Conclusion: The flowchart suggests using a DFS approach for evaluating the boolean binary tree.
This makes perfect sense for our problem because:
- We need to traverse the tree from root to leaves
- We must evaluate child nodes before we can evaluate their parent (post-order traversal)
- DFS naturally handles the recursive evaluation where each non-leaf node's value depends on its children's evaluated results
- The recursive nature of DFS perfectly matches the recursive evaluation rules (evaluate children first, then apply the boolean operation)
Intuition
The key insight is that this problem naturally follows a bottom-up evaluation pattern - we can't determine the value of any non-leaf node until we know the values of its children. This immediately suggests a recursive approach.
Think about how you would manually evaluate this tree: you'd start from the root, but quickly realize you need to go deeper to find the leaf nodes first. Once you reach a leaf, you know its value immediately (0
= False
, 1
= True
). Then you can work your way back up, combining children's values using their parent's operation.
The recursive structure mirrors the tree structure perfectly:
- Base case: When we hit a leaf node (no children), we simply return its boolean value
- Recursive case: For non-leaf nodes, we first evaluate both children, then apply the appropriate operation (
OR
for value2
,AND
for value3
)
Why does this work so elegantly? Because each subtree is itself a valid boolean expression that can be evaluated independently. The result of evaluating any subtree is just a boolean value that gets passed up to its parent.
The beauty of this approach is that we don't need to explicitly track anything - the call stack naturally handles the traversal order for us. We dive deep to the leaves, then bubble up the results, applying operations as we go. Each function call only needs to worry about its immediate children, making the logic clean and simple.
This is a classic example of how tree problems often have natural recursive solutions where the solution to the whole tree depends on solutions to its subtrees.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The solution implements a recursive DFS approach that evaluates the boolean tree from bottom to top.
Base Case - Leaf Node Detection:
The first check if root.left is None:
identifies leaf nodes. Since it's a full binary tree, if the left child is None
, the right child must also be None
. For leaf nodes, we simply convert the node's value to a boolean using bool(root.val)
:
0
becomesFalse
1
becomesTrue
Recursive Case - Non-Leaf Nodes: For non-leaf nodes, we need to:
- Determine which boolean operation to apply based on the node's value
- Recursively evaluate both children
- Apply the operation to combine the results
Operation Selection:
The code uses Python's or_
and and_
functions from the operator module (implicitly imported). The selection is elegant:
op = or_ if root.val == 2 else and_
- If
root.val == 2
, we use theOR
operation - If
root.val == 3
, we use theAND
operation
Recursive Evaluation: The final line combines everything:
return op(self.evaluateTree(root.left), self.evaluateTree(root.right))
This recursively evaluates both the left and right subtrees, then applies the selected operation to their results.
Time Complexity: O(n)
where n
is the number of nodes, as we visit each node exactly once.
Space Complexity: O(h)
where h
is the height of the tree, due to the recursive call stack. In the worst case of a skewed tree, this could be O(n)
.
The solution elegantly leverages Python's functional programming features to make the code concise while maintaining clarity. The recursive structure perfectly matches the problem's hierarchical nature, making it both intuitive and efficient.
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 concrete example to see how the recursive solution works:
Consider this binary tree:
3 (AND) / \ 2 1 (OR) (True) / \ 0 1 (False)(True)
Step-by-step evaluation:
-
Start at root (value = 3, AND operation)
- This is a non-leaf node, so we need to evaluate both children
- Set operation to
AND
- Recursively call on left child (value = 2)
-
Evaluate left subtree (value = 2, OR operation)
- This is a non-leaf node
- Set operation to
OR
- Recursively call on its left child (value = 0)
-
Evaluate leftmost leaf (value = 0)
- This is a leaf node (no children)
- Return
bool(0)
=False
-
Back to node with value = 2, now evaluate its right child (value = 1)
- This is a leaf node
- Return
bool(1)
=True
-
Complete evaluation of node with value = 2
- We have: left child =
False
, right child =True
- Apply OR operation:
False OR True = True
- Return
True
to parent
- We have: left child =
-
Back to root (value = 3), now evaluate right child (value = 1)
- This is a leaf node
- Return
bool(1)
=True
-
Complete evaluation of root
- We have: left child =
True
, right child =True
- Apply AND operation:
True AND True = True
- Return
True
as final result
- We have: left child =
The recursion naturally handles the bottom-up evaluation: we dive down to the leaves first, then bubble up the results while applying the appropriate boolean operations at each non-leaf node. The final answer is True
.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional
9
10class Solution:
11 def evaluateTree(self, root: Optional[TreeNode]) -> bool:
12 """
13 Evaluates a binary expression tree where:
14 - Leaf nodes have values 0 (False) or 1 (True)
15 - Non-leaf nodes have values 2 (OR) or 3 (AND)
16
17 Args:
18 root: The root node of the binary expression tree
19
20 Returns:
21 Boolean result of evaluating the expression tree
22 """
23 # Base case: if node is a leaf (no children), return its boolean value
24 if root.left is None:
25 return bool(root.val)
26
27 # Recursively evaluate left and right subtrees
28 left_result = self.evaluateTree(root.left)
29 right_result = self.evaluateTree(root.right)
30
31 # Apply the operation based on the node's value
32 if root.val == 2: # OR operation
33 return left_result or right_result
34 else: # root.val == 3, AND operation
35 return left_result and right_result
36
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 /**
18 * Evaluates a boolean expression tree.
19 * Leaf nodes have values 0 (false) or 1 (true).
20 * Non-leaf nodes have values 2 (OR) or 3 (AND).
21 *
22 * @param root The root node of the binary expression tree
23 * @return The boolean result of evaluating the expression tree
24 */
25 public boolean evaluateTree(TreeNode root) {
26 // Base case: if current node is a leaf node (no left child)
27 // Return true if value is 1, false if value is 0
28 if (root.left == null) {
29 return root.val == 1;
30 }
31
32 // Recursive case: evaluate based on operator type
33 // If node value is 2 (OR operator)
34 if (root.val == 2) {
35 // Return true if either left subtree OR right subtree evaluates to true
36 return evaluateTree(root.left) || evaluateTree(root.right);
37 }
38
39 // If node value is 3 (AND operator)
40 // Return true only if both left subtree AND right subtree evaluate to true
41 return evaluateTree(root.left) && evaluateTree(root.right);
42 }
43}
44
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 /**
15 * Evaluates a boolean expression tree and returns the result.
16 * Tree node values: 0 = false, 1 = true, 2 = OR operator, 3 = AND operator
17 * Leaf nodes contain boolean values (0 or 1).
18 * Non-leaf nodes contain operators (2 or 3).
19 *
20 * @param root The root node of the expression tree
21 * @return The boolean result of evaluating the expression tree
22 */
23 bool evaluateTree(TreeNode* root) {
24 // Base case: if current node is a leaf (no left child), return its boolean value
25 if (root->left == nullptr) {
26 return root->val == 1;
27 }
28
29 // Recursive case: evaluate based on the operator type
30 if (root->val == 2) {
31 // OR operation: return true if at least one subtree evaluates to true
32 return evaluateTree(root->left) || evaluateTree(root->right);
33 } else {
34 // AND operation (root->val == 3): return true only if both subtrees evaluate to true
35 return evaluateTree(root->left) && evaluateTree(root->right);
36 }
37 }
38};
39
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Evaluates a boolean expression tree where:
17 * - Leaf nodes have values 0 (false) or 1 (true)
18 * - Non-leaf nodes have values 2 (OR) or 3 (AND)
19 *
20 * @param root - The root node of the binary expression tree
21 * @returns The boolean result of evaluating the expression tree
22 */
23function evaluateTree(root: TreeNode | null): boolean {
24 // Handle null case (though typically root won't be null based on problem constraints)
25 if (!root) {
26 return false;
27 }
28
29 // Destructure node properties for cleaner access
30 const { val: nodeValue, left: leftChild, right: rightChild } = root;
31
32 // Base case: leaf node (no children)
33 // Return true if value is 1, false if value is 0
34 if (leftChild === null) {
35 return nodeValue === 1;
36 }
37
38 // Recursive case: internal node with operator
39 // If node value is 2, perform OR operation on children
40 if (nodeValue === 2) {
41 return evaluateTree(leftChild) || evaluateTree(rightChild);
42 }
43
44 // If node value is 3, perform AND operation on children
45 return evaluateTree(leftChild) && evaluateTree(rightChild);
46}
47
Time and Space Complexity
Time Complexity: O(n)
, where n
is the number of nodes in the binary tree.
The algorithm performs a depth-first traversal of the binary tree, visiting each node exactly once. At each node, it performs constant time operations:
- Checking if the node is a leaf (
root.left is None
) - Determining the operation type (
or_
orand_
) - Making recursive calls and applying the operation
Since every node is visited once and each visit takes O(1)
time, the total time complexity is O(n)
.
Space Complexity: O(n)
, where n
is the number of nodes in the binary tree.
The space complexity is determined by the recursive call stack. In the worst case, the tree could be completely unbalanced (essentially a linked list), resulting in a recursion depth equal to the number of nodes n
. Each recursive call adds a frame to the call stack, consuming O(1)
space per frame. Therefore, the maximum space used by the call stack is O(n)
.
For a balanced binary tree, the recursion depth would be O(log n)
, but we consider the worst-case scenario for space complexity analysis.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Leaf Node Detection
A common mistake is checking only one child to determine if a node is a leaf, or using incorrect logic:
Incorrect Implementation:
# Wrong: Only checking left child
if root.left is None:
return bool(root.val)
# Wrong: Checking if val is 0 or 1 to identify leaf
if root.val in [0, 1]:
return bool(root.val)
Why it's problematic: While the first approach works for full binary trees (since both children must be None together), it's not explicit and can cause confusion. The second approach assumes internal nodes never have values 0 or 1, which might not always be guaranteed in variations of this problem.
Correct Solution:
# Explicitly check both children
if root.left is None and root.right is None:
return bool(root.val)
2. Short-Circuit Evaluation Issues
Using Python's and
/or
operators directly might lead to unexpected behavior due to short-circuit evaluation:
Problematic Pattern:
# This might not evaluate the right subtree in some cases if root.val == 2: return self.evaluateTree(root.left) or self.evaluateTree(root.right)
Why it's problematic: If the left subtree evaluates to True
for an OR operation, Python won't evaluate the right subtree at all. While this gives the correct boolean result, it might cause issues if you need to ensure all nodes are visited (e.g., for side effects or validation).
Solution for Complete Traversal:
# Ensure both subtrees are always evaluated left_result = self.evaluateTree(root.left) right_result = self.evaluateTree(root.right) if root.val == 2: return left_result or right_result else: return left_result and right_result
3. Missing None Check for Root
Forgetting to handle the edge case where the root itself might be None:
Incomplete Implementation:
def evaluateTree(self, root: Optional[TreeNode]) -> bool:
# Directly accessing root.left without checking if root exists
if root.left is None:
return bool(root.val)
Robust Solution:
def evaluateTree(self, root: Optional[TreeNode]) -> bool:
if root is None:
return False # or raise an exception based on requirements
if root.left is None and root.right is None:
return bool(root.val)
# Rest of the logic...
4. Assuming Binary Values Without Validation
Not validating that leaf nodes actually contain 0 or 1, and non-leaf nodes contain 2 or 3:
Better Implementation with Validation:
def evaluateTree(self, root: Optional[TreeNode]) -> bool:
if root.left is None and root.right is None:
if root.val not in [0, 1]:
raise ValueError(f"Invalid leaf value: {root.val}")
return bool(root.val)
if root.val not in [2, 3]:
raise ValueError(f"Invalid operator value: {root.val}")
left_result = self.evaluateTree(root.left)
right_result = self.evaluateTree(root.right)
return (left_result or right_result) if root.val == 2 else (left_result and right_result)
Which of these properties could exist for a graph but not a tree?
Recommended Readings
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
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
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!