-1

This question builds on many similar ones like Construct hierarchy tree from flat list with parent field?

However the twist is that there is no parent id. e.g.

[
{id: 1, depth: 1, ...},
{id: 2, depth: 2, ...},
{id: 3, depth: 3, ...},
{id: 4, depth: 2, ...},

{id: 5, depth: 1, ...},
{id: 6, depth: 2, ...},
{id: 7, depth: 2, ...},

{id: 8, depth: 1, ...},
{id: 9, depth: 2, ...},


{id: 10, depth: 3, ...},
{id: 11, depth: 3, ...},
]

What is a performant way to construct the following tree? Note that the children always come after the parent i.e. one can see the tree from the depth value. For example, id 2 is a child of id 1 since its depth is 2 and id 1 has a depth of 1. id 3 is a child of id 2 since id 3 has a depth of 3. id 4 is a child of id 1 not id 3 because id 4 has a depth of 2 (a step up) from id 3's depth of 3

\\tree digram
1
  2
    3
  4
5
  6
  7
8
  9
    10
    11

Should have values like

[
{id:1, depth:1, children: [
  {id: 2, depth: 2, children: [...]},
  ...
]},
{id:5, depth:1, children: [...]},
{id:6, depth:1, children: [...]},
]
SumNeuron
  • 4,850
  • 5
  • 39
  • 107

3 Answers3

2

You can use an array for this that has an index for each depth. At every moment, it will represent a path from the (virtual) root to the current node. When dealing with a node, its parent will sit at index depth-1, where it can be inserted in that parent's children property, and the node itself will be placed at index depth:

function createForest(flatdata) {
    const path = [{ children: [] }];
    for (const obj of flatdata) {
        path[obj.depth - 1].children.push(path[obj.depth] = { ...obj, children: [] });
    }
    return path[0].children;
}

// demo
const flatdata = [{id: 1, depth: 1},{id: 2, depth: 2},{id: 3, depth: 3},{id: 4, depth: 2},{id: 5, depth: 1},{id: 6, depth: 2},{id: 7, depth: 2},{id: 8, depth: 1},{id: 9, depth: 2},{id: 10, depth: 3},{id: 11, depth: 3}];
const roots = createForest(flatdata);

console.log(roots);

Irregular depths

If the depth values do not correspond to the actual depth of the nodes, but leave gaps, then use a "dictionary" (a plain object) to record the mapping of the depth property values with which real depth they correspond with:

function createForest(flatdata) {
    const path = [{ children: [] }];
    const depthMap = { 0: 0 };
    for (const obj of flatdata) {
        path[(depthMap[obj.depth] ??= path.length) - 1].children.push(
            path[depthMap[obj.depth]] = { ...obj, children: []}
        );
    }
    return path[0].children;
}

// demo
const flatdata = [{id: 1, depth: 10},{id: 2, depth: 20},{id: 3, depth: 30},{id: 4, depth: 20},{id: 5, depth: 10},{id: 6, depth: 20},{id: 7, depth: 20},{id: 8, depth: 10},{id: 9, depth: 20},{id: 10, depth: 30},{id: 11, depth: 30}];
const roots = createForest(flatdata);

console.log(roots);

If however, the only irregularity is that the depth does not always start at 1, but sometimes at 2, it will be more efficient to prefix the input data with a dummy depth-one node, use the first function, and then remove the dummy "root" (with depth 1) from the result.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • How does pushing an assignment statement work? – SumNeuron May 18 '22 at 18:57
  • 1
    The value of an assignment expression is the value that was assigned. So first the expression is evaluated, which performs the assignment, and then the `push` happens, with that value. – trincot May 18 '22 at 18:58
  • Blowing my mind. Does this work if for some reason there is no depth 1? – SumNeuron May 18 '22 at 18:59
  • No, it needs the depths to never skip an unused depth. So a sequence of 1, 2, 4 will lead to problems at some point. Same with skipping 1 and immediately starting with 2. There is no problem with jumping *back* with bigger jumps, like 1, 2, 3, 4, 1 ... is fine. – trincot May 18 '22 at 19:01
  • 1
    If you want a version where the *meaning* of the depth value is not used, but they could even be outrageous numbers like 12, 25, 28, 25... (where 12 really *means* a depth of 1, and 25 a depth of 2) let me know, and I'll add a version that supports such gaps. – trincot May 18 '22 at 19:05
  • It would be nice for it to use relative depth. The use case is a table of contents (TOC) and for some reason the package I am using sometimes starts with 2 instead of 1, but the format that children follow the parent is solid – SumNeuron May 18 '22 at 19:06
  • 1
    Added a version that only relies on the relative comparison of depth-values, not their absolute value. They can have gaps, and 0 is reserved for the virtual root level. When `depth` values occur multiple times, they *must* all refer to the same level in the tree structure. So this sequence of depths is not allowed: 10, 15, 10, 12, 15. On the other hand, you can use *descending* values too: 1, 5, 3. Here 3 is considered deeper than 5. – trincot May 18 '22 at 19:23
1

Go through the array and add each item to the tree as well as to a trail of breadcrumbs. Each next item either goes as a child to the last one or you backtrack through the breadcrumb trail to the correct depth where it needs to be inserted:

const peek = arr =>
  arr[arr.length-1];

function toTree(arr) {
  const tree = [];
  const trail = [];
  
  for (const item of arr) {
    while ((peek(trail)?.depth ?? 0) >= item.depth) {
      trail.pop();
    }
    const current = peek(trail)?.children ?? tree;
    const treeNode = {...item, children: []};
    current.push(treeNode);
    trail.push(treeNode);
  }
  
  return tree;
}


const array = [
  {id: 1, depth: 1, },
  {id: 2, depth: 2, },
  {id: 3, depth: 3, },
  {id: 4, depth: 2, },

  {id: 5, depth: 1, },
  {id: 6, depth: 2, },
  {id: 7, depth: 2, },

  {id: 8, depth: 1, },
  {id: 9, depth: 2, },

  {id: 10, depth: 3  },
  {id: 11, depth: 3  },  
]

console.log(toTree(array));

This solution clones each item, in order to add the .children property. If no cloning is necessary, item can be directly mutated.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
1

You could take an array of the last inserted objects.

const
    data = [{ id: 1, depth: 1 }, { id: 2, depth: 2 }, { id: 3, depth: 3 }, { id: 4, depth: 2 }, { id: 5, depth: 1 }, { id: 6, depth: 2 }, { id: 7, depth: 2 }, { id: 8, depth: 1 }, { id: 9, depth: 2 }, { id: 10, depth: 3 }, { id: 11, depth: 3 }],
    result = data.reduce((r, { depth, ...o }) => {
        r[depth - 1].push({ ...o, children: r[depth] = [] });
        return r;
    }, [[]])[0];

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392