N-ary Tree
A rooted tree in which each node can have any number of children, stored as a child list, and traversed with the same DFS/BFS ideas as binary trees.
Definition
An N-ary tree drops the two-children limit of a Binary Tree: each node has an ordered list of zero or more children. File systems, DOM trees, organization charts, JSON documents and the recursion trees of Recursion & Backtracking searches are all N-ary trees.
Storage is either a children array per node, a parent array (parent[i]) for immutable trees, or a general Adjacency List when the tree is given as n - 1 edges. A classic trick, the left-child right-sibling encoding, represents any N-ary tree as a binary tree: left points to the first child, right to the next sibling.
Intuition
A mental model before the formal terms.
Think of a folder tree in a file explorer. A folder can hold any number of files or sub-folders. Computing the size of a folder means summing the sizes of everything inside — a postorder traversal. Listing every file at "depth 2" is a level-order traversal.
Every binary tree algorithm that recurses on left and right becomes a loop over children; that is the only change.
How it works
- Node:
valuepluschildren: Node[]. The root has no parent. - DFS preorder: visit the node, then recurse into each child in order. Postorder: recurse first, then visit — used for subtree sizes, heights, and "delete this subtree".
- BFS: queue the root; repeatedly pop, visit, push all children. Track level size for level-order output.
- When the tree arrives as edges, build an adjacency list, pick a root, and DFS while passing the parent to avoid walking back up the edge.
- Serialization: preorder with child counts (or a sentinel after each node's children) reconstructs the tree uniquely.
Why it works
The recursive definition (a node plus a list of subtrees) supports structural induction just like binary trees; correctness of a subtree computation lifts to the whole tree.
Every node is enqueued or recursed exactly once and every edge is examined once, so traversals are O(n) regardless of branching factor.
Operations
| Operation | Description | Cost |
|---|---|---|
| addChild(parent, value) | Append a new node to the parent's child list. | O(1) |
| traverse (DFS / BFS) | Visit all nodes; loop over children instead of left/right. | O(n) |
| height / size | Postorder aggregation over children. | O(n) |
| search | No ordering, so every node may need to be checked. | O(n) |
| removeChild | Remove a subtree from its parent's list. | O(k) |
Recognition
How to tell a problem wants this.
- The input has a parent/child relationship with variable fan-out: "manager of", "directory contains", "category has subcategories".
- Given
nnodes andn - 1edges and told it is a tree (connected, acyclic). - Problems on tree DP (Tree DP), subtree queries, or lowest common ancestor in a general tree.
Interactive demo
Play, step, change the input. ← → and space work too.
1dfs(node): visit(node)2 for child in node.children: dfs(child)3bfs(root): queue = [root]4 while queue: node = queue.popleft(); visit(node)5 for child in node.children: queue.append(child)Pseudocode
1dfs(node):2 visit(node)3 for child in node.children: dfs(child)4height(node):5 return 1 + max(height(c) for c in children, default 0)Implementation
1from collections import deque2from typing import Generic, Optional, TypeVar3 4T = TypeVar("T")5 6 71 · Node with a list of children8class NaryNode(Generic[T]):9 def __init__(self, val: T) -> None:10 self.val = val11 self.children: list["NaryNode[T]"] = []12 13 14class NaryTree(Generic[T]):15 def __init__(self) -> None:16 self.root: Optional[NaryNode[T]] = None17 182 · Add a child under a given parent19 def add_child(self, parent: Optional[NaryNode[T]], val: T) -> NaryNode[T]:20 node = NaryNode(val)21 if parent is None:22 self.root = node23 else:24 parent.children.append(node)25 return node26 273 · Preorder and postorder DFS28 def preorder(self) -> list[T]:29 out: list[T] = []30 31 def go(n: Optional[NaryNode[T]]) -> None:32 if n is None:33 return34 out.append(n.val)35 for c in n.children:36 go(c)37 38 go(self.root)39 return out40 41 def postorder(self) -> list[T]:42 out: list[T] = []43 44 def go(n: Optional[NaryNode[T]]) -> None:45 if n is None:46 return47 for c in n.children:48 go(c)49 out.append(n.val)50 51 go(self.root)52 return out53 544 · BFS level order55 def level_order(self) -> list[list[T]]:56 if self.root is None:57 return []58 levels: list[list[T]] = []59 q = deque([self.root])60 while q:61 level: list[T] = []62 for _ in range(len(q)):63 n = q.popleft()64 level.append(n.val)65 q.extend(n.children)66 levels.append(level)67 return levels68 695 · Height via postorder combine over all children70 def height(self) -> int:71 def go(n: Optional[NaryNode[T]]) -> int:72 if n is None:73 return 074 return 1 + max((go(c) for c in n.children), default=0)75 76 return go(self.root)NaryNode(Generic[T])usesTypeVarso type hints carry the value type.add_child(None, val)creates the root; otherwise the node is appended to the parent'schildrenlist.- Preorder and postorder are nested closures that loop over
n.children. level_orderusesdequeandq.extend(n.children)to enqueue all children in one call.heightusesmax(..., default=0)so leaves (empty generator) do not raiseValueError.
Python's 1000-frame recursion limit is hit by a 1000-deep path; use an explicit stack for deep trees.
max(iterable, default=0)is the idiomatic guard for possibly empty sequences.deque.extendis O(k) for k children and avoids a Python-level loop.- Nested dicts/lists are often used directly as n-ary trees in Python (e.g. parsed JSON).
- Calling
max()on an empty generator withoutdefault, which raisesValueError. - Using a mutable default
children=[]in__init__and sharing it across all nodes. - Recursing on deep trees; convert to an explicit stack when depth can exceed a few hundred.
- Empty-children max: Python needs
default=0, JS/TS need an explicit0argument toMath.max, C++ uses a running maximum. - JS/TS spread into
push/Math.maxhas an argument limit around 100k; C++ and Python have no equivalent limit. - TS and Python versions are generic over the value type; C++ would use a template; JS is dynamically typed anyway.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(n) | O(n) | |
| Search | O(n) | O(n) | |
| Insert | O(1) | O(1) | Given the parent node. |
| Delete | O(k) | O(k) | k = parent's child count (list removal). |
| Update | O(1) | O(1) | Given the node. |
| Traversal | O(n) | O(n) | |
| Height | O(n) | O(n) | |
| Space | O(n) | Child lists total n - 1 references. DFS stack O(h), BFS queue O(max level width). | |
Advantages & disadvantages
- Models real hierarchies directly without artificial binarization.
- All the binary tree algorithms transfer with a loop over children.
- Cheap
O(1)child insertion at the end of the list.
- No ordering invariant, so search is
O(n). - Variable-size child lists mean more allocations and worse locality than fixed-arity trees.
- Wide, shallow trees make recursion cheap but BFS queues large; deep narrow trees do the opposite.
Use cases
- File systems, DOM and XML/JSON document trees.
- Organizational charts, taxonomies and category hierarchies.
- Game trees and recursion trees in Recursion & Backtracking and Divide and Conquer.
- The Trie is an N-ary tree with alphabet-sized fan-out.
- Hierarchies with variable fan-out (files, DOM, org charts, taxonomies).
- Tree DP and subtree aggregation on a tree given as edges.
- Prefix structures — a Trie is the N-ary tree specialized to strings.
- You need ordered search — use a Binary Search Tree or B-Tree.
- The structure has cycles or multiple parents — that is a graph; use an Adjacency List.
- Fan-out is always exactly two — a Binary Tree is simpler and faster.
Alternatives
Common mistakes
- Walking back to the parent when the tree is given as undirected edges — pass the parent and skip it.
- Using
max([])without a default on leaf nodes, which throws in Python. - Deep recursion on a path-shaped tree of
10^5nodes; use an explicit stack. - Serializing without child counts or sentinels, making the structure ambiguous.
Interview patterns
- Level-order / max depth / preorder on an N-ary tree (direct binary-tree translations).
- Tree DP: subtree sums, longest path, rerooting.
- Encode an N-ary tree as a binary tree with left-child right-sibling.
- Clone or serialize a general tree.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Stack versus queueBeginner
- Recursion versus iterationIntermediate
- When space complexity mattersIntermediate
- Course ScheduleIntermediate
- Network Delay TimeAdvanced
- Number of IslandsIntermediate