如何从平面结构有效地构建树?


153

我有一堆扁平结构的物体。这些对象具有IDParentID属性,因此可以排列在树中。它们没有特定的顺序。每个ParentID属性不一定都与ID结构中的匹配。因此,它们可能是从这些对象中出现的几棵树。

您将如何处理这些对象以创建结果树?

我离解决方案并不遥远,但我敢肯定,这远非最佳方案...

我需要创建这些树,然后以正确的顺序将数据插入数据库。

没有循环引用。当ParentID == null或在其他对象中找不到ParentID时,节点是RootNode


“创建”是什么意思?在用户界面中呈现?以分层方式存储在XML或数据库中?
RedFilter

如何定义没有父节点的节点(即根节点)。ParentID为空?ParentID = 0?我假设没有正确的循环引用?
杰森·普尼

5
我觉得这个问题很酷。
nes1983

1
看看这篇文章:scip.be/index.php?Page
=ArticlesNET23

Answers:


119

将对象的ID存储在映射到特定对象的哈希表中。枚举所有对象,并找到它们的父对象(如果存在),并相应地更新其父指针。

class MyObject
{ // The actual object
    public int ParentID { get; set; }
    public int ID { get; set; }
}

class Node
{
    public List<Node> Children = new List<Node>();
    public Node Parent { get; set; }
    public MyObject AssociatedObject { get; set; }
}

IEnumerable<Node> BuildTreeAndGetRoots(List<MyObject> actualObjects)
{
    Dictionary<int, Node> lookup = new Dictionary<int, Node>();
    actualObjects.ForEach(x => lookup.Add(x.ID, new Node { AssociatedObject = x }));
    foreach (var item in lookup.Values) {
        Node proposedParent;
        if (lookup.TryGetValue(item.AssociatedObject.ParentID, out proposedParent)) {
            item.Parent = proposedParent;
            proposedParent.Children.Add(item);
        }
    }
    return lookup.Values.Where(x => x.Parent == null);
}

5
那是哪一种语言?(我将其
Jason S

3
该算法是(用非正式表示法)O(3N),其中,通过为非“遍历”父母实例化部分节点,或为非实例化的孩子保留二级查找表,可以轻松实现O(1N)解决方案。父母。对于大多数实际用途而言,这可能无关紧要,但是在大型数据集上可能意义重大。
Andrew Hanlon 2015年

15
@AndrewHanlon也许您应该发布0(1N)的sol
Ced,

1
@Ced Martin Schmidt在下面的回答与我将如何实现非常接近。可以看出,它使用单个循环,其余则是哈希表操作。
安德鲁·汉隆

26
O(3N)只是O(N);)
JakeWilson801 '16

34

基于Mehrdad Afshari的回答和Andrew Hanlon关于提速的评论,这是我的看法。

与原始任务的重要区别:根节点具有ID​​ == parentID。

class MyObject
{   // The actual object
    public int ParentID { get; set; }
    public int ID { get; set; }
}

class Node
{
    public List<Node> Children = new List<Node>();
    public Node Parent { get; set; }
    public MyObject Source { get; set; }
}

List<Node> BuildTreeAndGetRoots(List<MyObject> actualObjects)
{
    var lookup = new Dictionary<int, Node>();
    var rootNodes = new List<Node>();

    foreach (var item in actualObjects)
    {
        // add us to lookup
        Node ourNode;
        if (lookup.TryGetValue(item.ID, out ourNode))
        {   // was already found as a parent - register the actual object
            ourNode.Source = item;
        }
        else
        {
            ourNode = new Node() { Source = item };
            lookup.Add(item.ID, ourNode);
        }

        // hook into parent
        if (item.ParentID == item.ID)
        {   // is a root node
            rootNodes.Add(ourNode);
        }
        else
        {   // is a child row - so we have a parent
            Node parentNode;
            if (!lookup.TryGetValue(item.ParentID, out parentNode))
            {   // unknown parent, construct preliminary parent
                parentNode = new Node();
                lookup.Add(item.ParentID, parentNode);
            }
            parentNode.Children.Add(ourNode);
            ourNode.Parent = parentNode;
        }
    }

    return rootNodes;
}

