Facebook Pixel

965. Univalued Binary Tree

Problem Description

A uni-valued binary tree is a tree where all nodes contain the exact same value.

Given the root of a binary tree, you need to determine if the tree is uni-valued. Return true if every node in the tree has the same value, and false otherwise.

For example:

  • A tree with all nodes having value 1 would be uni-valued → return true
  • A tree where some nodes have value 1 and others have value 2 would not be uni-valued → return false
  • An empty tree or a single node tree is considered uni-valued → return true

The solution uses a depth-first search (DFS) approach. It stores the root node's value as x, then recursively checks if every node in the tree has the same value as x. The function traverses through all nodes, comparing each node's value with x. If any node has a different value, it returns false. The traversal continues through both left and right subtrees, and only returns true when all nodes have been verified to have the same value.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph with nodes and edges. Each node can have at most two children (left and right), connected by edges.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure. A tree is a connected acyclic graph, and in this case, we have a rooted binary tree.

DFS

  • Conclusion reached: Since we're dealing with a tree structure, the flowchart leads us directly to DFS (Depth-First Search).

The flowchart suggests using DFS for this tree problem, which makes perfect sense because:

  1. We need to visit every node in the tree to check if all values are the same
  2. DFS allows us to recursively traverse the tree structure efficiently
  3. We can compare each node's value with a reference value (the root's value) as we traverse
  4. The recursive nature of DFS naturally handles the tree's hierarchical structure

Conclusion: The flowchart correctly identifies that for a tree-based problem like checking if a binary tree is uni-valued, DFS is the appropriate algorithm pattern to use.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that for a tree to be uni-valued, every single node must have the same value. This immediately suggests we need to visit every node and check its value.

When we think about checking if all nodes have the same value, we need a reference point - what value should we compare against? The most natural choice is the root node's value. If the tree is uni-valued, then by definition, every node must have the same value as the root.

Now, how do we traverse the tree to check every node? Since we established through the flowchart that DFS is appropriate for tree problems, we can use recursion to naturally follow the tree's structure. At each node, we need to verify three things:

  1. The current node's value equals our reference value (the root's value)
  2. All nodes in the left subtree are uni-valued with the same value
  3. All nodes in the right subtree are uni-valued with the same value

This recursive structure maps perfectly to DFS - we check the current node, then recursively check both subtrees. The beauty of this approach is that if any node fails the check (has a different value), the entire recursion chain returns false immediately due to the logical AND operation.

The base case is straightforward: if we reach a null node (beyond a leaf), we return true because an empty subtree doesn't violate the uni-valued property. This allows the recursion to naturally terminate at the leaves of the tree.

By storing the root's value as x before starting the DFS, we ensure consistent comparison throughout the entire traversal, making the solution both elegant and efficient with O(n) time complexity where we visit each node exactly once.

Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.

Solution Approach

The solution implements a DFS traversal using a helper function to check if all nodes in the tree have the same value.

Step 1: Store the Reference Value First, we store the root node's value in a variable x. This will be our reference value that all other nodes must match for the tree to be uni-valued.

x = root.val

Step 2: Define the DFS Helper Function We create a recursive function dfs(root) that checks whether the current subtree rooted at root is uni-valued with value x.

The function logic follows this pattern:

  • Base Case: If the current node is None (we've reached beyond a leaf), return True since an empty subtree doesn't violate the uni-valued property.
  • Recursive Case: Check three conditions using logical AND:
    1. The current node's value equals x
    2. The left subtree is uni-valued (recursive call to dfs(root.left))
    3. The right subtree is uni-valued (recursive call to dfs(root.right))
def dfs(root: Optional[TreeNode]) -> bool:
    if root is None:
        return True
    return root.val == x and dfs(root.left) and dfs(root.right)

Step 3: Execute the DFS Call dfs(root) from the main function to start the traversal from the root and return its result.

The algorithm leverages short-circuit evaluation in the logical AND operation - if root.val != x, the function immediately returns False without checking the subtrees, providing an early termination optimization.

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, representing the maximum recursion stack depth. In the worst case (skewed tree), this could be O(n).

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate how the solution works.

Consider this binary tree:

       5
      / \
     5   5
    / \
   5   3

Step 1: Initialize Reference Value

  • We start at the root node with value 5
  • Store x = 5 as our reference value

Step 2: Begin DFS Traversal from Root

  • Call dfs(root) where root has value 5
  • Check: root.val == x5 == 5 ✓ (True)
  • Now need to check both subtrees

Step 3: Check Left Subtree

  • Call dfs(root.left) on the left child (value 5)
  • Check: 5 == 5 ✓ (True)
  • Check its left child: dfs on node with value 5
    • 5 == 5 ✓ (True)
    • Both children are None, return True
  • Check its right child: dfs on node with value 3
    • 3 == 5 ✗ (False)
    • Immediately return False (no need to check further)

Step 4: Short-Circuit Evaluation

  • Since the left subtree returned False, the AND operation in the root's check (root.val == x and dfs(root.left) and dfs(root.right)) evaluates to False
  • The right subtree is never checked due to short-circuit evaluation
  • Final result: False - the tree is not uni-valued

Alternative Example - Uni-valued Tree:

       2
      / \
     2   2
        / \
       2   2
  • Store x = 2
  • Every dfs call checks: current node value equals 2
  • All recursive calls return True
  • Final result: True - the tree is uni-valued

The algorithm efficiently identifies that the first tree is not uni-valued by finding the mismatched node (value 3) and immediately propagating False up through the recursion chain.

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 isUnivalTree(self, root: Optional[TreeNode]) -> bool:
12        """
13        Determines if a binary tree is a unival tree (all nodes have the same value).
14      
15        Args:
16            root: The root node of the binary tree
17          
18        Returns:
19            True if all nodes in the tree have the same value, False otherwise
20        """
21        # Store the value that all nodes should match (the root's value)
22        target_value = root.val
23      
24        def dfs(node: Optional[TreeNode]) -> bool:
25            """
26            Recursively checks if all nodes in the subtree have the target value.
27          
28            Args:
29                node: Current node being checked
30              
31            Returns:
32                True if this subtree is unival with the target value
33            """
34            # Base case: empty node is considered valid
35            if node is None:
36                return True
37          
38            # Check if current node matches target value
39            # and recursively check both left and right subtrees
40            return (node.val == target_value and 
41                    dfs(node.left) and 
42                    dfs(node.right))
43      
44        # Start DFS traversal from the root
45        return dfs(root)
46
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    // Store the universal value that all nodes should have
18    private int universalValue;
19  
20    /**
21     * Determines if a binary tree is univalued (all nodes have the same value)
22     * @param root The root node of the binary tree
23     * @return true if all nodes have the same value, false otherwise
24     */
25    public boolean isUnivalTree(TreeNode root) {
26        // Initialize the universal value with the root's value
27        universalValue = root.val;
28      
29        // Perform depth-first search to check all nodes
30        return isUnivalTreeDFS(root);
31    }
32  
33    /**
34     * Helper method to recursively check if all nodes have the same value
35     * @param node The current node being checked
36     * @return true if this subtree is univalued, false otherwise
37     */
38    private boolean isUnivalTreeDFS(TreeNode node) {
39        // Base case: null nodes are considered valid
40        if (node == null) {
41            return true;
42        }
43      
44        // Check if current node matches the universal value
45        // AND recursively check both left and right subtrees
46        return node.val == universalValue 
47            && isUnivalTreeDFS(node.left) 
48            && isUnivalTreeDFS(node.right);
49    }
50}
51
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     * Checks if a binary tree is a unival tree (all nodes have the same value)
16     * @param root - The root node of the binary tree
17     * @return true if all nodes have the same value, false otherwise
18     */
19    bool isUnivalTree(TreeNode* root) {
20        // Store the value that all nodes should match
21        int targetValue = root->val;
22      
23        // Define a recursive lambda function to traverse the tree
24        auto checkUnival = [&](this auto&& checkUnival, TreeNode* node) -> bool {
25            // Base case: null node is considered valid
26            if (!node) {
27                return true;
28            }
29          
30            // Check if current node matches target value
31            // and recursively check both subtrees
32            return node->val == targetValue && 
33                   checkUnival(node->left) && 
34                   checkUnival(node->right);
35        };
36      
37        // Start the recursive check from the root
38        return checkUnival(root);
39    }
40};
41
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 * Checks if a binary tree is a unival tree (all nodes have the same value)
17 * @param root - The root node of the binary tree
18 * @returns true if all nodes in the tree have the same value, false otherwise
19 */
20function isUnivalTree(root: TreeNode | null): boolean {
21    // Handle edge case where root is null
22    if (!root) {
23        return true;
24    }
25  
26    // Store the value that all nodes should match
27    const targetValue: number = root.val;
28  
29    /**
30     * Helper function to recursively check if all nodes have the same value
31     * @param node - Current node being checked
32     * @returns true if current node and all descendants have the target value
33     */
34    const checkUniformValue = (node: TreeNode | null): boolean => {
35        // Base case: null nodes are considered valid
36        if (!node) {
37            return true;
38        }
39      
40        // Check if current node matches target value and recursively check children
41        return node.val === targetValue && 
42               checkUniformValue(node.left) && 
43               checkUniformValue(node.right);
44    };
45  
46    // Start the recursive check from the root
47    return checkUniformValue(root);
48}
49

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node in the tree is visited exactly once. During each visit, the algorithm performs a constant-time operation (comparing root.val == x). Therefore, the total time complexity is O(n), where n is the number of nodes in the tree.

