Facebook Pixel

1469. Find All The Lonely Nodes 🔒

Problem Description

A lonely node in a binary tree is defined as a node that is the only child of its parent node - meaning its parent has exactly one child (either left or right, but not both). The root node can never be lonely since it has no parent.

Given the root of a binary tree, you need to find all lonely nodes in the tree and return their values in an array. The values can be returned in any order.

For example, if a parent node has only a left child, that left child is lonely. Similarly, if a parent node has only a right child, that right child is lonely. Nodes that have a sibling (their parent has both left and right children) are not lonely.

The solution uses a depth-first search (DFS) approach to traverse the tree. During traversal, it checks each node to see if it has exactly one child. If a node has only one child, that child's value is added to the result array. The key insight is to check from the parent's perspective - when at a parent node, we check if it has only one child and mark that child as lonely.

The condition root.left == root.right in the code is a clever way to check if both children are None (making it a leaf node) since None == None evaluates to True. The function then checks if either the left or right child is missing (but not both), and adds the existing child's value to the answer array before recursively exploring the subtree.

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 tree problem, a tree is a special type of graph (specifically, a connected acyclic graph). Binary trees have nodes and edges connecting parent nodes to child nodes.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure where we need to traverse from the root to find lonely nodes.

DFS

  • Yes: We arrive at DFS (Depth-First Search) as our algorithm choice.

Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem. This makes perfect sense because:

  1. We need to traverse the entire tree to find all lonely nodes
  2. DFS allows us to explore each path from root to leaves systematically
  3. At each parent node, we can check if it has exactly one child (making that child lonely)
  4. The recursive nature of DFS naturally fits the recursive structure of trees

The DFS approach lets us visit each node exactly once, check if its children are lonely, and collect all lonely node values in our answer array. This gives us an efficient O(n) time complexity solution where n is the number of nodes in the tree.

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

Intuition

The key insight is to think about lonely nodes from the parent's perspective rather than the node's own perspective. A node becomes lonely when its parent has exactly one child.

When we traverse the tree, at each parent node we can easily check: does this node have only a left child? Or only a right child? If either condition is true, that single child is lonely.

This naturally leads to a DFS traversal where we:

  1. Visit each node in the tree
  2. Check if it has exactly one child (either left OR right, but not both)
  3. If so, add that child's value to our result

The clever part of the solution is the condition root.left == root.right. This checks if both children are None (making it a leaf node), since None == None evaluates to True. Leaf nodes can't have lonely children, so we return early.

Why DFS? Because we need to examine every parent-child relationship in the tree. DFS lets us systematically visit every node and check its children. The recursive nature of DFS perfectly matches the recursive structure of the tree - we check the current node's children for loneliness, then recursively do the same for each subtree.

The algorithm flows naturally: start at the root, check if any of its children are lonely, then recursively apply the same logic to the left and right subtrees. This ensures we find all lonely nodes throughout the entire tree.

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

Solution Approach

The solution implements a DFS traversal using a recursive helper function. Let's walk through the implementation step by step:

Data Structure: We use a list ans to collect all lonely node values as we traverse the tree.

Main Algorithm - DFS Function:

The dfs function performs the following steps at each node:

  1. Base Case Check: if root is None or root.left == root.right:

    • If the current node is None, there's nothing to process
    • If root.left == root.right, both children are None (since None == None is True), meaning this is a leaf node with no children to be lonely
  2. Check for Lonely Left Child: if root.right is None:

    • If the right child is None but we haven't returned (meaning left child exists), then the left child is lonely
    • Add root.left.val to the answer array
  3. Check for Lonely Right Child: if root.left is None:

    • If the left child is None but we haven't returned (meaning right child exists), then the right child is lonely
    • Add root.right.val to the answer array
  4. Recursive Traversal:

    • Call dfs(root.left) to process the left subtree
    • Call dfs(root.right) to process the right subtree

Execution Flow:

  • Initialize an empty list ans to store results
  • Start DFS from the root node
  • The function recursively visits every node in the tree
  • At each parent node, it identifies and records lonely children
  • Return the collected list of lonely node values

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 recursion call stack. 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 concrete example to understand how the solution identifies lonely nodes.

Consider this binary tree:

        1
       / \
      2   3
       \
        4

