【发布时间】:2011-05-30 04:00:41
【问题描述】:
(第一次发帖,编程比较新,请耐心等待!)
我对打印格式化二叉树的高效通用算法(在 CLI 环境中)和 C 实现都感兴趣。这是我自己编写的一些代码(这是一个非常简化的原始版本,也是支持许多 BST 操作的更大程序的一部分,但它应该可以编译):
#include <stdbool.h> // C99, boolean type support
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define DATATYPE_IS_DOUBLE
#define NDEBUG // disable assertions
#include <assert.h>
#define WCHARBUF_LINES 20 // def: 20
#define WCHARBUF_COLMS 800 // def: 80 (using a huge number, like 500, is a good idea,
// in order to prevent a buffer overflow :)
#define RECOMMENDED_CONS_WIDTH 150
#define RECOMMENDED_CONS_WIDTHQ "150" // use the same value, quoted
/* Preprocessor directives depending on DATATYPE_IS_* : */
#if defined DATATYPE_IS_INT || defined DATATYPE_IS_LONG
#define DTYPE long int
#define DTYPE_STRING "INTEGER"
#define DTYPE_PRINTF "%*.*ld"
#undef DATATYPE_IS_CHAR
#elif defined DATATYPE_IS_FLOAT
#define DTYPE float
#define DTYPE_STRING "FLOAT"
#define DTYPE_PRINTF "%*.*f"
#undef DATATYPE_IS_CHAR
#elif defined DATATYPE_IS_DOUBLE
#define DTYPE double
#define DTYPE_STRING "DOUBLE"
#define DTYPE_PRINTF "%*.*lf"
#undef DATATYPE_IS_CHAR
#elif defined DATATYPE_IS_CHAR
#define DTYPE char
#define DTYPE_STRING "CHARACTER"
#define DTYPE_PRINTF "%*.*c" /* using the "precision" sub-specifier ( .* ) with a */
/* character will produce a harmless compiler warning */
#else
#error "DATATYPE_IS_* preprocessor directive undefined!"
#endif
typedef struct node_struct {
DTYPE data;
struct node_struct *left;
struct node_struct *right;
/* int height; // useful for AVL trees */
} node;
typedef struct {
node *root;
bool IsAVL; // useful for AVL trees
long size;
} tree;
static inline
DTYPE get_largest(node *n){
if (n == NULL)
return (DTYPE)0;
for(; n->right != NULL; n=n->right);
return n->data;
}
static
int subtreeheight(node *ST){
if (ST == NULL)
return -1;
int height_left = subtreeheight(ST->left);
int height_right = subtreeheight(ST->right);
return (height_left > height_right) ? (height_left + 1) : (height_right + 1);
}
void prettyprint_tree(tree *T){
if (T == NULL) // if T empty, abort
return;
#ifndef DATATYPE_IS_CHAR /* then DTYPE is a numeric type */
/* compute spaces, find width: */
int width, i, j;
DTYPE max = get_largest(T->root);
width = (max < 10) ? 1 :
(max < 100) ? 2 :
(max < 1000) ? 3 :
(max < 10000) ? 4 :
(max < 100000) ? 5 :
(max < 1000000) ? 6 :
(max < 10000000) ? 7 :
(max < 100000000) ? 8 :
(max < 1000000000) ? 9 : 10;
assert (max < 10000000000);
width += 2; // needed for prettier results
#if defined DATATYPE_IS_FLOAT || defined DATATYPE_IS_DOUBLE
width += 2; // because of the decimals! (1 decimal is printed by default...)
#endif // float or double
int spacesafter = width / 2;
int spacesbefore = spacesafter + 1;
//int spacesbefore = ceil(width / 2.0);
#else /* character input */
int i, j, width = 3, spacesbefore = 2, spacesafter = 1;
#endif // #ifndef DATATYPE_IS_CHAR
/* start wchar_t printing, using a 2D character array with swprintf() : */
struct columninfo{ // auxiliary structure
bool visited;
int col;
};
wchar_t wcharbuf[WCHARBUF_LINES][WCHARBUF_COLMS];
int line=0;
struct columninfo eachline[WCHARBUF_LINES];
for (i=0; i<WCHARBUF_LINES; ++i){ // initialization
for (j=0; j<WCHARBUF_COLMS; ++j)
wcharbuf[i][j] = (wchar_t)' ';
eachline[i].visited = false;
eachline[i].col = 0;
}
int height = subtreeheight(T->root);
void recur_swprintf(node *ST, int cur_line, const wchar_t *nullstr){ // nested function,
// GCC extension!
float offset = width * pow(2, height - cur_line);
++cur_line;
if (eachline[cur_line].visited == false) {
eachline[cur_line].col = (int) (offset / 2);
eachline[cur_line].visited = true;
}
else{
eachline[cur_line].col += (int) offset;
if (eachline[cur_line].col + width > WCHARBUF_COLMS)
swprintf(wcharbuf[cur_line], L" BUFFER OVERFLOW DETECTED! ");
}
if (ST == NULL){
swprintf(wcharbuf[cur_line] + eachline[cur_line].col, L"%*.*s", 0, width, nullstr);
if (cur_line <= height){
/* use spaces instead of the nullstr for all the "children" of a NULL node */
recur_swprintf(NULL, cur_line, L" ");
recur_swprintf(NULL, cur_line, L" ");
}
else
return;
}
else{
recur_swprintf(ST->left, cur_line, nullstr);
recur_swprintf(ST->right, cur_line, nullstr);
swprintf(wcharbuf[cur_line] + eachline[cur_line].col - 1, L"("DTYPE_PRINTF"",
spacesbefore, 1, ST->data);
//swprintf(wcharbuf[cur_line] + eachline[cur_line].col + spacesafter + 1, L")");
swprintf(wcharbuf[cur_line] + eachline[cur_line].col + spacesafter + 2, L")");
}
}
void call_recur(tree *tr){ // nested function, GCC extension! (wraps recur_swprintf())
recur_swprintf(tr->root, -1, L"NULL");
}
call_recur(T);
/* Omit empty columns: */
int omit_cols(void){ // nested function, GCC extension!
int col;
for (col=0; col<RECOMMENDED_CONS_WIDTH; ++col)
for (line=0; line <= height+1; ++line)
if (wcharbuf[line][col] != ' ' && wcharbuf[line][col] != '\0')
return col;
return 0;
}
/* Use fputwc to transfer the character array to the screen: */
j = omit_cols() - 2;
j = (j < 0) ? 0 : j;
for (line=0; line <= height+1; ++line){ // assumes RECOMMENDED_CONS_WIDTH console window!
fputwc('\n', stdout); // optional blanc line
for (i=j; i<j+RECOMMENDED_CONS_WIDTH && i<WCHARBUF_COLMS; ++i)
fputwc(wcharbuf[line][i], stdout);
fputwc('\n', stdout);
}
}
(也上传到a pastebin service,为了保留语法高亮)
它工作得很好,虽然自动宽度设置可能会更好。预处理器的魔法有点傻(甚至丑陋)并且与算法没有真正的关系,但它允许在树节点中使用各种数据类型(我认为这是一个使用预处理器进行一些实验的机会 - 记住,我是新手!)。
主程序应该调用
system("mode con:cols="RECOMMENDED_CONS_WIDTHQ"lines=2000");在调用 prettyprint_tree() 之前,在 cmd.exe 中运行时。
样本输出:
(106.0) (102.0) (109.0) (101.5) 空 (107.0) (115.0) NULL NULL (106.1) NULL (113.0) NULL 空 空 空 空理想情况下,输出应该是这样的(我使用 wprintf() 系列函数的原因是无论如何都能打印 Unicode 字符):
(107.0) ┌──────┴──────┐ (106.1) 空 ┌────┴────┐ 空空所以,我的问题:
- 您如何看待这段代码? (也非常欢迎编码风格的建议!)
- 能否以优雅的方式扩展它以包含画线字符? (很遗憾,我不这么认为。)
- C 或其他命令式语言(或命令式伪代码)中的任何其他算法?
- 有点不相关:您对嵌套函数(非便携式 GNU 扩展)有何看法?我认为这是编写函数的递归部分的一种优雅方式,而不必提供所有局部变量作为参数(也可用作实现隐藏技术),但这可能是我的 Pascal 过去 :-) 我感兴趣更有经验的程序员的意见。
提前感谢您的回复!
PS。问题不是与this one 重复。
编辑: Jonathan Leffler 写了一个 excellent answer 几天后很可能会成为“公认的答案”(除非有人发布同样棒的东西!)。由于篇幅限制,我决定在这里回复而不是评论。
- 上面的代码实际上是一个更大的“家庭作业”项目的一部分(在共享库中实现 BST 操作 + 使用该库的 CLI 应用程序)。然而,“prettyprint”功能不是要求的一部分;只是我决定自己添加的东西。
- 我还添加了一个“转换为不带旋转的 AVL”函数,它使用“arraystr”作为中间表示;-) 我忘记了这里没有使用它。我已经编辑了代码以将其删除。此外,
bool IsAVL结构成员还没有被使用。只是没有在这个特定的功能中使用。为了呈现上面引用的代码,我不得不从各种文件中复制/粘贴代码并进行大量更改。这是一个我不知道如何解决的问题。我很乐意发布整个程序,但它太大了,而且用我的母语(不是英语!)进行了评论。 - 整个项目大约有 1600 LOC(包括 cmets),具有多个构建目标(调试/发布/静态链接),并且它在启用
-Wall和-Wextra的情况下干净编译。根据构建目标自动启用/禁用断言和调试消息。我还认为嵌套函数不需要函数原型,毕竟嵌套函数不实现任何外部接口根据定义 - GCC 在这里当然没有抱怨。我不知道为什么 OSX 上有这么多警告 :( - 我在 Windows 7 上使用 GCC 4.4.1。
- 尽管在 Windows 上编写和测试了这个程序,但我实际上是一个 Linux 用户......不过,我无法忍受
vim并且我使用nano(在 GNU 屏幕内)或gedit代替(射击我)!无论如何,我更喜欢 K&R 大括号样式:) - 可移植性并不重要,对于 Linux 用户来说,GCC 几乎是事实上的...事实上它在 Windows 下也能很好地工作,这是一个很好的奖励。
- 我没有使用 VCS,也许我应该使用。我想试一试,但对于我的需求来说,它们似乎都太复杂了,我不知道如何选择一个:-)
- 您对深度溢出的检查绝对正确,幸好它很容易添加。
- 感谢
L' '的建议! - 我觉得您的建议(封装“整个绘图代码,以便屏幕图像和相关信息在一个结构中”)非常有趣...但我真的不明白你所说的“封装”是什么意思。您能否提供 3 或 4 行(伪)代码来显示可能的函数声明和/或可能的函数调用?
这是我的第一个“大型”(非平凡)程序,非常感谢您的建议。
编辑#2:
这是here提到的“快速而肮脏”方法的实现。
(编辑#3:我决定将其拆分为separate answer,因为它是对 OP 的有效答案。)
许多回复提到Graphviz。我已经知道它(许多 Linux 应用程序都与它相关联),但我认为对于 10KB 的 CLI 可执行文件来说这太过分了。不过,我会牢记在心,以备不时之需。看起来很棒。
【问题讨论】:
-
算法和/或例程的奖励积分 (1) 更短、更优雅且更易于理解,并且 (2) 不需要中间字符缓冲区(与我的不同)。
-
如果您不想要中间缓冲区,您必须知道如何扫描树并生成每一行输出:您需要处理哪些节点,或者您需要处理哪些行画。可以做到,但不容易。一个问题是树越深越宽。如果你的树是合理平衡的,它会更容易处理;不平衡的树木使生活更加艰难。您可能需要在树上进行两次遍历,一次是为了确定必要的大小,因为第一行上第一个节点的位置受树底部节点的影响,一次是为了绘制。
-
@Jonathan:是的,我认为使用缓冲区毕竟要好得多(也更灵活)。我只是希望有一个我不知道的更好/更简单的知名算法。
-
仔细看K&R;它们的功能以第 1 列中的大括号开头。但是,这是一个风格问题,而不是更严重的问题。
-
可惜这个不得不是CLI。否则,
graphviz只是 乞求 用于这种事情。
标签: c language-agnostic binary-tree pretty-print