Довольно распечатать дерево

Скажем, у меня есть структура данных двоичного дерева, определенная следующим образом

type 'a tree =
    | Node of 'a tree * 'a * 'a tree
    | Nil

У меня есть пример такого дерева:

let x =
  Node
    (Node (Node (Nil,35,Node (Nil,40,Nil)),48,Node (Nil,52,Node (Nil,53,Nil))),
     80,Node (Node (Nil,82,Node (Nil,83,Nil)),92,Node (Nil,98,Nil)))

Я пытаюсь красиво распечатать дерево, чтобы его было легко интерпретировать. Желательно напечатать дерево в окне консоли следующим образом:

        _______ 80 _______
       /                  \
    _ 48 _              _ 92 _
   /      \            /      \
 35       52         82       98
   \       \        /
    40      53    83

Как проще всего вывести мое дерево в этом формате?


person Juliet    schedule 14.11.2009    source источник
comment
Ого, кажется тривиальным, но, вероятно, очень сложным. Верхушка дерева должна знать, сколько потомков, чтобы раздвинуть 1-й набор ветвей достаточно широко, и так далее. Логический метод сейчас не у меня в голове - работать снизу вверх.   -  person o.k.w    schedule 14.11.2009
comment
@ o.k.w: на самом деле довольно легко отобразить дерево, чтобы каждый узел содержал информацию о высоте: pastebin.com/f1dd58c58   -  person Juliet    schedule 14.11.2009
comment
Если бы вы разместили это как код для гольфа, вы бы получили 20 реализаций на разных языках.   -  person Sam152    schedule 14.11.2009
comment
Эта статья кажется хорошей llimllib.github.com/pymag-trees   -  person Isaac Kleinman    schedule 13.01.2012


Ответы (5)


Если вы хотите, чтобы он был очень красивым, вы можете украсть около 25 строк кода из эту запись в блоге, чтобы нарисовать ее с помощью WPF.

Но, наверное, я скоро напишу и решение ascii.

РЕДАКТИРОВАТЬ

Хорошо, вау, это было сложно.

Я не уверен, что это полностью правильно, и я не могу не думать, что, вероятно, есть лучшая абстракция. Но в любом случае ... наслаждайтесь!

(См. В конце кода большой довольно красивый пример.)

type 'a tree =    
    | Node of 'a tree * 'a * 'a tree
    | Nil