Space Complexity: O(n)

The space complexity is determined by the recursive call stack used during the DFS traversal. In the worst case, the tree is completely unbalanced (resembling a linked list), where each node has only one child. This would result in a recursion depth of n, requiring O(n) space on the call stack. In the best case of a perfectly balanced tree, the recursion depth would be O(log n), but since we consider worst-case complexity, the space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Handling Empty Tree Edge Case

A common mistake is not considering what happens when the input tree is None (empty tree). The current solution assumes root is not None when accessing root.val, which would raise an AttributeError.

Problem Code:

def isUnivalTree(self, root: Optional[TreeNode]) -> bool:
    target_value = root.val  # This fails if root is None!
    # ... rest of code

Solution: Add a check for empty tree at the beginning:

def isUnivalTree(self, root: Optional[TreeNode]) -> bool:
    if root is None:
        return True  # Empty tree is considered uni-valued
  
    target_value = root.val
    # ... rest of code

2. Using Global Variables Instead of Closure

Some developers might try to use a class-level or global variable to store the target value, leading to issues with multiple test cases or concurrent execution.

Problem Code:

class Solution:
    def __init__(self):
        self.target_value = None  # Class variable - problematic!
  
    def isUnivalTree(self, root: Optional[TreeNode]) -> bool:
        self.target_value = root.val
        return self.dfs(root)
  
    def dfs(self, node):
        # Using self.target_value can cause issues
        return node.val == self.target_value and ...

Solution: Keep the target value as a local variable and use closure (as shown in the original solution) or pass it as a parameter:

def dfs(node: Optional[TreeNode], target: int) -> bool:
    if node is None:
        return True
    return node.val == target and dfs(node.left, target) and dfs(node.right, target)

# Call with: dfs(root, root.val)

3. Inefficient Short-Circuit Evaluation

Writing the condition check in a way that doesn't take advantage of short-circuit evaluation, causing unnecessary recursive calls.

Problem Code:

def dfs(node: Optional[TreeNode]) -> bool:
    if node is None:
        return True
    # This evaluates all conditions even if node.val != target_value
    left_result = dfs(node.left)
    right_result = dfs(node.right)
    return node.val == target_value and left_result and right_result

Solution: Use the logical AND operator directly in the return statement to enable short-circuiting:

def dfs(node: Optional[TreeNode]) -> bool:
    if node is None:
        return True
    # Short-circuits if node.val != target_value
    return node.val == target_value and dfs(node.left) and dfs(node.right)

4. Incorrect Base Case Logic

Some might incorrectly handle the base case by returning False for None nodes, thinking an empty node means the tree isn't uni-valued.

Problem Code:

def dfs(node: Optional[TreeNode]) -> bool:
    if node is None:
        return False  # Wrong! Empty subtree doesn't violate uni-valued property
    return node.val == target_value and dfs(node.left) and dfs(node.right)

Solution: Return True for None nodes since an empty subtree doesn't contain any nodes with different values:

def dfs(node: Optional[TreeNode]) -> bool:
    if node is None:
        return True  # Correct: empty subtree is valid
    return node.val == target_value and dfs(node.left) and dfs(node.right)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More