894. All Possible Full Binary Trees
Problem Description
You need to generate all possible full binary trees that contain exactly n
nodes.
A full binary tree is a special type of binary tree where every node has either 0 children (leaf node) or exactly 2 children. There are no nodes with just one child.
Given an integer n
, your task is to:
- Create all structurally unique full binary trees with
n
nodes - Each node in every tree must have
Node.val == 0
- Return a list containing the root node of each possible tree
- The trees can be returned in any order
For example:
- If
n = 1
, there's only one possible tree: a single node - If
n = 3
, there's one possible tree: a root with two leaf children - If
n = 5
, there are two possible trees with different structures
Note that full binary trees can only have an odd number of total nodes. This is because every internal node adds exactly 2 children, so starting from 1 node (the root), you can only have 1, 3, 5, 7, ... nodes.
The solution uses a recursive approach with memoization. It works by:
- If
n = 1
, returning a single node tree - For
n > 1
, trying all possible ways to split nodes between left and right subtrees - For each split where left subtree has
i
nodes, the right subtree hasn - 1 - i
nodes (accounting for the root) - Recursively generating all possible left and right subtrees
- Combining each left subtree with each right subtree to create complete trees
- Using
@cache
decorator to avoid recalculating the same subtree structures
Intuition
The key insight is recognizing that a full binary tree has a recursive structure - every full binary tree consists of a root node with two subtrees that are themselves full binary trees.
First, let's observe an important property: full binary trees can only have an odd number of nodes. Why? Because we start with 1 node (the root), and every time we expand a leaf into an internal node, we add exactly 2 new nodes. So we go from 1 → 3 → 5 → 7... always odd numbers.
Now, how do we build all possible full binary trees with n
nodes? We can think of it as a distribution problem. We have n
total nodes:
- 1 node is used for the root
- The remaining
n - 1
nodes must be distributed between the left and right subtrees
Since both subtrees must also be full binary trees (and thus have odd numbers of nodes), we need to split n - 1
into two odd numbers. This means we try:
- Left: 1 node, Right:
n - 2
nodes - Left: 3 nodes, Right:
n - 4
nodes - Left: 5 nodes, Right:
n - 6
nodes - And so on...
For each valid split, we recursively generate all possible full binary trees for the left side and all possible trees for the right side. Then we create new trees by combining each left possibility with each right possibility.
The beauty of this approach is that smaller subproblems (trees with fewer nodes) are solved first and can be reused when building larger trees. For instance, all trees with 3 nodes will be computed once and reused whenever we need a subtree of size 3. This naturally suggests using memoization to cache results and avoid redundant calculations.
The base case is straightforward: when n = 1
, there's only one possible tree - a single node with no children.
Learn more about Tree, Recursion, Memoization, Dynamic Programming and Binary Tree patterns.
Solution Approach
The implementation uses memoization search (dynamic programming with recursion) to efficiently generate all possible full binary trees.
Core Algorithm:
The solution defines a recursive function dfs(n)
that returns a list of all possible full binary trees with n
nodes:
-
Base Case: When
n = 1
, return a list containing a single node[TreeNode()]
. -
Recursive Case: For
n > 1
, enumerate all valid ways to split nodes between left and right subtrees:- Loop through possible left subtree sizes:
i = 0, 1, 2, ..., n-1
- Calculate corresponding right subtree size:
j = n - 1 - i
(accounting for the root node) - Note: Since full binary trees must have odd nodes, only odd values of
i
andj
will produce valid trees
- Loop through possible left subtree sizes:
-
Tree Construction: For each valid split
(i, j)
:- Recursively get all possible left subtrees:
dfs(i)
- Recursively get all possible right subtrees:
dfs(j)
- Create new trees by combining each left subtree with each right subtree using nested loops
- Each combination creates a new root node with
TreeNode(0, left, right)
- Recursively get all possible left subtrees:
-
Memoization: The
@cache
decorator automatically caches results ofdfs(n)
for each value ofn
. When the same subtree size is needed again, the cached result is returned immediately instead of recalculating.
Data Structures:
- TreeNode: Standard binary tree node with
val
,left
, andright
attributes - List: Stores all possible tree roots for each subproblem
- Cache (hidden): The
@cache
decorator maintains a dictionary mapping input values to results
Time Complexity Analysis:
The number of full binary trees with n
nodes is the (n-1)/2
-th Catalan number, which grows exponentially. With memoization, each unique subproblem is solved only once, but the total work is still proportional to the sum of all Catalan numbers up to (n-1)/2
.
Space Complexity:
The space is used for storing all the generated trees and the recursion stack. The dominant factor is the storage of all possible trees, which is also related to Catalan numbers.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through generating all full binary trees with n = 5
nodes.
Step 1: Initial Call
We call dfs(5)
to find all full binary trees with 5 nodes.
Step 2: Enumerate Splits
For n = 5
, we have 4 nodes to distribute between left and right subtrees (5 - 1 for root = 4).
We try each possible split where i
+ j
= 4:
i = 0, j = 4
: Invalid (0 is even, can't form a full binary tree)i = 1, j = 3
: Valid ✓i = 2, j = 2
: Invalid (2 is even)i = 3, j = 1
: Valid ✓i = 4, j = 0
: Invalid (4 is even)
Step 3: Process First Valid Split (i=1, j=3)
Get left subtrees with 1 node:
dfs(1)
returns[TreeNode(0)]
- just a single node
Get right subtrees with 3 nodes:
dfs(3)
needs to find all trees with 3 nodes- For
n = 3
, we split 2 nodes: onlyi=1, j=1
is valid dfs(1)
returns[TreeNode(0)]
for both left and right- Combine to create one tree with 3 nodes:
0 / \ 0 0
Combine left(1) and right(3):
- We have 1 left option × 1 right option = 1 tree:
0 / \ 0 0 / \ 0 0
Step 4: Process Second Valid Split (i=3, j=1)
Get left subtrees with 3 nodes:
dfs(3)
returns the cached result from Step 3: one tree with structure0(0,0)
Get right subtrees with 1 node:
dfs(1)
returns[TreeNode(0)]
- just a single node
Combine left(3) and right(1):
- We have 1 left option × 1 right option = 1 tree:
0 / \ 0 0 / \ 0 0
Step 5: Return Result
dfs(5)
returns a list containing both trees:
Tree 1:
0 / \ 0 0 / \ 0 0
Tree 2:
0 / \ 0 0 / \ 0 0
Key Observations:
- The function only explores odd-numbered splits since even numbers can't form full binary trees
dfs(1)
anddfs(3)
are computed once and cached for reuse- Each recursive call builds upon smaller subproblems
- The final result contains all structurally unique full binary trees with exactly 5 nodes
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 List, Optional
9from functools import cache
10
11class Solution:
12 def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]:
13 """
14 Generate all possible full binary trees with n nodes.
15 A full binary tree is a tree where each node has either 0 or 2 children.
16
17 Args:
18 n: Number of nodes in the tree
19
20 Returns:
21 List of all possible full binary tree root nodes
22 """
23
24 @cache
25 def generate_trees(num_nodes: int) -> List[Optional[TreeNode]]:
26 """
27 Recursively generate all full binary trees with given number of nodes.
28
29 Args:
30 num_nodes: Number of nodes to use for building trees
31
32 Returns:
33 List of root nodes of all possible full binary trees
34 """
35 # Base case: single node tree
36 if num_nodes == 1:
37 return [TreeNode()]
38
39 # Full binary trees only exist for odd number of nodes
40 if num_nodes % 2 == 0:
41 return []
42
43 result = []
44
45 # Try all possible splits of remaining nodes between left and right subtrees
46 # We need at least 1 node for each subtree, and both must have odd count
47 for left_nodes in range(1, num_nodes - 1, 2):
48 right_nodes = num_nodes - 1 - left_nodes # -1 for root node
49
50 # Generate all possible left subtrees
51 left_subtrees = generate_trees(left_nodes)
52
53 # Generate all possible right subtrees
54 right_subtrees = generate_trees(right_nodes)
55
56 # Create all combinations of left and right subtrees
57 for left_tree in left_subtrees:
58 for right_tree in right_subtrees:
59 # Create new root with current combination
60 root = TreeNode(0, left_tree, right_tree)
61 result.append(root)
62
63 return result
64
65 return generate_trees(n)
66
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 // Memoization cache to store results for each value of n
18 private List<TreeNode>[] memo;
19
20 /**
21 * Generates all possible full binary trees with n nodes.
22 * A full binary tree is a tree where every node has either 0 or 2 children.
23 *
24 * @param n The number of nodes in the tree
25 * @return List of all possible full binary trees with n nodes
26 */
27 public List<TreeNode> allPossibleFBT(int n) {
28 // Initialize memoization array with size n + 1
29 memo = new List[n + 1];
30 return dfs(n);
31 }
32
33 /**
34 * Recursive helper function to build all possible full binary trees.
35 * Uses dynamic programming with memoization to avoid redundant calculations.
36 *
37 * @param n The number of nodes to use for building trees
38 * @return List of all possible full binary trees with n nodes
39 */
40 private List<TreeNode> dfs(int n) {
41 // Return cached result if already computed
42 if (memo[n] != null) {
43 return memo[n];
44 }
45
46 // Base case: single node tree
47 if (n == 1) {
48 return List.of(new TreeNode());
49 }
50
51 // List to store all possible trees for current n
52 List<TreeNode> result = new ArrayList<>();
53
54 // Try all possible distributions of nodes between left and right subtrees
55 // Left subtree gets i nodes, right subtree gets (n - 1 - i) nodes
56 // The root takes 1 node, so total is i + 1 + (n - 1 - i) = n
57 for (int leftNodes = 1; leftNodes < n - 1; leftNodes += 2) {
58 // Calculate number of nodes for right subtree
59 int rightNodes = n - 1 - leftNodes;
60
61 // Generate all possible left subtrees with leftNodes nodes
62 List<TreeNode> leftSubtrees = dfs(leftNodes);
63
64 // Generate all possible right subtrees with rightNodes nodes
65 List<TreeNode> rightSubtrees = dfs(rightNodes);
66
67 // Combine each left subtree with each right subtree
68 for (TreeNode leftTree : leftSubtrees) {
69 for (TreeNode rightTree : rightSubtrees) {
70 // Create new root node with current left and right subtrees
71 TreeNode root = new TreeNode(0, leftTree, rightTree);
72 result.add(root);
73 }
74 }
75 }
76
77 // Cache and return the result
78 memo[n] = result;
79 return result;
80 }
81}
82
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<TreeNode*> allPossibleFBT(int n) {
15 // Memoization table to store all possible FBTs for each value of n
16 vector<vector<TreeNode*>> memo(n + 1);
17
18 // Recursive function with memoization to generate all possible full binary trees
19 function<vector<TreeNode*>(int)> generateTrees = [&](int nodeCount) -> vector<TreeNode*> {
20 // Return cached result if already computed
21 if (!memo[nodeCount].empty()) {
22 return memo[nodeCount];
23 }
24
25 // Base case: single node tree
26 if (nodeCount == 1) {
27 return vector<TreeNode*>{new TreeNode(0)};
28 }
29
30 vector<TreeNode*> result;
31
32 // Try all possible splits of nodes between left and right subtrees
33 // Full binary trees must have odd number of nodes
34 for (int leftNodes = 1; leftNodes < nodeCount - 1; leftNodes += 2) {
35 int rightNodes = nodeCount - 1 - leftNodes;
36
37 // Generate all possible left subtrees
38 vector<TreeNode*> leftSubtrees = generateTrees(leftNodes);
39
40 // Generate all possible right subtrees
41 vector<TreeNode*> rightSubtrees = generateTrees(rightNodes);
42
43 // Combine each left subtree with each right subtree
44 for (TreeNode* leftTree : leftSubtrees) {
45 for (TreeNode* rightTree : rightSubtrees) {
46 // Create new root node with current left and right subtrees
47 TreeNode* root = new TreeNode(0, leftTree, rightTree);
48 result.push_back(root);
49 }
50 }
51 }
52
53 // Cache and return the result
54 memo[nodeCount] = result;
55 return result;
56 };
57
58 // Full binary trees can only have odd number of nodes
59 if (n % 2 == 0) {
60 return {};
61 }
62
63 return generateTrees(n);
64 }
65};
66
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// Memoization cache to store previously computed results for each value of n
16const memoCache: Array<Array<TreeNode | null>> = new Array(21).fill(0).map(() => []);
17
18/**
19 * Helper function to recursively generate all possible full binary trees with n nodes
20 * @param nodeCount - The number of nodes in the tree
21 * @returns Array of all possible full binary trees with nodeCount nodes
22 */
23function generateFullBinaryTrees(nodeCount: number): Array<TreeNode | null> {
24 // Return cached result if already computed
25 if (memoCache[nodeCount].length > 0) {
26 return memoCache[nodeCount];
27 }
28
29 // Base case: a single node is a valid full binary tree
30 if (nodeCount === 1) {
31 memoCache[nodeCount].push(new TreeNode(0));
32 return memoCache[nodeCount];
33 }
34
35 // Result array to store all possible trees
36 const result: Array<TreeNode | null> = [];
37
38 // Try all possible distributions of nodes between left and right subtrees
39 // Full binary trees must have odd number of nodes, so we increment by 2
40 for (let leftNodes = 1; leftNodes < nodeCount - 1; leftNodes += 2) {
41 const rightNodes = nodeCount - 1 - leftNodes;
42
43 // Generate all possible left subtrees
44 const leftSubtrees = generateFullBinaryTrees(leftNodes);
45
46 // Generate all possible right subtrees
47 const rightSubtrees = generateFullBinaryTrees(rightNodes);
48
49 // Combine each left subtree with each right subtree
50 for (const leftTree of leftSubtrees) {
51 for (const rightTree of rightSubtrees) {
52 // Create a new root node with current left and right subtrees
53 result.push(new TreeNode(0, leftTree, rightTree));
54 }
55 }
56 }
57
58 // Cache and return the result
59 memoCache[nodeCount] = result;
60 return result;
61}
62
63/**
64 * Generates all possible full binary trees with n nodes
65 * A full binary tree is a tree where each node has either 0 or 2 children
66 * @param n - The number of nodes in the tree
67 * @returns Array of all possible full binary trees with n nodes
68 */
69function allPossibleFBT(n: number): Array<TreeNode | null> {
70 // Full binary trees can only have odd number of nodes
71 if (n % 2 === 0) {
72 return [];
73 }
74
75 return generateFullBinaryTrees(n);
76}
77
Time and Space Complexity
The time complexity is O(2^n / sqrt(n))
and the space complexity is O(2^n / sqrt(n))
, where n
is the number of nodes.
Time Complexity Analysis:
- This problem generates all possible full binary trees with
n
nodes - A full binary tree must have an odd number of nodes, and the number of such trees with
n
nodes is the(n-1)/2
-th Catalan number - The Catalan number
C_k = (2k)! / ((k+1)! * k!)
asymptotically equals4^k / (k^(3/2) * sqrt(π))
- For
n
nodes wheren = 2k + 1
, we haveC_k ≈ 4^k / (k * sqrt(πk)) = 2^(2k) / (k * sqrt(πk))
- Since
k = (n-1)/2
, this becomes approximately2^(n-1) / ((n-1)/2 * sqrt(π(n-1)/2))
which simplifies toO(2^n / sqrt(n))
- Each tree construction involves constant work due to memoization, so the total time complexity is
O(2^n / sqrt(n))
Space Complexity Analysis:
- The memoization cache stores all unique full binary trees for each value from 1 to
n
- The total number of trees stored is proportional to the sum of Catalan numbers up to
(n-1)/2
- The dominant term is the largest Catalan number, which is
O(2^n / sqrt(n))
- Each tree has
O(n)
nodes, but since we're counting the number of distinct trees rather than total nodes across all trees, the space complexity for storing all trees isO(2^n / sqrt(n))
- The recursion depth is at most
O(n)
, which is negligible compared to the cache storage - Therefore, the overall space complexity is
O(2^n / sqrt(n))
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting that Full Binary Trees Only Have Odd Number of Nodes
A very common mistake is not recognizing early that full binary trees can only have an odd number of nodes. This leads to unnecessary computation and incorrect results.
Pitfall Code:
@cache
def generate_trees(num_nodes: int) -> List[Optional[TreeNode]]:
if num_nodes == 1:
return [TreeNode()]
result = []
# Wrong: iterating through all values instead of just odd ones
for left_nodes in range(1, num_nodes):
right_nodes = num_nodes - 1 - left_nodes
# This will try to generate trees with even number of nodes
left_subtrees = generate_trees(left_nodes)
right_subtrees = generate_trees(right_nodes)
# ... rest of the code
Why it's problematic: This attempts to generate trees with even numbers of nodes, which is impossible for full binary trees. It wastes computation time and may cause infinite recursion or stack overflow.
Solution: Add an early check for even numbers and iterate only through odd values:
if num_nodes % 2 == 0:
return []
for left_nodes in range(1, num_nodes - 1, 2): # Step by 2 to get only odd numbers
right_nodes = num_nodes - 1 - left_nodes
2. Creating Shared Node References Instead of New Nodes
Another critical mistake is reusing the same node object across different trees, leading to trees that share nodes and create invalid structures.
Pitfall Code:
@cache
def generate_trees(num_nodes: int) -> List[Optional[TreeNode]]:
if num_nodes == 1:
# Wrong: returning the same node reference for all single-node trees
single_node = TreeNode()
return [single_node] # This same node will be used in multiple trees!
Why it's problematic: When this cached result is used multiple times, the same node object appears in different positions across different trees. Modifying one tree could affect others, and the tree structure becomes invalid with cycles or shared subtrees.
Solution: Always create new node instances:
if num_nodes == 1: return [TreeNode()] # Creates a new node each time # And in the combination loop: for left_tree in left_subtrees: for right_tree in right_subtrees: # Always create a new root node root = TreeNode(0, left_tree, right_tree) result.append(root)
3. Incorrect Node Count Distribution
A subtle but common error is miscalculating how to distribute nodes between left subtree, right subtree, and root.
Pitfall Code:
for left_nodes in range(1, num_nodes, 2): # Wrong upper bound
right_nodes = num_nodes - left_nodes # Forgot to account for root node
Why it's problematic: This doesn't account for the root node in the calculation. If you have n
total nodes and allocate i
to the left subtree, the right subtree should get n - 1 - i
nodes (not n - i
), because one node is used as the root.
Solution: Correctly account for all nodes:
for left_nodes in range(1, num_nodes - 1, 2): # Ensure at least 1 node for right
right_nodes = num_nodes - 1 - left_nodes # -1 accounts for the root node
4. Not Using Memoization or Using It Incorrectly
Without memoization, the solution recalculates the same subtree structures repeatedly, leading to exponential time complexity.
Pitfall Code:
def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]:
# No memoization - recalculates same subtrees multiple times
def generate_trees(num_nodes: int) -> List[Optional[TreeNode]]:
# ... recursive logic without caching
Why it's problematic: For example, when generating trees with 7 nodes, generate_trees(3)
might be called multiple times from different recursive paths, each time rebuilding the same structures from scratch.
Solution: Use @cache
decorator or maintain a manual memoization dictionary:
@cache # Automatically memoizes the function
def generate_trees(num_nodes: int) -> List[Optional[TreeNode]]:
# ... recursive logic
Note: When using @cache
with functions that return mutable objects (like tree nodes), be careful about modifying the returned trees, as they're shared across calls.
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
Want a Structured Path to Master System Design Too? Don’t Miss This!