2

Well, I have an array with objects where some elements depends of others elements.

So, I need to order it by importance (dependency of parent) to store this on a database and replace all the children's parent property by the respective parent id.

Example of the array:

[
    {
        "id": 1,
        "email": "a@b.com", // unique
        "parent": "c@b.com" // is nullable
    },
    {
        "id": 2,
        "email": "b@b.com",
        "parent": null
    },
    {
        "id": 3,
        "email": "c@b.com",
        "parent": "b@b.com"
    },
    {
        "id": 4,
        "email": "d@b.com",
        "parent": "a@b.com"
    },
    ...
]

Graphical example of dependency:

enter image description here

Expected result: Ordered by dependency (parent):

[
    {
        "id": 2,
        "email": "b@b.com",
        "parent": null
    },
    {
        "id": 3,
        "email": "c@b.com",
        "parent": 2
    },
    {
        "id": 1,
        "email": "a@b.com",
        "parent": 3 
    },
    {
        "id": 4,
        "email": "d@b.com",
        "parent": 1
    },
    ...
]

To set respective parent id I'm using (but it no ordering by parent level: parent, children, grandchildren...):

let users = [
{
    "id": 1,
    "email": "a@b.com", // unique
    "parent": "c@b.com" // is nullable
},
{
    "id": 2,
    "email": "b@b.com",
    "parent": null
},
{
    "id": 3,
    "email": "c@b.com",
    "parent": "b@b.com"
},
{
    "id": 4,
    "email": "d@b.com",
    "parent": "a@b.com"
}
];

users = users.map(user => {
    user.parent = _.findIndex(users, i => user.parent === i.email);
    return user;
});

P.S: In this case, the importance concept refers to parent level. So, First I need the parents, then the children, grandchildren, and so on...

I am sorry if this thread is poor in explanations, if you have doubts, I will look for the best way to express the idea.

Shidersz
  • 16,846
  • 2
  • 23
  • 48
Olaf Erlandsen
  • 5,817
  • 9
  • 41
  • 73
  • what do you mean by "importance"? which element is the "most important one"? – quirimmo Jan 18 '19 at 16:26
  • @quirimmo Good question, I just added the answer to your question. The order is parents, childrens, grandchildren, ... – Olaf Erlandsen Jan 18 '19 at 16:32
  • In your example, the `parent` refers to the parent's `email` property, but in your desired output, they switch to `id`. Is this intentional? **EDIT:** Nevermind, I see you've mentioned this: *"replace childs with respective Id."* – Tyler Roper Jan 18 '19 at 16:34
  • @TylerRoper in seconds paragraph i say "So, I need order its by importance(dependency of parent) to store on database and replace childs with respective Id." where respective Id is the parent – Olaf Erlandsen Jan 18 '19 at 16:37
  • @RandyCasburn hits is parent level: first need parent, children, grandchildren... thanks – Olaf Erlandsen Jan 18 '19 at 16:40
  • I dont think that you can achieve that without mapping tree structure of emails.. – bigless Jan 18 '19 at 16:44
  • 1
    You want a tree traveral. What you describe could sound like a breadth-first traversal or a preorder, depth-first traversal. Your sample is too small to know which you are looking for. Please search with those terms. – trincot Jan 18 '19 at 16:48
  • What's about orphaned items / sequences; or cycled sequences? Do you expect them? – Kosh Jan 18 '19 at 16:51
  • To echo @trincot, what you're describing is a [topological sort](https://en.wikipedia.org/wiki/Topological_sorting). You'll probably want to creat an adjacency list and then traverse it starting with nodes that don't have children. – Mark Jan 18 '19 at 16:52
  • @KoshVery I Edit my question and add an answer that comes close to what I need :) – Olaf Erlandsen Jan 18 '19 at 16:52
  • @MarkMeyer good! this is what I want! – Olaf Erlandsen Jan 18 '19 at 16:53

4 Answers4

2

you can use a recursive function

const data = [{
    "id": 1,
    "email": "a@b.com", // unique
    "parent": "c@b.com" // is nullable
  },
  {
    "id": 2,
    "email": "b@b.com",
    "parent": null
  },
  {
    "id": 3,
    "email": "c@b.com",
    "parent": "b@b.com"
  },
  {
    "id": 4,
    "email": "d@b.com",
    "parent": "a@b.com"
  },

]

const order = (arr, level) => {
  const children = arr.filter(e => e.parent === level); // get the elements that have the same parent ( level )
  const parent = arr.find(e => e.email === level); // get the parent
  
  return children.length 
    ? [
        ...children.map(e => 
          ({ ...e,
            parent: parent ? parent.id : null // update the parent to the id instead of email
          })),
        ...order(arr, children[0].email) // call the same function with the email of the first child of the current children array, it will become a parent
      ] 
    : children // otherwise return the array
}

const result = order(data, null)

console.log(result)
Taki
  • 17,320
  • 4
  • 26
  • 47
2

I will approach this by first generating a new input with the replacement of parent email by the parent id and a new property of the node level related to the tree they belong. Then we can sort the nodes by this level property, and on equal level we sort by the id.

