【问题标题】:Return a list of paths to leaf-nodes from a nested list of lists从列表的嵌套列表中返回叶节点的路径列表
【发布时间】:2020-02-03 12:31:05
【问题描述】:

我有一个动态树结构,它表示为列表列表 - 这是一个这样的示例,用空格布局以说明结构:

[['第一的', [0, 'list1'], [1, 'list2'], [2, 'list3']], ['第二', ['second_subda', [0, 'tup1'], [1, 'tup2']], ['second_subdb', [0, 'tup3'], [1, 'tup4']]], ['第三', ['third_subda', [0, 'a'], [1, 'b'], [2, 'c'], [3, ['d', [0, 'e'], [1, 'f'], [2, ['G', [0, 1], [1, 2], [2, 3]]]]]]]]

我想从中提取所有叶节点以及到达它们所需的路径:

例如从上面的结构,我想返回:

[ ('list1', ['first', 0 ]) , ('list2', ['first', 1 ]) , ('list3', ['first', 2 ]) , ('tup1', ['second', 'second_subda', 0]) , ('tup2',['second','second_subda',1]), ('tup3', ['second', 'second_subdb', 0]) , ( 'tup4' , ['second', 'second_subdb', 1 ] ) , ('a', ['third', 'third_subda', 0]) , ( 'b' , ['third', 'third_subda', 1 ] ) , ('c', ['third', 'third_subda', 2]) , ('e', ['third', 'third_subda', 3, 'd', 0]) , ('f',['third','third_subda',3,'d',1]), (1,['第三','third_subda',3,'d',2,'g',0]), ( 2 , ['third', 'third_subda', 3 , 'd', 2 , 'g' , 1 ]) , ( 3 , ['第三', 'third_subda', 3 , 'd', 2 , 'g' , 2 ])]

即对于每个“叶子”,我想提取一个包含所有叶子值的元组,以及描述到达该叶子项的唯一路径的所有初始列表条目的列表。我应该留下这些元组的列表,其中列表中的项目数对应于树中叶节点的数量。

我尝试在 networkx 之类的模块中构建此树,但对于我的用例而言,额外模块的开销太大了。我只想在可能的情况下坚持使用香草 python 代码。

【问题讨论】:

    标签: python list tree


    【解决方案1】:

    您可以将递归与生成器一起使用:

    data = [['first', [0, 'list1'], [1, 'list2'], [2, 'list3']], ['second', ['second_subda', [0, 'tup1'], [1, 'tup2']], ['second_subdb', [0, 'tup3'], [1, 'tup4']]], ['third', ['third_subda', [0, 'a'], [1, 'b'], [2, 'c'], [3, ['d', [0, 'e'], [1, 'f'], [2, ['g', [0, 1], [1, 2], [2, 3]]]]]]]]
    def get_paths(d, c = []):
      for a, *b in d:
        if len(b) == 1 and not isinstance(b[0], list):
          yield (b[0], c+[a])
        else:
          yield from get_paths(b, c+[a])
    
    print(list(get_paths(data)))
    

    输出:

    [('list1', ['first', 0]), 
     ('list2', ['first', 1]), 
     ('list3', ['first', 2]), 
     ('tup1', ['second', 'second_subda', 0]), 
     ('tup2', ['second', 'second_subda', 1]), 
     ('tup3', ['second', 'second_subdb', 0]), 
     ('tup4', ['second', 'second_subdb', 1]), 
     ('a', ['third', 'third_subda', 0]), 
     ('b', ['third', 'third_subda', 1]), 
     ('c', ['third', 'third_subda', 2]), 
     ('e', ['third', 'third_subda', 3, 'd', 0]), 
     ('f', ['third', 'third_subda', 3, 'd', 1]), 
     (1, ['third', 'third_subda', 3, 'd', 2, 'g', 0]), 
     (2, ['third', 'third_subda', 3, 'd', 2, 'g', 1]), 
     (3, ['third', 'third_subda', 3, 'd', 2, 'g', 2])]
    

    【讨论】:

    • 感谢您的回答,它看起来既可爱又优雅-我认为循环中的 a, *b 术语是一种将列表分成由第 [0] 个组成的分区的方法,这是对的吗和第 [1:] 个列表内容?我不认为我以前见过这种结构。
    • @ThomasKimber 你完全正确。 a, *b 被称为解包,其中a 是迭代器产生的元素的第一个元素,belement[1:]
    【解决方案2】:

    首先,如果可以,请为此使用 dicts 而不是列表列表。与具有线性查找时间的列表不同,字典具有恒定的键查找时间。

    关于您的问题,每当您处理动态树时,递归通常是一种方式。

    这适用于您的树:

    def get_leaf_paths(children: list, path_prefix:list=[], acc:list=[]):
        for child in children:
            path = path_prefix + [child[0]]
            if isinstance(child[1], list):
                get_leaf_paths(child[1:], path, acc)
            else:
                acc.append(
                    (child[1], path)
                )
        return acc
    
    get_leaf_paths(tree)
    

    然而,这很丑陋,而且有充分的理由。当 dict 结构更适合时,Python 不希望您实现这样的树。例如,通过索引 (child[1]) 引用叶值并不好,并且在同一列表中包含节点名称和子节点是有问题的(导致 child[1:] 迭代子节点,这不是描述性的)。在好的 python 代码中也应避免调用isinstance,但我们需要在这里检查是否有叶子。

    最佳实践规定叶子应该是具有None 子节点的节点 - 这使得检查叶子状态更容易。如果我们用 dict 的 dict 和 None children 来实现相同的叶子,该函数将清理为:

    def get_leaf_paths_dict(tree: dict, path=[], acc=[]):
        for node, children in tree.items():
            if children: # not leaf
                get_leaf_paths(children, path + [node], acc)
            else:
                acc.append((node, path))
        return acc
    
    get_leaf_paths_2(tree)
    

    读起来更舒服。要清楚,要使第二个工作,必须将树更改为 dicts 的 dict,即:

    {{'first':  {0: {'list1': None}, 
                 1: {'list2': None}, 
                 2: {'list3': None},
     {'second': { ... etc.
    

    顺便说一句,如果您像这样构建树,您可以使用 nx.from_dict_of_dicts 函数将其导入 Networkx 并执行 networkx api 从那里为您提供的所有操作。

    最后,我意识到,如果您是函数式编程的新手,我给出的两个函数可能都需要一些解释。树上的递归通过注意到树中的每个孩子本身都可以被视为一棵树来工作,因此我们可以通过让函数调用自身并传递累积的列表来节省很多行代码路径和当前路径以追加任何新路径。

    编辑:我什至会给你免费转换成字典的功能(注意相似之处):

    def to_dict_of_dicts(tree, acc={}):
        for child in tree:
            if isinstance(child[1], list):
                acc[child[0]] = to_dict_of_dicts(child[1:])
            else:
                return {child[1] : None}
        return acc
    
    print(to_dict_of_dicts(tree))
    

    【讨论】:

    • 感谢您的回答 - 从列表列表开始的原因是我的起点是一个任意复杂的 python 对象,由嵌套的字典、列表、元组和其他我想要的内容组成根据叶子中识别的搜索词构建提取索引。建立此索引后,我可以搜索每个元组的第一个元素,并返回路径以查找(和编辑)正在修订的对象部分。
    猜你喜欢
    • 2021-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多