C-单词统计 2.2

emmmmm.......

详解:

问题:


对读入的某个文本文件input.txt中,拆出英文单词,输出一个按字典顺序排列的单词表,结果输出在文本文件output.txt中,每个单词一行,并在单词后输出该单词出现的个数,两个字段之间用逗号分隔。约定单词仅由英文字母组成,单词间由非英文字母分隔,相同单词只输出一个,大小写不区分。
例如文本文件input.txt为:
Hello world.
Hello every one.
Let us go.
则输出文本文件output.txt为:
every,1
go,1
hello,2
let,1
one,1
us,1
world,1


给的结构体
struct myword
{
    int     num;
    char    *word;
    struct  myword  *next;    /*  指向下一个节点 */
};

这个问题拿到了之后,可以发现程序涉及两个文件一个input.txt,这个在程序里用的test.txt进行测试,输出文件out.txt。

我们从文件input文件中读单词,然后统计所有的单词个数,输出到output。

所以整个程序的流程可以划分为:

main()

1.打开文件input.txt
2.找单词,并将其存入链表 struct myword *find_words(FILE *input)
3.将链表输出到文件 void print_link_to_file(struct myword *head)
4.释放链表所占的空间 void destory_word(struct myword *head)

我们可以看下这个main()函数

void  main()
{
    /*读取infile_name文件,用find_words找到单词,存储到head链表,最后将链表输出到文件*/ 
    void print_link_to_file(struct myword *head);     //输出链表到文件 
    struct myword *find_words(FILE *input);           //找单词,输出单词链表 
    void destory_word(struct myword *head);         //释放链表内存

    char  infile_name[]={"word.txt"};
    struct myword *head;
    FILE *input;
    input = fopen(infile_name, "r");                  //打开文件 
    if (input == NULL)
    {
        printf("打开文件%s失败\n",infile_name);
        exit(0);
    }

    head = find_words(input);                         //找单词 
    fclose(input);                                    //关闭文件 
    print_link_to_file(head);                         //将链表输出到文件 
}

程序的主要功能有三个,对应上面的找单词并将其存入链表输出到文件释放链表空间

定义的是返回空类型的void main()函数,前三行是声明我们的三个函数。最后三行是使用。
具体实现我们到后面说:

  • find_words(FILE *input) 是一个输入文件指针类型的函数,输出的结构体指针
  • print_link_to_file(struct myword *head) 输入的是结构体指针,这里我们输入的是链表的头结点,空类型函数
  • destory_word(struct myword *head) 输入结构体指针,也是链表头节点,释放链表所占的空间
    其中最复杂的就是find_words()

我们倒着来,从简单的开始:

destory_word(struct myword *head) //释放内存

我们在使用完链表之后,程序结束了,但是这些链表还是在内存中,所以我们在用完了之后要将内存释放掉。方法很简单,遍历整个链表,将每一个节点的空间都是释放掉。每一个节点都是一个myword结构体。

void destory_word(struct myword *head)
{
    printf("start free!\n");
    struct myword *tmp;
    while(head!=NULL)
    {
        tmp = head;
        head  = head->next;
        free(tmp);
    }
    printf("Free Done!\n");
} 

遍历整个链表,用tmp保存当前头结点,然后,头结点指向下一个节点,然后释放tmp的内存,继续遍历下一个。
然后是输出到链表。

void print_link_to_file(struct myword *head)

这是个空类型函数,输入链表头,和刚才一样遍历链表,不过把释放空间的操作换成输出到文件。这里我们打开一个output.txt文件来保存文件。

void print_link_to_file(struct myword *head)
{
    /*输入链表,将链表内容输出到input.txt*/ 
    printf("NOW,print LINK  to File......\n");
    struct myword *p;
    FILE  *output;
    char outfile_name[]={"output.txt"};
    output = fopen(outfile_name, "w");
    if (output == NULL)
    {
        printf("打开文件%s失败\n",outfile_name);
        exit(0);
    }
    p = head->next;
    do
    {
        fprintf(output,"%s,%d\n",p->word,p->num);
        p = p->next;
    }while(p!=NULL);
    fclose(output);
    printf("print to file Done!\n");
 }

这里也是非常简单的,不过这里我用的do...while语句,这是书上看来的,也可以改写成while语句。这里不多说,下面开始找单词环节。

find_words(FILE *input)

在开始之前,我们先想一想如何找单词?根据题目来看,两个单词之间是用的非字符分割的,所以这里我们把类似于“point-to-point”这种看做三个单词而不是一个,降低难度。
我们读文件有三种,每次读一个字符,每次读一串字符,还有就是随机读写。
我在开始写之前想过好几种方案,还有的是室友想的,我一股脑先都放出来:

  • 每次读一行,将它存到数组中,然后判断数组中的单词数,当然也是遍历数组,遇到非字母就分隔开,将前面的看做一个单词。生成节点,插入链表,然后接着读文件。
  • 每次读一个字符,如果遇到字母,就将其存到数组中,直到遇到非字母,然后把数组中的字符串看做一个单词。生成节点,插入链表,然后接着读文件。
  • 每次读一个文件,如果是字母则写入文件tmp.txt中,如果遇到非字母,或者换行,则在tmp.txt中写入换行。完成所有的读写后,在重新开始读tmp.txt,每次读一行,把每一行的单词读出,生成节点,插入链表,然后接着读文件。

前两个我写的,第三个是室友想的。emmm。。。这个文件一定长。
先不管效率,三种理论上都能读出单词。
我是用的第二种方法:

struct myword *find_words(FILE *input)
{
    /*输入文件指针,找到单词,生成链表,输出链表头head,
    从文件中一个字符一个字符的读取,如果遇到字母则将其存入
    字符数组中,直到遇到非字母, 
    【在数组的后面加入'\0',以此来分割数组中的
    单词,因为前一个单词可能比当前单词长】 
    则数组中的就是一个单词,
    生成节点,并插入相应位置。 
    用字符数组保存当前单词*/ 
    printf("start find Words....\n");
    struct myword *head, *tmp;
    struct myword *instert_word(struct myword *tmp,struct myword *head);  //插入节点
    struct myword *new_word(char word[]);  //生成一个新的节点,返回指针 
    char ch;
    char word[25];
    int i=0;
    head = NULL;
    
    while (! feof(input))
    {
        ch = fgetc(input);
        if ((ch>=65 && ch <=90) || (ch>=97 && ch <=122))
        {
            word[i]=ch;
            i++;
        }
        else
        {
            word[i]='\0';
            if (strlen(word) !=0)
            {
                tmp = new_word(word);
                head = instert_word(tmp,head);
            }   
            i=0;
        }
    }
    printf("find Words Done!\n");
    return head;
}

我用了循环来读取文件,这里有个点需要注意,每次循环结束,每当我读到字母时,是将字母存入第i个位置,然后i++;如果是非字母则将当前数组的第i个位置赋值为"\0",当前面出现了单词,也就是strlen(word) !=0的情况,生成新的单词节点,并插入链表,然后不管有没有单词都将i=0。可以发现我并没有将数组的内容清空。只是每次将第i个位置复制为"\0",因为strlen(word)在统计字符串长度时,遇到"\0"就会停止,如果第一个位置就是"\0",则认为没有单词。
这是很重要的一点。

这里我们找出了单词,但是我们还没有链表,也没法统计单词的个数。
接下就是一个小的辅助函数,

new_word(word)

这个函数接受一个字符串数组,并返回一个指向对应的结构体的指针。

struct myword * new_word(char word[]) 
 {
    /*输入一个单词数组,返回一个myword结构体*/
    struct myword *tmp;
    char *t_word;
    tmp=(struct myword *) malloc(LEN);                //给一个新的节点申请空间 
    //动态申请一个空间来保存myword->word,因为这本身是个指针,需要指向一个数组来保存
    t_word = (char *) malloc(strlen(word));
    strcpy(t_word,strlwr(word));
    tmp->word = t_word;
    tmp->num = 1;
    tmp->next=NULL;
    return tmp;
 }

这里需要注意的是,给的结构体并没有给存放单词的字符串数组,而是给了一个字符型指针。所以我们要为每一个节点分配空间的同时,还要给每一个节点的单词分配空间。还有就是,字符串赋值 的时候要用strcpy()函数。
然后就是重头戏插入链表

instert_word(tmp,head)

这个函数输入链表的头结点和要插入的节点,返回一个指向链表头节点的指针。
题目的要求还要统计单词的个数,而且要排序。一开始我是这么想的:先生成一个没有排序的链表,然后在用排序算法,对链表进行排序,后来一想,为啥不在插入的时候就按顺序插入呢!
于是有了下面的:

struct myword *instert_word(struct myword *tmp,struct myword *head)
 {
    /*将新的tmp节点插入到head对应的位置
    ->遍历所有节点 
     # head空             放到第一个节点
     # 与当前节点相同     当前节点的num+1
     # 比当前节点大       如果比下一个节点小,则插在当前节点后
                          如果比下一个节点大 ,则遍历下一个节点 
     # 比第一个节点小     放在第一个节点前,头结点后
                        【因为链表是有序的,且在上一情况中处理,
                         在后面不存在比当前节点小的情况】 
     */ 
    struct myword *cur;
    if (head==NULL)
    {
        head = (struct myword *) malloc(LEN);
        head->next = tmp;
    }
    else
    {
        cur = head->next;
        while (cur!=NULL)
        {
            if (strcmp(tmp->word,cur->word)<0)
            {
                //小于第一个值 ,因为链表有序,如果比第一个小,那就是最小的 
                tmp->next = cur;
                head->next = tmp;
                break;
            }
            if (strcmp(tmp->word,cur->word)==0)
            {
                //等于当前值 
                cur->num++;
                free(tmp);                        //这个节点已经存在,释放tmp空间 
                break;
            }
            else if (strcmp(tmp->word,cur->word)>0)
            {   
                //大于当前值 
                if (cur->next!=NULL)// && (tmp->word,cur->next->word)<0
                {
                    //后面不为空 
                    if (strcmp(tmp->word,cur->next->word)<0)
                    {
                        //小于下一个值 ,插入当前节点之后 
                        tmp->next = cur->next;
                        cur->next = tmp;
                        break; 
                    }
                    //大于等于下一个值 
                    cur = cur->next;
                }
                else
                {
                    //后面为空 
                    cur->next= tmp; 
                    break; 
                }
            } 
        }
    }
    return head;
 }

值得引起注意的就是,第一个节点的插入。剩下的就是插入节点的操作了。在比较字符串大小的时候不能像字符或者数字一样直接比较,要用strcmp()。
这个函数只可意会不可言传!自己理解吧!

最后

我们先打开了文件,然后初始化链表头head指针,然后调用find_words()函数,返回统计好的单词链表。这是将返回的结果赋给head,这时head就是含有所有单词的链表了。然后调用我们的 print_link_to_file(head)函数,将文件输出到文件中,最后销毁链表。

最新的程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define LEN sizeof(struct myword)

struct myword
{
    int     num;
    char    *word;
    struct  myword  *next;    /*  指向下一个节点 */
};


void  main()
{
    /*读取infile_name文件,用find_words找到单词,存储到head链表,最后将链表输出到文件*/ 
    void print_link_to_file(struct myword *head);     //输出链表到文件 
    struct myword *find_words(FILE *input);           //找单词,输出单词链表 
    void destory_word(struct myword *head);
    char  infile_name[]={"test.txt"};
    struct myword *head;
    FILE *input;
    input = fopen(infile_name, "r");                  //打开文件 
    if (input == NULL)
    {
        printf("打开文件%s失败\n",infile_name);
        exit(0);
    }
    head = find_words(input);                         //找单词 
    fclose(input);                                    //关闭文件 
    print_link_to_file(head);                         //将链表输出到文件 
    destory_word(head);
}

void print_link_to_file(struct myword *head)
{
    /*输入链表,将链表内容输出到input.txt*/ 
    printf("NOW,print LINK  to File......\n");
    struct myword *p;
    FILE  *output;
    char outfile_name[]={"output.txt"};
    output = fopen(outfile_name, "w");
    if (output == NULL)
    {
        printf("打开文件%s失败\n",outfile_name);
        exit(0);
    }
    p = head->next;
    do
    {
        fprintf(output,"%s,%d\n",p->word,p->num);
        p = p->next;
    }while(p!=NULL);
    fclose(output);
    printf("print to file Done!\n");
 }
 
struct myword * new_word(char word[]) 
 {
    /*输入一个单词数组,返回一个myword结构体*/
    struct myword *tmp;
    char *t_word;
    tmp=(struct myword *) malloc(LEN);                //给一个新的节点申请空间 
    //动态申请一个空间来保存myword->word,因为这本身是个指针,需要指向一个数组来保存
    t_word = (char *) malloc(strlen(word));
    strcpy(t_word,strlwr(word));
    tmp->word = t_word;
    tmp->num = 1;
    tmp->next=NULL;
    return tmp;
 }
 
struct myword *instert_word(struct myword *tmp,struct myword *head)
 {
    /*将新的tmp节点插入到head对应的位置
    ->遍历所有节点 
     # head空             放到第一个节点
     # 与当前节点相同     当前节点的num+1
     # 比当前节点大       如果比下一个节点小,则插在当前节点后
                          如果比下一个节点大 ,则遍历下一个节点 
     # 比第一个节点小     放在第一个节点前,头结点后
                        【因为链表是有序的,且在上一情况中处理,
                         在后面不存在比当前节点小的情况】 
     */ 
    struct myword *cur;
    if (head==NULL)
    {
        head = (struct myword *) malloc(LEN);
        head->next = tmp;
    }
    else
    {
        cur = head->next;
        while (cur!=NULL)
        {
            if (strcmp(tmp->word,cur->word)<0)
            {
                //小于第一个值 ,因为链表有序,如果比第一个小,那就是最小的 
                tmp->next = cur;
                head->next = tmp;
                break;
            }
            if (strcmp(tmp->word,cur->word)==0)
            {
                //等于当前值 
                cur->num++;
                free(tmp);                        //这个节点已经存在,释放tmp空间 
                break;
            }
            else if (strcmp(tmp->word,cur->word)>0)
            {   
                //大于当前值 
                if (cur->next!=NULL)// && (tmp->word,cur->next->word)<0
                {
                    //后面不为空 
                    if (strcmp(tmp->word,cur->next->word)<0)
                    {
                        //小于下一个值 ,插入当前节点之后 
                        tmp->next = cur->next;
                        cur->next = tmp;
                        break; 
                    }
                    //大于等于下一个值 
                    cur = cur->next;
                }
                else
                {
                    //后面为空 
                    cur->next= tmp; 
                    break; 
                }
            } 
        }
    }
    return head;
 }
 
struct myword *find_words(FILE *input)
{
    /*输入文件指针,找到单词,生成链表,输出链表头head,
    从文件中一个字符一个字符的读取,如果遇到字母则将其存入
    字符数组中,直到遇到非字母, 
    【在数组的后面加入'\0',以此来分割数组中的
    单词,因为前一个单词可能比当前单词长】 
    则数组中的就是一个单词,
    生成节点,并插入相应位置。 
    用字符数组保存当前单词*/ 
    printf("start find Words....\n");
    struct myword *head, *tmp;
    struct myword *instert_word(struct myword *tmp,struct myword *head);  //插入节点
    struct myword *new_word(char word[]);  //生成一个新的节点,返回指针 
    char ch;
    char word[25];
    int i=0;
    head = NULL;
    
    while (! feof(input))
    {
        ch = fgetc(input);
        if ((ch>=65 && ch <=90) || (ch>=97 && ch <=122))
        {
            word[i]=ch;
            i++;
        }
        else
        {
            word[i]='\0';
            if (strlen(word) !=0)
            {
                tmp = new_word(word);
                head = instert_word(tmp,head);
            }   
            i=0;
        }
    }
    printf("find Words Done!\n");
    return head;
}

void destory_word(struct myword *head)
{
    printf("start free!\n");
    struct myword *tmp;
    while(head!=NULL)
    {
        tmp = head;
        head  = head->next;
        free(tmp);
    }
    printf("Free Done!\n");
} 

这次的收获不少:

  • 写代码前要思考清楚整体的框架
  • 命名要规范,见名知意,这样看到变量就不用发愁是那个了
  • 写长的程序,要分成一步一步来,可以单写一个test.c专门用来编写当前的功能,测试时,要注意到每一种可能,自己写一个测试的text,而不是直接用目标文件,太大浪费时间
  • 在关键的步骤一定要写清注释,不然五分钟后就不知道为什么这么写了
  • if-else if-else 用的时候要分清到底是只能选择一种执行,还是出现了就执行
  • 长的程序最好先用一个函数实现在之后,在考虑分模块,考虑优化,不然可能会做无用功
  • 有时候别人不能理解你在说什么,那可能是因为你真的很优秀
  • 但是为了更优秀,你需要让你说的话谁都能听懂,这说明你真的懂了

这是一种学习技巧————费曼技巧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容