【问题标题】:print a binary tree on its side在其一侧打印二叉树
【发布时间】:2012-06-19 07:17:23
【问题描述】:

如何在其一侧打印二叉树,使输出看起来像这样?

   __/a
__/  \b
  \   _/c
   \_/ \d
     \e

(欢迎使用更漂亮的 ascii-art)

下面是一些不太有效的代码:

def print_tree(tree):
    def emit(node,prefix):
        if "sequence" in node:
            print "%s%s"%(prefix[:-1],node["name"])
        else:
            emit(node["left"],"%s_/ "%prefix.replace("/ "," /")[:-1].replace("_"," "))
            emit(node["right"],"%s \\ "%prefix.replace("\\ "," \\")[:-1])
    emit(tree,"")    

哪个输出这个:

      _/hg19
    _/ \rheMac2
  _/ \mm9
  /\_/bosTau4
  /  \_/canFam2
_/     \pteVam1
 \_/loxAfr3
   \dasNov2

范围蔓延:如果你可以传入一个函数,该函数将返回字符串以打印任何节点,那就太好了;这样,我有时也可以打印有关非离开节点的信息。所以一个节点是否有要打印的东西是由作为参数传入的函数控制的。

以下是 JSON 格式的一些测试数据:

{
    "left": {
        "left": {
            "left": {
                "left": {
                    "name": "hg19", 
                    "sequence": 0
                }, 
                "right": {
                    "name": "rheMac2", 
                    "sequence": 1
                }
            }, 
            "right": {
                "name": "mm9", 
                "sequence": 2
            }
        }, 
        "right": {
            "left": {
                "name": "bosTau4", 
                "sequence": 3
            }, 
            "right": {
                "left": {
                    "name": "canFam2", 
                    "sequence": 4
                }, 
                "right": {
                    "name": "pteVam1", 
                    "sequence": 5
                }
            }
        }
    }, 
    "right": {
        "left": {
            "name": "loxAfr3", 
            "sequence": 6
        }, 
        "right": {
            "name": "dasNov2", 
            "sequence": 7
        }
    }
}

【问题讨论】:

  • 你试过什么?我可以想象它涉及计算树的属性(深度、宽度等)、布局计算和生成 ASCII 艺术。
  • @SimeonVisser 添加了一些损坏的代码
  • 看到这个让我觉得你也应该跟踪树的深度。我有一些基于您损坏的代码的基本代码,但看起来很糟糕。对于每一行,我试图弄清楚它应该有多少额外的空间,但该行的重建目前只占最低的分支
  • 您是否考虑过使用graphviz 语言对树进行序列化?有很多布局/渲染工具可以理解这种格式
  • @J.F.Sebastian 我真的很想避免外部依赖,虽然我是graphviz的粉丝

标签: python tree formatting


【解决方案1】:

这里有一些代码实现了其他地方描述的通用递归方法。树的内部表示是子节点的字符串(叶)或元组(对)。节点的中间“片段”的内部表示是元组(above, below, lines),其中abovebelow 是根上方和下方的行数,lines 是每个部分行上的迭代器(没有左边的空格)。

#!/usr/local/bin/python3.3

from itertools import chain
from random import randint


def leaf(t):
    return isinstance(t, str)

def random(n):
    def extend(t):
        if leaf(t):
            return (t+'l', t+'r')
        else:
            l, r = t
            if randint(0, 1): return (l, extend(r))
            else: return (extend(l), r)
    t = ''
    for _ in range(n-1): t = extend(t)
    return t