Step-by-step DFS traversal:

  1. Start at root (1):

    • Check if node 1 is None or a leaf? No (it has children)
    • Is right child (3) None? No
    • Is left child (2) None? No
    • Both children exist, so neither is lonely
    • Recursively call dfs(2) and dfs(3)
  2. Visit left child (2):

    • Check if node 2 is None or a leaf? No (it has a right child)
    • Is right child (4) None? No
    • Is left child None? Yes!
    • Since only the right child exists, node 4 is lonely
    • Add 4 to answer: ans = [4]
    • Recursively call dfs(None) and dfs(4)
  3. Visit None (left of 2):

    • Base case: return immediately
  4. Visit node 4:

    • Check if node 4 is None or a leaf? Yes (leaf node)
    • root.left == root.right → None == None → True
    • Return immediately (leaf nodes have no children to be lonely)
  5. Back to root, visit right child (3):

    • Check if node 3 is None or a leaf? Yes (leaf node)
    • root.left == root.right → None == None → True
    • Return immediately

Final Result: ans = [4]

Node 4 is the only lonely node because it's the only child of its parent (node 2). Nodes 2 and 3 are not lonely because they both have a sibling (node 1 has two children).

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, List
9
10class Solution:
11    def getLonelyNodes(self, root: Optional[TreeNode]) -> List[int]:
12        """
13        Find all lonely nodes in a binary tree.
14        A lonely node is defined as a node that has exactly one child.
15      
16        Args:
17            root: The root node of the binary tree
18          
19        Returns:
20            A list containing values of all lonely nodes
21        """
22      
23        def dfs(node: Optional[TreeNode]) -> None:
24            """
25            Depth-first search to traverse the tree and identify lonely nodes.
26          
27            Args:
28                node: Current node being processed
29            """
30            # Base case: if node is None or node is a leaf (both children are None)
31            if node is None or (node.left is None and node.right is None):
32                return
33          
34            # Check if current node has only right child (left is None)
35            if node.left is None:
36                result.append(node.right.val)
37          
38            # Check if current node has only left child (right is None)
39            if node.right is None:
40                result.append(node.left.val)
41          
42            # Recursively traverse left subtree
43            dfs(node.left)
44          
45            # Recursively traverse right subtree
46            dfs(node.right)
47      
48        # Initialize result list to store lonely node values
49        result = []
50      
51        # Start DFS traversal from root
52        dfs(root)
53      
54        return result
55
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    // List to store values of lonely nodes (nodes with exactly one sibling)
18    private List<Integer> lonelyNodesList;
19  
20    /**
21     * Finds all lonely nodes in a binary tree.
22     * A lonely node is defined as a node that is the only child of its parent.
23     * 
24     * @param root The root of the binary tree
25     * @return List of values of all lonely nodes
26     */
27    public List<Integer> getLonelyNodes(TreeNode root) {
28        lonelyNodesList = new ArrayList<>();
29        findLonelyNodes(root);
30        return lonelyNodesList;
31    }
32  
33    /**
34     * Performs depth-first search to identify lonely nodes.
35     * 
36     * @param node Current node being processed
37     */
38    private void findLonelyNodes(TreeNode node) {
39        // Base case: if node is null or is a leaf node (has no children)
40        if (node == null || (node.left == null && node.right == null)) {
41            return;
42        }
43      
44        // If only right child exists, right child is lonely
45        if (node.left == null) {
46            lonelyNodesList.add(node.right.val);
47        }
48      
49        // If only left child exists, left child is lonely
50        if (node.right == null) {
51            lonelyNodesList.add(node.left.val);
52        }
53      
54        // Recursively process left and right subtrees
55        findLonelyNodes(node.left);
56        findLonelyNodes(node.right);
57    }
58}
59
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    vector<int> getLonelyNodes(TreeNode* root) {
15        vector<int> result;
16      
17        // Helper function to traverse the tree and find lonely nodes
18        function<void(TreeNode*)> dfs = [&](TreeNode* node) -> void {
19            // Base case: if node is null or is a leaf node (has no children)
20            if (!node || (!node->left && !node->right)) {
21                return;
22            }
23          
24            // Check if the current node has only a right child
25            // If so, the right child is a lonely node
26            if (!node->left && node->right) {
27                result.push_back(node->right->val);
28            }
29          
30            // Check if the current node has only a left child
31            // If so, the left child is a lonely node
32            if (node->left && !node->right) {
33                result.push_back(node->left->val);
34            }
35          
36            // Recursively traverse the left subtree
37            dfs(node->left);
38          
39            // Recursively traverse the right subtree
40            dfs(node->right);
41        };
42      
43        // Start the DFS traversal from the root
44        dfs(root);
45      
46        return result;
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/**
16 * Finds all lonely nodes in a binary tree.
17 * A lonely node is a node that is the only child of its parent.
18 * 
19 * @param root - The root node of the binary tree
20 * @returns An array containing values of all lonely nodes
21 */
22function getLonelyNodes(root: TreeNode | null): number[] {
23    const result: number[] = [];
24  
25    /**
26     * Performs depth-first search to find lonely nodes
27     * 
28     * @param node - Current node being processed
29     */
30    const traverseTree = (node: TreeNode | null): void => {
31        // Base case: return if node is null or if node has no children
32        // (node.left === node.right means both are null)
33        if (!node || node.left === node.right) {
34            return;
35        }
36      
37        // If left child is null but right child exists, right child is lonely
38        if (!node.left && node.right) {
39            result.push(node.right.val);
40        }
41      
42        // If right child is null but left child exists, left child is lonely
43        if (!node.right && node.left) {
44            result.push(node.left.val);
45        }
46      
47        // Recursively traverse left and right subtrees
48        traverseTree(node.left);
49        traverseTree(node.right);
50    };
51  
52    // Start the traversal from root
53    traverseTree(root);
54  
55    return result;
56}
57

Time and Space Complexity

Time Complexity: O(n)

The algorithm uses depth-first search (DFS) to traverse the binary tree. Each node in the tree is visited exactly once. At each node, we perform constant time operations:

  • Checking if the node is None or if both children are equal (root.left == root.right)
  • Checking if either left or right child is None and appending to the result list if needed
  • Making recursive calls to left and right children

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

Space Complexity: O(n)

The space complexity consists of two components:

  1. Recursion call stack: In the worst case (a skewed tree), the recursion depth can be O(n). In the best case (a balanced tree), it would be O(log n).
  2. Result list (ans): In the worst case, almost all nodes could be lonely nodes (except the root and leaf nodes), so the result list could contain up to O(n) elements.

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

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

Common Pitfalls

1. Incorrectly Checking Node's Own Status Instead of Children's Status

A frequent mistake is trying to determine if the current node itself is lonely, rather than checking if its children are lonely. Remember that we need to check from the parent's perspective.

Incorrect approach:

def dfs(node, parent):
    if node is None:
        return
  
    # Wrong: Checking if current node is lonely
    if parent and ((parent.left == node and parent.right is None) or 
                   (parent.right == node and parent.left is None)):
        result.append(node.val)
  
    dfs(node.left, node)
    dfs(node.right, node)

Correct approach:

def dfs(node):
    if node is None or (node.left is None and node.right is None):
        return
  
    # Correct: Checking if children are lonely
    if node.left is None and node.right is not None:
        result.append(node.right.val)
    if node.right is None and node.left is not None:
        result.append(node.left.val)
  
    dfs(node.left)
    dfs(node.right)

2. Forgetting to Handle the Case Where Both Children Exist

When both children exist, neither is lonely. Some implementations mistakenly add both children when they both exist.

Incorrect:

def dfs(node):
    if node is None:
        return
  
    # Wrong: This would add children even when both exist
    if node.left:
        result.append(node.left.val)
    if node.right:
        result.append(node.right.val)

3. Not Properly Handling Leaf Nodes

Leaf nodes have no children, so they cannot have lonely children. Failing to skip leaf nodes can lead to null pointer exceptions.

Problematic code:

def dfs(node):
    if node is None:
        return
  
    # Dangerous: Not checking if children exist before accessing them
    if node.right is None:
        result.append(node.left.val)  # This could fail if node.left is also None

Safe implementation:

def dfs(node):
    if node is None or (node.left is None and node.right is None):
        return  # Properly skip leaf nodes
  
    if node.right is None:
        result.append(node.left.val)  # Safe because we know at least one child exists

4. Using root.left == root.right Without Understanding Its Purpose

While root.left == root.right is a clever way to check if both children are None, using it without understanding can lead to confusion or bugs if the tree node implementation changes.

More explicit alternative:

# Clearer for readability
if node is None or (node.left is None and node.right is None):
    return

This makes the intent more obvious and is less prone to misunderstanding during code maintenance.

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

How many times is a tree node visited in a depth first search?


Recommended Readings

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

Load More