0

I have a deep array of objects like {id, followers} where followers repeat the pattern:

{
  id: 0, followers: [{
    id: 1, followers: [{
      id: 11, followers: [{
        id: 111, followers: [...]
      }]
    }]
  }, {
    id: 2, followers: [{
      id: 21, followers: [{
        id: 211, followers: [...]
      }]
    }]
  }, {
    id: 3, followers: [{
      id: 31, followers: [...]
    }]
  }, ...]
}

I want to walk this tree and create a flattened array of type {id, depth}

I'm using the following recursive function:

function recurse(user, depth = 0, list = []) {
  for (const { id } of user.followers) {
    list.push({ id, depth });
  }
  for (const follower of user.followers) {
    recurse(
      user: follower,
      depth: depth + 1,
      list
    );
  }
  return list;
}

It goes deeper and deeper into every branch before moving on, which results in something like

[
  { id: 0, depth: 0},
  { id: 1, depth: 1},
  { id: 11, depth: 2},
  { id: 111, depth: 3},
  { id: 1111, depth: 4},
  { id: 11111, depth: 5},
  ...
  { id: 2, depth: 1},
  { id: 21, depth: 2},
  { id: 211, depth: 3},
  ...
  { id: 3, depth: 1},
  { id: 31, depth: 2},
  { id: 311, depth: 3},
  ...
]

But I want this to automatically come out as sorted by depth, like this:

[
  { id: 0, depth: 0},
  { id: 1, depth: 1},
  { id: 2, depth: 1},
  { id: 3, depth: 1},
  { id: 4, depth: 1},
  ...
  { id: 11, depth: 2},
  { id: 12, depth: 2},
  { id: 13, depth: 2},
  ...
  { id: 111, depth: 3},
  { id: 211, depth: 3},
  ...
]

I want to go elements depth-wise, i.e. all elements from depth 0, then all elements from depth 1, and so on.

My implementation doesn't respect the depth order. It simply follows the branches as deep as they go first, then moves on to the next branch.

I know I can just sort it later, but other problems prevent me from doing that. Mainly I have duplicate items and I want to filter those out. And while filtering, I want to keep the items with lowest depth. My approach doesn't allow that because it doesn't go through all elements of lowest depth first.

How can I traverse this tree depth-wise instead of branch-wise?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
rasocbo
  • 35
  • 6
  • Do you want that for performance reasons (maybe because you think that way you'll stop the recursion earlier) or just because at the end you would prefer that kind of sorting? – Ernesto Stifano Aug 01 '20 at 12:26
  • I know I can just sort it later, but I have duplicate items and I want to filter those out. And while filtering, I want to keep the items with lowest depth. My approach doesn't allow that because it doesn't go through all elements of lowest depth first. – rasocbo Aug 01 '20 at 12:27
  • is there any guarantee that id's are always in ascending order within a specific branch? could be that some brach has all id's that start with 1 but then there's another branch which also has an id which starts with 1? – Yos Aug 01 '20 at 13:23
  • @Yos No, Ids are pretty much random. – rasocbo Aug 01 '20 at 13:42
  • Then I would suggest editing your example because it looks like there's a specific pattern of id's there which is confusing because as you just confirmed the id's are totally random, so there's no pattern. – Yos Aug 01 '20 at 13:52
  • by the way your code doesn't compile because of this line `recurse( user: follower, depth: depth + 1, list )` – Yos Aug 01 '20 at 14:52

2 Answers2

0

I don't think that traversing the array in "depth-first" approach will solve your problems because:

  1. This still won't prevent duplicates.
  2. I don't think it's reasonable to assume that an id 111 will not be in the first branch where in your example all other 1-based id's are concentrated.

The function below performs a truly depth first search. Also it handles the root element which is not handled in the OP's example:

function traverse(user = {}, depth = 0, map = new Map()) {
  const { id, followers } = user;
  if (id != null && !map.has(id)) {
    map.set(id, { id, depth });
  }
  if (followers) {
    const [first, ...rest] = followers;
    traverse(first, depth + 1, map);
    for (const user of rest) {
      traverse(user, depth + 1, map);
    }
  }
  return map;
}

const resultMap = traverse(input);
const sortedResult = Array.from(resultMap).sort(([, a], [, b]) => a.depth - b.depth);
Yos
  • 1,276
  • 1
  • 20
  • 40
  • I know the approach itself won't prevent dupes, but I will manually check for them in the loop/recursion itself (based on id, and the list generated thus far). The problem isn't de-duplication, but rather that IF there are duplicates, the ones I want to keep should be ones that belong to the lower depth - which is proving impossible if it goes down a branch rather than processing all items of same depth first. – rasocbo Aug 01 '20 at 13:45
  • How will you check manually for duplicates? By going over the full intermediate result each time? This will lead to horrible performance. – Yos Aug 01 '20 at 13:50
  • Are you saying that 2 same id's can have different values? – Yos Aug 01 '20 at 13:54
  • Since I'm outputting `{id, depth}` to the final list, which is different from the original form (`{id, followers}`), I pretty much have to rely on checking manually (comparing IDs in the list) – rasocbo Aug 01 '20 at 13:58
  • Why do you need to check the id's manually if you can sort once instead? What is your real goal? It looks like your real goal is to sort id's isn't it? – Yos Aug 01 '20 at 14:02
  • Not ids, but depth - my goal is to sort by depth. And to remove all duplicates only in that way - i.e. remove duplicate IDs that occur in the higher depths (rather than lower depths). That's why I need to go through each depth first, ***then*** go deeper into each of their respective branches. – rasocbo Aug 01 '20 at 14:07
  • I don't understand if you have 2 duplicates one from depth 1, another from depth 5. They represent the same object right? So why does it matter which one is selected? – Yos Aug 01 '20 at 14:14
  • As long as final item (in the flattened list) has `{depth: 1}` (rather than `{depth: 5}`), it works for me. I'm interested in the `depth` property in the final list - it should represent the lowest value among the duplicate items. – rasocbo Aug 01 '20 at 14:24
0

Found an solution. Apparently it's a Breadth-first_search and requires a maintaining a queue:

function traverse(user) {
  const queue = [];
  const list = [];
  queue.push({ followers: user.followers, depth: 0 });

  let item;
  while (item = queue.shift()) {
    const { followers, depth } = item;
    for (const follower of followers) {
      if (list.find(l => l.id === follower.id)) continue;
      list.push({ id: follower.id, depth });
      queue.push({ followers: follower.followers, depth: depth + 1 });
    }
  }

  return list;
}
rasocbo
  • 35
  • 6
  • what is the advantage of this answer? The question stipulates that the answer must do depth first search. Time complexity of the answer is **always** O(n^2) which is worse that my answers which have O(nlogn) on average. Your answer is also more complicated because it uses 2 data structures while mine only uses 1. Also your answer doesn't handle root element – Yos Aug 01 '20 at 15:37