引文
统计一个文本中的单词数量,相信大家不感到陌生,在不考虑性能的情况下,程序可以很简单。获取一个单词,再与已有的单词库一个一个做对比,对比相同时,将次数加1;对比到最后一个也没有找到相同单词时,则添加到末尾,并赋次数为1。文本小时很可取,简单方便直观。但是当文本增大时呢?这样线性的对比,10M的文本就足够让程序运行半个钟头了。
以下先介绍Python的解决方法
#Py3.4
import sys
try:
fin = open('mission.txt','r')
except:
print ("Can't open the file!")
sys.exit(1)
Words = {}
for line in fin:
line = line.rstrip() #Strip '\n'
line = line.lower()
line = line.split(' ')
for word in line:
if word not in Words:
Words[word] = 1
else:
Words[word] = Words[word] + 1
fin.close()
for word in Words:
print (word,Words[word])
建立字典,单词作为键,出现次数作为与键对应的值。先获取单词,再与字典中的单词对比,当获取到的单词在字典的键中,则出现次数加1,当键不在字典中,就添加键/值对。由于Python对字典的键检索做足了优化,所以以上程序性能很好,处理10M文本小问题。“小布”让它处理了一个172MB的文本,总共花了21s(Py3.4+win7)。
以下介绍建立树状结构的处理方案。
正文
单词的长度是有限的,一般不会太长,并且经过大小写转换后,只会由26个字母和符号'表示(it's)。所以可以用单词中的一个字母作为树的一个节点,每个单词都是从树根到树叶的一条路径。每一个节点可以如下定义:
一个节点后面有a~z,' 共27种可能的节点(注意按照这个顺序),在代码中可以如下实现:
#define N 27 //26个字母加一个'字符
struct alphabet
{
char letter; //保存当前节点字母
string spell; //保存此路径拼写
int times; //保存此拼写次数,若times=-1,后面就不再有节点
alphabet *next[N]; //当前节点下的分枝
}
例如单词"word"和"world",可以建立如下树状图(Head仅作为头节点,真正的单词从第二层开始)
上图中层次最深为:最长单词+1 (层)。将单词的拼写作为建树的依据,这样能够省时的关键在于:这样的树,每个单词都分别有一个固定的位置,对于一个单词,不是通过一一对比获取它的位置,而是通过计算得来的,例如单词"me",长度为2,只要计算两次就能找到他的位置。m为字母表第13个字母,e为字母表第5个字母,则me在树上的位置为Head->next[12]->next[4]。word在树上的位置为:Head->next[22]->next[14]->next[17]->next[3]。
"小布"同样让它处理了一个172MB的文本,总共花了59s(VS2013+win7)。程序是半年前之写的,当时是按字节读取文本,即每次读取一个字节,就要将输入流入栈,转而处理该字节,而Python的程序是一次读取一行文本,在输入上性能就优于C++的程序了。在引文部分说的线性对比方式,也编写了一个程序,"小布"没敢用172M文本作为测试,因为处理一个6.5M的文本就用了12+分钟,172M == OMG。
什么?还要赋源程序?拿不出手怎么办...
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
#define MAX 50 //默认最长单词50个字符
#define N 27 //26个字母加一个'字符
unsigned long points = 0;
struct alphabet
{
char letter; //保存当前节点字母
string spell; //保存此路径拼写
int times; //保存此拼写次数,若times=-1,后面就不再有节点
alphabet *next[N]; //当前节点下的分枝
}*head, *tail, *now = NULL, *point[MAX];
unsigned long statistic();
void search(char str[MAX]);
void cr8_link(char letter);
void put_del(ofstream &output, int i);
void main()
{
int i = 0;
unsigned long num = 0; //记录总共词汇数量,最大2^64=1.844674407371×10^19个词汇
head = tail = new alphabet;
head->letter = 'A'; //头字母记为大写,保证不可能匹配上
head->spell = "The statistical number of word is";
for (int i = 0; i<N; i++) //将head指向的所有节点赋值为空
head->next[i] = NULL;
num = statistic(); //开始统计任务
if (num == 0) return;
head->times = num; //总词数放在head指向的times中
now = head; //从头开始输出
ofstream output;
output.open("result.txt");
output << "The number of points is:" << points << "\n";
put_del(output, 0); //输出结果于result.txt中
output.close();
}
unsigned long statistic()
{
int i = 0;
unsigned long num = 0; //记录总词汇数
register char c, ch = '\0';
char str[MAX]; //保存一个单词的拼写
ifstream input;
input.open("mission.txt");
if (!input)
{
cerr << "Please rename the file as\"mission.txt\",then try again latter\n";
return 0;
}
while (!input.eof())
{
i = 0;
if (ch >= 'A'&&ch <= 'Z') { ch += 32; str[i] = ch; i++; ch = '\0'; } //ch中保存的数据使用后一定置0,防止下次再次使用
else if ((ch >= 'a'&&ch <= 'z')) { str[i] = ch; i++; ch = '\0'; }
while (!input.eof())
{
c = input.get();
if (c >= 'A'&&c <= 'Z') { c += 32; str[i] = c; i++; }
else if ((c >= 'a'&&c <= 'z') || (c == '\'')) { str[i] = c; i++; }
else break;
}
str[i] = '\0';
while (!(input.eof()) && ((c = input.get())<'A' || (c>'Z'&&c<'a') || (c>'z'))); //跳过后面的空格、回车和标点,循环结束时,到达文本末尾或读进一个字母
if (!input.eof()) ch = c; //如果未到达文本末尾,则读进一个字母,保存
if (i != 0) //如果i的值未改变,则首次输入为空格或回车
{
num++; //否则输入为单词,单词总数加1
search(str);
}
}
input.close();
return num;
}
void search(char str[MAX])
{
int i = 0;
tail = now = head; //从头开始查找
while (str[i] != '\0') //本单词遍历每个字母才代表本单词的枝建立完全
{
while ((now != NULL) && (str[i] != '\0'))
{
tail = now; //tail保存now上一节点,判断now下移后是否空
if (str[i] == '\'') //字符'的ASCII码为39,对应节点数为26
now = now->next[str[i++] - 13]; //39-13=26,正好对'的节点索引
else
now = now->next[str[i++] - 97]; //str[i]-97正好是字母对应0~26的节点索引
}
if (now == NULL)
cr8_link(str[i - 1]); //出循环两个口,如果now为空,即该建立相应的节点
}
now->spell = str; //本单词遍历每个字符,说明本枝建立完全,在节点处保存拼写以便输出
now->times++;
}
void cr8_link(char letter) //在当前位置添加一个字母节点
{
points++;
now = new alphabet; //now指向新节点
for (int i = 0; i<N; i++) //节点后27个节点指针指向空
now->next[i] = NULL;
now->times = 0;
now->letter = letter;
if (letter == '\'') letter = '{'; //对字符'的特殊处理
tail->next[letter - 97] = now; //有tail保存上一节点,建立的新节点应该跟在tail后面
}
void put_del(ofstream &output, int i) //采用递归输出结果
{
point[i] = now; //point[i]保存now经过的节点路径,能够原路返回,实现按序输出
if ((now->times != 0) && (now != NULL)) //次数不为0的节点才是单词,次数为0的节点只是路径
output << now->spell << ":" << now->times << endl;
for (int j = 0; j<N; j++) //对于一个节点point[i],后面有27个节点
{
now = point[i]->next[j]; //now遍历point[i]的27个节点
if (now != NULL) //如果now非空,则now又作为一个节点,后面有27个节点
{
put_del(output, i + 1);
delete now; //递归回来时,数据都已经输出,直接删除节点
}
}
}