Facebook Pixel

671. Second Minimum Node In a Binary Tree

Problem Description

You are given a special binary tree with the following properties:

  1. Every node has exactly 2 children or no children (it's either a leaf node with 0 children or an internal node with 2 children)

  2. All node values are non-negative integers

  3. Parent-child value relationship: If a node has two children, its value equals the minimum of its two children's values. In other words, parent.val = min(left_child.val, right_child.val)

Your task is to find the second smallest value among all the values in the entire tree.

Key Points:

  • The root node will always contain the smallest value in the tree (due to the min-heap property)
  • You need to find the next smallest distinct value after the root's value
  • If there is no second minimum value (for example, if all nodes have the same value), return -1

Example scenarios:

  • If the tree has values like [2, 2, 5, 5, 7], the minimum is 2 and the second minimum is 5
  • If all nodes have the same value, there's no second minimum, so return -1

The solution uses a depth-first search (DFS) to traverse the tree and track the smallest value greater than the root's value. Since the root contains the global minimum, any value larger than the root is a candidate for the second minimum. The algorithm compares all such candidates and returns the smallest one.

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 where each node has at most two children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure with nodes connected in a hierarchical manner without cycles.

DFS

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

The flowchart analysis confirms that DFS is the appropriate approach for this problem. This makes sense because:

  1. We need to traverse the entire tree to examine all node values
  2. We're looking for a specific value (second minimum) that could be anywhere in the tree
  3. DFS allows us to systematically explore each branch of the tree from root to leaves
  4. The tree structure naturally lends itself to recursive DFS implementation

In the given solution, DFS is implemented recursively to:

  • Visit every node in the tree
  • Compare each node's value with the root value (which is the minimum)
  • Track the smallest value that is greater than the root value (which becomes our second minimum)

Conclusion: The flowchart correctly guides us to use DFS for traversing the binary tree to find the second minimum value.

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

Intuition

The key insight comes from understanding the special property of this tree: the root always contains the minimum value of the entire tree. This is because each parent node equals the minimum of its children, creating a min-heap-like structure where values can only increase or stay the same as we go down the tree.

Since we know the root is the smallest value, finding the second minimum becomes straightforward: we need to find the smallest value that is strictly greater than the root's value.

Think about it this way:

  • The root value root.val is guaranteed to be the global minimum
  • Any node with value > root.val is a candidate for being the second minimum
  • We just need to find the smallest among all these candidates

Why does DFS work perfectly here? Because we need to examine every single node in the tree to ensure we don't miss any potential second minimum value. A node deep in the tree might have the second smallest value, so we can't stop early.

The algorithm naturally follows:

  1. Remember the root value as our baseline (the minimum)
  2. Traverse the entire tree using DFS
  3. For each node encountered, if its value is greater than the root value, it's a candidate
  4. Keep track of the smallest candidate seen so far
  5. After traversing all nodes, the smallest candidate is our answer

The beauty of this approach is its simplicity - we're essentially doing a single pass through the tree looking for the smallest value that's bigger than what we already know is the minimum. If we never find such a value (all nodes equal the root), we return -1.

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

Solution Approach

The implementation uses a recursive DFS traversal with a simple tracking mechanism for the second minimum value.

Data Structures Used:

  • Two variables: ans to store the second minimum (initialized to -1) and v to store the root value (the global minimum)
  • The nonlocal keyword in Python to modify outer scope variables from within the nested function

Algorithm Steps:

  1. Initialize tracking variables:

    ans, v = -1, root.val
    • v stores the root value (guaranteed minimum)
    • ans starts at -1 (our default return value if no second minimum exists)
  2. Define the DFS function:

    def dfs(root):
        if root:
            dfs(root.left)
            dfs(root.right)
            # Process current node after visiting children

    The DFS recursively visits left subtree, then right subtree, then processes the current node.

  3. Core logic for finding second minimum:

    if root.val > v:
        ans = root.val if ans == -1 else min(ans, root.val)
    • Only consider nodes with values greater than the minimum (root.val > v)
    • If ans is still -1 (first candidate found), set it to the current node's value
    • Otherwise, keep the smaller value between the current ans and the new candidate

Why this works:

  • Since we traverse every node, we're guaranteed to examine all possible candidates
  • The condition root.val > v filters out all nodes equal to the minimum
  • The min() operation ensures we always keep track of the smallest valid candidate
  • If no node has a value greater than the root, ans remains -1

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.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with a concrete example tree:

       2
      / \
     2   5
        / \
       5   7

Step 1: Initialize variables

  • ans = -1 (no second minimum found yet)
  • v = 2 (root value, which is the global minimum)

Step 2: Start DFS traversal from root (value = 2)

The DFS visits nodes in this order:

  1. Visit root (2)

    • Recursively visit left child
  2. Visit left child (2)

    • This is a leaf, so no children to visit
    • Check: Is 2 > v (i.e., 2 > 2)? No, so skip this node
    • Return to parent
  3. Back at root, now visit right child (5)

    • Recursively visit its left child
  4. Visit node (5) - left child of node (5)

    • This is a leaf
    • Check: Is 5 > v (i.e., 5 > 2)? Yes!
    • Since ans == -1, set ans = 5
    • Return to parent
  5. Back at node (5), visit right child (7)

    • This is a leaf
    • Check: Is 7 > v (i.e., 7 > 2)? Yes!
    • Since ans == 5, compute min(5, 7) = 5
    • Keep ans = 5
    • Return to parent
  6. Back at node (5) - process this node

    • Check: Is 5 > v (i.e., 5 > 2)? Yes!
    • Since ans == 5, compute min(5, 5) = 5
    • Keep ans = 5

Step 3: DFS complete, return result

  • Return ans = 5

The second smallest value in the tree is 5.

Key observations from this walkthrough:

  • Node with value 2 (equal to root) was skipped
  • All nodes with values greater than 2 were considered (both 5s and the 7)
  • The algorithm correctly identified 5 as the smallest value greater than the minimum (2)

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 findSecondMinimumValue(self, root: Optional[TreeNode]) -> int:
12        """
13        Find the second minimum value in a special binary tree.
14        In this tree, each node has exactly 0 or 2 children, and
15        the value of a parent is the minimum of its two children.
16      
17        Args:
18            root: The root node of the binary tree
19          
20        Returns:
21            The second minimum value in the tree, or -1 if it doesn't exist
22        """
23      
24        def dfs(node: Optional[TreeNode]) -> None:
25            """
26            Depth-first search to traverse the tree and find the second minimum value.
27          
28            Args:
29                node: Current node being processed
30            """
31            # Base case: if node is None, return
32            if not node:
33                return
34          
35            # Recursively traverse left and right subtrees
36            dfs(node.left)
37            dfs(node.right)
38          
39            # Access outer scope variables
40            nonlocal second_min, first_min
41          
42            # If current node value is greater than the minimum (root value),
43            # it's a candidate for second minimum
44            if node.val > first_min:
45                # Update second_min: either set it for first time or take minimum
46                if second_min == -1:
47                    second_min = node.val
48                else:
49                    second_min = min(second_min, node.val)
50      
51        # Initialize variables
52        # second_min: stores the second minimum value (-1 if not found)
53        # first_min: stores the minimum value (which is always the root value)
54        second_min = -1
55        first_min = root.val
56      
57        # Start DFS traversal from root
58        dfs(root)
59      
60        # Return the second minimum value
61        return second_min
62
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    // Instance variable to store the second minimum value
18    private int secondMinValue = -1;
19  
20    /**
21     * Finds the second minimum value in a binary tree.
22     * The tree has a special property where each node has either 0 or 2 children,
23     * and if a node has two children, its value is the smaller value among its children.
24     * 
25     * @param root The root node of the binary tree
26     * @return The second minimum value in the tree, or -1 if it doesn't exist
27     */
28    public int findSecondMinimumValue(TreeNode root) {
29        // The root value is always the minimum value in this special tree
30        int minValue = root.val;
31      
32        // Perform depth-first search to find the second minimum value
33        dfs(root, minValue);
34      
35        return secondMinValue;
36    }
37  
38    /**
39     * Performs a depth-first search to find the second minimum value.
40     * 
41     * @param node The current node being processed
42     * @param minValue The minimum value in the tree (root's value)
43     */
44    private void dfs(TreeNode node, int minValue) {
45        // Base case: if node is null, return
46        if (node == null) {
47            return;
48        }
49      
50        // Recursively traverse the left subtree
51        dfs(node.left, minValue);
52      
53        // Recursively traverse the right subtree
54        dfs(node.right, minValue);
55      
56        // If current node's value is greater than the minimum value,
57        // it's a candidate for the second minimum value
58        if (node.val > minValue) {
59            // Update secondMinValue if it hasn't been set yet or if we found a smaller candidate
60            if (secondMinValue == -1) {
61                secondMinValue = node.val;
62            } else {
63                secondMinValue = Math.min(secondMinValue, node.val);
64            }
65        }
66    }
67}
68
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    int findSecondMinimumValue(TreeNode* root) {
15        // Initialize the second minimum value as -1 (indicating not found)
16        int secondMin = -1;
17      
18        // Start DFS traversal with the root's value as the minimum
19        dfs(root, root->val, secondMin);
20      
21        return secondMin;
22    }
23
24private:
25    /**
26     * Performs depth-first search to find the second minimum value in the tree
27     * @param node - current node being processed
28     * @param minVal - the minimum value in the tree (root's value)
29     * @param secondMin - reference to store the second minimum value found
30     */
31    void dfs(TreeNode* node, int minVal, int& secondMin) {
32        // Base case: if node is null, return
33        if (!node) {
34            return;
35        }
36      
37        // Recursively traverse left subtree
38        dfs(node->left, minVal, secondMin);
39      
40        // Recursively traverse right subtree
41        dfs(node->right, minVal, secondMin);
42      
43        // If current node's value is greater than the minimum value,
44        // it's a candidate for the second minimum
45        if (node->val > minVal) {
46            // Update secondMin: if not set yet, use current value;
47            // otherwise, keep the smaller of the two
48            secondMin = (secondMin == -1) ? node->val : min(secondMin, node->val);
49        }
50    }
51};
52
1/**
2 * Definition for a binary tree node.
3 */
4interface TreeNode {
5    val: number;
6    left: TreeNode | null;
7    right: TreeNode | null;
8}
9
10/**
11 * Finds the second minimum value in a special binary tree where each node has exactly 0 or 2 children
12 * and the value of a parent node is the minimum of its two children
13 * @param root - The root node of the binary tree
14 * @returns The second minimum value in the tree, or -1 if it doesn't exist
15 */
16function findSecondMinimumValue(root: TreeNode | null): number {
17    // Initialize result as -1 (indicates no second minimum found)
18    let secondMinimum: number = -1;
19  
20    // Store the root value (which is the minimum value in the tree)
21    const minimumValue: number = root!.val;
22  
23    /**
24     * Depth-first search to traverse the tree and find the second minimum value
25     * @param node - Current node being processed
26     */
27    function dfs(node: TreeNode | null): void {
28        // Base case: if node is null, return
29        if (!node) {
30            return;
31        }
32      
33        // Recursively traverse left subtree
34        dfs(node.left);
35      
36        // Recursively traverse right subtree
37        dfs(node.right);
38      
39        // Check if current node value is greater than the minimum value
40        if (node.val > minimumValue) {
41            // Update secondMinimum if it hasn't been set or if current value is smaller
42            if (secondMinimum === -1 || secondMinimum > node.val) {
43                secondMinimum = node.val;
44            }
45        }
46    }
47  
48    // Start DFS traversal from root
49    dfs(root);
50  
51    // Return the second minimum value (or -1 if not found)
52    return secondMinimum;
53}
54

