统计所有单词出现的次数:二叉数

来源:互联网 发布:js有几种数据类型 编辑:程序博客网 时间:2024/05/16 08:25

缘由

这是 K&R书中121页的一个例子,它本身使用来说明的C语言的自引用结构的,但是我认为其涉及一个更为重要的问题:二叉数
故想以此将二叉数的内容也一并总结了吧。

分析问题

统计所有单词出现的次数的背景是:给出一篇文章,统计所有单词的数量。
这表示事先并不知道这些有哪些单词,便无法排序,更不要说使用查找了。
  • 书中指出:也不能分别对输入中的每个单词都执行一次线性查找,看其是否已经出现过。(程序执行的时间将会与输入单词的数量的二次方成比例)
说实话,我对这句话不是特别明白,或者认同,怎么会是二次方成正比呢?当然,确实不能使用一次线性查找。对此问题,我的分析如下:
  • 折半查找的话,必须要将排序放入一个数组内(因为需要对下标进行计算),那样的话,新来的单词,即使我们找到它该存在的位置而没有它,我们打算将其插入数组,由会涉及数组元素的挪动,说到挪动,肯定是又要增加时间
  • 顺序查找:那顺序查找为什么不可以呢?顺序查找可以用使用链表了了吧,我们只要在查找时找到这个新单词该出现的位置,没有找到它的话,那么我们再插入它,对于列表来说插入一个单词是非常方便的。当然,即使如此,查找的时间复杂度为O(n)
  • 二叉数:如果使用二叉数来完成这个问题,我们将会得到更好的时间复杂度O(lgn),n为节点个数。也可以说,所花费的时间将会和树的高度成正比。二叉数的所有操作的时间复杂度都是这么多,包括插入、删除、建立、查找(来自:算法导论 第三版 第12章)。所以,对于一个单词而言,如果已经存放在了树中,那么就是查找,将新单词放入的就是插入。然而,实际上这两个动作应该是结合起来的,因为在其该在的位置找不到的时候,我们已经得到新单词该在的位置,所以直接插入即可。具体请看下面代码的实现。
使用二叉数,我们以字典顺序排序,为了更好的理解,我们构建的二叉数的节点大概是这样的:
对于这样一句话:now is the time for all good men to come to the aid of their party


针对问题的数据结构

二叉数的数据结构中基本的有三个:
  1. 自身数据
  2. 左孩子
  3. 右孩子
我们该题中,自身数据就是单词,而且还需要多一个统计单词次数的数值。
code如下:
struct tnode {char *word;int count;struct tnode *left;struct tnode *right;};

面对过程的流程以及代码

作为一般讲c语言的书,很显然是会用面向过程的思维方式来完成的,由下面的main函数可以具体的流程:
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;}
main函数中的addtree(root, word)包含了更多的流程,我们来看看addtree函数:
struct tnode *addtree(struct tnode *p, char *w){//判断是否是新节点if (p == NULL) {/* a new word has arrived */p = talloc();/* make a new node */p->word = strdup1(w);//复制w的过程,如单纯的赋值,就会使p->word和w指向了一处。p->count = 1;p->left = p->right = NULL;return p;}//不是新节点int cond;cond = strcmp(w, p->word);if (cond== 0){p->count++;/* repeated word */}else if (cond < 0){//就到左子树去找p->left = addtree(p->left, w);}else{//就到右子树去找p->right = addtree(p->right, w);}return p;}
通过上述代码,我相信不仅对本题的流程有了基本的认识,并且也对二叉树的查找和插入有了一定的了解。实际上,而二叉数建立的过程就是查找和插入的过程。

其中的两个函数的代码:
/* talloc: make a tnode *//** * 为新节点分配内存 */struct tnode *talloc(void){return (struct tnode *) malloc(sizeof(struct tnode));}/** * 完成对单词的复制,避免不同指针指向同一个单词,对单词产生干扰 */char *strdup1(char *s){char *p;/* make a duplicate of s */p = (char *) malloc(strlen(s)+1); /* +1 for ’\0’ */if (p != NULL)strcpy(p, s);return p;}

读入单词

/* getword: get next word or character from input *//** * 没有调用高级的库函数使得这段代码略显复杂 * 但是这些基本的方式能够让我更深刻的体会了一些编程的高级技巧 */int getword(char *word, int lim){int c, getch(void);void ungetch(int);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];}#define BUFSIZE 100char buf[BUFSIZE];int bufp = 0;/* buffer for ungetch *//* next free position in buf */int getch(void) /* get a (possibly pushed-back) character */{return (bufp > 0) ? buf[--bufp] : getchar();}void ungetch(int c)/* push character back on input */{if (bufp >= BUFSIZE)printf("ungetch: too many characters\n");elsebuf[bufp++] = c;}

打印结果

都快忘了
  • 先序遍历:先根,再左,后右
  • 中序遍历:先左,再根,后右
  • 后序遍历:先左,再右,后中
/* treeprint: in-order print of tree p *//** * 递归打印,采用了中序遍历。 */void treeprint(struct tnode *p){if (p != NULL) {treeprint(p->left);printf("%4d %s\n", p->count, p->word);treeprint(p->right);}}

打印实例

就拿我们编写的代码作一个测试,结果如下,不能给中文做这样测试,因为中文里面的字都没有空格
zy@zy:~/Documents/eclipseWorkSpace/test/src$ ./a.out    1 EOF   3 MAXWORD   1 NULL   2 addtree   4 char   1 count   1 ctype   1 define   2 getword   4 h   1 if   4 include   4 int   1 isalpha   1 left   1 main   1 return   1 right   5 root   1 stdio   1 stdlib   1 string   7 struct   7 tnode   2 treeprint   1 void   1 while   5 wordzy@zy:~/Documents/eclipseWorkSpace/test/src$ 

源代码

#include <stdio.h>#include <ctype.h>#include <string.h>#include <stdlib.h>#define MAXWORD 100struct tnode *addtree(struct tnode *, char *);void treeprint(struct tnode *);int getword(char *, int);struct tnode {char *word;int count;struct tnode *left;struct tnode *right;};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;}struct tnode *talloc(void);char *strdup1(char *s);struct tnode *addtree(struct tnode *p, char *w){//判断是否是新节点if (p == NULL) {/* a new word has arrived */p = talloc();/* make a new node */p->word = strdup1(w);//复制w的过程,如单纯的赋值,就会使p->word和w指向了一处。p->count = 1;p->left = p->right = NULL;return p;}//不是新节点int cond;cond = strcmp(w, p->word);if (cond== 0){p->count++;/* repeated word */}else if (cond < 0){//就到左子树去找p->left = addtree(p->left, w);}else{//就到右子树去找p->right = addtree(p->right, w);}return p;}/* treeprint: in-order print of tree p *//** * 递归打印,采用了中序遍历。 */void treeprint(struct tnode *p){if (p != NULL) {treeprint(p->left);printf("%4d %s\n", p->count, p->word);treeprint(p->right);}}/* talloc: make a tnode *//** * 为新节点分配内存 */struct tnode *talloc(void){return (struct tnode *) malloc(sizeof(struct tnode));}/** * 完成对单词的复制,避免不同指针指向同一个单词,对单词产生干扰 */char *strdup1(char *s){char *p;/* make a duplicate of s */p = (char *) malloc(strlen(s)+1); /* +1 for ’\0’ */if (p != NULL)strcpy(p, s);return p;}/* getword: get next word or character from input *//** * 没有调用高级的库函数使得这段代码略显复杂 * 但是这些基本的方式能够让我更深刻的体会了一些编程的高级技巧 */int getword(char *word, int lim){int c, getch(void);void ungetch(int);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];}#define BUFSIZE 100char buf[BUFSIZE];int bufp = 0;/* buffer for ungetch *//* next free position in buf */int getch(void) /* get a (possibly pushed-back) character */{return (bufp > 0) ? buf[--bufp] : getchar();}void ungetch(int c)/* push character back on input */{if (bufp >= BUFSIZE)printf("ungetch: too many characters\n");elsebuf[bufp++] = c;}


二叉数的删除问题

今天为此我又顺带复习了一下二叉数的一些问题,二叉树的几个基本的操作部分,我认为比较难的就是删除。
想不到算法导论还将其描述的有点复杂,所以我主要看了我考研时候一本书,个人觉得非常好:2014版数据结构高分笔记(第2版),里面对此的描述比较清楚。

其中最复杂的就是情况(D):
假如要删除z,那么我们就先找到与z数值上最接近的节点,也就是从其右孩子一直往左走,就是上图中的y,然后将y与z对换,再删除对换后的z即可,x很容易判断该放在r的哪里。(2014年6月10日:复习时,发现上面的说法和图中的画法不符合,图中的应该是实际上应该让Y成为r的父节点,然后再删除Z节点,再让Z子节点的左孩子成为Y的左孩子,Z节点的父节点成为Y的父节点即可

关于二叉树更深入的内容

  1. 虽然二叉树的各项操作的时间复杂度为O(lgN),但是在最坏的情况下还是O(n),那么可以进一步了解红黑树,它保证了在最坏的情况下的时间复杂是O(lgN)
  2. 此外还有B树,适用于二级磁盘储存器上的数据库维护

关于本题进一步扩展

刚买了unix环境高级编程,想看的慌,这三道题就留着以后做把

Exercise 6-2. Write a program that reads a C program and prints in alphabetical order each group of variable names that are identical in the first 6 characters, but different somewhere thereafter. Don’t count words within strings and comments. Make 6 a parameter that can be set from the command line.

Exercise 6-3. Write a cross-referencer that prints a list of all words in a document, and for each word, a list of the line numbers on which it occurs. Remove noise words like ‘‘the,’’ ‘‘and,’’ and so on.

Exercise 6-4. Write a program that prints the distinct words in its input sorted into decreasing order of frequency of occurrence. Precede each word by its count.
0 0