N-ary Tree Recursive/Level Traversal
Prerequisite Knowledge
Before reading this article, you should first learn:
Summary in One Sentence
A multi-way tree structure is an extension of the binary tree structure, where a binary tree is a special case of a multi-way tree. A forest refers to a collection of multiple multi-way trees.
Traversing a multi-way tree is an extension of binary tree traversal.
The nodes of a binary tree look like this, with each node having two child nodes:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
}
class TreeNode {
public:
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
var TreeNode = function(val) {
this.val = val;
this.left = null;
this.right = null;
};
A node in an n-ary tree looks like this, where each node has an arbitrary number of child nodes:
class Node {
int val;
List<Node> children;
}
class Node {
public:
int val;
vector<Node*> children;
};
class Node:
def __init__(self, val: int):
self.val = val
self.children = []
type Node struct {
val int
children []*Node
}
var Node = function(val) {
this.val = val;
this.children = [];
}
That's the only difference, nothing else.
Forest
Here, let's introduce the term "forest," which will be used later when discussing the Union-Find algorithm.
Simply put, a forest is a collection of multiple multi-way trees. A single multi-way tree is actually a special kind of forest.
In the Union-Find algorithm, we often manage root nodes of several multi-way trees simultaneously, and the collection of these root nodes is called a forest.
Next, let's talk about traversing multi-way trees, which, like binary trees, can be done using two methods: recursive traversal (DFS) and level-order traversal (BFS).
Recursive Traversal (DFS)
Let's compare the traversal framework of multi-way trees with that of binary trees:
// traversal framework for binary tree
void traverse(TreeNode root) {
if (root == null) {
return;
}
// pre-order position
traverse(root.left);
// in-order position
traverse(root.right);
// post-order position
}
// traversal framework for N-ary tree
void traverse(Node root) {
if (root == null) {
return;
}
// pre-order position
for (Node child : root.children) {
traverse(child);
}
// post-order position
}
// binary tree traversal framework
void traverse(TreeNode* root) {
if (root == nullptr) {
return;
}
// pre-order position
traverse(root->left);
// in-order position
traverse(root->right);
// post-order position
}
// N-ary tree traversal framework
void traverse(Node* root) {
if (root == nullptr) {
return;
}
// pre-order position
for (Node* child : root->children) {
traverse(child);
}
// post-order position
}
# traversal framework for binary trees
def traverse_binary_tree(root):
if root is None:
return
# pre-order position
traverse_binary_tree(root.left)
# in-order position
traverse_binary_tree(root.right)
# post-order position
# traversal framework for N-ary trees
def traverse_n_ary_tree(root):
if root is None:
return
# pre-order position
for child in root.children:
traverse_n_ary_tree(child)
# post-order position
// Traversal framework for binary tree
func traverse(root *TreeNode) {
if root == nil {
return
}
// Preorder position
traverse(root.Left)
// Inorder position
traverse(root.Right)
// Postorder position
}
// Traversal framework for N-ary tree
func traverseNary(root *Node) {
if root == nil {
return
}
// Preorder position
for _, child := range root.Children {
traverseNary(child)
}
// Postorder position
}
// binary tree traversal framework
var traverse = function(root) {
if (root === null) {
return;
}
// pre-order position
traverse(root.left);
// in-order position
traverse(root.right);
// post-order position
};
// N-ary tree traversal framework
var traverse = function(root) {
if (root === null) {
return;
}
// pre-order position
for (var i = 0; i < root.children.length; i++) {
traverse(root.children[i]);
}
// post-order position
};
The only difference is that an N-ary tree does not have an inorder position, because it can have multiple nodes, making the concept of an inorder position meaningless.
Level Order Traversal (BFS)
The level order traversal of an N-ary tree is similar to the level order traversal of a binary tree, using a queue for implementation. The only difference is replacing the left and right child nodes of a binary tree with all child nodes of an N-ary tree. Therefore, there are three methods for performing level order traversal in an N-ary tree, which are listed below.
Method One
The first method for level order traversal:
void levelOrderTraverse(Node root) {
if (root == null) {
return;
}
Queue<Node> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
Node cur = q.poll();
// visit the cur node
System.out.println(cur.val);
// add all children of cur to the queue
for (Node child : cur.children) {
q.offer(child);
}
}
}
void levelOrderTraverse(Node* root) {
if (root == nullptr) {
return;
}
std::queue<Node*> q;
q.push(root);
while (!q.empty()) {
Node* cur = q.front();
q.pop();
// visit the cur node
std::cout << cur->val << std::endl;
// add all children of cur to the queue
for (Node* child : cur->children) {
q.push(child);
}
}
}
from collections import deque
def level_order_traverse(root):
if root is None:
return
q = deque()
q.append(root)
while q:
cur = q.popleft()
# visit the cur node
print(cur.val)
# add all child nodes of cur to the queue
for child in cur.children:
q.append(child)
func levelOrderTraverse(root *Node) {
if root == nil {
return
}
q := []*Node{}
q = append(q, root)
for len(q) > 0 {
cur := q[0]
q = q[1:]
// visit cur node
fmt.Println(cur.Val)
// add all children of cur to the queue
for _, child := range cur.Children {
q = append(q, child)
}
}
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
var q = [];
q.push(root);
while (q.length !== 0) {
var cur = q.shift();
// visit cur node
console.log(cur.val);
// add all child nodes of cur to the queue
for (var child of cur.children) {
q.push(child);
}
}
}
Method Two
The second method for level-order traversal that can record the depth of nodes:
void levelOrderTraverse(Node root) {
if (root == null) {
return;
}
Queue<Node> q = new LinkedList<>();
q.offer(root);
// Record the current depth being traversed (root node is considered level 1)
int depth = 1;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
// Visit the cur node and know its level
System.out.println("depth = " + depth + ", val = " + cur.val);
for (Node child : cur.children) {
q.offer(child);
}
}
depth++;
}
}
#include <iostream>
#include <queue>
#include <vector>
void levelOrderTraverse(Node* root) {
if (root == nullptr) {
return;
}
std::queue<Node*> q;
q.push(root);
// record the current level being traversed (root node is considered level 1)
int depth = 1;
while (!q.empty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
Node* cur = q.front();
q.pop();
// visit the cur node and know its level
std::cout << "depth = " << depth << ", val = " << cur->val << std::endl;
for (Node* child : cur->children) {
q.push(child);
}
}
depth++;
}
}
from collections import deque
def levelOrderTraverse(root):
if root is None:
return
q = deque()
q.append(root)
# record the current level being traversed (root node is considered level 1)
depth = 1
while q:
sz = len(q)
for i in range(sz):
cur = q.popleft()
# visit the cur node and know the level it is on
print(f"depth = {depth}, val = {cur.val}")
for child in cur.children:
q.append(child)
depth += 1
func levelOrderTraverse(root *Node) {
if root == nil {
return
}
q := []*Node{root}
// record the current depth being traversed (root node is considered as level 1)
depth := 1
for len(q) > 0 {
sz := len(q)
for i := 0; i < sz; i++ {
cur := q[0]
q = q[1:]
// visit the cur node and know its depth
fmt.Printf("depth = %d, val = %d\n", depth, cur.val)
for _, child := range cur.children {
q = append(q, child)
}
}
depth++
}
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
var q = [];
q.push(root);
// record the current depth being traversed (root node is considered level 1)
var depth = 1;
while (q.length !== 0) {
var sz = q.length;
for (var i = 0; i < sz; i++) {
var cur = q.shift();
// visit the cur node and know its depth level
console.log("depth = " + depth + ", val = " + cur.val);
for (var j = 0; j < cur.children.length; j++) {
q.push(cur.children[j]);
}
}
depth++;
}
}
Method Three
The third method that can adapt to edges with different weights:
class State {
Node node;
int depth;
public State(Node node, int depth) {
this.node = node;
this.depth = depth;
}
}
void levelOrderTraverse(Node root) {
if (root == null) {
return;
}
Queue<State> q = new LinkedList<>();
// record the current depth of traversal (root node is considered as level 1)
q.offer(new State(root, 1));
while (!q.isEmpty()) {
State state = q.poll();
Node cur = state.node;
int depth = state.depth;
// visit the cur node while knowing its depth
System.out.println("depth = " + depth + ", val = " + cur.val);
for (Node child : cur.children) {
q.offer(new State(child, depth + 1));
}
}
}
class State {
public:
Node* node;
int depth;
State(Node* node, int depth) : node(node), depth(depth) {}
};
void levelOrderTraverse(Node* root) {
if (root == nullptr) {
return;
}
std::queue<State> q;
// Record the current level being traversed (root node is considered as level 1)
q.push(State(root, 1));
while (!q.empty()) {
State state = q.front();
q.pop();
Node* cur = state.node;
int depth = state.depth;
// Visit the cur node and know its level
std::cout << "depth = " << depth << ", val = " << cur->val << std::endl;
for (Node* child : cur->children) {
q.push(State(child, depth + 1));
}
}
}
class State:
def __init__(self, node, depth):
self.node = node
self.depth = depth
def levelOrderTraverse(root):
if root is None:
return
q = deque()
# Record the current level being traversed (root node is considered as level 1)
q.append(State(root, 1))
while q:
state = q.popleft()
cur = state.node
depth = state.depth
# Visit the cur node while knowing its level
print(f"depth = {depth}, val = {cur.val}")
for child in cur.children:
q.append(State(child, depth + 1))
type State struct {
node *Node
depth int
}
func levelOrderTraverse(root *Node) {
if root == nil {
return
}
q := []State{}
// record the current level being traversed (root node is considered as level 1)
q = append(q, State{root, 1})
for len(q) > 0 {
state := q[0]
q = q[1:]
cur := state.node
depth := state.depth
// visit the cur node and know its level
fmt.Printf("depth = %d, val = %d\n", depth, cur.Val)
for _, child := range cur.Children {
q = append(q, State{child, depth + 1})
}
}
}
function State(node, depth) {
this.node = node;
this.depth = depth;
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
var q = [];
// record the current level being traversed (consider the root node as level 1)
q.push(new State(root, 1));
while (q.length !== 0) {
var state = q.shift();
var cur = state.node;
var depth = state.depth;
// visit the cur node and know its level
console.log("depth = " + depth + ", val = " + cur.val);
for (var i = 0; i < cur.children.length; i++) {
q.push(new State(cur.children[i], depth + 1));
}
}
}
Nothing much to say. If there's anything unclear, refer to the level-order traversal code and visualization panel in the previous article Binary Tree Traversal.