Time and Space Complexity

Time Complexity: O(n) where n is the number of nodes in the binary tree. The DFS traversal visits each node exactly once, performing constant time operations at each node (comparison and assignment operations).

Space Complexity: O(h) where h is the height of the binary tree. This space is used by the recursive call stack during the DFS traversal. In the worst case of a skewed tree, this could be O(n), while in the best case of a balanced tree, it would be O(log n). The additional space used for the variables ans and v is O(1).

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

Common Pitfalls

1. Early Termination Optimization Leading to Incorrect Results

A common pitfall is trying to optimize the traversal by stopping early when we think we've found the answer. For example:

Incorrect Approach:

def findSecondMinimumValue(self, root: Optional[TreeNode]) -> int:
    def dfs(node):
        if not node:
            return
      
        nonlocal second_min, first_min
      
        # WRONG: Early termination when we find a value > first_min
        if node.val > first_min:
            second_min = node.val
            return  # Stop searching - THIS IS WRONG!
      
        dfs(node.left)
        dfs(node.right)
  
    second_min = -1
    first_min = root.val
    dfs(root)
    return second_min

Why this fails: Due to the tree's special property where parent.val = min(left.val, right.val), the second minimum value might not be at the shallowest level. Consider this tree:

      2
     / \
    2   5
       / \
      5   3

