【问题标题】:Algorithm to Render a Horizontal Binary-ish Tree in Text/ASCII form以文本/ASCII 形式呈现水平二叉树的算法
【发布时间】:2010-06-17 22:41:23
【问题描述】:

这是一棵非常普通的二叉树,只是其中一个节点可能为空。

我想找一种方式,横向输出(即根节点在左边,向右展开)。

我有一些垂直扩展树的经验(根节点在顶部,向下扩展),但在这种情况下我不确定从哪里开始。

最好遵循以下几条规则:

  • 如果一个节点只有一个子节点,则可以将其作为冗余节点跳过(始终显示没有子节点的“结束节点”)
  • 相同深度的所有节点必须垂直对齐;所有节点都必须位于所有较浅节点的右侧和所有较深节点的左侧。
  • 节点具有包含其深度的字符串表示形式。
  • 每个“端节点”都有自己独特的线路;也就是说,行数就是树中的结束节点的数量,当一个结束节点在一行上时,在该结束节点之后的那一行上可能没有其他内容。
  • 作为最后一条规则的结果,根节点可能在左上角或左下角更好;首选左上角。

例如,这是一棵有效的树,有六个末端节点(节点由名称及其深度表示):编辑:请参阅问题底部的替代方案,更容易呈现 [a0]-----------[b3]------[c5]------[d8] \ \ \----------[e9] \ \----[f5] \-[g1]--------[h4]------[i6] \ \--------------------[j10] \-[k3]

代表垂直的显式二叉树:

0个 / \ 1 克 * / \ \ 2 * * * / \ \ 3 k * b // \ 4 小时 * * / \ \ \ 5 * * f c / \ / \ 6 * 我 * * // \ 7 * * * // \ 8 * * 天 // 9 * e / 10 焦

(为紧凑而折叠的分支;* 代表冗余的单子节点;请注意,*实际节点,每个节点存储一个子节点,只是此处省略了名称以进行演示清酒)

(另外,澄清一下,我想生成第一棵水平树;而不是这棵垂直树)

我说与语言无关是因为我只是在寻找一种算法;我说 ruby​​ 是因为我最终还是要在 ruby​​ 中实现它。

假设每个Node数据结构只存储了它的id、一个左节点和一个右节点。

一个主 Tree 类跟踪所有节点并有足够的算法来查找:

  • 节点的第 n 个祖先
  • 节点的第 n 个后代
  • 一个节点的所有端节点后代,及其计数
  • 节点的生成
  • 两个给定节点的最低共同祖先

已经知道

  • 端节点数

有人知道我可以从哪里开始吗?我应该采用递归方法吗?迭代? 一些伪代码也会很酷,非常感谢 =)


进展

根据 walkytalky 的建议,我决定看看将每个“相关”或重要节点映射到网格会是什么样子,其中列是深度,行可以由它们的末端节点识别。以下是发生的情况(跳过第 7 列,因为深度 7 中没有重要节点):

深度:0 1 2 3 4 5 6 8 9 10 A B C D e F 我 j ķ

使用广度优先或深度优先搜索生成此网格应该很容易。也许最简单的方法是简单地保留一个二维数组并将找到的每个重要节点放入其中,为每个“第二个孩子”插入一行。

现在,了解这些事实:

  • 一行中的最后一个节点必须是结束节点
  • 子节点始终位于其父节点的右侧,并且在同一行或更低的位置。
  • 所有非端节点必须恰好有两个子节点
  • 因此,所有非结束节点的子节点其列右侧的第一个,第一个子节点在同一行,第二个子节点 n 它们下面的行,其中 n 是其右侧的节点数。

我们可以看到,给定任何有效的网格,可以说,有一种明确的方式来“连接点”;代表的是一棵明确的树。

现在,“连接点”不再是一个二叉树结构问题……它只是一个装饰问题。我们只需要构建一个算法来将正确的-\ 正确放置在它们可以去的地方,也许只遵循简单的网格/字典规则,而不是二叉树-结构规则。

