【问题标题】:Convert parent-child array to tree将父子数组转换为树
【发布时间】:2013-03-25 10:34:17
【问题描述】:

任何人都可以帮助转换以下父子对象列表:

[ { “名称”:“根”, "_id":"root_id", }, { “名称”:“a1”, “parentAreaRef”:{ "id":"root_id", }, "_id":"a1_id", }, { “名称”:“a2”, “parentAreaRef”:{ "id":"a1_id", }, "_id":"a2_id", }, { “名称”:“a3”, “parentAreaRef”:{ "id":"a2_id", }, "_id":"a3_id", }, { “名称”:“b1”, “parentAreaRef”:{ "id":"root_id", }, "_id":"b1_id", }, { “名称”:“b2”, “parentAreaRef”:{ "id":"b1_id", }, "_id":"b2_id", }, { “名称”:“b3”, “parentAreaRef”:{ "id":"b1_id", }, "_id":"b3_id", } ]

变成显示父子关系的树形结构:

[ { “名称”:“根”, "_id":"root_id", “孩子们”: [ { “名称”:“a1”, "_id":"a1_id", “孩子们” : [ { “名称”:“a2”, "_id":"a2_id", “孩子们” : [ { “名称”:“a3” "_id":"a3_id" } ] } ] }, { “名称”:“b1”, "_id":"b1_id", “孩子们” : [ { “名称”:“b2” "_id":"b2_id" }, { “名称”:“b3” "_id":"b3_id" } ] } ] } ]

(输出结构是一个允许多个根的数组,但如果我们能得到一个处理单个根的解决方案也很棒。)

输出树如下所示:

根 | --a1 | | | - a2 | | | --a3 | -- b1 | -- b2 -- b3

谢谢!

【问题讨论】:

  • 很多 :) 但(显然)还没有找到解决方案。我可以发布一些我一直在研究的代码 sn-ps,但我认为它们会引起更多的混乱而不是清晰
  • @Scobal 请发布 sn-ps。您可能非常接近解决方案,我们可以告诉您解决方案是什么。
  • 你需要解析和映射。
  • 这是我目前的尝试:jsfiddle.net/5AgqT 希望对您有所帮助!
  • @Scobal 我已经发布了一个解决方案。

标签: javascript algorithm


【解决方案1】:

我有一个可行的解决方案。我可以给你提示,只要解决它。好消息是您的数据不包含对节点的任何前向引用。因此,您只需通过数组即可创建树。如果需要注意,您将需要先遍历整个数组以构建 id 到节点的映射。

您的算法将如下所示。

  1. 创建一个将 id 映射到节点的映射。这将使查找节点变得容易。
  2. 循环遍历节点数组。
  3. 对于每个元素。
    1. 在地图中添加一个条目。
    2. 向该节点添加一个children 属性(一个数组)。
    3. 元素是否有父元素?如果不是,它必须是根,所以将这个元素分配给树的根。
    4. 此元素有父节点,因此查找父节点,然后将此当前节点添加为父节点的子节点(将其添加到children 数组中)。

这应该可以帮助您解决问题。如果您对此算法有特定问题,我可以指出问题出在哪里以及如何解决它或发布解决方案并解释我是如何解决它的。

更新

我查看了您的解决方案。您实际上不需要递归,您可以使用我上面描述的算法迭代地执行此操作。您还在就地修改结构,这使算法更加复杂。但你在某种程度上是在正确的轨道上。以下是我的解决方法:

var idToNodeMap = {}; //Keeps track of nodes using id as key, for fast lookup
var root = null; //Initially set our loop to null

//loop over data
data.forEach(function(datum) {

    //each node will have children, so let's give it a "children" poperty
    datum.children = [];

    //add an entry for this node to the map so that any future children can
    //lookup the parent
    idToNodeMap[datum._id] = datum;

    //Does this node have a parent?
    if(typeof datum.parentAreaRef === "undefined") {
        //Doesn't look like it, so this node is the root of the tree
        root = datum;        
    } else {        
        //This node has a parent, so let's look it up using the id
        parentNode = idToNodeMap[datum.parentAreaRef.id];

        //We don't need this property, so let's delete it.
        delete datum.parentAreaRef;

        //Let's add the current node as a child of the parent node.
        parentNode.children.push(datum);        
    }
});

现在root 指向整棵树。

Fiddle.