If we stop at the first node with value > 2 (which is 5), we miss the actual second minimum value 3.

Solution: Always traverse the entire tree to ensure we find the global second minimum.

2. Incorrect Handling of Duplicate Values

Another pitfall is not properly handling trees where multiple nodes have the minimum value:

Incorrect Approach:

def findSecondMinimumValue(self, root: Optional[TreeNode]) -> int:
    def dfs(node):
        if not node:
            return
      
        nonlocal second_min
      
        # WRONG: Not checking if value is actually greater than minimum
        if node.val != root.val:  # This seems logical but...
            if second_min == -1:
                second_min = node.val
            else:
                second_min = min(second_min, node.val)
      
        dfs(node.left)
        dfs(node.right)

Why this might seem correct but has issues: While node.val != root.val is equivalent to node.val > root.val in this specific tree structure (since parent = min of children), using inequality comparison makes the logic clearer and more maintainable.

3. Forgetting the Tree's Special Properties

A critical pitfall is attempting pruning optimizations without fully understanding the tree structure:

Incorrect Pruning:

def findSecondMinimumValue(self, root: Optional[TreeNode]) -> int:
    def dfs(node):
        if not node:
            return
      
        nonlocal second_min, first_min
      
        # WRONG: Assuming we can skip subtrees
        if node.val == first_min:
            # Skip this subtree - INCORRECT!
            return
      
        if node.val > first_min:
            second_min = node.val if second_min == -1 else min(second_min, node.val)
      
        dfs(node.left)
        dfs(node.right)

Why this fails: Even if a node has the minimum value, its children might have different values that could be candidates for the second minimum. The tree property only guarantees parent = min(children), not that all descendants have the same value.

Correct Solution: The provided solution avoids all these pitfalls by:

  1. Traversing the entire tree without early termination
  2. Using clear comparison logic (node.val > first_min)
  3. Not making assumptions about subtrees based on parent values
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More