Facebook Pixel

653. Two Sum IV - Input is a BST

Problem Description

You are given the root of a binary search tree (BST) and an integer k. Your task is to determine whether there exist two different nodes in the BST whose values sum up to exactly k.

The problem asks you to return true if you can find any two distinct elements in the tree that add up to the target value k, and false if no such pair exists.

For example:

  • If the BST contains nodes with values [5, 3, 6, 2, 4, 7] and k = 9, you should return true because 3 + 6 = 9
  • If the BST contains nodes with values [5, 3, 6, 2, 4, 7] and k = 28, you should return false because no two values sum to 28

The solution uses a depth-first search (DFS) traversal combined with a hash set. As it visits each node, it:

  1. Checks if the complement value (k - current_value) has already been seen in previous nodes
  2. If the complement exists, it means we found two numbers that sum to k, so return true
  3. Otherwise, add the current value to the set of visited values and continue searching
  4. The search continues recursively through the left and right subtrees until either a valid pair is found or all nodes are processed

The vis set keeps track of all node values encountered so far, enabling constant-time lookup to check if the required complement exists.

Flowchart Walkthrough

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

Is it a graph?

  • Yes: While this is a binary search tree problem, a tree is a special type of graph (specifically, an acyclic connected graph). Trees can be traversed using graph traversal algorithms.

Is it a tree?

  • Yes: The problem explicitly states we're working with a Binary Search Tree (BST), which is indeed a tree data structure.

DFS

  • Yes: Since we're dealing with a tree, DFS (Depth-First Search) is a natural choice for traversal.

Conclusion: The flowchart suggests using DFS for this problem.

This makes perfect sense for our Two Sum IV problem because:

  1. We need to visit every node in the tree to check all possible pairs
  2. DFS allows us to traverse the tree systematically while maintaining a set of previously seen values
  3. As we traverse using DFS, we can check if the complement (k - current_value) exists in our set of visited nodes
  4. The recursive nature of DFS fits well with the tree structure, allowing us to explore left and right subtrees naturally

The solution implements this DFS approach by recursively visiting each node, checking for the complement value, and adding the current value to the visited set before continuing the traversal.

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

Intuition

The key insight for this problem comes from recognizing that we're essentially solving the classic Two Sum problem, but with values stored in a tree structure instead of an array.

In the standard Two Sum problem, we use a hash set to track values we've seen so far. For each number x, we check if k - x exists in our set. If it does, we've found our pair. The same principle applies here - we just need to traverse the tree to access all values.

Why does DFS work well here? As we traverse the tree, we're building up our "seen values" set one node at a time. At each node, we ask: "Have I seen the number that would complete this pair?" This is checking if k - current_value exists in our set. If yes, we're done. If not, we add the current value to our set and keep searching.

The beauty of this approach is that it doesn't matter that we have a BST specifically - we're not using the sorted property. We're simply visiting every node once and checking against our growing collection of seen values. The DFS traversal ensures we systematically explore all nodes while maintaining our set of visited values across the entire traversal.

The recursive nature of DFS matches the recursive structure of the tree perfectly. We process the current node, then recursively check the left subtree, and if that doesn't find a pair, check the right subtree. The or operation in dfs(root.left) or dfs(root.right) ensures we stop as soon as we find a valid pair, making the solution efficient.

This approach has a time complexity of O(n) where we visit each node once, and space complexity of O(n) for storing the visited values in the set plus the recursion stack.

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

Solution Approach

The solution implements a DFS traversal with a hash set to track visited values. Let's walk through the implementation step by step:

Data Structure Used:

  • A hash set vis to store all node values we've encountered during traversal

Algorithm Implementation:

  1. Initialize the visited set: We create an empty set vis that will store all node values we've seen so far.

  2. Define the DFS function: The recursive dfs function takes a node as input and:

    • Base case: If the current node is None, return False (no pair found in this empty subtree)
    • Check for complement: Calculate the complement value as k - root.val. If this complement exists in our vis set, we've found two numbers that sum to k, so return True
    • Add current value: Add the current node's value to the vis set using vis.add(root.val)
    • Recursive exploration: Recursively search the left and right subtrees using dfs(root.left) or dfs(root.right). The or operator ensures we return True as soon as either subtree finds a valid pair
  3. Start the traversal: Call dfs(root) to begin the search from the root node.

Key Pattern - Two Sum with DFS: The pattern here combines the classic Two Sum hash set approach with tree traversal. As we visit each node during DFS:

  • We check: "Does k - current_value exist in our set?"
  • If yes → we found our pair
  • If no → add current value to set and continue searching

Why this works:

  • The set gives us O(1) lookup time to check if a complement exists
  • DFS ensures we visit every node exactly once
  • The recursive structure naturally handles the tree traversal
  • We don't need to worry about using the same element twice because we check for the complement before adding the current value to the set

