JavaScript: 构建一个分层树形结构

20

我的数据具有以下属性:

  1. 每个条目都有一个唯一标识符(Id)
  2. 每个条目都有一个指向父节点Id的Parent字段。
  3. 一个节点可以有多个子节点,但只能有一个父节点。

我第一次尝试构建树如下。由于递归导致了无限循环,因此存在缺陷。即使我解决了这个问题,我也不确定是否有更好的方法来处理这个问题。目前,我是通过两次遍历来完成它的。

由于我的数据量较大,我希望它尽可能高效。它还需要动态重建树(根节点可以是任何节点)

程序中有示例数据:

 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

我希望输出的结果是一个有效的JSON结构,其中节点作为“value”字段,子节点作为数组。可能存在嵌套结构错误,因为我手动编写了它。请注意保留HTML标记。
{
 "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": []
     }
   ]
..
}

示例程序:

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;
}

我已经想出了3个好的解决方案,希望投票能提供更多惯用的、高效的实现方法。我不确定我的数据是否具有以下属性:输入数组集合中只有一个根元素,而且这个根元素始终是给出的,任何一种实现方法都可能更好。我还应该学习如何进行基准测试,因为我的要求是重建树的效率(快速/占用较少内存)。例如,输入已经被缓存(数组),并重新构建树形结构。

convertToHierarchy(parentid)
....
convertToHierarchy(parentid2)
...
6个回答

28

这是一个解决方案:

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));​

感谢您提供了一个干净的工作解决方案。我会等待投票来看看其他人对更有效的解决方案持何种看法。在我的情况下,根节点已经给定(比如,在此父级别重建树),这是否会对解决方案产生必然的影响?再次感谢您提供简明扼要的解决方案。 - bsr
5年后,仍然挽救了我的生命! - Steven X
完美地工作了。我能够将它转换为Java供我使用。谢谢。 - amadamala
有人可以指导我如何在插入新节点后更新这个层级树吗? - Shantanu

8
虽然上述解决方案确实可行,但我认为它们非常缓慢,并且使用了过多的循环和过时的方法(我们将使用ES6语法)。我建议使用下面优化过的解决方案来提高性能。阅读这篇博客文章以了解其工作原理。 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' }));

以下是其他海报的一些结果和比较:

这里输入图片描述

http://jsben.ch/ekTls


如果您需要带参数的版本,以获得更动态但略慢的版本,请参见下文:

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;
}

愉快的黑客技术


7
这是另一个例子,适用于多个根节点:

这里有另外一个例子。这应该适用于多个根节点:

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;
}

你有几个错误。首先,你的 convertToHierarchy() 函数实际上并没有返回任何东西,你只需要为 roots 添加一个返回语句即可。另外,检查 currentNode.value.Id == "" 没有任何作用,因为没有一个 Id 是空的。除此之外都很好 :) - Bill
谢谢,比尔,发现得好。不正确的 currentNode.value.Id == "" 已经被更改为检查父级。我承认,代码的那部分甚至没有运行过 ;) - nick_w
+1。我会和其他人一起评估。我的数据保证根节点唯一。这是否会改变解决方案?我的考虑是,如果我指定根节点,树能以最快的速度、使用最少的内存重建。 - bsr
一个单一的根节点就可以了。 - nick_w
只有在节点正确排序的情况下,此方法才有效。如果顺序是随机的,则会产生错误的结果。 - nightwolf555

3
我会这样做。它处理多个根节点,而且我认为相当易读。
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));

Tanks Jan。与上面的评论相同。将为了投票/评论而工作,以查看其他人的比较。再次感谢。 - bsr
如果速度是最大的问题,使用大量输入数据并进行基准测试。如果可读性和可维护性是您最关心的问题,请选择对您来说最有意义的代码。 - Jan

1

使用ES6实现,附有简单的样例输入。可以在浏览器控制台进行测试。

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))

0

大家好,如果我正在使用Node.js并且需要创建嵌套的ul/li而不是JSON,应该怎么办?请写出代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接