简体   繁体   中英

Javascript: Building a hierarchical tree

My data has these properties:

  1. Each entry has a unique id (Id)
  2. Each has a Parent field, which points to the Id of the parent.
  3. A node can have multiple children, but only one parent.

My first attempt to build a tree is below. It is buggy as the recursion causes an infinite loop. Even if I solve it, I am not sure if there is a better approach to do this. Currently, I am doing it in 2 passes.

I would like it to be as efficient as possible as I have a decent amount of data. It also needs to rebuild the tree dynamically (the root can be any node)

There is sample data in the program below:

 arry = [{"Id":"1", "Name":"abc", "Parent":""}, {"Id":"2", "Name":"abc", "Parent":"1"},
    {"Id":"3", "Name":"abc", "Parent":"2"},{"Id":"4", "Name":"abc", "Parent":"2"}]//for testing

I was hoping the output to be (it might be wrong nested structure, as I manually wrote it. but, what I am hoping is a valid JSON structure with node as a field 'value' and children as an array.)

{
 "value": {"Id":"1", "Name":"abc", "Parent":""},
 "children": [
  {
   "value": {"Id":"2", "Name":"abc", "Parent":"1"},
   "children": [
    {
     "value": {"Id":"3", "Name":"abc", "Parent":"2"},
     "children": []
     },
     {
     "value": {"Id":"4", "Name":"abc", "Parent":"2"},
     "children": []
     }
   ]
..
}

Sample program:

function convertToHierarchy(arry, root) 
{
//root can be treated a special case, as the id is known
    arry = [{"Id":"1", "Name":"abc", "Parent":""}, {"Id":"2", "Name":"abc", "Parent":"1"},
    {"Id":"3", "Name":"abc", "Parent":"2"},{"Id":"4", "Name":"abc", "Parent":"2"}]//for testing


    var mapping = {}; // parent : [children]
    for (var i = 0; i < array.length; i++) 
    {
        var node = arry[i];

    if (!mapping[node.Id]) { 
          mapping[node.Id] = {value: node, children:[] } ;
        }else{
      mapping[node.Id] = {value: node} //children is already set    
    }

    if (!mapping[node.Parent]) { //TODO what if parent doesn't exist.
                mapping[node.Parent] =  {value: undefined, children:[ {value: node,children:[]} ]};
        }else {//parent is already in the list
        mapping[node.Parent].children.push({value: node,children:[]} )
    }

    }
    //by now we will have an index with all nodes and their children.

    //Now, recursively add children for root element.

    var root = mapping[1]  //hardcoded for testing, but a function argument
    recurse(root, root, mapping)
    console.log(root)

    //json dump
}

function recurse(root, node, mapping)
{
    var nodeChildren = mapping[node.value.Id].children;
    root.children.push({value:node.value, children:nodeChildren})
   for (var i = 0; i < nodeChildren.length; i++) {
        recurse(root, nodeChildren[i], mapping);
    }
    return root;
}

I have 3 good solutions so far, and hope the upvotes suggest more idiomatic, efficient implementation. I am not sure, utilizing the property of my data that, there will be only one root element in the set of input array, and also the root is always given, any of these implementation could be better. I should also be learning how to benchmark, as my requirement is how efficiently (fast/without much memory) the tree can be rebuild. For example, the input is already cached (array) and rebuild the tree like

convertToHierarchy(parentid)
....
convertToHierarchy(parentid2)
...

Here's one solution:

var items = [
    {"Id": "1", "Name": "abc", "Parent": "2"},
    {"Id": "2", "Name": "abc", "Parent": ""},
    {"Id": "3", "Name": "abc", "Parent": "5"},
    {"Id": "4", "Name": "abc", "Parent": "2"},
    {"Id": "5", "Name": "abc", "Parent": ""},
    {"Id": "6", "Name": "abc", "Parent": "2"},
    {"Id": "7", "Name": "abc", "Parent": "6"},
    {"Id": "8", "Name": "abc", "Parent": "6"}
];

function buildHierarchy(arry) {

    var roots = [], children = {};

    // find the top level nodes and hash the children based on parent
    for (var i = 0, len = arry.length; i < len; ++i) {
        var item = arry[i],
            p = item.Parent,
            target = !p ? roots : (children[p] || (children[p] = []));

        target.push({ value: item });
    }

    // function to recursively build the tree
    var findChildren = function(parent) {
        if (children[parent.value.Id]) {
            parent.children = children[parent.value.Id];
            for (var i = 0, len = parent.children.length; i < len; ++i) {
                findChildren(parent.children[i]);
            }
        }
    };

    // enumerate through to handle the case where there are multiple roots
    for (var i = 0, len = roots.length; i < len; ++i) {
        findChildren(roots[i]);
    }

    return roots;
}

console.log(buildHierarchy(items));​

Here's another one. This should work for multiple root nodes:

function convertToHierarchy() { 

    var arry = [{ "Id": "1", "Name": "abc", "Parent": "" }, 
    { "Id": "2", "Name": "abc", "Parent": "1" },
    { "Id": "3", "Name": "abc", "Parent": "2" },
    { "Id": "4", "Name": "abc", "Parent": "2"}];

    var nodeObjects = createStructure(arry);

    for (var i = nodeObjects.length - 1; i >= 0; i--) {
        var currentNode = nodeObjects[i];

        //Skip over root node.
        if (currentNode.value.Parent == "") {
            continue;
        }

        var parent = getParent(currentNode, nodeObjects);

        if (parent == null) {
            continue;
        }

        parent.children.push(currentNode);
        nodeObjects.splice(i, 1);
    }

    //What remains in nodeObjects will be the root nodes.
    return nodeObjects;
}

function createStructure(nodes) {
    var objects = [];

    for (var i = 0; i < nodes.length; i++) {
        objects.push({ value: nodes[i], children: [] });
    }

    return objects;
}

function getParent(child, nodes) {
    var parent = null;

    for (var i = 0; i < nodes.length; i++) {
        if (nodes[i].value.Id == child.value.Parent) {
            return nodes[i];
        }
    }

    return parent;
}

While the above solutions do work - I think they are very slow and unoptimised with too many loops and outdate methods (we will use ES6 syntax). I recommend using the bellow optimised solution which will give you a performance boost. Read this blog post to understand how this works.

javascript

 const hierarchy = (data) => { const tree = []; const childOf = {}; data.forEach((item) => { const { Id, Parent } = item; childOf[Id] = childOf[Id] || []; item.children = childOf[Id]; Parent ? (childOf[Parent] = childOf[Parent] || []).push(item) : tree.push(item); }); return tree; }; // print console.log(hierarchy([{"Id":"1", "Name":"abc", "Parent":""}, {"Id":"2", "Name":"abc", "Parent":"1"}, {"Id":"3", "Name":"abc", "Parent":"2"},{"Id":"4", "Name":"abc", "Parent":"2"}], { idKey: 'Id', parentKey: 'Parent' }));

Here some results and comparisons between other posters

在此处输入图片说明

http://jsben.ch/ekTls


If you looking for version with parameters for a more dynamic but slightly slower version here it is bellow:

const hierarchy = (data = [], { idKey = 'id', parentKey = 'parentId', childrenKey = 'children' } = {}) => {
    const tree = [];
    const childrenOf = {};
    data.forEach((item) => {
        const { [idKey]: id, [parentKey]: parentId = 0 } = item;
        childrenOf[id] = childrenOf[id] || [];
        item[childrenKey] = childrenOf[id];
        parentId ? (childrenOf[parentId] = childrenOf[parentId] || []).push(item) : tree.push(item);
    });
    return tree;
}

Happy hacking

I'd have done something like this. It handles multiple root nodes and is fairly readable IMO.

array = [{"Id":"1", "Name":"abc", "Parent":""}, 
    {"Id":"2", "Name":"abc", "Parent":"1"},
    {"Id":"3", "Name":"abc", "Parent":"2"},
    {"Id":"4", "Name":"abc", "Parent":"2"},
    {"Id":"5", "Name":"abc", "Parent":""},
    {"Id":"6", "Name":"abc", "Parent":"5"}];


function buildHierarchy(source)
{

    Array.prototype.insertChildAtId = function (strId, objChild)
    {
        // Beware, here there be recursion
        found = false;
        for (var i = 0; i < this.length ; i++)
        {
            if (this[i].value.Id == strId)
            {
                // Insert children
                this[i].children.push(objChild);
                return true;
            }
            else if (this[i].children)
            {
                // Has children, recurse!
                found = this[i].children.insertChildAtId(strId, objChild);
                if (found) return true;
            }
        }
        return false;
    };

    // Build the array according to requirements (object in value key, always has children array)
    var target = [];
    for (var i = 0 ; i < array.length ; i++)
        target.push ({ "value": source[i], "children": []});

    i = 0;
    while (target.length>i)
    {
        if (target[i].value.Parent)
        {
            // Call recursion to search for parent id
            target.insertChildAtId(target[i].value.Parent, target[i]); 
            // Remove node from array (it's already been inserted at the proper place)
            target.splice(i, 1); 
        }
        else
        {
            // Just skip over root nodes, they're no fun
            i++; 
        }
    }
    return target;
}

console.log(buildHierarchy(array));

Implemented in ES6, with a simple sample input. Can test in the browser console

let array = [{ id: 'a', children: ['b', 'c'] }, { id: 'b', children: [] }, { id: 'c', children: ['b', 'd'] }, { id: 'd', children: ['b'] }],
  tree = (data) => {
      let nodes = Object.create(null),
          result = {};
      data.forEach((item) => {
        if (!nodes[item.id]) {
          nodes[item.id] = {id: item.id, children: []}
          result = nodes
        }
        item.children.forEach((child) => {
          nodes[child] = {id: child, children: []}
          nodes[item.id].children.push(nodes[child])
        })
      })
      return result
    }

console.log(tree(array))

guys what if I am using it inside nodejs and need to created nested ul/li instead of json? can please write the code

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