This approach elegantly solves the problem in a single pass through the tree with O(n) time and space complexity.

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 walk through a concrete example to see how the DFS with hash set approach works.

Example Tree:

       5
      / \
     3   6
    / \   \
   2   4   7

Target k = 9

Step-by-step execution:

  1. Start at root (5):

    • Check: Is 9 - 5 = 4 in vis? No (vis is empty)
    • Add 5 to vis: vis = {5}
    • Recurse to left child (3)
  2. Visit node (3):

    • Check: Is 9 - 3 = 6 in vis? No (vis only has {5})
    • Add 3 to vis: vis = {5, 3}
    • Recurse to left child (2)
  3. Visit node (2):

    • Check: Is 9 - 2 = 7 in vis? No (vis has {5, 3})
    • Add 2 to vis: vis = {5, 3, 2}
    • No children, return False
    • Backtrack to node 3, recurse to right child (4)
  4. Visit node (4):

    • Check: Is 9 - 4 = 5 in vis? Yes! (5 is in vis)
    • Return True immediately

The function returns true because we found that 4 + 5 = 9. The algorithm discovered this pair by finding that when we visited node 4, its complement (5) was already in our visited set.

Key insight: Notice how we found the pair without visiting all nodes. Once we found a valid pair (4 and 5), the recursion stopped due to the or operator short-circuiting. If no pair existed, we would continue until all nodes were visited.

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
8class Solution:
9    def findTarget(self, root: Optional[TreeNode], k: int) -> bool:
10        """
11        Finds if there exist two elements in the BST such that their sum equals k.
12      
13        Args:
14            root: Root node of the binary search tree
15            k: Target sum to find
16          
17        Returns:
18            True if two elements sum to k, False otherwise
19        """
20      
21        def dfs(node: Optional[TreeNode]) -> bool:
22            """
23            Performs depth-first search to find two nodes that sum to k.
24          
25            Args:
26                node: Current node being processed
27              
28            Returns:
29                True if pair found, False otherwise
30            """
31            # Base case: reached null node
32            if node is None:
33                return False
34          
35            # Check if complement of current value exists in visited set
36            complement = k - node.val
37            if complement in visited_values:
38                return True
39          
40            # Add current node value to visited set
41            visited_values.add(node.val)
42          
43            # Recursively search left and right subtrees
44            return dfs(node.left) or dfs(node.right)
45      
46        # Initialize set to store visited node values
47        visited_values = set()
48      
49        # Start DFS from root
50        return dfs(root)
51
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    // Set to store visited node values for O(1) lookup
18    private Set<Integer> visitedValues = new HashSet<>();
19  
20    // Target sum we're looking for
21    private int targetSum;
22
23    /**
24     * Finds if there exist two elements in the BST that add up to the target sum.
25     * 
26     * @param root The root of the binary search tree
27     * @param k The target sum to find
28     * @return true if two elements sum to k, false otherwise
29     */
30    public boolean findTarget(TreeNode root, int k) {
31        this.targetSum = k;
32        return depthFirstSearch(root);
33    }
34
35    /**
36     * Performs depth-first search to find two numbers that sum to target.
37     * Uses the two-sum approach: for each node value x, check if (target - x) exists.
38     * 
39     * @param node Current node being processed
40     * @return true if a valid pair is found, false otherwise
41     */
42    private boolean depthFirstSearch(TreeNode node) {
43        // Base case: null node
44        if (node == null) {
45            return false;
46        }
47      
48        // Check if complement (targetSum - current value) exists in visited set
49        if (visitedValues.contains(targetSum - node.val)) {
50            return true;
51        }
52      
53        // Add current node value to visited set
54        visitedValues.add(node.val);
55      
56        // Recursively search left and right subtrees
57        return depthFirstSearch(node.left) || depthFirstSearch(node.right);
58    }
59}
60
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     * Find if there exist two elements in a BST that sum up to target k
16     * @param root: The root of the binary search tree
17     * @param k: The target sum to find
18     * @return: True if two elements sum to k, false otherwise
19     */
20    bool findTarget(TreeNode* root, int k) {
21        // Hash set to store visited node values
22        unordered_set<int> visitedValues;
23      
24        // Lambda function for depth-first search traversal
25        function<bool(TreeNode*)> dfs = [&](TreeNode* currentNode) -> bool {
26            // Base case: if node is null, return false
27            if (!currentNode) {
28                return false;
29            }
30          
31            // Check if complement (k - current value) exists in visited set
32            // If it exists, we found two numbers that sum to k
33            if (visitedValues.count(k - currentNode->val)) {
34                return true;
35            }
36          
37            // Add current node's value to visited set
38            visitedValues.insert(currentNode->val);
39          
40            // Recursively search left and right subtrees
41            // Return true if either subtree contains the pair
42            return dfs(currentNode->left) || dfs(currentNode->right);
43        };
44      
45        // Start DFS from root
46        return dfs(root);
47    }
48};
49
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// Set to store visited node values
16let visitedValues: Set<number>;
17
18/**
19 * Performs depth-first search to find if two numbers in the tree sum to target
20 * @param node - Current node being processed
21 * @param target - The target sum we're looking for
22 * @returns true if a pair is found, false otherwise
23 */
24function depthFirstSearch(node: TreeNode | null, target: number): boolean {
25    // Base case: if node is null, return false
26    if (!node) {
27        return false;
28    }
29  
30    // Check if the complement (target - current value) exists in our set
31    const complement: number = target - node.val;
32    if (visitedValues.has(complement)) {
33        return true;
34    }
35  
36    // Add current node's value to the set of visited values
37    visitedValues.add(node.val);
38  
39    // Recursively search left and right subtrees
40    // Return true if either subtree finds a valid pair
41    return depthFirstSearch(node.left, target) || depthFirstSearch(node.right, target);
42}
43
44/**
45 * Finds if there exist two elements in the BST that sum to k
46 * @param root - Root of the binary search tree
47 * @param k - Target sum to find
48 * @returns true if two elements sum to k, false otherwise
49 */
50function findTarget(root: TreeNode | null, k: number): boolean {
51    // Initialize the set to track visited values
52    visitedValues = new Set<number>();
53  
54    // Start the depth-first search from root
55    return depthFirstSearch(root, k);
56}
57

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 search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. For each node visited, the algorithm performs:

  • A constant time check to see if k - root.val exists in the hash set (O(1) average case)
  • A constant time insertion of root.val into the hash set (O(1) average case)