def format(t):
    def pad(prefix, spaces, previous):
        return prefix + (' ' * spaces) + previous
    def merge(l, r):
        l_above, l_below, l_lines = l
        r_above, r_below, r_lines = r
        gap = r_below + l_above
        gap_above = l_above
        gap_below = gap - gap_above
        def lines():
            for (i, line) in enumerate(chain(r_lines, l_lines)):
                if i < r_above:
                    yield ' ' + line
                elif i - r_above < gap_above:
                    dash = '_' if i - r_above == gap_above - 1 else ' '
                    if i < r_above + r_below:
                        yield pad(dash + '/', 2 * (i - r_above), line)
                    else:
                        yield pad(dash + '/', 2 * gap_below - 1, line)
                elif i - r_above - gap_above < gap_below:
                    if i < r_above + r_below:
                        yield pad(' \\', 2 * gap_above - 1, line)
                    else:
                        spaces = 2 * (r_above + gap_above + gap_below - i - 1)
                        yield pad(' \\', spaces, line)
                else:
                    yield ' ' + line
        return (r_above + gap_above, gap_below + l_below, lines())
    def descend(left, t):
        if leaf(t):
            if left:
                return (1, 0, [t])
            else:
                return (0, 1, [t])
        else:
            l, r = t
            return merge(descend(True, l), descend(False, r))
    def flatten(t):
        above, below, lines = t
        for (i, line) in enumerate(lines):
            if i < above: yield (' ' * (above - i - 1)) + line
            else: yield (' ' * (i - above)) + line
    return '\n'.join(flatten(descend(True, t)))


if __name__ == '__main__':
    for n in range(1,20,3):
        tree = random(n)
        print(format(tree))

这是一些示例输出:

          _/rrrr
        _/ \_/rrrlr
       / \   \rrrll
     _/   \_/rrlr
    / \     \rrll
   /   \   _/rlrr
  /     \_/ \rlrl
_/        \_/rllr
 \          \_/rlllr
  \           \rllll
   \        _/lrrr
    \     _/ \lrrl
     \   / \_/lrlr
      \_/    \lrll
        \   _/llrr
         \_/ \llrl
           \_/lllr
             \_/llllr
               \lllll

还有一点不对称的,也许可以说明为什么我不会在左边用空格填充行直到最后(通过flatten)。如果下半部分在左侧有衬垫,则上臂的一些部分会穿过衬垫区域。

               _/rrrrr
             _/ \rrrrl
           _/ \rrrl
         _/ \_/rrlr
        / \   \rrll
       /   \_/rlr
      /      \rll
     /        /lrrr
    /       _/  _/lrrlrr
   /       / \_/ \lrrlrl
  /       /    \lrrll
_/      _/     _/lrlrrr
 \     / \   _/ \lrlrrl
  \   /   \_/ \lrlrl
   \_/      \lrll
     \      _/llrrr
      \   _/ \llrrl
       \_/ \llrl
         \lll

这是“显而易见的”递归算法——魔鬼在细节中。不带“_”最容易写,逻辑稍微复杂一些。

也许唯一的“洞察力”是gap_above = l_above - 这就是说右“臂”具有左子树右侧的长度(您需要阅读几次)。它使事情相对平衡。请参阅上面的不对称示例。

更详细地理解事物的一个好方法是修改pad 例程以使用一个字符而不是' ',并为每个调用赋予不同的字符。然后你可以确切地看到哪个逻辑生成了哪个空间。这就是你使用 A.B、C 和 D 来调用从上到下、上填充的结果(当空间量为零时显然没有字符):

             _/rrrr
            / \rrrl
          _/B _/rrlrr
         / \_/ \rrlrl
        /AA  \rrll
      _/BBB  _/rlrrr
     / \DD _/ \rlrrl
    /AA \_/ \_/rlrlr
   /AAAA  \C  \rlrll
  /AAAAAA  \_/rllr
_/AAAAAAAA   \rlll
 \DDDDDDDD   _/lrrrr
  \DDDDDD  _/ \lrrrl
   \DDDD  / \lrrl
    \DD _/B _/lrlrr
     \_/ \_/ \lrlrl
       \C  \lrll
        \_/llr
          \lll

还有更多解释here(虽然树有很大的不同)。

