基于Tire树(字典树)与倒排索引实现文件词频统计工具

知识共享许可协议 版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons


本篇博文我们使用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 输入输出流即可解决,然后我们可以使用 i s a l p h a ( ) isalpha() 函数判断是否是字母字符,但是注意如 d o n t don&#x27;t 这样的单词,在单词中间有单撇号的我们认为是一个单词,还有文章中也有些单词首部是单撇号,我们需要将其删除。具体的实现代码如下所示:

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);
}

交互如下图所示:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/92605037