【问题标题】:Build nested tree-like dict from an array of dicts with children从带有孩子的字典数组构建嵌套的树状字典
【发布时间】:2015-10-18 05:54:21
【问题描述】:

我有一个从 Web API 检索到的字典数组。每个 dict 都有一个 namedescription、'parent' 和 children 键。 children 键有一个字典数组作为它的值。为了清楚起见,这里是一个虚拟示例:

[
  {'name': 'top_parent', 'description': None, 'parent': None,
   'children': [{'name': 'child_one'},
                {'name': 'child_two'}]},
  {'name': 'child_one', 'description': None, 'parent': 'top_parent',
   'children': []},
  {'name': 'child_two', 'description': None, 'parent': 'top_parent',
   'children': [{'name': 'grand_child'}]},
  {'name': 'grand_child', 'description': None, 'parent': 'child_two',
   'children': []}
]

数组中的每一项。一个项目可能是最顶层的父项,因此不存在于任何children 数组中。一个项目既可以是孩子也可以是父母。或者一个项目只能是一个孩子(没有自己的孩子)。

所以,在树形结构中,你会有这样的东西:

top_parent
  child_one
  child_two
    grand_child

在这个人为的简化示例中,top_parent 是父母,但不是孩子; child_one 是孩子但不是父母; child_two是父母和孩子; grand_child 是孩子,但不是父母。这涵盖了所有可能的状态。

我想要的是能够迭代 dicts 数组 1 次并生成一个正确表示树结构的嵌套 dict(但是,它 1 次是不可能的,可能是最有效的方式)。所以,在这个例子中,我会得到一个看起来像这样的字典:

{
  'top_parent': {
    'child_one': {},
    'child_two': {
      'grand_child': {}
    }
  }    
}

严格来说,没有子项的 item 没有必要不是 key,但这是更可取的。

【问题讨论】:

  • 您想忽略最终字典中的名称和描述?
  • 它们可以被忽略,但它们不需要。关键目标是正确表示对象的父子关系的嵌套字典。
  • 名称是否保证唯一?
  • 是的。名称将是唯一的。这是由网络服务保证的。
  • 我猜 BFS 或 DFS 可以正常工作。每个节点只会被访问一次,您可以按照自己喜欢的方式将结果存储在字典中。

标签: python arrays dictionary tree


【解决方案1】:

您可以保持从名称到节点的惰性映射,然后通过仅处理parent 链接来重建层次结构(我假设数据是正确的,所以如果A 被标记为B 的父级iff B 列在A 的子代中)。

nmap = {}
for n in nodes:
    name = n["name"]
    parent = n["parent"]
    try:
        # Was this node built before?
        me = nmap[name]
    except KeyError:
        # No... create it now
        if n["children"]:
            nmap[name] = me = {}
        else:
            me = None
    if parent:
        try:
            nmap[parent][name] = me
        except KeyError:
            # My parent will follow later
            nmap[parent] = {name: me}
    else:
        root = me

输入的children 属性仅用于了解该元素是否应该在其父元素中存储为None(因为没有子元素),或者它是否应该是字典,因为它在重建过程结束。将没有子节点的节点存储为空字典可以避免这种特殊情况,从而稍微简化代码。

使用collections.defaultdict也可以简化代码以创建新节点

import collections
nmap = collections.defaultdict(dict)
for n in nodes:
    name = n["name"]
    parent = n["parent"]
    me = nmap[name]
    if parent:
        nmap[parent][name] = me
    else:
        root = me

这个算法是O(N) 假设常量时间字典访问并且只对输入进行一次传递并且需要O(N) 空间用于名称->节点映射(空间要求是O(Nc) 用于原始nochildren->None其中Nc 是有子节点的版本)。

【讨论】:

  • 我将编辑问题,但None 不是必需值。所以,我很想看看你的简化版没有那个限制。
【解决方案2】:

我的尝试:

persons = [\
  {'name': 'top_parent', 'description': None, 'parent': None,\
   'children': [{'name': 'child_one'},\
                {'name': 'child_two'}]},\
  {'name': 'grand_child', 'description': None, 'parent': 'child_two',\
   'children': []},\
  {'name': 'child_two', 'description': None, 'parent': 'top_parent',\
   'children': [{'name': 'grand_child'}]},\
  {'name': 'child_one', 'description': None, 'parent': 'top_parent',\
   'children': []},\
]