1
很好,这基本上就是我提到的方法。但是,我只使用伪根节点(ID = 0且父节点为空)并删除自引用要求。
安德鲁·汉隆

此示例中唯一缺少的是将“父级”字段分配给每个子节点。为此,我们只需要在将子级添加到“父级集合”后设置“父级”字段即可。像这样:parentNode.Children.Add(ourNode); ourNode.Parent = parentNode;
plauriola

@plauriola是的,谢谢,我添加了这个。另一种选择是只删除Parent属性,对于核心算法而言则没有必要。
马丁·施密特

4
由于我找不到实现O(n)解决方案的npm模块,因此我创建了以下模块(经过测试的单元,100%的代码覆盖率,大小仅为0.5 kb,包括键入内容。也许对某些人有所
Philip Stanislaus

31

这是一个简单的JavaScript算法,用于将平面表解析为可在N时间内运行的父/子树结构:

var table = [
    {parent_id: 0, id: 1, children: []},
    {parent_id: 0, id: 2, children: []},
    {parent_id: 0, id: 3, children: []},
    {parent_id: 1, id: 4, children: []},
    {parent_id: 1, id: 5, children: []},
    {parent_id: 1, id: 6, children: []},
    {parent_id: 2, id: 7, children: []},
    {parent_id: 7, id: 8, children: []},
    {parent_id: 8, id: 9, children: []},
    {parent_id: 3, id: 10, children: []}
];

var root = {id:0, parent_id: null, children: []};
var node_list = { 0 : root};

for (var i = 0; i < table.length; i++) {
    node_list[table[i].id] = table[i];
    node_list[table[i].parent_id].children.push(node_list[table[i].id]);
}

console.log(root);

尝试将这种方法转换为C#。
hakan

意识到,如果id从像1001这样的大对象开始,那么我们将获得索引超出范围的异常……
hakan

2
提示:用于console.log(JSON.stringify(root, null, 2));漂亮地打印输出。
aloisdg移至codidact.com,

14

Python解决方案

def subtree(node, relationships):
    return {
        v: subtree(v, relationships) 
        for v in [x[0] for x in relationships if x[1] == node]
    }

例如:

# (child, parent) pairs where -1 means no parent    
flat_tree = [
     (1, -1),
     (4, 1),
     (10, 4),
     (11, 4),
     (16, 11),
     (17, 11),
     (24, 17),
     (25, 17),
     (5, 1),
     (8, 5),
     (9, 5),
     (7, 9),
     (12, 9),
     (22, 12),
     (23, 12),
     (2, 23),
     (26, 23),
     (27, 23),
     (20, 9),
     (21, 9)
    ]

subtree(-1, flat_tree)

产生:

{
    "1": {
        "4": {
            "10": {}, 
            "11": {
                "16": {}, 
                "17": {
                    "24": {}, 
                    "25": {}
                }
            }
        }, 
        "5": {
            "8": {}, 
            "9": {
                "20": {}, 
                "12": {
                    "22": {}, 
                    "23": {
                        "2": {}, 
                        "27": {}, 
                        "26": {}
                    }
                }, 
                "21": {}, 
                "7": {}
            }
        }
    }
}

你好 如何在输出中添加另一个属性?即。名称,parent_id
简单的家伙,

迄今为止最优雅!
ccpizza

@simpleguy:如果需要更多控制,可以展开列表理解,例如:def recurse(id, pages): for row in rows: if row['id'] == id: print(f'''{row['id']}:{row['parent_id']} {row['path']} {row['title']}''') recurse(row['id'], rows)
ccpizza

8

返回一个根或根数组的JS版本,每个根或根数组都有一个包含相关子元素的Children数组属性。不依赖于有序输入,相当紧凑,并且不使用递归。请享用!

// creates a tree from a flat set of hierarchically related data
var MiracleGrow = function(treeData, key, parentKey)
{
    var keys = [];
    treeData.map(function(x){
        x.Children = [];
        keys.push(x[key]);
    });
    var roots = treeData.filter(function(x){return keys.indexOf(x[parentKey])==-1});
    var nodes = [];
    roots.map(function(x){nodes.push(x)});
    while(nodes.length > 0)
    {

        var node = nodes.pop();
        var children =  treeData.filter(function(x){return x[parentKey] == node[key]});
        children.map(function(x){
            node.Children.push(x);
            nodes.push(x)
        });
    }
    if (roots.length==1) return roots[0];
    return roots;
}


// demo/test data
var treeData = [

    {id:9, name:'Led Zep', parent:null},
    {id:10, name:'Jimmy', parent:9},
    {id:11, name:'Robert', parent:9},
    {id:12, name:'John', parent:9},

    {id:8, name:'Elec Gtr Strings', parent:5},
    {id:1, name:'Rush', parent:null},
    {id:2, name:'Alex', parent:1},
    {id:3, name:'Geddy', parent:1},
    {id:4, name:'Neil', parent:1},
    {id:5, name:'Gibson Les Paul', parent:2},
    {id:6, name:'Pearl Kit', parent:4},
    {id:7, name:'Rickenbacker', parent:3},

    {id:100, name:'Santa', parent:99},
    {id:101, name:'Elf', parent:100},

];
var root = MiracleGrow(treeData, "id", "parent")
console.log(root)

2
这个问题已有7年历史了,已经有一个投票并被接受的答案。如果您认为自己有更好的解决方案,那么最好在代码中添加一些解释。
Jordi Nebot '16

这种方法适用于这种无序类型的数据。
科迪C

4

在此处找到了一个很棒的JavaScript版本:http : //oskarhane.com/create-a-nested-array-recursively-in-javascript/

假设您有一个像这样的数组:

const models = [
    {id: 1, title: 'hello', parent: 0},
    {id: 2, title: 'hello', parent: 0},
    {id: 3, title: 'hello', parent: 1},
    {id: 4, title: 'hello', parent: 3},
    {id: 5, title: 'hello', parent: 4},
    {id: 6, title: 'hello', parent: 4},
    {id: 7, title: 'hello', parent: 3},
    {id: 8, title: 'hello', parent: 2}
];

并且您希望将对象嵌套如下:

const nestedStructure = [
    {
        id: 1, title: 'hello', parent: 0, children: [
            {
                id: 3, title: 'hello', parent: 1, children: [
                    {
                        id: 4, title: 'hello', parent: 3, children: [
                            {id: 5, title: 'hello', parent: 4},
                            {id: 6, title: 'hello', parent: 4}
                        ]
                    },
                    {id: 7, title: 'hello', parent: 3}
                ]
            }
        ]
    },
    {
        id: 2, title: 'hello', parent: 0, children: [
            {id: 8, title: 'hello', parent: 2}
        ]
    }
];

这是一个使之实现的递归函数。

function getNestedChildren(models, parentId) {
    const nestedTreeStructure = [];
    const length = models.length;

    for (let i = 0; i < length; i++) { // for-loop for perf reasons, huge difference in ie11
        const model = models[i];

        if (model.parent == parentId) {
            const children = getNestedChildren(models, model.id);

            if (children.length > 0) {
                model.children = children;
            }

            nestedTreeStructure.push(model);
        }
    }

    return nestedTreeStructure;
}

使用方式:

const models = [
    {id: 1, title: 'hello', parent: 0},
    {id: 2, title: 'hello', parent: 0},
    {id: 3, title: 'hello', parent: 1},
    {id: 4, title: 'hello', parent: 3},
    {id: 5, title: 'hello', parent: 4},
    {id: 6, title: 'hello', parent: 4},
    {id: 7, title: 'hello', parent: 3},
    {id: 8, title: 'hello', parent: 2}
];
const nestedStructure = getNestedChildren(models, 0);

对于每个parentId,它都会循环整个模型-这不是O(N ^ 2)吗?
Ed Randall

4

对于对Eugene解决方案的C#版本感兴趣的任何人,请注意,node_list是作为映射访问的,因此请使用Dictionary。

请记住,仅当tableparent_id排序时,此解决方案才有效。

var table = new[]
{
    new Node { parent_id = 0, id = 1 },
    new Node { parent_id = 0, id = 2 },
    new Node { parent_id = 0, id = 3 },
    new Node { parent_id = 1, id = 4 },
    new Node { parent_id = 1, id = 5 },
    new Node { parent_id = 1, id = 6 },
    new Node { parent_id = 2, id = 7 },
    new Node { parent_id = 7, id = 8 },
    new Node { parent_id = 8, id = 9 },
    new Node { parent_id = 3, id = 10 },
};

var root = new Node { id = 0 };
var node_list = new Dictionary<int, Node>{
    { 0, root }
};

foreach (var item in table)
{
    node_list.Add(item.id, item);
    node_list[item.parent_id].children.Add(node_list[item.id]);
}

节点定义如下。

class Node
{
    public int id { get; set; }
    public int parent_id { get; set; }
    public List<Node> children = new List<Node>();
}

1
这是太旧了,但货品8 new Node { parent_id = 7, id = 9 },防止node_list.Add(item.id, item);才能完成,因为密钥不能重复; 这是一个错字;因此,代替id = 9,输入id = 8
Marcelo Scofano

固定。谢谢@MarceloScofano!
乔尔·马龙

3

我根据@Mehrdad Afshari的回答松散地在C#中编写了一个通用解决方案:

void Example(List<MyObject> actualObjects)
{
  List<TreeNode<MyObject>> treeRoots = actualObjects.BuildTree(obj => obj.ID, obj => obj.ParentID, -1);
}

public class TreeNode<T>
{
  public TreeNode(T value)
  {
    Value = value;
    Children = new List<TreeNode<T>>();
  }

  public T Value { get; private set; }
  public List<TreeNode<T>> Children { get; private set; }
}

public static class TreeExtensions
{
  public static List<TreeNode<TValue>> BuildTree<TKey, TValue>(this IEnumerable<TValue> objects, Func<TValue, TKey> keySelector, Func<TValue, TKey> parentKeySelector, TKey defaultKey = default(TKey))
  {
    var roots = new List<TreeNode<TValue>>();
    var allNodes = objects.Select(overrideValue => new TreeNode<TValue>(overrideValue)).ToArray();
    var nodesByRowId = allNodes.ToDictionary(node => keySelector(node.Value));

    foreach (var currentNode in allNodes)
    {
      TKey parentKey = parentKeySelector(currentNode.Value);
      if (Equals(parentKey, defaultKey))
      {
        roots.Add(currentNode);
      }
      else
      {
        nodesByRowId[parentKey].Children.Add(currentNode);
      }
    }

    return roots;
  }
}

下选民,请发表评论。我很高兴知道自己做错了什么。
HuBeZa

2

这是Mehrdad Afshari的答案的Java解决方案。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class Tree {

    Iterator<Node> buildTreeAndGetRoots(List<MyObject> actualObjects) {
        Map<Integer, Node> lookup = new HashMap<>();
        actualObjects.forEach(x -> lookup.put(x.id, new Node(x)));
        //foreach (var item in lookup.Values)
        lookup.values().forEach(item ->
                {
                    Node proposedParent;
                    if (lookup.containsKey(item.associatedObject.parentId)) {
                        proposedParent = lookup.get(item.associatedObject.parentId);
                        item.parent = proposedParent;
                        proposedParent.children.add(item);
                    }
                }
        );
        //return lookup.values.Where(x =>x.Parent ==null);
        return lookup.values().stream().filter(x -> x.parent == null).iterator();
    }

}

class MyObject { // The actual object
    public int parentId;
    public int id;
}

class Node {
    public List<Node> children = new ArrayList<Node>();
    public Node parent;
    public MyObject associatedObject;

    public Node(MyObject associatedObject) {
        this.associatedObject = associatedObject;
    }
}

您应该解释一下代码背后的想法。
Ziad Akiki

这是刚才的答复只是Java的翻译
VIMAL布哈特

1

在我看来问题很模糊,我可能会创建一个从ID到实际对象的映射。在伪Java中(我没有检查它是否可以工作/编译),它可能类似于:

Map<ID, FlatObject> flatObjectMap = new HashMap<ID, FlatObject>();

for (FlatObject object: flatStructure) {
    flatObjectMap.put(object.ID, object);
}

并查找每个父母:

private FlatObject getParent(FlatObject object) {
    getRealObject(object.ParentID);
}

private FlatObject getRealObject(ID objectID) {
    flatObjectMap.get(objectID);
}

通过重用getRealObject(ID)并执行从对象到对象集合(或它们的ID)的映射,您还将获得一个parent-> children映射。


1

假设Dictionary类似于TreeMap,我可以用4行代码和O(n log n)时间来做到这一点。

dict := Dictionary new.
ary do: [:each | dict at: each id put: each].
ary do: [:each | (dict at: each parent) addChild: each].
root := dict at: nil.

编辑:好的,现在我读到一些parentID是假的,因此请忘记上面的内容,并执行以下操作:

dict := Dictionary new.
dict at: nil put: OrderedCollection new.
ary do: [:each | dict at: each id put: each].
ary do: [:each | 
    (dict at: each parent ifAbsent: [dict at: nil]) 
          add: each].
roots := dict at: nil.

1

大多数答案都假设您正在数据库之外进行此操作。如果您的树本质上是相对静态的,并且只需要以某种方式将树映射到数据库中,则可能需要考虑在数据库侧使用嵌套的集合表示形式。查阅Joe Celko的书籍(或在此处 查看Celko的概述)。

如果仍然与Oracle数据库绑定,请查看其CONNECT BY以获取直接的SQL方法。

无论采用哪种方法,您都可以完全跳过映射树,然后再将数据加载到数据库中。只是以为我会提供这种选择,它可能完全不适合您的特定需求。原始问题的整个“正确顺序”部分在某种程度上暗示您出于某种原因需要数据库中的顺序“正确”吗?这也可能会促使我朝那里的树木走去。


1

它与请求者所寻找的并不完全相同,但是我很难将头放在此处提供的模棱两可的短语答案周围,但我仍然认为此答案适合标题。

我的答案是将平面结构映射到直接在对象上的树,其中ParentID每个对象上只有一个。ParentIDnull0如果它是根。与询问者相反,我假设所有有效ParentID的都指向列表中的其他内容:

var rootNodes = new List<DTIntranetMenuItem>();
var dictIntranetMenuItems = new Dictionary<long, DTIntranetMenuItem>();

//Convert the flat database items to the DTO's,
//that has a list of children instead of a ParentID.
foreach (var efIntranetMenuItem in flatIntranetMenuItems) //List<tblIntranetMenuItem>
{
    //Automapper (nuget)
    DTIntranetMenuItem intranetMenuItem =
                                   Mapper.Map<DTIntranetMenuItem>(efIntranetMenuItem);
    intranetMenuItem.Children = new List<DTIntranetMenuItem>();
    dictIntranetMenuItems.Add(efIntranetMenuItem.ID, intranetMenuItem);
}

foreach (var efIntranetMenuItem in flatIntranetMenuItems)
{
    //Getting the equivalent object of the converted ones
    DTIntranetMenuItem intranetMenuItem = dictIntranetMenuItems[efIntranetMenuItem.ID];

    if (efIntranetMenuItem.ParentID == null || efIntranetMenuItem.ParentID <= 0)
    {
        rootNodes.Add(intranetMenuItem);
    }
    else
    {
        var parent = dictIntranetMenuItems[efIntranetMenuItem.ParentID.Value];
        parent.Children.Add(intranetMenuItem);
        //intranetMenuItem.Parent = parent;
    }
}
return rootNodes;

1

这是一个红宝石实现:

它将按属性名称或方法调用的结果进行分类。

CatalogGenerator = ->(depth) do
  if depth != 0
    ->(hash, key) do
      hash[key] = Hash.new(&CatalogGenerator[depth - 1])
    end
  else
    ->(hash, key) do
      hash[key] = []
    end
  end
end

def catalog(collection, root_name: :root, by:)
  method_names = [*by]
  log = Hash.new(&CatalogGenerator[method_names.length])
  tree = collection.each_with_object(log) do |item, catalog|
    path = method_names.map { |method_name| item.public_send(method_name)}.unshift(root_name.to_sym)
  catalog.dig(*path) << item
  end
  tree.with_indifferent_access
end

 students = [#<Student:0x007f891d0b4818 id: 33999, status: "on_hold", tenant_id: 95>,
 #<Student:0x007f891d0b4570 id: 7635, status: "on_hold", tenant_id: 6>,
 #<Student:0x007f891d0b42c8 id: 37220, status: "on_hold", tenant_id: 6>,
 #<Student:0x007f891d0b4020 id: 3444, status: "ready_for_match", tenant_id: 15>,
 #<Student:0x007f8931d5ab58 id: 25166, status: "in_partnership", tenant_id: 10>]

catalog students, by: [:tenant_id, :status]

# this would out put the following
{"root"=>
  {95=>
    {"on_hold"=>
      [#<Student:0x007f891d0b4818
        id: 33999,
        status: "on_hold",
        tenant_id: 95>]},
   6=>
    {"on_hold"=>
      [#<Student:0x007f891d0b4570 id: 7635, status: "on_hold", tenant_id: 6>,
       #<Student:0x007f891d0b42c8
        id: 37220,
        status: "on_hold",
        tenant_id: 6>]},
   15=>
    {"ready_for_match"=>
      [#<Student:0x007f891d0b4020
        id: 3444,
        status: "ready_for_match",
        tenant_id: 15>]},
   10=>
    {"in_partnership"=>
      [#<Student:0x007f8931d5ab58
        id: 25166,
        status: "in_partnership",
        tenant_id: 10>]}}}

1

可接受的答案对我来说似乎太复杂了,因此我要为其添加Ruby和NodeJS版本

假设平面节点列表具有以下结构:

nodes = [
  { id: 7, parent_id: 1 },
  ...
] # ruby

nodes = [
  { id: 7, parentId: 1 },
  ...
] # nodeJS

将上面的平面列表结构变成树的功能如下

对于Ruby:

def to_tree(nodes)

  nodes.each do |node|

    parent = nodes.find { |another| another[:id] == node[:parent_id] }
    next unless parent

    node[:parent] = parent
    parent[:children] ||= []
    parent[:children] << node

  end

  nodes.select { |node| node[:parent].nil? }

end

对于NodeJS:

const toTree = (nodes) => {

  nodes.forEach((node) => {

    const parent = nodes.find((another) => another.id == node.parentId)
    if(parent == null) return;

    node.parent = parent;
    parent.children = parent.children || [];
    parent.children = parent.children.concat(node);

  });

  return nodes.filter((node) => node.parent == null)

};

我相信null需要检查undefined
Ullauri

NodeJS null == undefined => true中的@Ullauri
Hirurg103

1

一种优雅的方法是将列表中的项目表示为字符串,其中包含点分隔的父母列表,最后是一个值:

server.port=90
server.hostname=localhost
client.serverport=90
client.database.port=1234
client.database.host=localhost

组装一棵树时,您将得到类似以下内容的结果:

server:
  port: 90
  hostname: localhost
client:
  serverport=1234
  database:
    port: 1234
    host: localhost

我有一个配置库,可从命令行参数(列表)实现此替代配置(树)。这里是将单个项目添加到列表中树的算法


0

您是否只使用那些属性?如果不是,最好创建一个子节点数组,在其中可以循环遍历所有这些对象一次以建立此类属性。从那里,选择有孩子但没有父母的节点,然后从上到下迭代构建树。


0

Java版本

// node
@Data
public class Node {
    private Long id;
    private Long parentId;
    private String name;
    private List<Node> children = new ArrayList<>();
}

// flat list to tree
List<Node> nodes = new ArrayList();// load nodes from db or network
Map<Long, Node> nodeMap = new HashMap();
nodes.forEach(node -> {
  if (!nodeMap.containsKey(node.getId)) nodeMap.put(node.getId, node);
  if (nodeMap.containsKey(node.getParentId)) {
    Node parent = nodeMap.get(node.getParentId);
    node.setParentId(parent.getId());
    parent.getChildren().add(node);
  }
});

// tree node
List<Node> treeNode = nodeMap .values().stream().filter(n -> n.getParentId() == null).collect(Collectors.toList());
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.