【问题标题】:Tree Structure from Adjacency List邻接表的树结构
【发布时间】:2014-01-24 21:15:57
【问题描述】:

我正在尝试从具有父 ID 的平面数组生成分层树对象。

// `parent` represents an ID and not the nesting level.
var flat = [
    { id: 1, name: "Business", parent: 0 },
    { id: 2, name: "Management", parent: 1 },
    { id: 3, name: "Leadership", parent: 2 },
    { id: 4, name: "Finance", parent: 1 },
    { id: 5, name: "Fiction", parent: 0 },
    { id: 6, name: "Accounting", parent: 1 },
    { id: 7, name: "Project Management", parent: 2  }
];

最终的树对象应如下所示:

{ 
    id: 1, 
    name: "Business", 
    children: [
        { 
            id: 2, 
            name: "Management", 
            children: [
                { id: 3, name: "Leadership" },
                { id: 7, name: "Project Management" }
            ]
        }
        // [...]
    ]
}
// [...]

您可以在this fiddle 上看到我目前的工作,但它只适用于前两个级别。

我考虑收集孤儿(来自flat 的对象在tree 中还没有父对象),然后再次循环它们以查看它们现在是否有父对象。但这可能意味着树对象上的许多循环,尤其是多个级别的许多类别。

我确信有一个更优雅的解决方案。

【问题讨论】:

  • 您将需要一个尾递归类型的解决方案,它实际上由 2 个数组组成。给我几分钟,我看看我能不能把它写出来。这实际上是一个非常好的问题,因为在许多语言中,您希望在父/子数组上一次构建树。
  • 您可以在每个对象中添加一个“父”字段。
  • @TreyE 感谢关键字tail-recursion,它看起来像是一个理论主题,但也许我可以跟随一个真正的 JavaScript 示例。 @CBredlow 我已经有一个 parent 属性,代表父类别的父 ID。还是你的意思是别的?
  • 看起来尾递归实际上是一个错误的线索。请参阅下面的不涉及它的简单解决方案。

标签: javascript recursion tree adjacency-list


【解决方案1】:

看起来无论树的深度如何,您都可以在 2 遍中完成此操作:

var flat = [
    { id: 1, name: "Business", parent: 0 },
    { id: 2, name: "Management", parent: 1 },
    { id: 3, name: "Leadership", parent: 2 },
    { id: 4, name: "Finance", parent: 1 },
    { id: 5, name: "Fiction", parent: 0 },
    { id: 6, name: "Accounting", parent: 1 },
    { id: 7, name: "Project Management", parent: 2  }
];

var nodes = [];
var toplevelNodes = [];
var lookupList = {};

for (var i = 0; i < flat.length; i++) {
    var n = {
        id: flat[i].id,
        name: flat[i].name,
        parent_id: ((flat[i].parent == 0) ? null : flat[i].parent),
        children: []
        };
    lookupList[n.id] = n;
    nodes.push(n);
    if (n.parent_id == null) {
        toplevelNodes.push(n);
    }
}

for (var i = 0; i < nodes.length; i++) {
  var n = nodes[i];
  if (!(n.parent_id == null)) {
      lookupList[n.parent_id].children = lookupList[n.parent_id].children.concat([n]);
  }
}

console.log(toplevelNodes);

