题目
原问题链接
编程统计出input.txt文件保存的文章中,每个单词出现的次数。文章内容如下:
In this chapter we will be looking at files and directories and how to manipulate them. We will learn how to create files, open them, read, write and close them. We'll also learn how programs can manipulate directories, to create, scan and delete them, for example. After the last chapter's diversion into shells, we now start programming in C.
Before proceeding to the way UNIX handles file I/O, we'll review the concepts associated with files, directories and devices. To manipulate files and directories, we need to make system calls (the UNIX parallel of the Windows API), but there also exists a whole range of library functions, the standard I/O library (stdio), to make file handling more efficient.
这段文字来自网络。为了统计更有意义,加入两个条件:
统计过程中不考虑空格和标点符号
不区分大小写(可以把所有字母转成小写后参与统计)
解题思路
做一个词频统计工具
观察文本发现,两个单词之间都有空格隔开。这就比较好处理了,因为我们可以很轻松的实现一次读取一个单词。
如果实际要处理的文本有标点之后没空格的情况,如
I love you.My program.
那我们还要加一个deliver函数把you和My分开
如何存储出现过的单词
1.二维数组搭配一个一维数组实现
2.利用结构体做一个链表实现
我选择了第二种,因为我觉得结构体是通往抽象编程的路,而且使用结构体可读性更好。
具体思路
1.一次从文本读取一个字符串word
2.把字符串word的大写字母转化为小写
3.把字符串word里的 ,.() 四种标点消除(想做成一个好用的词频统计工具的话,根据实际需要增添要消除的标点符号)
4.检查word是否出现过
没有出现过: 把他存入结构体
出现过:找到存储这个word的结构体,并把里面的计数变量加一
5.打印所有存储了单词的结构体
源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FILE_PATH "..\\要统计的文本.txt" //宏存储文本相对路径
#define WORD_LENGTH 60
#define LENGTH sizeof(WORD)
struct WORD
{
char word[WORD_LENGTH]; //存放单词
int wordCount; //本单词出现次数
struct WORD *pNext; //next指针
};
struct WORD pHeader; //链表的头结点
WORD *countWord(char *word, WORD *pLastWord);
void changeToLower(char *word);
void checkPunct(char *word);
int checkCountWord(char *word);
void addcount(char * word);
WORD * addNewWord(char *word, WORD *pLastWord);
void printResult();
void freeAll();
void main()
{
WORD *pLastWord = &pHeader;//指向最后一个结点的指针
char tempWord[WORD_LENGTH];//储存本次读取的字符串
pHeader.pNext = NULL;//把头节点的指向初始化为空
FILE *fp;
if ((fp = fopen(FILE_PATH, "r")) == NULL)//如果打开文本为空
{
printf("System can`t find this file!\n");
exit(0);
}
while ((fscanf(fp, "%s", tempWord)) != EOF)//如果文件还没读取完
{
pLastWord = countWord(tempWord, pLastWord);//统计词频
}
fclose(fp);//关闭文件
printResult();//打印统计结果
freeAll();//释放所有动态申请的内存
system("PAUSE");
}
WORD *countWord(char *word,WORD *pLastWord)
{
changeToLower(word);//把Word全部转换为小写字母
checkPunct(word);//去除word中的 (),. 如果文本含有其他特殊标点,这里需要再增加一点代码
if (checkCountWord(word))//如果本单词在前文出现过
{
addcount(word);//把这个单词的计数加一
}
else
{
pLastWord = addNewWord(word, pLastWord);//建立一个新的单词结点
}
return pLastWord;
}
void changeToLower(char *word)//把字符串全部转化为小写字母
{
int i;
for (i = 0; word[i] != '\0'; i++)
{
if (word[i] >= 65 && word[i] <= 90)
{
word[i] += 32;
}
}
}
void checkPunct(char *word)//去除标点符号
{
int i, k;
for (i = 0; word[i] != '\0'; i++)//遍历字符串
{
if (word[i] == '(' || word[i] == ')' || word[i] == ',' || word[i] == '.')//如果含有标点
{
for (k = i; word[k] != '\0'; k++)
{
word[k] = word[k + 1];
}
i--;//防止"sdio)."这种连续多个特殊字符出现的情况,再次检查当前index下的字符
}
}
}
int checkCountWord(char *word)//不在已存储单词中返回0. 在就返回1
{
WORD *p;
p = &pHeader;
if (pHeader.pNext == NULL)//如果检查的是第一个单词
{
return 0;
}
do
{
p = p->pNext;
if (strcmp(word, p->word) == 0)
{
return 1;
}
} while (p->pNext != NULL);
return 0;
}
void addcount(char * word)//把出现过的单词计数加一
{
WORD *p;
p = &pHeader;
do
{
p = p->pNext;
if (strcmp(word, p->word) == 0)//遍历到重复的结点,并把计数 +1
{
p->wordCount++;
return;
}
} while (p->pNext != NULL);
}
WORD * addNewWord(char *word, WORD *pLastWord)//加一个新单词结点
{
WORD *pTemp;
if (pHeader.pNext == NULL)//如果添加的是第一个结点
{
pHeader.pNext = (WORD *)malloc(LENGTH);
strcpy(pHeader.pNext->word, word);
pHeader.pNext->wordCount = 1;
pHeader.pNext->pNext = NULL;
pLastWord = pHeader.pNext;
}
else
{
pTemp = pLastWord;
pLastWord = (WORD *)malloc(LENGTH);
strcpy(pLastWord->word, word);
pLastWord->wordCount = 1;
pLastWord->pNext = NULL;
pTemp->pNext = pLastWord;
}
return pLastWord;
}
void printResult()//打印存储的所有单词结点
{
WORD *p;
int sign = 0;
p = &pHeader;
do
{
p = p->pNext;
printf("%-12s%d\t\t", p->word, p->wordCount);
sign++;
if (sign % 3 == 0)
{
printf("\n");
}
} while (p->pNext != NULL);
}
void freeAll()
{
WORD *p ,*piror;
p = pHeader.pNext;//头节点不是动态申请的不能释放
do
{
piror = p;
p = p->pNext;
free(piror);
} while (p->pNext != NULL);
free(p);
}
执行结果
后来改成了双向链表并在打印前加了排序的函数,下载了一本英文版的 <百年孤独> 进行了词频统计
很好用(乱码是因为下载的文本文件本身就有乱码...)
就是运行效率有些低
统计<百年孤独>运行时间记录(800K的文本文件)
- 打印结果占用2秒
- 排序代码占用1秒
- 其他代码占用3秒
- 总计运行时间6秒
总结
去年考英语六级时,我下载了一本英文电子版的<The Great Gatsby>,想统计出它的高频单词并背诵。我用了一个在线词频统计工具来统计,因为各种特殊字符的存在结果特别不理想。
现在学会了写自己的程序,我可以根据自己的实际需求来改写出合适的程序。It's really cool!
而且这个链表思维和我写的贪吃蛇实现蛇身体的思维很相似,我可以优化我的贪吃蛇程序了!