Introduction
Recursion can be tricky. There are four fundamental patterns good to know.
To be honest, I just came up with the names myself lmao, but these patterns are pretty common.
The "Return Value Chain" Pattern
function findInTree(node, target) {
// Common mistake: forgetting to return the result
if (node.value === target) {
return node; // Found it!
}
if (node.children) {
for (const child of node.children) {
// Common mistake: not capturing the return value
const found = findInTree(child, target); // Important!
if (found) return found; // Pass it up!
}
}
return null; // Nothing found in this branch
}
When calling findInTree(child, target)
again, you can't return it then. Otherwise what would happen is that the function would stop after the first child since it would return right away even though you didn't find the target.
The "Modify as You Go" Pattern
function multiplyAllValues(node, multiplier) {
// Modify current node
node.value *= multiplier;
// Recurse on children if they exist
if (node.children) {
node.children.forEach((child) => {
multiplyAllValues(child, multiplier);
});
}
// No return needed if just modifying
}
Here we modify the value in place. Because objects are passed by reference, the changes will be reflected in the original object. When we say "reference" we mean that the object is not copied. The same object in the function points to the same memory location as the original object. That's why when you modify what e.g. field
of obj
points to, it will be reflected in the memory location of obj
.
The "Collect Results" Pattern
function getAllLeafNodes(node, results = []) {
// Base case: it's a leaf
if (!node.children || node.children.length === 0) {
results.push(node);
} else {
// Recurse on all children
node.children.forEach((child) => {
getAllLeafNodes(child, results);
});
}
return results;
}
Here we're modifying the results
array as we go. Arrays are objects under the hood, so they are passed by reference. That means that the changes will be reflected in the original array (the same one throughout the recursion).
It's important not to forget to pass the results
array when calling the function recursively. And make sure to return the results
array at the end. Otherwise nothing will be returned.
The "Keep track of path" Pattern
function findNodeAndPath(
nodes: Array<Node>,
targetId: string,
path: Array<Node> = []
): Array<Node> | null {
for (const node of nodes) {
path.push(node);
if (node.id === targetId) {
return path;
}
if ("children" in node && node.children.length > 0) {
const found = findNodeAndPath(node.children, targetId, path);
if (found) return found;
}
path.pop();
}
return null;
}
Here we're keeping track of the path to the target node as we go. When we return the path, the last element is the target node. The first element would be the upper most parent node.