Related
I've come across this problem before, say with Categories or Tags.
You have multiple tags that can be children of each other:
{ id: 1, name: 'Sports', parent_id: null }
{ id: 2, name: 'Fruits', parent_id: null }
{ id: 3, name: 'Citrus', parent_id: 2 }
{ id: 4, name: 'Orange', parent_id: 3 }
{ id: 5, name: 'Hockey', parent_id: 1 }
Another representation of these nodes:
Sports -> Hockey
Fruits -> Citrius -> Orange
What is the algorithm that efficiently finds the top-most parent for each node? So we can go from Orange -> Fruits in O(1) time.
(Requires some preprocessing).
As another answer suggests, you could first create a dictionary keyed by node id. But then when finding the top-level parent of a node, that information should bubble up from recursion and be used to update all intermediate nodes, since they have the same top-level parent. This will save on the work to do and make the whole proces O(n).
const categories = [
{ id: 1, name: 'Sports', parent_id: null },
{ id: 2, name: 'Fruits', parent_id: null },
{ id: 3, name: 'Citrus', parent_id: 2 },
{ id: 4, name: 'Orange', parent_id: 3 },
{ id: 5, name: 'Hockey', parent_id: 1 },
{ id: 6, name: 'Apple', parent_id: 2 }
];
// Build a Map of nodes keyed by node id:
const map = new Map(categories.map(node => [node.id, node]));
// Enrich each node with parent and top node-references
function extendNode(node) {
if (!node.top) {
node.parent = map.get(node.parent_id);
node.top = node.parent ? extendNode(node.parent) : node;
}
return node.top;
}
categories.forEach(extendNode);
// Example use:
const orange = map.get(4);
console.log(orange.name, "=>", orange.top.name);
.as-console-wrapper { max-height: 100% !important }
Idea:
First create a dictionary for each id that takes O(n) where n is the length of categories array.
Traverse each array element and look for its top parent so it takes O(n*d) where d is the depth of its parent and build a dictionary that contains keys as child names and values as parent properties.
So this approach takes O(n*d) for preprocessing.
But if you use DP for getParent() the time complexity would be O(n*α) where α is a slowly growing function. so it's not exactly O(n) but pretty close to the linear runtime.
Idea of DP in details:
We can also reduce the computations of getParent() by using Dynamic programming. For example, look at this dependency path Fruits -> Citrius -> Orange so when we visit Citrius we know that ends up in Fruits so we need to memoize the result. so when next time when we start from Orange the next node is Citrius and we know from Citrius we can have the result Fruits. So no need to visit all nodes.
const categories = [
{ id: 1, name: 'Sports', parent_id: null },
{ id: 2, name: 'Fruits', parent_id: null },
{ id: 3, name: 'Citrus', parent_id: 2 },
{ id: 4, name: 'Orange', parent_id: 3 },
{ id: 5, name: 'Hockey', parent_id: 1 },
{ id: 6, name: 'Apple', parent_id: 2 }
];
//Build a dictionary for each id so we can look up its parent efficiently while traversing
const refDict = categories.reduce((acc, item) => ({ ...acc,
[item.id]: item
}), {});
//it looks for its parent
const getParent = (id) => {
const child = refDict[id];
if (child.parent_id === null) return { ...child
};
return getParent(child.parent_id);
}
//final dict which contains keys as the name of every child and value as its parent's properties
//So we can look at any topmost parent in constant time
const finalDict = categories.reduce((acc, item) => (item.parent_id === null ? { ...acc
} : { ...acc,
[item.name]: getParent(item.id)
}), {});
console.log(finalDict);
console.log("parent - ", finalDict["Orange"]);
.as-console-wrapper { max-height: 100% !important }
Hope it helps!
This question already has answers here:
group array of objects by id
(8 answers)
Closed 1 year ago.
I want to group the array of objects based on the key and concat all the grouped objects into a single array. GroupBy based on the id
example,
payload
[
{
id: 1,
name: 'a'
},
{
id: 1,
name: 'b'
},
{
id: 1,
name: 'c'
},
{
id: 2,
name: 'b'
},
{
id: 2,
name: 'c'
}
]
expected response
[
[
{
id: 1,
name: 'a'
},
{
id: 1,
name: 'b'
},
{
id: 1,
name: 'c'
}
],
[
{
id: 2,
name: 'b'
},
{
id: 2,
name: 'c'
}
]
]
All the matched elements are in the same array and all the arrays should be in a single array.
Array.redue will help
const input = [
{ id: 1, name: 'a' },
{ id: 1, name: 'b' },
{ id: 1, name: 'c' },
{ id: 2, name: 'b' },
{ id: 2, name: 'c' }
];
const output = input.reduce((acc, curr) => {
const node = acc.find(item => item.find(x => x.id === curr.id));
node ? node.push(curr) : acc.push([curr]);
return acc;
}, []);
console.log(output)
Extract the ids using Set so you have a unique set of them,
then loop over those ids and filter the original array based on it.
let objects = [
{
id: 1,
name: 'a'
},
{
id: 1,
name: 'b'
},
{
id: 1,
name: 'c'
},
{
id: 2,
name: 'b'
},
{
id: 2,
name: 'c'
}
]
let ids = [...new Set(objects.map(i => i.id))]
let result = ids.map(id => objects.filter(n => id === n.id))
console.log(result)
you can create a object with ids array by using Array.reduce method, and get the object values by Object.values
var s = [{
id: 1,
name: 'a'
},
{
id: 1,
name: 'b'
},
{
id: 1,
name: 'c'
},
{
id: 2,
name: 'b'
},
{
id: 2,
name: 'c'
}
];
//go through the input array and create a object with id's, group the values to gather
var ids = s.reduce((a, c) => {
//check object has the `id` property, if not create a property and assign empty array
if (!a[c.id])
a[c.id] = [];
//push the value into desidred object property
a[c.id].push(c)
//return the accumulator
return a;
}, {});
//get the grouped array as values
var outPut = Object.values(ids);
console.log(outPut);
1) You can easily achieve the result using Map and forEach easily
const arr = [
{
id: 1,
name: "a",
},
{
id: 1,
name: "b",
},
{
id: 1,
name: "c",
},
{
id: 2,
name: "b",
},
{
id: 2,
name: "c",
},
];
const map = new Map();
arr.forEach((o) => !map.has(o.id) ? map.set(o.id, [o]) : map.get(o.id).push(o));
const result = [...map.values()];
console.log(result);
/* This is not a part of answer. It is just to give the output full height. So IGNORE IT */
.as-console-wrapper { max-height: 100% !important; top: 0; }
2) You can also achieve the result using reduce
const arr = [
{
id: 1,
name: "a",
},
{
id: 1,
name: "b",
},
{
id: 1,
name: "c",
},
{
id: 2,
name: "b",
},
{
id: 2,
name: "c",
},
];
const result = [...arr.reduce((map, curr) => {
!map.has(curr.id) ? map.set(curr.id, [curr]) : map.get(curr.id).push(curr);
return map;
}, new Map()).values()];
console.log(result);
/* This is not a part of answer. It is just to give the output full height. So IGNORE IT */
.as-console-wrapper { max-height: 100% !important; top: 0; }
I'm trying to build a small React.js clone,
In the code snippet below, i made a simple component tree with a succession of functional components
function Text(props) {
return createElement('p', null, props.content)
}
function Hello(props) {
return createElement(Text, props.content, null)
}
function Home() { // this is the root element
return createElement('div', null,
createElement(Hello, {content: "hello world 1"}, null),
createElement(Hello, {content: "hello world 2"}, null)
)
}
The createElement function checks the type of the current node, assigning it an id and pushes it into the data Array. But to reconstitute the component tree, i need to get the parentId of each components that have been pushed into data.
I assume that if the value of i is zero, it means that the current element is the root element. But if not, how to find the id of the parent who created the current element ?
const data = [];
let i = 0;
function createElement(node, props, children) {
if(typeof node === "string") {
data.push({ name: node, id: i, parentId: i > 0 ? i : null });
i++;
};
if(typeof node === "function") {
let functionalComponent = constructFunctionComponent(node);
data.push({ name: node.name, id: i, parentId: i > 0 ? i : null });
i++;
createElement(functionalComponent(props)());
};
}
function constructFunctionComponent(fc) {
return (props) => (children) => fc(props, children);
}
Here is what a console.log displays if we execute the Home() function.
Here the parentId keys are obviously all false (except the first one because it is the root element)
// current output :
// note that here the parentId keys of each index are not correct (this is what i'm trying to resolve)
[
{ name: 'Home', id: 0, parentId: null },
{ name: 'Hello', id: 1, parentId: 1 },
{ name: 'Text', id: 2, parentId: 2 },
{ name: 'p', id: 3, parentId: 3 },
{ name: 'Hello', id: 4, parentId: 4 },
{ name: 'Text', id: 5, parentId: 5 },
{ name: 'p', id: 6, parentId: 6 },
{ name: 'div', id: 7, parentId: 7 }
]
// expected output:
// here, each parentId keys is a "reference" to the parent that added the index to the array
[
{ name: 'Home', id: 0, parentId: null },
{ name: 'Hello', id: 1, parentId: 7 },
{ name: 'Text', id: 2, parentId: 1 },
{ name: 'p', id: 3, parentId: 2 },
{ name: 'Hello', id: 4, parentId: 7 },
{ name: 'Text', id: 5, parentId: 4 },
{ name: 'p', id: 6, parentId: 5 },
{ name: 'div', id: 7, parentId: 0 }
]
I made a codeSandbox which contains the code. Any help would be greatly appreciated !
Here is a link to the codeSandbox example
Thanks,
As you're doing it now, the structure is being evaluated from the leaf nodes up, so the parent ID is not known at the time of each element's creation. You'll have to separate the generation of the IDs from the generation of the elements. Here's an example of what I mean (it's not pretty; you can probably come up with a more elegant way to do it):
function createElement(node, props, children) {
if(typeof node === "string") {
data.push({ name: node, id: props.id, parentId: props.parentId });
};
if(typeof node === "function") {
let functionalComponent = constructFunctionComponent(node);
data.push({ name: node.name, id: props.id, parentId: props.parentId });
createElement(functionalComponent(props)());
};
}
function Home() {
homeId = 0;
createElement
(
'div',
homeId,
createElement(Hello, {content: "hello 1", parentId: homeId, id: (hello1Id = ++homeId)}),
createElement(Hello, {content: "hello 2", parentId: homeId, id: (hello2Id = ++hello1Id)})
);
}
Now the ID is being created as part of the call to createElement, so it can be known and used in any further child creation.
I have an array of objects and I'm wondering how I can format it into a select box, using hyphens to represent each level deeper. Here's my object -
let elements = {
0: {
id: 1,
name: 'Parent folder',
parent_id: null
},
1: {
id: 2,
name: 'Another parent folder',
parent_id: null
},
2: {
id: 3,
name: 'Child folder 1',
parent_id: 1
},
3: {
id: 4,
name: 'Child folder 2',
parent_id: 1
},
4: {
id: 5,
name: 'Child of a child',
parent_id: 4,
},
}
I would like elements to then be re-formatted like below -
let elements = {
0: {
id: 1,
name: 'Parent folder',
parent_id: null,
depth: 0
},
1: {
id: 3,
name: 'Child folder 1',
parent_id: 1,
depth: 1
},
2: {
id: 4,
name: 'Child folder 2',
parent_id: 1,
depth: 1
},
3: {
id: 5,
name: 'Child of a child',
parent_id: 4,
depth: 2
},
4: {
id: 2,
name: 'Another parent folder',
parent_id: null,
depth: 0
},
}
This way I could loop through the object and generate a select in the following structure based on the depth variable -
Parent folder
- Child folder 1
- Child folder 2
-- Child of a child
Another parent folder
Currently I am looping my object through a process and getting a multi level object, so maybe I just need to work out how to convert this back into a single depth array of objects?
if(elements.length > 0) {
for (let i = 0; i < elements.length; i++) {
let obj = Object.assign({}, elements[i]);
let depth = 0;
obj.items = [];
map[obj.id] = obj;
let parent = obj.parent_id || '-';
if (!map[parent]) {
map[parent] = { items: [] }
}
map[parent].items.push(obj);
}
console.log(map);
return map['-'].items;
}
I feel like there is a relatively simple answer to this but I'm struggling! Look forward to your thoughts, thanks!
You could create a tree first, to reflect the relationship and then build a flat array which later became an object.
function getTree(array) {
var o = {};
array.forEach(function ({ id, name, parent_id }) {
Object.assign(o[id] = o[id] || {}, { id, name, parent_id });
o[parent_id] = o[parent_id] || {};
o[parent_id].children = o[parent_id].children || [];
o[parent_id].children.push(o[id]);
});
return o.null.children;
}
function getFlat(array = [], level = 0) {
return array.reduce((r, { id, name, parent_id, children }) =>
r.concat({ id, name, parent_id, level }, getFlat(children, level + 1)), []);
}
var elements = { 0: { id: 1, name: 'Parent folder', parent_id: null }, 1: { id: 2, name: 'Another parent folder', parent_id: null }, 2: { id: 3, name: 'Child folder 1', parent_id: 1 }, 3: { id: 4, name: 'Child folder 2', parent_id: 1 }, 4: { id: 5, name: 'Child of a child', parent_id: 4 } },
tree = getTree(Object.assign([], elements)),
flat = getFlat(tree);
console.log(flat.map(({ name, level }) => '-'.repeat(level) + name).join('\n'));
console.log(Object.assign({}, flat));
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Let's say we have an array that looks like this:
[
{
id: 0,
name: 'A'
},
{
id: 1,
name:'A'
},
{
id: 2,
name: 'C'
},
{
id: 3,
name: 'B'
},
{
id: 4,
name: 'B'
}
]
I want to keep only this objects that have the same value at 'name' key. So the output looks like this:
[
{
id: 0,
name: 'A'
},
{
id: 1,
name:'A'
},
{
id: 3,
name: 'B'
},
{
id: 4,
name: 'B'
}
]
I wanted to use lodash but I don't see any method for this case.
You can try something like this:
Idea:
Loop over the data and create a list of names with their count.
Loop over data again and filter out any object that has count < 2
var data = [{ id: 0, name: 'A' }, { id: 1, name: 'A' }, { id: 2, name: 'C' }, { id: 3, name: 'B' }, { id: 4, name: 'B' }];
var countList = data.reduce(function(p, c){
p[c.name] = (p[c.name] || 0) + 1;
return p;
}, {});
var result = data.filter(function(obj){
return countList[obj.name] > 1;
});
console.log(result)
A lodash approach that may (or may not) be easier to follow the steps of:
const originalArray = [{ id: 0, name: 'A' }, { id: 1, name: 'A' }, { id: 2, name: 'C' }, { id: 3, name: 'B' }, { id: 4, name: 'B' }];
const newArray =
_(originalArray)
.groupBy('name') // when names are the same => same group. this gets us an array of groups (arrays)
.filter(group => group.length == 2) // keep only the groups with two items in them
.flatten() // flatten array of arrays down to just one array
.value();
console.log(newArray)
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.11/lodash.min.js"></script>
A shorter solution with array.filter and array.some:
var data = [ { ... }, ... ]; // Your array
var newData = data.filter((elt, eltIndex) => data.some((sameNameElt, sameNameEltIndex) => sameNameElt.name === elt.name && sameNameEltIndex !== eltIndex));
console.log("new table: ", newTable);
You could use a hash table and a single loop for mapping the objects or just an empty array, then concat the result with an empty array.
var data = [{ id: 0, name: 'A' }, { id: 1, name: 'A' }, { id: 2, name: 'C' }, { id: 3, name: 'B' }, { id: 4, name: 'B' }],
hash = Object.create(null),
result = Array.prototype.concat.apply([], data.map(function (o, i) {
if (hash[o.name]) {
hash[o.name].update && hash[o.name].temp.push(hash[o.name].object);
hash[o.name].update = false;
return o;
}
hash[o.name] = { object: o, temp: [], update: true };
return hash[o.name].temp;
}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }