简体   繁体   中英

Find all ancestry of certain elements in a flat array of objects

I'm trying to achieve some specific use case but come to a dead end. I need to, given an flat array of objects like this (Copying it from another similar post, as I found several similar posts but none matching my use case, or at least I haven't been smart enough to realise how to tweak possible solutions to fit my use-case):

const myArr = [
{
    id: '1',
    parentId: '0',
   
},
{
    id: '2',
    parentId: '1',
    
},
{
    id: '3',
    parentId: '2',
    
},
{
    id: '4',
    parentId: '2',
   
},
{
    id: '5',
    parentId: '2',
    
},
{
    id: '6',
    parentId: '2',
    
},
{
    id: '7',
    parentId: '6',
    
},
{
    id: '8',
    parentId: '7',
    
}

]

And then I have another array of IDs like so:

const idArr = [2, 4, 8]

So I need to filter from the first array, elements with matching ID, ie the element with ID 2, then ID 4 and ID 8

And then, for each element present in the filtered array, I need to find it's ancestry until reach the root level, then build a tree

The problem here is that I already achieved it, but in real life this array will be huge, with thousands of elements in it, and the code will run most likely a lot of times

So I am looking for the technically most performant possible solution: I'd say building a tree recursively is pretty much done in a performant way, but somehow I am in a dead end with step 2, getting all the ancestry of certain elements.

Could anyone bring some light here? Thanks a lot in advance

 const myArr = [ { id: '1', parentId: '0', }, { id: '2', parentId: '1', }, { id: '3', parentId: '2', }, { id: '4', parentId: '2', }, { id: '5', parentId: '2', }, { id: '6', parentId: '2', }, { id: '7', parentId: '6', }, { id: '8', parentId: '7', } ] const idArr = [2, 4, 8] elObj = {} for (const el of myArr) { elObj[el.id] = {"parentId": el.parentId} } const getAncestory = (id, res) => { if (elObj[id].parentId === '0') { res.push('0') return res } res.push(elObj[id].parentId) return getAncestory(elObj[id].parentId, res) } const res = [] idArr.forEach(el => { res.push(getAncestory(el.toString(), [el.toString()])) }) console.log(elObj) console.log(res)

Here's how the above code works and performs in terms of time complexity:

  1. Creating an object of objects where elements can be accessed in constant time based on their ids is a linear operation, both time and space-wise. We could have skipped this step if there was a guarantee that say element with id i is at index i of the initial array.
  2. Creating each ancestry list takes O(m) time where m is the distance of the initial element to the root element of the tree. Note that we have assumed that all elements are eventually connected to the root element (our base case of parentId === '0' ). If this assumption is not correct, we need to amend the code for that.
  3. Assuming that there are n elements that you need to build the ancestry lists for (length of idArr ), this whole process takes O(n * m) , since the operations are all constant.
  4. This algorithm can deteriorate into a quadratic one in terms of the number of nodes in the tree in case the tree has the shape of a flat linked list and you want the ancestry of all of its elements. That's because we would need to list 1 + 2 +... n-1 + n elements where the closest element to the root takes 1 step and the farthest away takes n steps. This leads to n * (n+1)/2 steps which is O(n^2) in Big O terms.
  5. One way to amend it is to change the representation of the tree with parent to child pointers. Then we can start from the root and backtrack, traversing all the possible paths and saving those of interest. This approach could be beneficial or worse the proposed one depending on data and the exact requirements for the output.

Note: If you have a few thousands of objects and are looking for the ancestry of a few hundreds of them, the above approach is fine (I'm making a lot of assumptions about the data). To make an educated guess one needs more details about the data and requirements.

It's not entirely clear what you're trying to generate. This answer makes the guess that you want a tree that includes only the nodes whose ids are specified, as well as their ancestors. It returns a structure like this:

[
  {id: "1", children: [
    {id: "2", children: [
      {id: "4", children: []},
      {id: "6", children: [
        {id: "7", children: [
          {id: "8", children: []}
        ]}
      ]}
    ]}
  ]}
]

Note that this is not a tree but a forest. I see nothing to demonstrate that every lineage ends up at a single root node, so the output might have multiple roots. But if your data does enforce the single root, you can just take the first element.

This code will do that:

 const unfold = (fn, init, res = []) => fn (init, (x, s) => unfold (fn, s, res.concat (x)), () => res) const filterByIds = (xs, map = Object.fromEntries(xs.map (({id, parentId}) => [id, parentId]))) => ( ids, selected = [...new Set (ids.map (String).flatMap ( id => unfold ((i, next, stop) => i in map? next (i, map [i]): stop(), id) ))] ) => xs.filter (({id}) => selected.includes (id)) const makeForest = (xs, root = 0) => xs.filter (({parentId}) => parentId == root).map (({id, parentId, ...rest}) => ({ id, ...rest, children: makeForest (xs, id) })) // [0] /* if the data really forms a tree */ const extractForest = (xs, ids) => makeForest (filterByIds (xs) (ids)) const myArr = [{id: "1", parentId: "0"}, {id: "2", parentId: "1"}, {id: "3", parentId: "2"}, {id: "4", parentId: "2"}, {id: "5", parentId: "2"}, {id: "6", parentId: "2"}, {id: "7", parentId: "6"}, {id: "8", parentId: "7"}] console.log ( extractForest (myArr, [2, 4, 8]) )
 .as-console-wrapper {max-height: 100%;important: top: 0}

We start with the somewhat interesting unfold helper function. This lets you start with a seed value and turn it into an array of values by repeatedly calling the function you supply with the current seed and two function, one to pass along a new value and the next seed, the other to stop processing and return the list of values returned so far. We use this to track the lineage of each id. This is by no means the only way we could have done so. A while loop is probably more familiar, but it's a useful tool, and it doesn't involve any reassignment or mutable variables, which I really appreciate.

(An example of how unfold works might be a simple Fibonacci number generator:

const fibsTo = (n) => 
  unfold (([a, b], next, stop) => b <= n ? next (b, [b, a + b]) : stop (), [0, 1])

fibsTo (100) //=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

This calls next with the next Fibonnaci number and a new seed including the current value and the one which would come next, starting with the seed [0, 1] When the total passes our target number, we instead call stop .)

Next we have the function filterByIds that takes your input array, and returns a function that accepts a list of ids and filters the array to include just those elements which are in the ancestry of one of those ids. We do this in three steps. First, we create an Object ( map ) mapping the ids of our input values to those actual values. Second, we flatmap the ids with a function to retrieve the list of their ancestors; this uses our unfold above, but could be rewritten with a while loop. And we use [... new Set (/* above */)] to collect the unique values from this list. Third, we filter the original list to include only the elements whose ids are in this new list ( selected .)

The function makeForest -- like unfold is fairly generic, taking a flat list of {id, parentId, ...more} nodes and nesting them recursively in an {id, ...more, children: []} structure. Uncomment the [0] in the last line if your data is singly rooted.

And finally we have our main function extractForest which calls makeForest pm the result of filterByIds .

I would like to stress that unfold and makeForest are both quite generic. So the custom code here is mostly filterByIds , and the simple extractForest wrapper.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM