'How to flatten tree structure into array of arrays
Given a structure like this:
var node = { children: [] }
That is to say:
- It only has
children
property. - No
parent
property. - No
nextSibling
property.
How to build a flat list of arrays with paths to all the leaf nodes, using a top-down approach:
- The algorithm doesn't use any helper methods, just plain JavaScript.
- The algorithm builds the arrays as it traverses the tree from the top.
So here is an example data:
var node = {
item: 1,
children: [
{
item: 2,
children: [
{
item: 3,
children: [
{
item: 4,
children: []
},
{
item: 5,
children: []
},
{
item: 6,
children: [
{
item: 7,
children: []
},
{
item: 8,
children: []
},
{
item: 9,
children: []
}
]
}
]
},
{
item: 10,
children: [
{
item: 11,
children: []
},
{
item: 12,
children: [
{
item: 13,
children: []
},
{
item: 14,
children: []
}
]
}
]
}
]
}
]
}
And the function should return:
[
[1, 2, 3, 4],
[1, 2, 3, 5],
[1, 2, 3, 6, 7],
[1, 2, 3, 6, 8],
[1, 2, 3, 6, 9],
[1, 2, 10, 11],
[1, 2, 10, 12, 13],
[1, 2, 10, 12, 14]
]
My attempt so far has been variations of:
function aggregateNodes(node) {
var stack = [node]
var array = []
while (stack.length) {
var node = stack.pop()
array.push(node.item)
node.children.forEach(function(child){
stack.push(child)
})
}
return array
}
function aggregateNodesRecursive(node) {
var array = [node.item]
node.children.forEach(function(child){
array.push(child.item)
child.children.forEach(function(confusedNow){
array.push(confusedNow.item)
aggregateNodesRecursive(confusedNow)
})
})
return array
}
Solution 1:[1]
A recursive approach would be:
function traverse(node, path = [], result = []){
if(!node.children.length)
result.push(path.concat(node.item));
for(const child of node.children)
traverse(child, path.concat(node.item), result);
return result;
}
Or some hacky ES6:
const traverse = node =>
(node.children.length?[]:[[node.item]]).concat(...node.children.map(child =>
traverse(child).map(arr =>
[node.item].concat(arr)
)
));
Now calling traverse(node)
will return the desired result.
Solution 2:[2]
You can create recursive function and first check if current element is array or object and store previous item numbers in one variable.
var node = {"item":1,"children":[{"item":2,"children":[{"item":3,"children":[{"item":4,"children":[]},{"item":5,"children":[]},{"item":6,"children":[{"item":7,"children":[]},{"item":8,"children":[]},{"item":9,"children":[]}]}]},{"item":10,"children":[{"item":11,"children":[]},{"item":12,"children":[{"item":13,"children":[]},{"item":14,"children":[]}]}]}]}]}
const result = []
function flat(data, prev = '') {
if (Array.isArray(data)) {
data.forEach(e => flat(e, prev))
} else {
prev = prev + (prev.length ? '.' : '') + data.item;
if (!data.children.length) result.push(prev.split('.').map(Number))
else flat(data.children, prev)
}
}
flat(node)
console.log(result)
Solution 3:[3]
Here is a solution using object-scan. It's handy for all kinds of data processing - once you wrap your head around it.
// const objectScan = require('object-scan');
const find = (tree) => objectScan(['**.children'], {
reverse: false,
filterFn: ({ value, parents, context }) => {
if (value.length === 0) {
context.push(
parents
.filter((p) => 'item' in p)
.map(({ item }) => item)
.reverse()
);
}
}
})(tree, []);
const node = { item: 1, children: [{ item: 2, children: [{ item: 3, children: [{ item: 4, children: [] }, { item: 5, children: [] }, { item: 6, children: [{ item: 7, children: [] }, { item: 8, children: [] }, { item: 9, children: [] }] }] }, { item: 10, children: [{ item: 11, children: [] }, { item: 12, children: [{ item: 13, children: [] }, { item: 14, children: [] }] }] }] }] };
console.log(find(node));
/* =>
[ [ 1, 2, 3, 4 ],
[ 1, 2, 3, 5 ],
[ 1, 2, 3, 6, 7 ],
[ 1, 2, 3, 6, 8 ],
[ 1, 2, 3, 6, 9 ],
[ 1, 2, 10, 11 ],
[ 1, 2, 10, 12, 13 ],
[ 1, 2, 10, 12, 14 ] ]
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>
Disclaimer: I'm the author of object-scan
Edit: And here is a more efficient solution, in case performance is important
// const objectScan = require('object-scan');
const find = (tree) => objectScan(['**.children'], {
reverse: false,
breakFn: ({ isMatch, parent, context: { stack } }) => {
if (isMatch) {
stack.push(parent.item);
}
},
filterFn: ({ value: { length }, context: { result, stack } }) => {
if (length === 0) {
result.push([...stack]);
}
stack.pop();
}
})(tree, { stack: [], result: [] }).result;
const node = { item: 1, children: [{ item: 2, children: [{ item: 3, children: [{ item: 4, children: [] }, { item: 5, children: [] }, { item: 6, children: [{ item: 7, children: [] }, { item: 8, children: [] }, { item: 9, children: [] }] }] }, { item: 10, children: [{ item: 11, children: [] }, { item: 12, children: [{ item: 13, children: [] }, { item: 14, children: [] }] }] }] }] };
console.log(find(node));
/* =>
[ [ 1, 2, 3, 4 ],
[ 1, 2, 3, 5 ],
[ 1, 2, 3, 6, 7 ],
[ 1, 2, 3, 6, 8 ],
[ 1, 2, 3, 6, 9 ],
[ 1, 2, 10, 11 ],
[ 1, 2, 10, 12, 13 ],
[ 1, 2, 10, 12, 14 ] ]
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>
Disclaimer: I'm the author of object-scan
Solution 4:[4]
You could take an iterative and recursive approach with a function which looks for children or returns the path the last node.
function flatNodes(node) {
const iter = temp => ({ item, children = [] }) => children.length
? children.forEach(iter(temp.concat(item)))
: nodes.push(temp.concat(item));
var nodes = [];
[node].forEach(iter([]));
return nodes;
}
var node = { item: 1, children: [{ item: 2, children: [{ item: 3, children: [{ item: 4, children: [] }, { item: 5, children: [] }, { item: 6, children: [{ item: 7, children: [] }, { item: 8, children: [] }, { item: 9, children: [] }] }] }, { item: 10, children: [{ item: 11, children: [] }, { item: 12, children: [{ item: 13, children: [] }, { item: 14, children: [] }] }] }] }] };
console.log(flatNodes(node).map(a => JSON.stringify(a)));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Solution 5:[5]
The accepted answer above throws when a node does not have a children
property.
Edit: I realize that the solution I offer below does not satisfy Lance's task. However, it may be useful for flattening a tree structure into a single array.
If the goal is to just create a flat array of all the nodes' item
values, this may be better:
const traverse = (node, flattened = [node.item]) => {
node.children?.map((child) => {
flattened.push(child.item);
traverse(child, flattened);
});
return flattened;
}
The test:
const tree = {
item: 'a',
children: [
{ item: 'b'},
{
item: 'c',
children: [
{ item: 'd'}
]
}
]
};
expect(traverse(tree)).toEqual(['a', 'b', 'c', 'd']);
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Nenad Vracar |
Solution 3 | |
Solution 4 | |
Solution 5 |