def findParent(name,parent,tree,found = False):
    if tree == {}:
        return False
    if parent in tree:
        tree[parent][name] = {}
        return True
    else:
        for p in tree:
            found = findParent(name,parent,tree[p],False) or found
        return found

tree = {}
outOfOrder = []
for person in persons:
    if person['parent'] == None:
        tree[person['name']] = {}
    else:
        if not findParent(person['name'],person['parent'],tree):
            outOfOrder.append(person)
for person in outOfOrder:
    if not findParent(person['name'],person['parent'],tree):
        print 'parent of ' + person['name']  + ' not found
print tree

结果:

{'top_parent': {'child_two': {'grand_child': {}}, 'child_one': {}}}

它还会选择尚未添加父级的任何子级,然后在最后进行协调。

【讨论】:

    【解决方案3】:

    第四次编辑,展示了三个版本,稍微整理了一下。第一个版本自上而下工作并按照您的要求返回 None,但本质上循环遍历顶层数组 3 次。下一个版本只循环一次,但返回空字典而不是 None。

    最终版本自下而上,非常干净。它可以通过单个循环返回空字典,或者通过附加循环返回 None:

    from collections import defaultdict
    
    my_array = [
      {'name': 'top_parent', 'description': None, 'parent': None,
       'children': [{'name': 'child_one'},
                    {'name': 'child_two'}]},
      {'name': 'child_one', 'description': None, 'parent': 'top_parent',
       'children': []},
      {'name': 'child_two', 'description': None, 'parent': 'top_parent',
       'children': [{'name': 'grand_child'}]},
      {'name': 'grand_child', 'description': None, 'parent': 'child_two',
       'children': []}
    ]
    
    def build_nest_None(my_array):
        childmap = [(d['name'], set(x['name'] for x in d['children']) or None)
                    for d in my_array]
        all_dicts = dict((name, kids and {}) for (name, kids) in childmap)
        results = all_dicts.copy()
        for (name, kids) in ((x, y) for x, y in childmap if y is not None):
            all_dicts[name].update((kid, results.pop(kid)) for kid in kids)
        return results
    
    def build_nest_empty(my_array):
        all_children = set()
        all_dicts = defaultdict(dict)
        for d in my_array:
            children = set(x['name'] for x in d['children'])
            all_dicts[d['name']].update((x, all_dicts[x]) for x in children)
            all_children.update(children)
        top_name, = set(all_dicts) - all_children
        return {top_name: all_dicts[top_name]}
    
    
    def build_bottom_up(my_array, use_None=False):
        all_dicts = defaultdict(dict)
        for d in my_array:
            name = d['name']
            all_dicts[d['parent']][name] = all_dicts[name]
    
        if use_None:
            for d in all_dicts.values():
                for x, y in d.items():
                    if not y:
                        d[x] = None
    
        return all_dicts[None]
    
    print(build_nest_None(my_array))
    print(build_nest_empty(my_array))
    print(build_bottom_up(my_array, True))
    print(build_bottom_up(my_array))
    

    结果:

    {'top_parent': {'child_one': None, 'child_two': {'grand_child': None}}}
    {'top_parent': {'child_one': {}, 'child_two': {'grand_child': {}}}}
    {'top_parent': {'child_one': None, 'child_two': {'grand_child': None}}}
    {'top_parent': {'child_one': {}, 'child_two': {'grand_child': {}}}}
    

    【讨论】:

    • build_bottom_up() 函数,无需使用None,是(你是对的)最干净的。显然,“魔法”发生在for 循环内的最后一行。我很想听听您详细了解那里正在发生的事情。
    • 其实“魔法”就在defaultdict里面。由于我将dict 作为 all_dicts 值的构造函数传入,因此在常规字典会给出 KeyError 的任何索引操作上,defaultdict 将构造一个新的默认对象(一个空字典)并将其添加到字典中。这种行为意味着先遇到父对象还是子对象都没有关系——在任何一种情况下,都将根据需要构造字典,然后在遇到另一个对象时可以使用和使用相同的字典。如果不清楚,我会再试一次...
    • 我现在看到 6502 已经编辑了他的答案以使用 defaultdict 添加一个选项,这使得它基本上与这个答案相同。那和这之间的唯一区别是(a)他使事情更加明确;并且(b)他没有将顶部字典存储在默认字典中。这两个特性可能会使他的代码更容易理解和遵循,但它们都会降低性能。
    猜你喜欢
    • 2019-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-23
    • 2022-11-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-10
    相关资源
    最近更新 更多