LeetCode 2196 - Create Binary Tree From Descriptions
The problem asks us to construct a binary tree from a list of relationships, where each relationship is given as [parent, child, isLeft].
Difficulty: 🟡 Medium
Topics: Array, Hash Table, Tree, Binary Tree
Solution
Problem Understanding
The problem asks us to construct a binary tree from a list of relationships, where each relationship is given as [parent, child, isLeft]. Here, parent is the value of a node, child is the value of its direct descendant, and isLeft indicates whether child is a left child (1) or a right child (0). All node values are unique, so each value corresponds to exactly one tree node.
The input is a 2D array of size up to 10,000, and values can be as large as 100,000. This tells us that the solution needs to be efficient in both time and space, as a naive O(n²) search to find parents or children could be too slow.
The output is the root node of the tree, which is defined as the node that does not appear as a child in any description. The problem guarantees that the binary tree is valid, meaning there are no cycles and each child has exactly one parent.
Important edge cases include the tree being essentially a linked list (all nodes either left or right), or the smallest possible input where there is only one parent-child pair.
Approaches
A brute-force approach would create tree nodes on demand and, for each description, search through existing nodes to attach the child to the correct parent. This approach is correct but inefficient because searching for nodes repeatedly could lead to O(n²) complexity.
The optimal approach leverages hash maps to efficiently store and retrieve nodes and to determine the root. We can create all nodes in one pass using a value → TreeNode map and another set to keep track of children. The node that never appears as a child is the root. This avoids repeated searches and allows us to attach children in O(1) time per description.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Repeatedly searches for parent nodes in a growing list of nodes |
| Optimal | O(n) | O(n) | Uses hash maps to track nodes and children efficiently |
Algorithm Walkthrough
- Initialize an empty dictionary
nodesto map values toTreeNodeobjects. This allows instant retrieval or creation of nodes by value. - Initialize an empty set
childrento keep track of all node values that appear as a child. This will help us identify the root later. - Iterate through each description
[parent, child, isLeft]. For each value, check if it exists innodes. If not, create a newTreeNodeand add it tonodes. - Update the
childrenset by addingchild. - Attach the
childnode to theparentnode as a left child ifisLeft == 1or as a right child ifisLeft == 0. - After processing all descriptions, the root is the node whose value is in
nodesbut not inchildren. Retrieve and return this node.
Why it works: Each node is created exactly once and attached according to the descriptions. Since the problem guarantees a valid tree, there will be exactly one node that is not a child, ensuring a unique root. Using hash maps ensures that node retrieval is O(1), giving an overall linear solution.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def createBinaryTree(self, descriptions: list[list[int]]) -> TreeNode:
nodes = {}
children = set()
for parent_val, child_val, is_left in descriptions:
if parent_val not in nodes:
nodes[parent_val] = TreeNode(parent_val)
if child_val not in nodes:
nodes[child_val] = TreeNode(child_val)
children.add(child_val)
if is_left:
nodes[parent_val].left = nodes[child_val]
else:
nodes[parent_val].right = nodes[child_val]
for val in nodes:
if val not in children:
return nodes[val]
The Python implementation uses a dictionary to store nodes and a set to track children. Each description is processed once, creating nodes as needed and linking them based on the isLeft flag. Finally, we iterate over the node values to find the one not in the children set, which is the root.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func createBinaryTree(descriptions [][]int) *TreeNode {
nodes := make(map[int]*TreeNode)
children := make(map[int]bool)
for _, desc := range descriptions {
parentVal, childVal, isLeft := desc[0], desc[1], desc[2]
if _, ok := nodes[parentVal]; !ok {
nodes[parentVal] = &TreeNode{Val: parentVal}
}
if _, ok := nodes[childVal]; !ok {
nodes[childVal] = &TreeNode{Val: childVal}
}
children[childVal] = true
if isLeft == 1 {
nodes[parentVal].Left = nodes[childVal]
} else {
nodes[parentVal].Right = nodes[childVal]
}
}
for val, node := range nodes {
if !children[val] {
return node
}
}
return nil
}
The Go implementation mirrors the Python solution but uses a map[int]*TreeNode for nodes and map[int]bool for tracking children. The logic for attaching children and identifying the root is identical, with nil handling implicit when returning the root if not found.
Worked Examples
Example 1: [[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]]
| Step | Nodes Map | Children Set | Operation |
|---|---|---|---|
| 1 | {20, 15} | {15} | Add left child 15 to 20 |
| 2 | {20, 15, 17} | {15,17} | Add right child 17 to 20 |
| 3 | {20,15,17,50} | {15,17,20} | Add left child 20 to 50 |
| 4 | {20,15,17,50,80} | {15,17,20,80} | Add right child 80 to 50 |
| 5 | {20,15,17,50,80,19} | {15,17,20,80,19} | Add left child 19 to 80 |
| Final | Root is 50 (not in children set) | Return node 50 |
Example 2: [[1,2,1],[2,3,0],[3,4,1]]
| Step | Nodes Map | Children Set | Operation |
|---|---|---|---|
| 1 | {1,2} | {2} | Add left child 2 to 1 |
| 2 | {1,2,3} | {2,3} | Add right child 3 to 2 |
| 3 | {1,2,3,4} | {2,3,4} | Add left child 4 to 3 |
| Final | Root is 1 (not in children set) | Return node 1 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | We iterate over each description once and perform O(1) operations per description using hash maps |
| Space | O(n) | We store all nodes in a map and track all children in a set, both of size O(n) |
The algorithm is linear because every description is processed exactly once, and hash map lookups and insertions are O(1).
Test Cases
# test cases
sol = Solution()
# Example 1
assert sol.createBinaryTree([[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]]).val == 50
# Example 2
assert sol.createBinaryTree([[1,2,1],[2,3,0],[3,4,1]]).val == 1
# Single pair
assert sol.createBinaryTree([[10,5,1]]).val == 10
# Left-skewed tree
assert sol.createBinaryTree([[1,2,1],[2,3,1],[3,4,1]]).val == 1
# Right-skewed tree
assert sol.createBinaryTree([[1,2,0],[2,3,0],[3,4,0]]).val == 1
# Mixed tree
assert sol.createBinaryTree([[5,3,1],[5,8,0],[3,2,1],[3,4,0],[8,7,1]]).val == 5
| Test | Why |
|---|---|
| Example 1 | Validates complex tree with multiple branches |
| Example 2 | Validates linear chain structure |
| Single pair | Edge case with minimal input |
| Left-skewed tree | Tests all left children |