(*
For any given tree
     ddd
     / \
   lll rrr  
we think about it as these three sections, left|middle|right (L|M|R):
     d | d | d
     / |   | \
   lll |   | rrr  
M is always exactly one character.
L will be as wide as either (d's width / 2) or L's width, whichever is more (and always at least one)
R will be as wide as either ((d's width - 1) / 2) or R's width, whichever is more (and always at least one)
     (above two lines mean 'dddd' of even length is slightly off-center left)
We want the '/' to appear directly above the rightmost character of the direct left child.
We want the '\' to appear directly above the leftmost character of the direct right child.
If the width of 'ddd' is not long enough to reach within 1 character of the slashes, we widen 'ddd' with
    underscore characters on that side until it is wide enough.
*)

// PrettyAndWidthInfo : 'a tree -> string[] * int * int * int
// strings are all the same width (space padded if needed)
// first int is that total width
// second int is the column the root node starts in
// third int is the column the root node ends in
// (assumes d.ToString() never returns empty string)
let rec PrettyAndWidthInfo t =
    match t with
    | Nil -> 
        [], 0, 0, 0
    | Node(Nil,d,Nil) -> 
        let s = d.ToString()
        [s], s.Length, 0, s.Length-1
    | Node(l,d,r) ->
        // compute info for string of this node's data
        let s = d.ToString()
        let sw = s.Length
        let swl = sw/2
        let swr = (sw-1)/2
        assert(swl+1+swr = sw)  
        // recurse
        let lp,lw,_,lc = PrettyAndWidthInfo l
        let rp,rw,rc,_ = PrettyAndWidthInfo r
        // account for absent subtrees
        let lw,lb = if lw=0 then 1," " else lw,"/"
        let rw,rb = if rw=0 then 1," " else rw,"\\"
        // compute full width of this tree
        let totalLeftWidth = (max (max lw swl) 1)
        let totalRightWidth = (max (max rw swr) 1)
        let w = totalLeftWidth + 1 + totalRightWidth
(*
A suggestive example:
     dddd | d | dddd__
        / |   |       \
      lll |   |       rr
          |   |      ...
          |   | rrrrrrrrrrr
     ----       ----           swl, swr (left/right string width (of this node) before any padding)
      ---       -----------    lw, rw   (left/right width (of subtree) before any padding)
     ----                      totalLeftWidth
                -----------    totalRightWidth
     ----   -   -----------    w (total width)
*)
        // get right column info that accounts for left side
        let rc2 = totalLeftWidth + 1 + rc
        // make left and right tree same height        
        let lp = if lp.Length < rp.Length then lp @ List.init (rp.Length-lp.Length) (fun _ -> "") else lp
        let rp = if rp.Length < lp.Length then rp @ List.init (lp.Length-rp.Length) (fun _ -> "") else rp
        // widen left and right trees if necessary (in case parent node is wider, and also to fix the 'added height')
        let lp = lp |> List.map (fun s -> if s.Length < totalLeftWidth then (nSpaces (totalLeftWidth - s.Length)) + s else s)
        let rp = rp |> List.map (fun s -> if s.Length < totalRightWidth then s + (nSpaces (totalRightWidth - s.Length)) else s)
        // first part of line1
        let line1 =
            if swl < lw - lc - 1 then
                (nSpaces (lc + 1)) + (nBars (lw - lc - swl)) + s
            else
                (nSpaces (totalLeftWidth - swl)) + s
        // line1 right bars
        let line1 =
            if rc2 > line1.Length then
                line1 + (nBars (rc2 - line1.Length))
            else
                line1
        // line1 right padding
        let line1 = line1 + (nSpaces (w - line1.Length))
        // first part of line2
        let line2 = (nSpaces (totalLeftWidth - lw + lc)) + lb 
        // pad rest of left half
        let line2 = line2 + (nSpaces (totalLeftWidth - line2.Length))
        // add right content
        let line2 = line2 + " " + (nSpaces rc) + rb
        // add right padding
        let line2 = line2 + (nSpaces (w - line2.Length))
        let resultLines = line1 :: line2 :: ((lp,rp) ||> List.map2 (fun l r -> l + " " + r))
        for x in resultLines do
            assert(x.Length = w)
        resultLines, w, lw-swl, totalLeftWidth+1+swr
and nSpaces n = 
    String.replicate n " "
and nBars n = 
    String.replicate n "_"

let PrettyPrint t =
    let sl,_,_,_ = PrettyAndWidthInfo t
    for s in sl do
        printfn "%s" s

let y = Node(Node (Node (Nil,35,Node (Node(Nil,1,Nil),88888888,Nil)),48,Node (Nil,777777777,Node (Nil,53,Nil))),     
             80,Node (Node (Nil,82,Node (Nil,83,Nil)),1111111111,Node (Nil,98,Nil)))
let z = Node(y,55555,y)
let x = Node(z,4444,y)

PrettyPrint x
(*
                                   ___________________________4444_________________
                                  /                                                \
                      ________55555________________                         ________80
                     /                             \                       /         \
            ________80                      ________80             _______48         1111111111
           /         \                     /         \            /        \            /  \
   _______48         1111111111    _______48         1111111111 35         777777777  82   98
  /        \            /  \      /        \            /  \      \             \       \
35         777777777  82   98   35         777777777  82   98     88888888      53      83
  \             \       \         \             \       \            /
  88888888      53      83        88888888      53      83           1
     /                               /
     1                               1
*)     
person Brian    schedule 14.11.2009
comment
+1: круто! Это определенно намного сложнее, чем кажется (и я уверен, что решение J не более 50 символов). Я посмотрю, смогу ли я придумать что-нибудь попроще :) - person Juliet; 14.11.2009

Если вы не против повернуть голову набок, вы можете сначала распечатать глубину дерева, от одного узла к строке, рекурсивно передавая глубину вниз по дереву и напечатав depth*N пробелов в строке перед узлом.

Вот код Lua:

tree={{{nil,35,{nil,40,nil}},48,{nil,52,{nil,53,nil}}},
      80,{{nil,82,{nil,83,nil}},92 {nil,98,nil}}}

function pptree (t,depth) 
  if t ~= nil
  then pptree(t[3], depth+1)
    print(string.format("%s%d",string.rep("  ",depth), t[2]))
    pptree(t[1], depth+1)
  end
end

Контрольная работа:

> pptree(tree,4)
        98
      92
          83
        82
    80
          53
        52
      48
          40
        35
> 
person Doug Currie    schedule 14.11.2009
comment
Эта схема проста и на практике вполне читаема. Для каждого узла дерева глубины n распечатайте тип узла (например, 80) с отступом n, затем распечатайте дочерние элементы (рекурсивно) с отступом глубины N + 1. - person Ira Baxter; 14.11.2009
comment
На самом деле я печатаю свои деревья боком, используя именно то, что вы описываете (здесь код: pastebin.com/f1a50baef), и это работает для небольших деревьев. Однако отчасти любопытство к алгоритму и отчасти предпочтение вертикальных деревьев заставили меня искать лучшее визуальное представление. - person Juliet; 14.11.2009

Может быть, это поможет: Рисование деревьев в ML

person desco    schedule 11.07.2010

Хотя это не совсем правильный результат, я нашел ответ на странице http://www.christiankissig.de/cms/files/ocaml99/problem67.ml:

(* A string representation of binary trees

Somebody represents binary trees as strings of the following type (see example opposite):

a(b(d,e),c(,f(g,)))

a) Write a Prolog predicate which generates this string representation, if the tree 
is given as usual (as nil or t(X,L,R) term). Then write a predicate which does this 
inverse; i.e. given the string representation, construct the tree in the usual form. 
Finally, combine the two predicates in a single predicate tree_string/2 which can be 
used in both directions.

b) Write the same predicate tree_string/2 using difference lists and a single 
predicate tree_dlist/2 which does the conversion between a tree and a difference 
list in both directions.

For simplicity, suppose the information in the nodes is a single letter and there are 
no spaces in the string. 
*)