基本上,这意味着渲染树的问题现在是渲染网格的简单得多的问题,带有花哨的装饰。

谁能提出任何制定这些规则的方法?或者完全不同的方法?


编辑

我构思了一个更简单的最终渲染:

--d0----d1----d3----d4----d5----d6----d8----d9----d10-- => 指导线 (未渲​​染) [a0]-------[b3]--------[c5]-------[d8] | | \---------------[e9] | \---------[f5] \---[g1 ]-------[h4 ]-------[i6 ] | \---------------------------------------[j10] \---[k3] --d0----d1----d3----d4----d5----d6----d8----d9----d10-- => 指导线 (未渲​​染)

尝试创建这个可能会更容易,而不是我之前发布的那个。一方面,它保留了一个漂亮的网格形状,你不必用对角线变化无常。这些行都沿着清晰可见的列线映射。不幸的是,它远没有第一个漂亮。

【问题讨论】:

  • 你的数据结构是什么样的?
  • 这不是您真正要的,但您可以渲染到dot,然后告诉graphvis 为您制作图像。
  • @user257493 - 我会说数据结构无关紧要,不是自命不凡或特别无用,而是因为我将直接提取原始数据,并将其解析为最适合这项任务的任何数据结构。
  • 关于您的编辑:举个简单的例子,将每行 n 偏移 n 个前导空格,用斜杠替换所有管道。这是一个与偏移量之间的微不足道的转换。
  • @Jimmy - 谢谢!我想我会使用它;现在似乎更容易解决首先构建直线示例,然后将其转换为弯曲示例的问题?

标签: ruby algorithm language-agnostic text binary-tree


【解决方案1】:

如果有 N 结束节点,则必须有 N-1 内部节点和 2 个子节点。 (可以有任意数量的具有 1 个子节点的内部节点,我们必须对其进行计数才能获得深度,否则会忽略。)因此,生成树等同于将这些节点定位在网格上,其中:

  • 网格中的行数为N
  • 认为列数介于1+floor(log2(N))2*N-1 之间,具体取决于重叠的程度;不过,这对我们的目的来说可能并不重要
  • 每个端点出现在不同的行上
  • 同一深度的所有节点出现在同一列中
  • 所有内部节点与其最右边的后代端点出现在同一行中

那么,让我们看看:

  • 树深度优先,从右到左。
  • 对于每个端点,记录其深度和标签。
  • 对于每个 2 子内部,记录其深度、标签以及最右侧和最左侧子端点的索引。
  • 按深度对整个批次进行排序——这为您提供了列排序,不同深度的数量给出了实际的列数。 (我认为,所有其他排序都应该从 walk 中自动得出,但这里不是这种情况,因为任何分支都可以是任何深度。)
  • 将所有节点放在网格中。
  • 将每个非端点节点右侧的空单元格标记为水平分支。
  • 将每个内部节点向下到其左子节点上方的行的空单元格标记为垂直分支,并将左子节点级别的单元格标记为交汇点。

  • 使用适当的 ASCII 装饰打印。

更新:

正如您所说,定位足以明确确定连接,但您仍然需要做一些自下而上的工作才能做到这一点,所以我可能仍然会在网格构建期间执行“标记”步骤。

我有点认为印刷是微不足道的,可以掩盖,但是:

  • 向下迭代每一列并将列宽确定为size of fixed elements + max label length + floor(log10(depth) + 1)。 (例如,固定元素可能是[]-。我们可以将]\n 替换为端点的后缀。)
  • 对于每一行
    • 对于每一列
      • 如果单元格包含节点或端点
        • 打印固定前缀
        • 打印标签
        • 打印深度
        • 打印填充空间(最大标签长度 - 当前标签长度)
        • 打印适当的后缀
        • 如果节点是端点,则跳到下一行
      • 如果单元格为空,则将填充空格打印到列宽
      • 如果单元格包含垂直,打印一些选定的空格前缀数,一个条形,并用空格填充
      • 如果单元格包含连接,则打印一些选定的空格前缀数、反斜杠并用连字符填充
      • 如果单元格包含水平,则打印整个列宽的连字符

