Create Binary Tree From Descriptions
MedPrompt
You are given a 2D integer array descriptions where descriptions[i] = [parenti, childi, isLefti] indicates that parenti is the parent of childi in a binary tree of unique values. Furthermore,
- If
isLefti == 1, thenchildiis the left child ofparenti. - If
isLefti == 0, thenchildiis the right child ofparenti.
Construct the binary tree described by descriptions and return its root.
The test cases will be generated such that the binary tree is valid.
Example 1:
Input: descriptions = [[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]]
Output: [50,20,80,15,17,19]
Explanation: The root node is the node with value 50 since it has no parent.
The resulting binary tree is shown in the diagram.Example 2:
Input: descriptions = [[1,2,1],[2,3,0],[3,4,1]]
Output: [1,2,null,null,3,4]
Explanation: The root node is the node with value 1 since it has no parent.
The resulting binary tree is shown in the diagram.
Constraints:
1 <= descriptions.length <= 104descriptions[i].length == 31 <= parenti, childi <= 1050 <= isLefti <= 1- The binary tree described by
descriptionsis valid.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach separates the problem into two distinct phases: first, building the complete tree structure, and second, identifying the root node. It uses a HashMap to keep track of created nodes to avoid duplicates and a HashSet to record all nodes that are children. The root is then found by identifying a parent node that never appears in the set of children.
Algorithm
- Initialize a
HashMap<Integer, TreeNode>to map node values toTreeNodeobjects. - Initialize a
HashSet<Integer>to store the values of all child nodes. - First Pass (Build Tree & Identify Children): Iterate through each
description[parent, child, isLeft].- For both
parentandchildvalues, if they are not already in the map, create a newTreeNodeand add it to the map. - Retrieve the
parentNodeandchildNodefrom the map. - Link them: if
isLeftis 1,parentNode.left = childNode; otherwise,parentNode.right = childNode. - Add the
childvalue to thechildrenset.
- For both
- Second Pass (Find Root): Iterate through each
descriptionagain.- Check if the
parentvalue from the current description exists in thechildrenset. - If it does not, this
parentis the root. - Return the
TreeNodecorresponding to this root value from the map.
- Check if the
Walkthrough
This approach involves two main stages. First, we construct the entire tree structure, and second, we identify the root node.
-
Data Structures: We use two primary data structures:
- A
HashMap<Integer, TreeNode>to store each unique node. The key is the node's integer value, and the value is theTreeNodeobject itself. This allows for O(1) average time access to any node by its value, preventing us from creating duplicate nodes. - A
HashSet<Integer>to keep track of all values that appear as a child in any description. This set will be crucial for identifying the root later.
- A
-
Pass 1: Tree Construction:
- We iterate through every description
[parent, child, isLeft]in the input array. - For each description, we ensure that both the parent and child nodes exist in our
nodeMap. If a node for a given value isn't in the map, we create a newTreeNodeand add it. - We then retrieve the
parentNodeandchildNodeobjects from the map. - Based on the
isLeftflag (1 for left, 0 for right), we establish the parent-child relationship:parentNode.left = childNodeorparentNode.right = childNode. - Finally, we add the
child's value to ourchildrenset.
- We iterate through every description
-
Pass 2: Root Identification:
- After the first pass, all nodes are created and linked, and we have a complete set of all child nodes. The root of the tree is the only node that is a parent but never a child.
- We can find this node by iterating through the
descriptionsarray a second time. For eachparentvalue, we check if it's present in ourchildrenset. - The first
parentvalue we encounter that is not in thechildrenset must be the root. We then retrieve its correspondingTreeNodefrom thenodeMapand return it.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public TreeNode createBinaryTree(int[][] descriptions) { Map<Integer, TreeNode> nodeMap = new HashMap<>(); Set<Integer> children = new HashSet<>(); // Pass 1: Build the tree structure and identify all children for (int[] desc : descriptions) { int parentVal = desc[0]; int childVal = desc[1]; boolean isLeft = desc[2] == 1; nodeMap.putIfAbsent(parentVal, new TreeNode(parentVal)); nodeMap.putIfAbsent(childVal, new TreeNode(childVal)); TreeNode parentNode = nodeMap.get(parentVal); TreeNode childNode = nodeMap.get(childVal); if (isLeft) { parentNode.left = childNode; } else { parentNode.right = childNode; } children.add(childVal); } // Pass 2: Find the root TreeNode root = null; for (int[] desc : descriptions) { int parentVal = desc[0]; if (!children.contains(parentVal)) { root = nodeMap.get(parentVal); break; } } return root; }}Complexity
Time
O(N), where N is the number of descriptions. The first pass iterates through all N descriptions, and the map/set operations take average O(1) time. The second pass also iterates through N descriptions. Thus, the total time is O(N + N) = O(N).
Space
O(M), where M is the number of unique nodes in the tree. The `HashMap` stores a `TreeNode` for each of the M unique nodes, and the `HashSet` stores all child values (at most M-1).
Trade-offs
Pros
Conceptually straightforward and easy to implement.
Reliably constructs the tree and finds the root by clearly separating concerns.
Cons
Requires two full passes over the input
descriptionsarray, making it slightly less efficient than a single-pass solution.
This approach optimizes the process by building the tree and identifying all child nodes in a single pass over the input descriptions. After this pass, the nodeMap contains all the nodes and the childrenSet contains all nodes with a parent. The root is then found by identifying the single node in the map that is not in the children set.
Algorithm
- Initialize a
HashMap<Integer, TreeNode>callednodeMapto store created nodes. - Initialize a
HashSet<Integer>calledchildrenSetto store the values of all child nodes. - Single Pass (Build Tree & Track Children): Iterate through each
description[parentVal, childVal, isLeft].- Get or create the parent and child nodes using
nodeMap.computeIfAbsent(). - Retrieve the
parentNodeandchildNodefromnodeMap. - Link the nodes based on
isLeft. - Add
childValto thechildrenSet.
- Get or create the parent and child nodes using
- Find Root: After the loop, iterate through the keys in
nodeMap.- The first key (node value) that is not found in
childrenSetis the root's value. - Return the
TreeNodeassociated with this root value fromnodeMap.
- The first key (node value) that is not found in
Walkthrough
This approach improves on the two-pass method by combining tree construction and data gathering into a single, more efficient pass.
- Algorithm:
- Initialization: We start by initializing a
HashMap<Integer, TreeNode>namednodeMapto map node values to their respectiveTreeNodeobjects and aHashSet<Integer>namedchildrenSetto record all node values that are children. - Single Pass: We iterate through the
descriptionsarray just once. For each[parentVal, childVal, isLeft]entry:- We use
nodeMap.computeIfAbsent(value, k -> new TreeNode(k))for bothparentValandchildVal. This is a concise way to get theTreeNodeif it exists or create and add it to the map if it doesn't. - We retrieve the
parentNodeandchildNodeobjects. - We connect them according to the
isLeftflag:parentNode.left = childNodeorparentNode.right = childNode. - We add
childValto thechildrenSet.
- We use
- Root Finding: After the loop completes,
nodeMapcontains all the nodes of the tree, andchildrenSetcontains all the nodes that have a parent. By definition, the root is the only node in the tree that does not have a parent. - Therefore, we can find the root by iterating through all the node values present in
nodeMap.keySet(). The first value we find that is not contained inchildrenSetis the value of the root node. - We then return the
TreeNodecorresponding to this root value fromnodeMap. Given the problem's guarantee of a valid tree, a single unique root is guaranteed to be found.
- Initialization: We start by initializing a
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public TreeNode createBinaryTree(int[][] descriptions) { Map<Integer, TreeNode> nodeMap = new HashMap<>(); Set<Integer> childrenSet = new HashSet<>(); // Single pass to build the tree and populate the children set for (int[] description : descriptions) { int parentVal = description[0]; int childVal = description[1]; boolean isLeft = description[2] == 1; TreeNode parentNode = nodeMap.computeIfAbsent(parentVal, k -> new TreeNode(k)); TreeNode childNode = nodeMap.computeIfAbsent(childVal, k -> new TreeNode(k)); if (isLeft) { parentNode.left = childNode; } else { parentNode.right = childNode; } childrenSet.add(childVal); } // Find the root: the node that is never a child for (int nodeVal : nodeMap.keySet()) { if (!childrenSet.contains(nodeVal)) { return nodeMap.get(nodeVal); } } return null; // Should not be reached }}Complexity
Time
O(N), where N is the number of descriptions. We iterate through the `descriptions` array once (O(N)). Then, we iterate through the unique nodes in the `nodeMap` to find the root. The number of unique nodes, M, is at most 2*N. So the total time complexity is O(N + M) which simplifies to O(N).
Space
O(M), where M is the number of unique nodes. We use a `HashMap` to store M nodes and a `HashSet` to store up to M-1 child nodes. The space is proportional to the number of unique nodes.
Trade-offs
Pros
More efficient than the two-pass approach as it processes the input array only once to build the tree structure.
Maintains the same clear logic of using a map for nodes and a set for children.
Code can be made very concise using methods like
computeIfAbsent.
Cons
Still requires extra space for the
HashSetand a second loop (over the map's keys) to find the root after the main processing loop.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public TreeNode createBinaryTree ( int [][] descriptions ) { Map < Integer , TreeNode > m = new HashMap <>(); Set < Integer > vis = new HashSet <>(); for ( int [] d : descriptions ) { int p = d [ 0 ], c = d [ 1 ], isLeft = d [ 2 ]; if (! m . containsKey ( p )) { m . put ( p , new TreeNode ( p )); } if (! m . containsKey ( c )) { m . put ( c , new TreeNode ( c )); } if ( isLeft == 1 ) { m . get ( p ). left = m . get ( c ); } else { m . get ( p ). right = m . get ( c ); } vis . add ( c ); } for ( Map . Entry < Integer , TreeNode > entry : m . entrySet ()) { if (! vis . contains ( entry . getKey ())) { return entry . getValue (); } } return null ; } }/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {number[][]} descriptions * @return {TreeNode} */ var createBinaryTree = function (descriptions) { const nodes = {}; const children = new Set(); for (const [parent, child, isLeft] of descriptions) { if (!nodes[parent]) { nodes[parent] = new TreeNode(parent); } if (!nodes[child]) { nodes[child] = new TreeNode(child); } if (isLeft) { nodes[parent].left = nodes[child]; } else { nodes[parent].right = nodes[child]; } children.add(child); } for (const [k, v] of Object.entries(nodes)) { if (!children.has(+k)) { return v; } } };/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: TreeNode * createBinaryTree ( vector < vector < int >>& descriptions ) { unordered_map < int , TreeNode *> m ; unordered_set < int > vis ; for ( auto & d : descriptions ) { int p = d [ 0 ], c = d [ 1 ], left = d [ 2 ]; if ( ! m . count ( p )) m [ p ] = new TreeNode ( p ); if ( ! m . count ( c )) m [ c ] = new TreeNode ( c ); if ( left ) m [ p ] -> left = m [ c ]; else m [ p ] -> right = m [ c ]; vis . insert ( c ); } for ( auto & [ v , node ] : m ) { if ( ! vis . count ( v )) return node ; } return nullptr ; } };# 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 ]]) -> Optional [ TreeNode ]: g = defaultdict ( TreeNode ) vis = set () for p , c , left in descriptions : if p not in g : g [ p ] = TreeNode ( p ) if c not in g : g [ c ] = TreeNode ( c ) if left : g [ p ]. left = g [ c ] else : g [ p ]. right = g [ c ] vis . add ( c ) for v , node in g . items (): if v not in vis : return nodeVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.