【讨论】:

  • 漂亮!请链接到博客文章。一个扩展目标是能够控制用于每个分支的 - 字符串,并使它们能够是可变长度。
  • acooke.org/cute/Printingbi0.html 发帖 - 这是非常相似的代码,但没有“_”并且有更多的 cmets。您可以通过比较两者来确定如何添加任意字符串。
【解决方案2】:

制作一个表示结构,包含一个字符串数组和一个“花瓣”的行号。

rep(leaf) 为 [0, repr(leaf value)]

rep(nonleaf),给定 top = nonleaf.leftbottom = nonleaf.right:

如果在顶部的花瓣上方,则用空格填充 rep(top) 的每一行,如果在下方,则在适当的位置用斜线填充。类似地,如果低于底部的花瓣,则用空格填充 rep(bottom) 的每一行,如果高于底部的花瓣,则在适当的位置使用反斜杠。 repr(nonleaf) 是 [顶部的高度,顶部的填充线,然后是底部的填充线]。

例子:

rep(a): [0, ["a"]]
rep(b): [0, ["b"]]
rep(ab): [1, ["/"+"a", "\"+"b"]]
rep(c): [0, ["c"]]
rep(d): [0, ["d"]]
rep(cd): [1, ["/"+"c", "\"+"d"]]
rep(e): [0, ["e"]]
rep(cde): [2, [" "+"/c", "/" + "\d", "\" + "e"]]
rep(abcde): [2, [" "+"/a", "/"+"\b", "\ "+" /c", " \" + "/\d", "  " + "\e"]]

注意,在上面的情况下,填充的宽度是花瓣下面的行数;在底部情况下,填充的宽度对应于花瓣。因此,在 (abcde) 的情况下,顶部有 2 行和花瓣 1,所以填充是 (2 - 1 == 1) 一个字符;底部有花瓣 2,所以填充是 2 个字符。

如果需要,您还可以在第 (petal-1) 行的每个非叶中添加一个“_”(并为所有其他行添加一个额外的空格)。

显然,这些都不是代码(“\”是等待发生的语法错误),但从这里实现应该不会太难。

【讨论】:

    【解决方案3】:

    您需要递归处理此问题,并跟踪各个子树的大小。特别是根在哪里。不平衡的树很容易看起来像这样:

    /
    \/
     \/
      \/
       \
    

    现在考虑你已经构建了这棵树,在添加父级别时你需要将它转换为以下内容。

      /
     /\/
    /  \/
    \   \/
     \   \
      \
    

    关键思想是从树叶开始。它们是微不足道的。然后定义一种聚合两个子树的方法,假设它们具有不同的行数和子树根节点的不同位置。

    【讨论】:

    • 这是一种方法,但您不是在掩饰困难的部分吗? ;)
    • 好吧,我已经给了你关键的想法:从叶到根,跟踪大小和子树根位置。您必须自己弄清楚确切的字符串操作。我没有为你准备好代码。这就是我 10 年前通过输出原始后记绘制家谱树时解决这个问题的方法。就算我能把这段代码挖出来,对你也没什么用。
    【解决方案4】:

    这是一个很好的侧向树,它帮助我调试了一个项目: http://www.acooke.org/cute/ASCIIDispl0.html

    结果类似于 VIM NERDtree 插件的目录布局,如果你看过的话。

    这是我在二叉树中重新实现为__str__ 方法:

    def __str__(self):
        """Recursive __str__ method of an isomorphic node."""
        # Keep a list of lines
        lines = list()
        lines.append(self.name)
        # Get left and right sub-trees
        l = str(self.left).split('\n')
        r = str(self.right).split('\n')
        # Append first left, then right trees
        for branch in l, r:
            # Suppress Pipe on right branch
            alt = '| ' if branch is l else '  '
            for line in branch:
                # Special prefix for first line (child)
                prefix = '+-' if line is branch[0] else alt
                lines.append(prefix + line)
        # Collapse lines
        return '\n'.join(lines)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-07
      • 2019-09-21
      相关资源
      最近更新 更多