type bin_tree = 
    Leaf of string
|   Node of string * bin_tree * bin_tree
;;

let rec tree_to_string t =
    match t with
            Leaf s -> s
    |       Node (s,tl,tr) -> 
                    String.concat "" 
                            [s;"(";tree_to_string tl;",";tree_to_string tr;")"]
;;
person Chip Uni    schedule 14.11.2009

Это интуиция, я уверен, что кому-то вроде Кнута пришла в голову идея, мне лень проверять.

Если вы посмотрите на свое дерево как на одномерную структуру, вы получите массив (или вектор) длины L. Это легко построить с помощью упорядоченного рекурсивного обхода дерева: влево, корень, вправо необходимо выполнить некоторые вычисления, чтобы заполнить пробелы когда дерево неуравновешено

2 измерение

                    _______ 80 _______
                   /                  \
                _ 48 _              _ 92 _
               /      \            /      \
             35       52         82       98
               \       \        /
                40      53    83
    
    

1 измерение:

             35 40 48   52 53 80 83 82    92    98   
           0 1  2  3  4  5  6  7  8  9 10 11 12 13 14 

Красивое напечатанное дерево можно построить с использованием этого массива (возможно, с чем-то рекурсивным), сначала используя значения в позиции L / 2, позиция X - это значение L / 2 * длина по умолчанию (здесь это 2 символа)

                              80
    
    then (L/2) - (L/4)  and  (L/2) + (L/4) 
    
                   48                    92
    then L/2-L/4-L/8, L/2-L/4+L/8, L/2+L/4-L/8 and L/2+L/4+L/8 
    
              35        52         82          98
    
    ...

Добавление красивых веток вызовет больше позиционной арифметики, но здесь это тривиально.

Вы можете объединить значения в строку вместо использования массива, объединение будет де-факто вычислить лучшую позицию X и позволит разный размер значения, делая более компактное дерево. В этом случае вам нужно будет подсчитать слова в строке, чтобы извлечь значения. Пример: для первого элемента используется L / 2-е слово строки вместо L / 2-го элемента массива. Позиция X в строке такая же в дереве.

N 35 40 48 N 52 53 80 83 82 N 92 N 98 N 
                   80
        48                    92
  35         52          82        98
     40         53    83                  
person mahn    schedule 13.01.2011