如果您首先生成直接版本然后在字符数组中进行一些替换,则将其转换为打印对角线可能是最简单的 - 否则您可能会遇到在与该列不同的列中呈现长垂直分支的情况它起源于哪里。

在某个时候,我可能会尝试将其放入代码中,但可能不会是今天——要做的事情!

【讨论】:

  • 我喜欢在网格上定位每个相关节点的方法;我会更多地研究这个:)
  • 有趣的注意事项:给定建议的网格,在不知道任何连接的情况下,有一种明确的方法可以将各个部分连接在一起。我将编辑我的帖子以包含此内容。
  • 所有答案都非常好;你的方法是我最终采用的方法,因为它让我从另一个角度看待和理解一切,我可以轻松攻击。
【解决方案2】:

看起来是个有趣的问题;如果我有更多时间,我很乐意尝试一下。

我可能会采用以下方法:

  1. 开始渲染“右”(或在您的情况下为“顶部”)节点,直到我到达终点。 (即:渲染 a、b、c 和 d)
  2. 回到最后一个有孩子的节点(即:c),递归地做同样的事情

您必须保留一个全局变量来指示您正在打印的行。每次递归调用都会增加这个变量。

编辑:好的,忍不住想写一些未经测试的伪代码,希望它有效:

function print_tree(Node n) {
    print "\n" // begin on a fresh new line
    childs = new Array();
    do {
        if (n.hasLeftChild) {
            childs.push(n.leftChild)
        }
        print "---" + n.id    //this needs a lot of tweaking, but you get the idea
    } while(n = n.rightChild)
    childs.reverse()
    foreach(child in childs) {
        print_tree(child);
    }
}

