Validate Binary Tree Nodes
MedPrompt
You have n binary tree nodes numbered from 0 to n - 1 where node i has two children leftChild[i] and rightChild[i], return true if and only if all the given nodes form exactly one valid binary tree.
If node i has no left child then leftChild[i] will equal -1, similarly for the right child.
Note that the nodes have no values and that we only use the node numbers in this problem.
Example 1:
Input: n = 4, leftChild = [1,-1,3,-1], rightChild = [2,-1,-1,-1]
Output: trueExample 2:
Input: n = 4, leftChild = [1,-1,3,-1], rightChild = [2,3,-1,-1]
Output: falseExample 3:
Input: n = 2, leftChild = [1,0], rightChild = [-1,-1]
Output: false
Constraints:
n == leftChild.length == rightChild.length1 <= n <= 104-1 <= leftChild[i], rightChild[i] <= n - 1
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a Union-Find (DSU) data structure to detect cycles and ensure all nodes form a single connected component. It also separately verifies the parent-child rules of a binary tree, namely that each node has at most one parent.
Algorithm
- Initialize a Union-Find (DSU) data structure with
ncomponents, one for each node. - Initialize an
inDegreearray of sizento all zeros to track the number of parents for each node. - Iterate through each node
pfrom0ton-1. - For each child
c(leftChild[p]orrightChild[p]):- If
cis not-1:- If
calready has a parent (inDegree[c] == 1), it violates the binary tree property. Returnfalse. - If
pandcare already in the same connected component (find(p) == find(c)), adding the edgep -> cwould create a cycle. Returnfalse. - If the checks pass, increment
inDegree[c]and performunion(p, c)to merge their sets.
- If
- If
- After iterating through all nodes, we must have exactly one connected component and exactly one root.
- The number of components in the DSU must be 1.
- The number of nodes with an in-degree of 0 (roots) must be 1.
- If both conditions hold, return
true; otherwise, returnfalse.
Walkthrough
A valid binary tree must be a single connected component, contain no cycles, and every node (except the root) must have exactly one parent. This approach tackles these conditions using a combination of an in-degree count and a DSU data structure.
-
Parent Uniqueness: We use an
inDegreearray to track the number of parents for each node. As we process theleftChildandrightChildarrays, we check the child's in-degree. If a child node is about to be assigned a second parent, we immediately know the structure is invalid. -
Cycle Detection & Connectivity: A DSU data structure is initialized with
nnodes, each in its own set. For each parent-child relationshipp -> c, we first check for parent uniqueness. Then, we check ifpandcare already in the same set using thefindoperation. If they are, it implies that an undirected path already exists between them. Sincechas no other parent, this must meancis an ancestor ofp, so adding the edgep -> cwould form a cycle. If no cycle is formed, weunionthem, merging their sets and decrementing the component count. -
Final Validation: After processing all edges, a valid tree must consist of a single component with a single root. We check that the final number of components in the DSU is 1 and that the number of nodes with an in-degree of 0 is also exactly 1.
class Solution { class DSU { private int[] parent; private int components; public DSU(int n) { parent = new int[n]; components = n; for (int i = 0; i < n; i++) { parent[i] = i; } } public int find(int i) { if (parent[i] == i) { return i; } return parent[i] = find(parent[i]); } public boolean union(int i, int j) { int rootI = find(i); int rootJ = find(j); if (rootI != rootJ) { parent[rootJ] = rootI; components--; return true; } return false; } public int getComponents() { return components; } } public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) { DSU dsu = new DSU(n); int[] inDegree = new int[n]; for (int i = 0; i < n; i++) { int lc = leftChild[i]; int rc = rightChild[i]; if (lc != -1) { if (inDegree[lc] > 0) return false; // Already has a parent if (dsu.find(i) == dsu.find(lc)) return false; // Cycle dsu.union(i, lc); inDegree[lc]++; } if (rc != -1) { if (inDegree[rc] > 0) return false; // Already has a parent if (dsu.find(i) == dsu.find(rc)) return false; // Cycle dsu.union(i, rc); inDegree[rc]++; } } int rootCount = 0; for (int i = 0; i < n; i++) { if (inDegree[i] == 0) { rootCount++; } } return rootCount == 1 && dsu.getComponents() == 1; }}Complexity
Time
O(N * α(N)), where N is the number of nodes and α is the Inverse Ackermann function. The main loop runs N times, and each union/find operation is nearly constant time.
Space
O(N) to store the DSU's parent array and the in-degree array.
Trade-offs
Pros
Provides a structured way to check for connectivity and cycles simultaneously.
Union-Find is a standard, efficient data structure for problems involving disjoint sets.
Cons
More complex to implement correctly compared to a simple traversal.
The logic combines multiple concepts (in-degrees, DSU for cycles, DSU for components), making it less straightforward.
Can be slightly less performant in practice due to the overhead of the DSU data structure.
This is a more direct and efficient approach. It first identifies a single potential root node by checking the in-degrees of all nodes. Then, it performs a single graph traversal (like Breadth-First Search) starting from that root to ensure all nodes are reachable and form a single connected component. This series of checks holistically validates the structure as a single binary tree.
Algorithm
- Create an
inDegreearray of sizenand initialize it to all zeros. - Populate the
inDegreearray by iterating throughleftChildandrightChildarrays. For each valid child, increment its in-degree. - Find the root of the potential tree. Iterate through the
inDegreearray:- A node
iis a potential root ifinDegree[i] == 0. - If there is not exactly one node with an in-degree of 0, return
false. - If any node has an in-degree greater than 1, return
false.
- A node
- If a unique root is found, perform a graph traversal (BFS or DFS) starting from that root.
- Keep a count of the number of nodes visited during the traversal.
- After the traversal, if the number of visited nodes is equal to
n, it means all nodes are connected in a single component. Returntrue. - Otherwise, the graph is disconnected (a forest), so return
false.
Walkthrough
A valid binary tree has key properties that we can check sequentially for an elegant and efficient solution:
-
Single Parent & Single Root Rule: A binary tree must have exactly one root (a node with no parent), and every other node must have exactly one parent. We can verify this by computing the in-degree for every node. We iterate through all parent-child connections, build an
inDegreearray, and then scan this array. If we find any node with an in-degree greater than 1, or if we don't find exactly one node with an in-degree of 0, the structure is invalid. -
Connectivity & Acyclicity: If the parent/root rules hold, the graph consists of one or more components, each of which is a tree. To be a single valid binary tree, all
nnodes must belong to the component of the single root we identified. We can verify this by starting a traversal (BFS or DFS) from the root. By counting the nodes visited, we can determine if the graph is fully connected. If the traversal visits exactlynnodes, then all nodes are reachable from the single root, forming one connected component. This also implicitly confirms there are no cycles, as a cycle would have violated the in-degree rules.
This leads to a clean, multi-step algorithm that is easy to implement and understand.
import java.util.Queue;import java.util.LinkedList; class Solution { public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) { int[] inDegree = new int[n]; for (int i = 0; i < n; i++) { if (leftChild[i] != -1) { inDegree[leftChild[i]]++; } if (rightChild[i] != -1) { inDegree[rightChild[i]]++; } } int root = -1; for (int i = 0; i < n; i++) { if (inDegree[i] > 1) { return false; // Node with more than one parent } if (inDegree[i] == 0) { if (root != -1) { return false; // More than one root } root = i; } } if (root == -1) { return false; // No root found (e.g., a cycle) } // BFS to check connectivity and count visited nodes Queue<Integer> queue = new LinkedList<>(); boolean[] visited = new boolean[n]; int visitedCount = 0; queue.offer(root); visited[root] = true; visitedCount++; while (!queue.isEmpty()) { int u = queue.poll(); int lc = leftChild[u]; if (lc != -1) { // This check is technically redundant if in-degree checks passed, // but provides extra safety. if (visited[lc]) return false; visited[lc] = true; queue.offer(lc); visitedCount++; } int rc = rightChild[u]; if (rc != -1) { if (visited[rc]) return false; visited[rc] = true; queue.offer(rc); visitedCount++; } } return visitedCount == n; }}Complexity
Time
O(N), where N is the number of nodes. Calculating in-degrees takes O(N), finding the root takes O(N), and the BFS/DFS traversal visits each node and edge once, which is also O(N).
Space
O(N) to store the in-degree array, the queue for BFS, and the visited array.
Trade-offs
Pros
Highly efficient with linear time and space complexity.
Conceptually straightforward, breaking the problem down into clear, verifiable properties.
Robust and less prone to subtle implementation errors compared to DSU on directed graphs.
Cons
Requires multiple passes over the data (one for in-degrees, one for traversal), though this does not affect the overall asymptotic complexity.
Solutions
Solution
class Solution {private int[] p;public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) { p = new int[n]; for (int i = 0; i < n; ++i) { p[i] = i; } boolean[] vis = new boolean[n]; for (int i = 0, m = n; i < m; ++i) { for (int j : new int[]{leftChild[i], rightChild[i]}) { if (j != -1) { if (vis[j] || find(i) == find(j)) { return false; } p[find(i)] = find(j); vis[j] = true; --n; } } } return n == 1; }private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; }}class Solution {public: bool validateBinaryTreeNodes(int n, vector<int> &leftChild, vector<int> &rightChild) { int p[n]; iota(p, p + n, 0); bool vis[n]; memset(vis, 0, sizeof(vis)); function<int(int)> find = [&](int x) { return p[x] == x ? x : p[x] = find(p[x]); }; for (int i = 0, m = n; i < m; ++i) { for (int j : {leftChild[i], rightChild[i]}) { if (j != -1) { if (vis[j] || find(i) == find(j)) { return false; } p[find(i)] = find(j); vis[j] = true; --n; } } } return n == 1; }};class Solution: def validateBinaryTreeNodes(self, n: int, leftChild: List[int], rightChild: List[int]) -> bool: def find(x: int) -> int: if p[x] != x: p[x] = find(p[x]) return p[x] p = list(range(n)) vis = [False] * n for i, (a, b) in enumerate(zip(leftChild, rightChild)): for j in (a, b): if j != - 1: if vis[j] or find(i) == find(j): return False p[find(i)] = find(j) vis[j] = True n -= 1 return n == 1Video 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.