Since we visit each of the n nodes once and perform O(1) operations at each node, the overall time complexity is O(n).

Space Complexity: O(n) where n is the number of nodes in the binary tree.

The space complexity consists of two components:

  • The hash set vis that stores the values of visited nodes. In the worst case, we might store all n node values in the set before finding the target sum or determining it doesn't exist, requiring O(n) space.
  • The recursive call stack for DFS. In the worst case (a skewed tree), the recursion depth can be O(n). In the best case (a balanced tree), the recursion depth would be O(log n).

Taking the worst case for both components, the overall space complexity is O(n).

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

Common Pitfalls

Pitfall 1: Using the Same Element Twice

The Problem: A common mistake is forgetting to handle the case where k is exactly double a node's value. For instance, if a node has value 5 and k = 10, the algorithm might incorrectly return true by using the same node twice (5 + 5 = 10).

Why It Happens: After adding a value to the visited set, if we encounter the same value again in the tree, the complement check might match with itself.

The Solution: The current implementation actually handles this correctly! By checking for the complement before adding the current value to the set, we ensure we never match a node with itself. The order matters:

# CORRECT: Check first, then add
if complement in visited_values:  # Check for complement
    return True
visited_values.add(node.val)      # Then add current value

# WRONG: Add first, then check (could match with itself)
visited_values.add(node.val)      # If we add first...
if complement in visited_values:  # ...we might match the same element
    return True

Pitfall 2: Not Utilizing BST Properties

The Problem: The current solution treats the tree as a generic binary tree and doesn't leverage the BST property (left subtree < root < right subtree). This results in O(n) space complexity for the hash set.

Why It Matters: For very large trees, storing all values in memory could be problematic.

Alternative Solution: Use two pointers on an in-order traversal:

def findTarget(self, root: Optional[TreeNode], k: int) -> bool:
    # Convert BST to sorted array via in-order traversal
    def inorder(node):
        if not node:
            return []
        return inorder(node.left) + [node.val] + inorder(node.right)
  
    # Use two pointers on sorted array
    arr = inorder(root)
    left, right = 0, len(arr) - 1
  
    while left < right:
        curr_sum = arr[left] + arr[right]
        if curr_sum == k:
            return True
        elif curr_sum < k:
            left += 1
        else:
            right -= 1
  
    return False

Pitfall 3: Integer Overflow in Other Languages

The Problem: In languages like C++ or Java, calculating k - node.val could cause integer overflow if k is at the minimum integer value and node.val is positive.

Why It Happens: When k = INT_MIN and we subtract a positive value, the result could overflow.

The Solution: In Python, this isn't an issue due to arbitrary precision integers. In other languages, use careful bounds checking or cast to a larger data type:

// Java example - use long to avoid overflow
long complement = (long)k - node.val;

Pitfall 4: Modifying the Visited Set During Recursion

The Problem: If you accidentally pass the visited set by reference and modify it incorrectly in recursive calls, you might lose track of previously seen values.

Why It Happens: Confusion about variable scope in nested functions.

The Solution: The current implementation correctly uses a closure where the visited_values set is defined in the outer function scope and shared across all recursive calls. This ensures all nodes' values persist in the set throughout the entire traversal.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

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

Load More