对于元素数组按任意顺序排列的情况,您必须先初始化idToNodeMap。算法的其余部分或多或少保持不变(除了您在地图中存储节点的行;这不是必需的,因为您在第一遍中就已经这样做了):

var idToNodeMap = data.reduce(function(map, node) {
    map[node._id] = node;
    return map;
}, {});

【讨论】:

  • 仅供参考,当前小提琴给出:Uncaught TypeError: Cannot read property '#<Object>' of undefined 错误。否则,我喜欢这个实现 - 简单明了,+1。
  • @LasseChristiansen-sw_lasse 哎呀;我链接到旧版本。我已经修复了链接。
  • 此解决方案隐含地假定原始“数组”数据已经以这样一种方式排序,即 parentNode = idToNodeMap[datum.parentAreaRef.id] 将已经包含对您查找的 parentNode 的引用。 OP 可能已经从他们在问题中的示例数据中指出了这种情况,但我认为在提议的解决方案中承认这一事实是值得的,因为这是该算法的严重限制。
  • @arcseldon 我在回答的一开始就已经承认了这一点。
【解决方案2】:

试一试:

   var obj = {};
   obj.rootElements = [];
   var currentRoot;
   var currentParent;
   for (s in a) {
       var t = a[s];
       var id = t._id;
       if (t.parentAreaRef) {
           var parentId = t.parentAreaRef.id;
           if (parentId == currentParent._id) {
               //add children
               if (!currentParent.children) {
                   currentParent.children = [];
               }
               currentParent.children.push(t);
           }
           else {
               addChildToParent(t, parentId);
           }

       }
       else // is root
       {
           currentRoot = t;
           currentParent = t;
           obj.rootElements.push(currentRoot);
       }
   }

   var t = currentRoot

   function addChildToParent(child, parentId, root) {
       for (p in a) {
           if (a[p]._id.toString() == parentId.toString()) {
               if (!a[p].children) {
                   a[p].children = [];
               }
               a[p].children.push(t);
           }
       }
   }

