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:
- We need to traverse the entire tree to find all lonely nodes
- DFS allows us to explore each path from root to leaves systematically
- At each parent node, we can check if it has exactly one child (making that child lonely)
- 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.
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:
- Visit each node in the tree
- Check if it has exactly one child (either left OR right, but not both)
- 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:
-
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 areNone
(sinceNone == None
isTrue
), meaning this is a leaf node with no children to be lonely
- If the current node is
-
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
- If the right child is
-
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
- If the left child is
-
Recursive Traversal:
- Call
dfs(root.left)
to process the left subtree - Call
dfs(root.right)
to process the right subtree
- Call
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 EvaluatorExample 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:
-
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)
-
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)
-
Visit None (left of 2):
- Base case: return immediately
-
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)
-
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:
- 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 beO(log n)
. - 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 toO(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.
How many times is a tree node visited in a depth first search?
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
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!