【讨论】:

    【解决方案3】:

    如果您从每个级别的标签宽度开始(不包括 [] 字符),则等于该宽度的最大标签(在此示例中,宽度大多为 2,除了 j10 为 3 和级别 2和 7 是 0)。

    让具有非零最大标签宽度的每个级别等间距,每个级别之间有一个 - 字符,以便您可以计算初始级别 y 位置。

    给每个节点它的行号。

    然后根据关卡子级之间的最大行数调整关卡位置。

    为 a0 到 g1 添加了 2 到级别 1
    为 g1 到 k3 添加了 1 到级别 2
    将 b3 的 1 添加到级别 4 到 [ ]

    对角线使用\` 字符。

    [a0]---------[b3]------[c5]------[d8] \ \ `----------[e9] \`-----[f5] `[g1]--------[h4]------[i6] \ `--------------------[j10] `[k3]

    【讨论】:

    • 我喜欢这种方法;但是,我不确定我是否完全理解它。您的“行初始化”似乎只涵盖了第一行,即第一行……您最初将如何渲染,例如,第 2 行? 3?
    【解决方案4】:

    以下是功能齐全的 C# 代码,完全符合您的要求。它是如何做到的:

    • 树表示为继承自Node 的类中的对象
    • 首先计算叶子的数量并创建一个包含那么多行的数组
    • 然后对于每个级别:
      • 找出我们要写哪些行
      • 对于这些行,计算这些行上已有的最大值
      • 将所有节点写入列max(上一步的数字,上一级的结束)+1;在前面加上 - 以进入该列
      • 从所有二元节点到其右子节点的行写对角线(在我的程序中,第一个子节点在左边,第二个在右边,反之亦然)
      • 提升一级

    该算法确保每个级别仅在之前的结束之后才开始。这对于短名称可能是不错的选择,但对于较长的名称,这可能不应该强制执行。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace SO_ASCII_tree
    {
        class Program
        {
            static void Main()
            {
                Node root = …;
    
                StringBuilder[] lines = Enumerable.Range(0, root.Leaves).Select(i => new StringBuilder()).ToArray();
    
                Node[] currentLevel = new Node[] { root };
                int level = 0;
                int min = 0;
                int max = 0;
                while (currentLevel.Any())
                {
                    NamedNode[] namedNodes = currentLevel.OfType<NamedNode>().ToArray();
                    if (namedNodes.Any())
                    {
                        min = namedNodes.Select(node => lines[node.Line].Length).Max();
                        min = Math.Max(min, max);
                        if (min != 0)
                            min++;
                        foreach (NamedNode namedNode in namedNodes)
                            WriteAtPosition(lines[namedNode.Line], namedNode.Write(level), min, '-');
                        max = namedNodes.Select(node => lines[node.Line].Length).Max();
                        // change to max = min + 1; for long names
                    }
    
                    foreach (Node node in currentLevel)
                        node.SetChildLines();
    
                    Binary[] binaries = namedNodes.OfType<Binary>().ToArray();
                    foreach (Binary binary in binaries)
                        GoDown(lines, binary.Line, binary.Right.Line);
    
                    currentLevel = currentLevel.SelectMany(node => node.Children).ToArray();
                    level++;
                }
    
                foreach (StringBuilder line in lines)
                    Console.WriteLine(line.ToString());
            }
    
            static void WriteAtPosition(StringBuilder line, string message, int position, char prepend = ' ')
            {
                if (line.Length > position)
                    throw new ArgumentException();
                line.Append(prepend, position - line.Length);
                line.Append(message);
            }
    
            static void GoDown(StringBuilder[] lines, int from, int to)
            {
                int line = from + 1;
                int position = lines[from].Length;
                for (; line <= to; line++, position++)
                    WriteAtPosition(lines[line], "\\", position);
            }
        }
    
        abstract class Node
        {
            public int Line
            { get; set; }
    
            public abstract int Leaves
            { get; }
    
            public abstract IEnumerable<Node> Children
            { get; }
    
            public virtual void SetChildLines()
            { }
        }
    
        abstract class NamedNode : Node
        {
            public string Name
            { get; set; }
    
            public string Write(int level)
            {
                return '[' + Name + level.ToString() + ']';
            }
        }
    
        class Binary : NamedNode
        {
            public Node Left
            { get; set; }
            public Node Right
            { get; set; }
    
            int? leaves;
            public override int Leaves
            {
                get
                {
                    if (leaves == null)
                        leaves = Left.Leaves + Right.Leaves;
                    return leaves.Value;
                }
            }
    
            public override IEnumerable<Node> Children
            {
                get
                {
                    yield return Left;
                    yield return Right;
                }
            }
    
            public override void SetChildLines()
            {
                Left.Line = Line;
                Right.Line = Line + Left.Leaves;
            }
        }
    
        class Unary : Node
        {
            public Node Child
            { get; set; }
    
            int? leaves;
            public override int Leaves
            {
                get
                {
                    if (leaves == null)
                        leaves = Child.Leaves;
                    return leaves.Value;
                }
            }
    
            public override IEnumerable<Node> Children
            {
                get
                {
                    yield return Child;
                }
            }
    
            public override void SetChildLines()
            {
                Child.Line = Line;
            }
        }
    
        class Leaf : NamedNode
        {
            public override int Leaves
            {
                get
                {
                    return 1;
                }
            }
    
            public override IEnumerable<Node> Children
            {
                get
                {
                    yield break;
                }
            }
        }
    }
    

    编辑:您的示例树的渲染与您的渲染完全相同:

    [a0]-----------[b3]------[c5]------[d8]
        \              \         \----------[e9]
         \              \----[f5]
          \-[g1]--------[h4]------[i6]
                \           \--------------------[j10]
                 \-[k3]
    

    【讨论】:

    • 你有给定树的输出吗?实际上,我一直打算安装 .NET 和 C# 编译器……现在就做……
    【解决方案5】:

    如果不是整个树的搜索,您可能需要执行深度优先搜索,以便正确调整其大小以沿 2 维输出。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多