【讨论】:

    【解决方案2】:

    更新 2,(六年后)

    我碰巧又一次偶然发现了这一点,并意识到这可以通过一点 ES6 解构和简单的递归来简单得多:

    const makeForest = (id, xs) => 
      xs .filter (({parent}) => parent == id)
         .map (({id, parent, ...rest}) => ({id, ...rest, children: makeForest (id, xs)}))
    
    var flat = [{id: 1, name: "Business", parent: 0}, {id: 2, name: "Management", parent: 1}, {id: 3, name: "Leadership", parent: 2}, {id: 4, name: "Finance", parent: 1}, {id: 5, name: "Fiction", parent: 0}, {id: 6, name: "Accounting", parent: 1}, {id: 7, name: "Project Management", parent: 2}];
    
    console .log (
      makeForest (0, flat)
    )
    .as-console-wrapper {min-height: 100% !important; top: 0}

    注意名称更改。我们不是在制作一棵树,而是在整个森林中,根的每个直接子节点都有自己的节点。如果您有一个用于根的结构,那么将其更改为生成一棵树将是相对微不足道的。另请注意,这将使我们找到以 any id 为根的树。它不必是整个根。

    性能

    正如您所担心的那样,这确实具有O(n^2) 的最坏情况性能。实际运行时间类似于O(n * p),其中 p 是列表中不同父节点的数量。因此,只有当我们的树几乎与列表的长度一样深时,我们才接近最坏的情况。我会使用这样的东西,除非并且直到我可以证明它是我代码中的热点。我的猜测是我永远不会发现需要更换它。

    课程

    永远不要排除递归。这比这里的任何其他答案都简单得多,包括我下面的两个版本。


    更新 1

    我对原始解决方案中的额外复杂性不满意。我正在添加另一个版本来降低这种复杂性。它设法一次性构建数据。如有必要,它还使人们有机会以不同于原始格式的方式重组树中的记录。 (默认情况下,它只删除parent 节点。)

    更新版本

    可通过 JSFiddle 获得。

    var makeTree = (function() {
        var defaultClone = function(record) {
            var newRecord = JSON.parse(JSON.stringify(record));
            delete newRecord.parent;
            return newRecord;
        };
        return function(flat, clone) {
            return flat.reduce(function(data, record) {
                var oldRecord = data.catalog[record.id];
                var newRecord = (clone || defaultClone)(record);
                if (oldRecord && oldRecord.children) {
                    newRecord.children = oldRecord.children;
                }
                data.catalog[record.id] = newRecord;
                if (record.parent) {
                    var parent = data.catalog[record.parent] = 
                            (data.catalog[record.parent] || {id: record.parent});
                    (parent.children = parent.children || []).push(newRecord);
                } else {
                    data.tree.push(newRecord);
                }
                return data;
            }, {catalog: {}, tree: []}).tree;
        }
    }());
    

    请注意,无论平面列表的顺序如何,这都会起作用——父节点不必在其子节点之前——尽管这里没有任何东西可以对节点进行排序。


    原版

    我的解决方案(也在 JSFiddle 上):

    var makeTree = (function() {
        var isArray = function(obj) {return Object.prototype.toString.call(obj) == "[object Array]"; };
        var clone = function(obj) {return JSON.parse(JSON.stringify(obj));};
        var buildTree = function(catalog, structure, start) {
            return (structure[start] || []).map(function(id, index) {
                var record = catalog[id];
                var keys = structure[start][index];
                var children = isArray(keys) ? keys.map(function(key) {
                    return buildTree(catalog, structure, key);
                }) : buildTree(catalog, structure, keys);
                if (children.length) {
                    record.children = children;
                }
                return record;
            })
        };
        return function(flat) {
            var catalog = flat.reduce(function(catalog, record) {
                catalog[record.id] = clone(record);
                delete(catalog[record.id].parent);
                return catalog;
            }, {});
            var structure = flat.reduce(function(tree, record) {
                (tree[record.parent] = tree[record.parent] || []).push(record.id);
                return tree;
            }, {});
            return buildTree(catalog, structure, 0); // this might be oversimplified.
        }
    }());
    

    【讨论】:

    • 谢谢你,Scott,真的很感激,虽然我仍然不能完全理解发生了什么;)
    • 这仍然太复杂,但基本思想是创建一个目录,您可以在其中按 id 查找对象,以及一个列出每个子项的 id 的树形结构。 (如果需要,这些显然可以组合起来。)然后使用这些,您可以从此结构构建最终树,仅包括您关心的原始项目中的那些属性(例如,忽略“父”属性。)
    • 更新了答案以降低复杂性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-17
    • 1970-01-01
    相关资源
    最近更新 更多