const input = [
    {"id": 1, "email": "a@b.com", "parent": "c@b.com"},
    {"id": 2, "email": "b@b.com", "parent": null},
    {"id": 3, "email": "c@b.com", "parent": "b@b.com"},
    {"id": 4, "email": "d@b.com", "parent": "a@b.com"},
    {"id": 5, "email": "x@b.com", "parent": "b@b.com"},
    {"id": 6, "email": "z@b.com", "parent": "x@b.com"},
    {"id": 7, "email": "y@b.com", "parent": null},
    {"id": 8, "email": "m@b.com", "parent": "y@b.com"}
];

const findParent = (mail) => input.find(x => x.email === mail);

const getLevel = (mail, lvl) =>
{    
    return mail ? getLevel(findParent(mail).parent, lvl + 1) : lvl;
}

let newInput = input.map(({id, email, parent}) =>
{
    return {
        id: id,
        email: email,
        parent: findParent(parent) ? findParent(parent).id : null,
        lvl: getLevel(parent, 0)
    };
});

let sortedInput = newInput.sort((a, b) =>
{
    return (a.lvl - b.lvl) ? a.lvl - b.lvl : a.id - b.id;
});

console.log(sortedInput);
Shidersz
  • 16,846
  • 2
  • 23
  • 48
  • This look like whats I want, but it no sort elements by parent level(parent, child, grandchild...): see here: https://jsbin.com/rumolulige/ – Olaf Erlandsen Jan 18 '19 at 17:13
  • Oh, so do you want element with no parents first (maybe ordered by id), then first level nodes, second levels nodes, and so on... (if we think this as a tree)? – Shidersz Jan 18 '19 at 17:28
  • Thanks too much, this work! I try it with 2.5MM elements ;) – Olaf Erlandsen Jan 18 '19 at 22:23
1

Below is an iterative approach (as opposed to the recursive solution provided) that you can use to achieve your result. Basically, start by finding the root element and then iterate over the original array looking for elements that have the current element as it's parent.

To achieve replacing parent email with ID just keep a map of parent names to IDs:

var data = [{
  "id": 1,
  "email": "a@b.com", // unique
  "parent": "c@b.com" // is nullable
}, {
  "id": 2,
  "email": "b@b.com",
  "parent": null
}, {
  "id": 3,
  "email": "c@b.com",
  "parent": "b@b.com"
}, {
  "id": 4,
  "email": "d@b.com",
  "parent": "a@b.com"
}]

//Map email addresses to IDs
var map = data.reduce((accum, el) => {
  accum[el.email] = {
    id: el.id
  }
  return accum;
}, {});


var [root] = data.filter(el => !el.parent);
var users = [root];
var cur;
var children;
while (users.length < data.length) {
  cur = users[users.length - 1];
  //Find elments that have cur as parent
  children = data.filter(el => el.parent === cur.email);
  children.forEach(el => {
    users.push({
      id: el.id,
      email: el.email,
      parent: map[el.parent].id
    });
  });
}

console.log(users)
Tom O.
  • 5,730
  • 2
  • 21
  • 35
0

All the supplied answers are fine, but slow time-complexity-wise (O(n^2)). They are going over all the the nodes, and for each node they are searching for its parent (going over nodes - O(n), and in every iteration looking for the parent - O(n) * O(n) = O(n^2))

A better solution would be creating a tree structure and using pre-order (DFS) for creating a topological sort.

function createTree(nodesWithParentArray) {
    const initialTree = nodesWithParentArray.reduce(
      (acc, node) => {
        acc[node.id] = { data: node, children: [] }

        return acc
      },
      { null: { children: [] } }
    )

    const tree = nodesWithParentArray.reduce((acc, node) => {
      acc[node.parent].children.push(acc[node.id])

      return acc
    }, initialTree)

    return tree[null].children[0] // root
}

// test it like that:
createTree([{id:1, parent:2},{id:2, parent:null},{id:3, parent:2},{id:4, parent:3}])

The func above would return a nested tree structure, with a pointer to the root node. What left to do is using pre-order traversal to create a topological sort (O(n), as we are going over each node just once):

function topologicalSort(tree) {
    const sortedList = []
    const queue = [treeRoot]

    while (queue.length) {
      const curNode = queue.shift()
      sortedList.push(curNode.data)
      queue.push(...curNode.children)
    }

    return sortedList
}

Creating the tree above would also be O(n) as it loops over the input array just once, so the final time complexity would be O(n) + O(n) => O(n)

Shay Yzhakov
  • 975
  • 2
  • 13
  • 31
  • I think this isn't O(N) in the worst case. `Array.shift` has complexity O(M), where M is the length of the array. Thus, in the worst-case with all nodes being children of the root, you have N `shift` operations with an average length of N/2, making this O(Nˆ2). – Arno Hilke Feb 01 '22 at 14:07
  • if you're right regarding `Array.shift`, it is easy to replace it with some other techniques (like an array with a pointer to the first relevant element) – Shay Yzhakov Feb 03 '22 at 09:12