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 用的时候要分清到底是只能选择一种执行,还是出现了就执行
- 长的程序最好先用一个函数实现在之后,在考虑分模块,考虑优化,不然可能会做无用功
- 有时候别人不能理解你在说什么,那可能是因为你真的很优秀
- 但是为了更优秀,你需要让你说的话谁都能听懂,这说明你真的懂了
这是一种学习技巧————费曼技巧