文章目录
本篇博文我们使用C++语言来实现一个文件词频的统计工具,它具有以下功能:
- 统计出每个单词出现的频率,按照由高到低的顺序存入结果文件中
- 高效统计输出文件单词总数、文件单词去重后的总数
- 高效查询指定前缀的单词数量 - 基于Trie树
- 高效查询指定前缀的所有单词 - 基于Trie树
- 高效查询指定单词出现的文件编号、频数、位置 - 基于倒排索引
我们首先讲解文件的读写操作,然后基于简单文件读写实现
文件读写操作
首先我们来看一下C风格的文件读取:
C风格文件读取
int main() {
char c;
FILE* fp = NULL;//需要注意
fopen_s(&fp, "01.txt", "r");
if (NULL == fp)
return -1;//要返回错误代码
while (fscanf_s(fp, "%c", &c, sizeof(char)) != EOF)
{
printf("%c", c); //从文本中读入并在控制台打印出来
}
fclose(fp);
fp = NULL;//需要指向空,否则会指向原打开文件地址
return 0;
}
接下来我们使用C++风格的文件输入输出流来实现文件读取:
具体可参考《C++文件读写详解》、《C++中文件读取处理(按行或者单词)》
C++风格按行读取
使用 getline() 函数即可,注意要加上 #include<string> 头文件。
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
using namespace std;
int main()
{
fstream fin;
string filename = "01.txt";
/* ios::in ----> 为输入(读)而打开文件 */
fin.open(filename.c_str(), ios::in);
vector<string> vec;
string str;
/* 按行读取 */
while (getline(fin, str))
{
vec.push_back(str);
}
for (string val : vec)
{
cout << val << endl;
}
}
C++风格按单词读取
下面是按单词读取文件内容:
#include<iostream>
#include<fstream>
#include<vector>
using namespace std;
int main()
{
fstream fin;
string filename = "01.txt";
/* ios::in ----> 为输入(读)而打开文件 */
fin.open(filename.c_str(), ios::in);
vector<string> vec;
string str;
/* 按照空格分割单词 */
while (fin >> str)
{
vec.push_back(str);
}
for (string val : vec)
{
cout << val << endl;
}
}
实现文件词频统计工具
英文文章单词的正确分割
首先我们来研究一下如何实现正确的单词分割,当然这和我们具体要统计分析的文章是关系的。本文我们统计《Harry Potter and the Sorcerer‘s Stone 哈利波特与魔法石》文章,文章字数 7万+。
分析了文章具体内容后,发现了非法字符有 空格、短横线、点符号、数字字符等,对于空格分割我们直接使用fstream 输入输出流即可解决,然后我们可以使用 函数判断是否是字母字符,但是注意如 这样的单词,在单词中间有单撇号的我们认为是一个单词,还有文章中也有些单词首部是单撇号,我们需要将其删除。具体的实现代码如下所示:
void calWordFquency :: readFile(string filename, map<string, int>& mapResult)
{
fstream fin;
fin.open(filename.c_str(), ios::in);
string str;
/* 按空格分割单词,再进行具体过滤 */
while (fin >> str)
{
auto it = str.begin();
while (it != str.end())
{
/* isalpha 判断是否是数字字符、中间的单撇号不删除、首部单撇号删除 */
if ((!isalpha(*it) && *it != '\'') || str[0] == '\'')
{
it = str.erase(it);
}
else
{
++it;
}
}
/* 若str不为空,则将其加入trie树中,并加入map中,更新单词总个数 */
if (str.length())
{
trie.insert(str);
mapResult[str]++;
wordSum++;
}
}
}
接下来的两个版本中都是用到了Trie树,之前我的博文中有过详细的讲解,不再赘述。
基于Trie树实现文件词频统计
简单说一下设计思路,我们具体是使用C++ STL中的map容器,使用map实现词频统计是非常简单的,一句代码搞定~
结构设计如下:
class calWordFquency
{
public:
/* 公共API接口 :向终端输出交互提示,并处理*/
void oridinary();
private:
/* 非重复单词建立Trie树 */
TrieTree trie;
/* 单词总个数 */
int wordSum;
/* 读取文件内容,分割单词,并存入map、Trie树中 */
void readFile(string filename, map<string, int>& mapResult);
/* 对pair类型按值排序,sort函数的自定义比较方式 */
static bool compare(const pair<string, int>& x, const pair<string, int>& y);
/* 将单词频数统计的排序结果存入文件中(频数降序排序) */
void calculate(map<string, int>& mapResult);
};
方法的具体实现如下:
bool calWordFquency :: compare(const pair<string, int>& x, const pair<string, int>& y)
{
/* 按值由高到低排序 */
return x.second > y.second;
}
/* 读取文件内容,分割单词,并存入map、Trie树中 */
void calWordFquency::readFile(string filename, map<string, int>& mapResult)
{
fstream fin;
fin.open(filename.c_str(), ios::in);
string str;
/* 按空格分割单词,再进行具体过滤 */
while (fin >> str)
{
auto it = str.begin();
while (it != str.end())
{
/* isalpha 判断是否是数字字符、中间的单撇号不删除、首部单撇号删除 */
if ((!isalpha(*it) && *it != '\'') || str[0] == '\'')
{
it = str.erase(it);
}
else
{
++it;
}
}
/* 若str不为空,则将其加入trie树中,并加入map中,更新单词总个数 */
if (str.length())
{
trie.insert(str);
mapResult[str]++;
wordSum++;
}
}
/* 注意文件最后一定要关闭 */
fin.close();
}
/**
* 由于sort函数无法直接对map类型排序,因此我们将map中的数据
* 存入vector中,当然vector中存储的都是pair类型了,之后我们
* vector进行排序,然后进行输出进result.dat文件中
*/
void calWordFquency::calculate(map<string, int>& mapResult)
{
/* 建立vector存储pair类型数据 */
vector<pair<string, int>> resTmp;
auto it = mapResult.begin();
while (it != mapResult.end())
{
resTmp.push_back(make_pair(it->first, it->second));
++it;
}
/* 对pair按值排序,即按单词频数降序排序 */
sort(resTmp.begin(), resTmp.end(), compare);
/* 将排序好的数据写入存储结果数据的文件中 */
ofstream out;
out.open("result.dat", ios::in | ios::trunc);
int len = resTmp.size();
for (int i = 0; i < len; i++)
{
string str;
str += resTmp[i].first;
str += " : ";
str += to_string(resTmp[i].second);
out << str.c_str() << endl;
}
/* 注意文件最后一定要关闭 */
out.close();
}
/**
* 公共API接口:主要负责向终端输出统计信息,和与用户进行交互
* 输出单词总个数、去重的单词总个数,然后向用户输出终端提示符
* 用户输入单词或者前缀,打印出该单词的频数、以该单词为前缀的
* 单词总数与具体单词。
*/
void calWordFquency::oridinary()
{
map<string, int> result;
string filename = "01.txt";
readFile(filename, result);
calculate(result);
string str;
cout << "article words count : " << wordSum << endl;
cout << "no repeat words count : " << result.size() << endl;
while (1)
{
cout << ">>";
string word;
cin >> word;
if (word == "#") break;
cout << "frequence : " << result[word] << endl;
cout << "\"" << word << "*\" count : " << trie.getPriNum(word) << endl << endl;
cout << "\"" << word << "*\" words : " << endl;
trie.printPriWord(word);
}
}
程序运行时候,会自动在当前工程目录下生成结果文件:
结果文件的内容格式如下(单词 :频数)
终端交互如下:
基于Trie树实现带倒排索引的文件词频统计
有关倒排索引的原理及实现在之前我的博文中有过详细的讲解,不再赘述。
《C++高级数据结构算法 | 倒排索引(inverted index)》
结构定义如下:
class calWordFquency
{
public:
/* 公共API接口,调用内层封装功能函数 */
void InvertIndex();
private:
TrieTree trie;
int wordSum;
/* 存储单词频率与位置 */
struct FileNode
{
int TF; // 频率
string pos; // 单词位置
};
/* 存储单词与倒排索引表的映射 ————— 单词 - <文件号-次数-位置信息> */
typedef map<string, map<int, FileNode>> indexMap;
typedef map<int, FileNode> invertMap;
/* 读取文件、分割单词、建立倒排索引、建立Trie树 */
void createIndex(indexMap& map);
/* 交互并向终端输出指定的单词的倒排索引表、单词总数、去重单词总数 */
void queryFileInfo(indexMap& map);
};
方法的具体实现如下:
/* 读取文件、分割单词、建立倒排索引、建立Trie树 */
void calWordFquency::createIndex(indexMap& map)
{
ifstream fin;
/**
* 遍历三个文件,文件号 从 1-3,按照空格分割单词,并将其
* 频率和位置都加入到map中,实现单词与倒排索引表的映射
* 并将单词加入到Trie树中
*/
for (int i = 1; i <= 3; i++)
{
string fileName = to_string(i) + ".txt";
fin.open(fileName.c_str(), ios::in);
string str;
/* posNum表示文件单词相对于文件起始的位置 */
int posNum = 0;
while (fin >> str)
{
auto it = str.begin();
while (it != str.end())
{
/* 实现单词分割 */
if ((!isalpha(*it) && *it != '\'') || str[0] == '\'')
{
it = str.erase(it);
}
else
{
++it;
}
}
/**
* 向Trie树中添加单词、根据将单词频率和位置都加入对应文件号(i)的map中
* 并更新单词总个数
*/
if (str.length())
{
posNum++; /* 单词位置 */
trie.insert(str);
map[str][i].TF++;
map[str][i].pos += to_string(posNum) + " ";
wordSum++;
}
}
/* 使用完毕后关闭文件 */
fin.close();
}
}
/* 交互并向终端输出指定的单词的倒排索引表、单词总数、去重单词总数 */
void calWordFquency::queryFileInfo(indexMap& map)
{
string str;
/* 输出文章单词总个数 */
cout << "article words count : " << wordSum << endl;
/* 输出文章单词去重后的总个数 */
cout << "no repeat words count : " << map.size() << endl;
/* 用户交互、处理交互部分 */
while (true)
{
cout << ">>";
cin >> str;
/* 输出 # 结束 */
if (str == "#") break;
if (map.find(str) == map.end())
{
cout << "word isn't exist!" << endl;
continue;
}
auto it = map[str].begin();
while (it != map[str].end())
{
/* 输出文件号信息与单词频数 */
cout << "fileNode : " << it->first
<< " frequence : " << it->second.TF << endl;
/* 输出单词位置信息 */
cout << "pos = < " << it->second.pos
<< ">" << endl << endl;
++it;
}
/* 输出指定单词为前缀的单词总数 */
cout << "\"" << str << "*\" count : " << trie.getPriNum(str) << endl << endl;
/* 输出指定单词为前缀的单词 */
cout << "\"" << str << "*\" words : " << endl;
trie.printPriWord(str);
}
}
void calWordFquency::InvertIndex()
{
indexMap map;
createIndex(map);
queryFileInfo(map);
}
交互如下图所示: