花了两天时间才把这段程序敲出来、跑得起来、然后看懂,包括树的创建和树的打印。
原书内容:假设我们需要处理一个更一般化的问题:统计输入中的所有单词的出现次数。因为预先不知道单词出现的单词列表,所以无法方便地排序,并使用折半查找法,看它在前面是否已经出现,这样做,程序的执行将花费大量的时间。(更具体地说,程序的执行时间是与输入单词数目的二次方成比例的。)我们该如何组织这些数据,才能有效地处理一系列任意的单词呢?
一种解决方法是,在读取输入中任意单词的同时,就讲它放到正确的位置,从而始终保证所有的单词是按顺序排列的。虽然这可以不用通过线性数组中移动单词来实现,但它仍然会导致程序执行时间过长。我们可以使用一种称为二叉树的数据结构来取而代之。
每个不同的单词在树中都是一个节点,每个节点包含:
l 一个指向该单词的指针
l 一个统计出现次数的计数值
l 一个指向左子树的指针
l 一个指向右子树的指针
任何节点最多拥有两个子树,也可能只有一个子树或一个都没有。
对节点的所有操作要保证,任何节点的左子树只包含那些字典序小于该节点中单词的那些单词,右子树只包含字典序大于该节点中单词的那些单词。(也就是左子树<节点<右子树……)
要查找一个新单词是否已经在树中,可以从根节点开始,比较新单词与该节点中的单词。若匹配,计数值加1。若新单词小于该节点中的单词,则在左子树中继续查找,否则在右子树中查找。若搜寻方向上无子树,则说明新单词不在树中,并且,当前的空位置就是存放新加入单词的正确位置。因为从任意节点出发的查找都要按照同样的方式查找它的一个子树,所以该过程是递归的。相应的,在插入和打印操作中使用递归过程也是很自然的。
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#define MAXWORD 100
#define BUFSIZE 100 //缓冲区大小
char buf[BUFSIZE]; //缓冲区
int bufp = 0;
struct tnode{
char *word;
int count;
struct tnode *left;
struct tnode *right;
};
struct tnode *addtree(struct tnode *, char *);
void treeprint(struct tnode *);
int getword(char *, int);
struct tnode *talloc(void);
char *strdup(char *);
int getch(void);
void ungetch(int c);
//单词出现的频率统计
int main()
{
struct tnode *root;
char word[MAXWORD];
root = NULL;
while (getword(word, MAXWORD) != EOF)
if (isalpha(word[0]))
root = addtree(root, word);
treeprint(root);
return 0;
}
//addtree函数:在p的位置或p的下方增加一个w节点
struct tnode *addtree(struct tnode *p, char *w)
{
int cond;
if (p == NULL){
p = talloc();
p->word = strdup(w);
p->count = 1;
p->left = p->right = NULL;
} else if ((cond = strcmp(w, p->word)) == 0)
p->count++;
else if (cond < 0)
p->left = addtree(p->left, w);
else
p->right = addtree(p->right, w);
return p;
}
//treeprint函数:按序打印树p
void treeprint(struct tnode *p)
{
if (p != NULL){
treeprint(p->left);
printf("%4d %s\n",p->count, p->word);
treeprint(p->right);
}
}
//talloc函数:创建一个tnode
struct tnode *talloc(void)
{
return (struct tnode *) malloc(sizeof(struct tnode));
}
//strdup函数:把实参传入的字符串保存到某个安全位置
char *strdup(char *s)
{
char *p;
p = (char *)malloc(strlen(s)+1);//加1操作为了在结尾加上字符'\0'
if (p)
strcpy(p, s);
return p;
}
//getword函数:从输入中读取下一个单词或字符
int getword(char *word, int lim)
{
int c;
char *w = word;
while (isspace(c = getch()))
;
if (c != EOF)
*w++ = c;
if (!isalpha(c)){
*w = '\0';
return c;
}
for ( ; --lim > 0; w++)
if (!isalnum(*w = getch())){
ungetch(*w);
break;
}
*w = '\0';
return word[0];
}
//如果如果缓冲区中有内容,就从缓冲区中读,否则从输入流中读
int getch(void)
{
return (bufp > 0) ? buf[--bufp] : getchar();
}
//将一个字符压入缓冲区
void ungetch(int c)
{
if (bufp >= BUFSIZE)
printf("ungetch: too many characters\n");
else
buf[bufp++] = c;
}
在递归打印树的时候,先打印左子树,再打印节点,最后打印右子树,如此循环。如图例,打印顺序如下1->7。
需要注意的是,如果单词不是按照随机顺序到达的,树将会变得不平衡,这种情况下,程序的运行时间将大大增加。最坏的情况下,若单词已排好序,则程序模拟线性查找的开销非常大。