Alex Rivera | Logout

Algorithm to Render a Horizontal Binary-ish Tree in Text/ASCII form

Asked 2010-06-16T20:35:06.573
9

It's a pretty normal binary tree, except for the fact that one of the nodes may be empty.

I'd like to find a way to output it in a horizontal way (that is, the root node is on the left and expands to the right).

I've had some experience expanding trees vertically (root node at the top, expanding downwards), but I'm not sure where to start, in this case.

Preferably, it would follow these couple of rules:

  • If a node has only one child, it can be skipped as redundant (an "end node", with no children, is always displayed)
  • All nodes of the same depth must be aligned vertically; all nodes must be to the right of all less-deep nodes and to the left of all deeper nodes.
  • Nodes have a string representation which includes their depth.
  • Each "end node" has its own unique line; that is, the number of lines is the number of end nodes in the tree, and when an end node is on a line, there may be nothing else on that line after that end node.
  • As a consequence of the last rule, the root node might be better off in either the top left or the bottom left corner; top left is preferred.

For example, this is a valid tree, with six end nodes (node is represented by a name, and its depth): EDIT: Please see bottom of question for an alternative, easier rendering

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

Which represents the vertical, explicit binary tree:

0              a
              / \
1            g   *
            / \   \
2          *   *   *
          /     \   \
3        k       *   b
                /   / \
4              h   *   *
              / \   \   \
5            *   *   f   c
            /     \     / \
6          *       i   *   *
          /           
Edit
Report

2 Answers

4

If there are N end nodes, there must be N-1 internal nodes with 2 children. (There can be any number of internal nodes with 1 child, which we will have to count to get the depths but otherwise ignore.) Generating the tree is thus equivalent to positioning these nodes on a grid, where:

  • the number of rows in the grid is N
  • I think the number of columns is between 1+floor(log2(N)) and 2*N-1, depending on how much overlap there is; this probably doesn't matter much for our purposes, though
  • each endpoint appears on a different row
  • all nodes at the same depth appear in the same column
  • all internal nodes appear on the same row as their rightmost descendant endpoint

So, let's see:

  • Walk the tree depth-first, right-to-left.
  • For each endpoint, record its depth and label.
  • For each 2-child internal, record its depth, label and the indices of both rightmost and leftmost child endpoints.
  • Sort the whole lot by depth -- this gives you the column ordering, with the number of distinct depths giving the actual number of columns. (All other ordering should come out automatically from the walk, I think, but that's not the case here because any branch can be any depth.)
  • Place all the nodes in the grid.
  • Mark empty cells to the right of each non-endpoint node as horizontal branches.
  • Mark empty cells down from each internal node to the row above its left child as vertical branches, and the cell at the level of the left child as a junction.

  • Print with appropriate ASCII decoration.

Update:

As you say, the positioning is enough to unambiguously determine the connections, but you still need to do some bottom-up work to get that right, so I'd probably still do the "mark" steps during the grid building.

I sort of thought t

answered 2010-06-16T23:21:58.227
1

Below is fully functional C# code that does exactly what you want. How it does it:

  • The tree is represented as objects from classes that inherit from Node
  • First compute the number of leaves and create an array of that much lines
  • Then for each level:
    • find out on what lines are we going to write
    • for those lines, compute the maximum of what is already on those lines
    • write the all the nodes to column max(number from previous step, end of previous level)+1; prepend with - to get to that column
    • write diagonal lines from all binary nodes up to the line of their right child (in my program first child is left, second is right, you have it the other way around)
    • advance one level

The algorithm makes sure that each level starts only after previous ends. That is probably good choice for short names, but for longer names, this probably shouldn't be enforced.

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(lev
answered 2010-06-17T03:49:55.923

Your Answer