【讨论】:

    【解决方案3】:

    我知道我为时已晚,但是由于我刚刚完成了对如何完成此操作的示例实现的贡献,因此我想我会分享它,因为它可能会很有用/或启发替代解决方案。

    可以在这里找到实现:http://jsfiddle.net/sw_lasse/9wpHa/

    实现的主要思想围绕以下递归函数:

    // Get parent of node (recursive)
    var getParent = function (rootNode, rootId) {
    
        if (rootNode._id === rootId)
            return rootNode;
    
        for (var i = 0; i < rootNode.children.length; i++) {
            var child = rootNode.children[i];
            if (child._id === rootId)
                return child;
    
            if (child.children.length > 0)
                var childResult = getParent(child, rootId);
    
            if (childResult != null) return childResult;
        }
        return null;
    };
    

    ...用于构建树。

    【讨论】:

    • 谢谢!必须将您的代码更改为 if (!child.children)
    【解决方案4】:

    我知道已经很晚了,但我刚刚完成了这个算法,也许它可以帮助其他想要解决同样问题的人:http://jsfiddle.net/akerbeltz/9dQcn/

    它的好处是它不需要对原始对象进行任何特殊排序。

    如果您需要根据自己的需要进行调整,请更改以下几行:

    1. 根据您的结构更改 _id 和 parentAreaRef.id。

      if (String(tree[i]._id) === String(item.parentAreaRef.id)) {

    2. 根据您的结构更改 parentAreaRef。

      if (tree[idx].parentAreaRef) buildTree(tree, tree.splice(idx, 1)[0])

    希望对你有帮助!

    更新

    根据@Gerfried 评论在此处添加代码:

    var buildTree = function(tree, item) {
        if (item) { // if item then have parent
            for (var i=0; i<tree.length; i++) { // parses the entire tree in order to find the parent
                if (String(tree[i]._id) === String(item.parentAreaRef.id)) { // bingo!
                    tree[i].childs.push(item); // add the child to his parent
                    break;
                }
                else buildTree(tree[i].childs, item); // if item doesn't match but tree have childs then parses childs again to find item parent
            }
        }
        else { // if no item then is a root item, multiple root items are supported
            var idx = 0;
            while (idx < tree.length)
                if (tree[idx].parentAreaRef) buildTree(tree, tree.splice(idx, 1)[0]) // if have parent then remove it from the array to relocate it to the right place
                else idx++; // if doesn't have parent then is root and move it to the next object
        }
    }
    
    for (var i=0; i<data.length; i++) { // add childs to every item
        data[i].childs = [];
    }
    buildTree(data);
    console.log(data);
    

    谢谢!

    【讨论】:

    • 这对我很有用。谢谢
    • 最好在此处发布您的代码 - jsfiddle 有时可能会删除它。
    • 完成@Gerfried。谢谢!
    【解决方案5】:

    你的字符串有错误

    a[p].children.push(t);
    

    应该是

    a[p].children.push(child);
    

    我也很少优化它:

    var data = [{"id":1,"name":"X","parentId":null},{"id":2,"name":"Y","parentId":1},{"id":3,"name":"D","parentId":2},{"id":2,"name":"S","parentId":1},{"id":5,"name":"K","parentId":4}]
        var obj = {};
        obj.rootElements = [];
        for (i in data) {
            var _elem = data[i];
            if (_elem.parentId) {
                var _parentId = _elem.parentId;
                if (_parentId == _elem.id) {
                    // check children, if false - add
                    if (!_elem.children) {
                        _elem.children = [];
                    }
                    _elem.children.push(_elem);
                }
                else {
                    addChildToParent(_elem, _parentId);
                }
            }
            else // is root
            {
                obj.rootElements.push(_elem);
            }
        }
        function addChildToParent(child, parentId, root) {
            for (j in data) {
                if (data[j].id.toString() == parentId.toString()) {
                    if (!data[j].children) {
                        data[j].children = [];
                    }
                    data[j].children.push(child);
                }
            }
        }
        res.send(obj.rootElements); 
    

    【讨论】:

      【解决方案6】:

      你可以使用来自 npm 的 array-to-tree 模块。

      【讨论】:

      • 或者使用我的性能更高的实现(单元测试,100% 的代码覆盖率,大小只有 0.5 kb 并且包括类型):npmjs.com/package/performant-array-to-tree
      【解决方案7】:

      借用 Vivin Paliath 的回答中的缓存逻辑,我创建了一个可重用函数,用于将具有子父关系的数据列表转换为树。

      var data = [
        { "id" : "root"                     },
        { "id" : "a1",   "parentId" : "root", },
        { "id" : "a2",   "parentId" : "a1",   },
        { "id" : "a3",   "parentId" : "a2",   },
        { "id" : "b1",   "parentId" : "root", },
        { "id" : "b2",   "parentId" : "b1",   },
        { "id" : "b3",   "parentId" : "b1",   }
      ];
      var options = {
        childKey  : 'id',
        parentKey : 'parentId'
      };
      var tree = walkTree(listToTree(data, options), pruneChildren);
      
      document.body.innerHTML = '<pre>' + JSON.stringify(tree, null, 4) + '</pre>';
      
      function listToTree(list, options) {
        options = options || {};
        var childKey    = options.childKey    || 'child';
        var parentKey   = options.parentKey   || 'parent';
        var childrenKey = options.childrenKey || 'children';
        var nodeFn      = options.nodeFn      || function(node, name, children) {
          return { name : name, children : children };
        };
        var nodeCache = {};
        return list.reduce(function(tree, node) {
          node[childrenKey] = [];
          nodeCache[node[childKey]] = node;
          if (typeof node[parentKey] === 'undefined' || node[parentKey] === '') {
            tree = nodeFn(node, node[childKey], node[childrenKey]);
          } else {
            parentNode = nodeCache[node[parentKey]];
            parentNode[childrenKey].push(nodeFn(node, node[childKey], node[childrenKey]));
          }
          return tree;
        }, {});
      }
      
      function walkTree(tree, visitorFn, parent) {
        if (visitorFn == null || typeof visitorFn !== 'function') {
          return tree;
        }
        visitorFn.call(tree, tree, parent);
        if (tree.children && tree.children.length > 0) {
          tree.children.forEach(function(child) {
            walkTree(child, visitorFn, tree);
          });
        }
        return tree;
      }
      
      function pruneChildren(node, parent) {
        if (node.children.length < 1) {
          delete node.children;
        }
      }

      【讨论】:

      猜你喜欢
      • 2021-08-21
      • 2019-01-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-15
      • 2015-12-09
      • 2019-03-07
      相关资源
      最近更新 更多