PTA习题解析——基于词频的文件相似度

禁止码迷,布布扣,豌豆代理,码农教程,爱码网等第三方爬虫网站爬取!

基于词频的文件相似度#

情景需求#

测试样例#

输入样例#

Copy Highlighter-hljs
3 Aaa Bbb Ccc # Bbb Ccc Ddd # Aaa2 ccc Eee is at Ddd@Fff # 2 1 2 1 3

输出样例#

Copy Highlighter-hljs
50.0% 33.3%

情景解析#

这个情景的实现可以分为 2 个部分,分别是按文件存储单词比较两个文件的相似度。首先来看第 1 部分,这部分的存储方式很灵活,可以像类似于存储图结构一样,关注点可以用邻接表或邻接矩阵,关注边可以使用边集数组来存储。这里可以关注文件来存储,即根据文件把属于该文件的单词组织到一个结构上。也可以关注单词,即做一个单词索引表,每个单词都标注其在哪个文件中出现过。
但是无论是使用哪种手法,都需要对输入的字符串进行处理。如测试样例所示,我们拿到的字符串并不是干净的结构,例如 “Ddd@Fff” 就需要切片为 “Ddd” 和 “Fff”。在这里可以遍历输入的字符串,如果是字母就继续遍历,如果是其他字符就暂停遍历,并且把单词部分拷贝出来,这里使用 isalpha() 函数来判断是否是字母是个好的选择。同时这道题忽视大小写,因此可以在切片时统一把字母搞成大写或小写,可以使用 tolower() 函数或 touppre() 函数实现。当然了,你用 string 类或字符数组处理都可以,string 集成处理字符串更为方便。别忘了单个单词长度小于 10,大于 2。虽然具体组织到的结构不同,但是这个操作是共有的,因此给出伪代码。

接下来根据关注的内容不同,我给出 3 种实现方法。

关注文件,构建文件单词表#

思路分析#

这种手法就要求把单词按照文件的归属,存储到每个文件的结构中,达到的效果是在一个结构中保存了属于该文件的所有单词。由于这里的文件是给定的序号,因此可以使用哈希表来存储,冲突处理使用直接定值法。而对于每个单词而言,可以使用哈希链来做,不过这里可以用 STL 库的 set 容器来存放。这里需要解释一下,如果使用动态的结构链表、list、vector 也是可以,但是这里会出现单个文件的重复单词,这就需要对结构进行去除,而以上结构中 list 和 vector 的去重能力较弱。set 容器主打的特点就是去重,并且内部实现的结构是红黑树,这样查找起来在内部的运行速度也很快。
接下来就是如何查找相同单词的问题了,除了内部可以借用红黑树带来的效率,还有如何在 2 个文件之间建立联系。方法和我们当时做“一元多项式的乘法与加法运算”的思想差不多,就是遍历其中一个文件,然后拿这个文件的每个单词去和另一个文件中查看看有没有相同的单词。此处可以选择单词数较少的单词为基准,去另一个结构中查找,可以使用.size()方法轻松得到一个文件的单词数量。由于涉及到 STL 容器的遍历问题,我们需要申请一个迭代器,并运作 set 自己的.find()方法。

伪代码#

代码实现#

Copy Highlighter-hljs
#include <iostream> #include <string> #include <set> using namespace std; #define MAXSIZE 101 int main() { int count, fre; //文件数、查找次数 string str; //单次输入的单词 string a_word; //单个分片的单词 set<string> files[MAXSIZE]; //文件单词表,使用 HASH 思想实现 int files_a, files_b; //待查找的文件编号 int number_same = 0, number_all = 0; //重复单词数、合计单词数 set<string>::iterator a_iterator; //set 容器迭代器,查找时用 cin >> count; for (int i = 1; i <= count; i++) { cin >> str; while (str != "#") { for (int j = 0; j <= str.size(); j++) { if (isalpha(str[j]) != 0) //判断是否是字母 { if (a_word.size() < 10) //限制单词长度上限 { a_word += tolower(str[j]); //把单个字母加到末尾 } } else //遇到符号,分片单词 { if (a_word.size() > 2) //限制单词下限 { files[i].insert(a_word); //将单词插入对于文件的 set 容器中 } a_word.clear(); //清空字符串 } } cin >> str; } } cin >> fre; for (int i = 0; i < fre; i++) { cin >> files_a >> files_b; if (files[files_a].size() > files[files_b].size()) //选择单词数较小的文件为基准 { count = files_a; files_a = files_b; files_b = count; } number_all = files[files_a].size() + files[files_b].size(); number_same = 0; for (a_iterator = files[files_a].begin(); a_iterator != files[files_a].end(); a_iterator++) { //遍历其中一个文件 if (files[files_b].find(*a_iterator) != files[files_b].end()) //找到重复单词 { number_same++; number_all--; } } printf("%.1f%%\n", 100.0 * number_same / number_all); } return 0; }

关注单词,构建单词索引表#

思路分析#

这种手法就要求把文件按照单词的归属,存储到每个单词的结构中,达到的效果是在一个结构中保存了含有该单词的所有文件。由于这里的文件是给定的序号,因此标记该单词出现在哪个文件中,可以使用哈希表来存储,冲突处理使用直接定值法,通过这种手法可以直接确定单词的出现位置。而对于每个单词而言,可以使用哈希链来做,不过这里可以用 STL 库的 map 容器来存放。这里需要解释一下,所谓单词索引就是通过单词直接找到它出现在那些文件,map 容器主打的特点就是构建一个映射,并且内部实现的结构是红黑树,这样查找起来在内部的运行速度也很快。而这时就可以以单词本身作为 key,而 value 就连接到一个起到 HASH 作用的数组,在这里用数组绰绰有余。
接下来就是如何查找相同单词的问题了,这里就会遇到一个小问题,就是文件中有哪些单词是完全未知的。解决方法是直接用迭代器遍历 map 容器,对于每个单词都进行检查,若该单词同时出现在 2 个文件中,就修正重复单词数和重复单词数,若进出现在一个文件就只修正重复单词数。这里单词的规模会对效率进行限制,不过确定单词存在于那些文件的速度是很快的,可以用下标直接访问数组。

伪代码#

代码实现#

Copy Highlighter-hljs
#include <iostream> #include <string> #include <map> using namespace std; int main() { int count, fre; //文件数、查找次数 string str; //单次输入的单词 string a_word; //单个分片的单词 map<string, int[101]> Index_table; //单词索引表 int files_a, files_b; //待查找的文件编号 int number_same = 0, number_all = 0; //重复单词数、合计单词数 map<string, int[101]>::iterator a_iterator; //map 容器迭代器,遍历时用 cin >> count; for (int i = 1; i <= count; i++) { cin >> str; while (str != "#") { for (int j = 0; j <= str.size(); j++) { if (isalpha(str[j]) != 0) //判断是否是字母 { if (a_word.size() < 10) //限制单词长度上限 { a_word += tolower(str[j]); //把单个字母加到末尾 } } else //遇到符号,分片单词 { if (a_word.size() > 2) //限制单词下限 { Index_table[a_word][i] = 1; //对单词构建映射 } a_word.clear(); //清空字符串 } } cin >> str; } } cin >> fre; for (int i = 0; i < fre; i++) { cin >> files_a >> files_b; number_all = number_same = 0; for (a_iterator = Index_table.begin(); a_iterator != Index_table.end(); a_iterator++) { //遍历 Index_table 中所有单词 if (a_iterator->second[files_a] == 1 && a_iterator->second[files_b] == 1) { //单词在 2 个文件中都出现 number_same++; number_all++; } else if(a_iterator->second[files_a] == 1 || a_iterator->second[files_b] == 1) { //单词出现在 2 个文件其中之一 number_all++; } } printf("%.1f%%\n", 100.0 * number_same / number_all); } return 0; }

文件单词表、单词索引表协同工作#

思路分析#

既然我可以用 2 种不同的视角去构建结构,为什么不同时用起来呢?同时构建的方法也很简单,就是在分片单词的时候同时做就行。这么做可以同时继承以上 2 种手法的特点,即遍历其中一个文件的单词,但是这时无需去另一个文件查找,而是直接去单词索引表查看是否在另一个文件中出现,因此选择单词数更小的文件来遍历效率更高。
不过,如果是仅仅这个情景,效率其实并没有第一种手法快,因为维护 2 种容器的内部开销不可避免。但是如果跳出这个情景,把它当成一个应用程序来看,这种手法无疑有更加的健壮性。因为我同时拥有 2 种不同信息的表,这为我添加更多的功能提供了基础。例如可以对多个文件进行总体的词频统计,这个就可以利用单词索引表实现,而仅仅有文件单词表就需要把所有表全部都遍历一遍,那效率就太低了!

伪代码#

代码实现#

Copy Highlighter-hljs
#include <iostream> #include <string> #include <set> #include <map> using namespace std; int main() { int count, fre; //文件数、查找次数 string str; //单次输入的单词 string a_word; //单个分片的单词 //单词索引表 int files_a, files_b; //待查找的文件编号 int number_same = 0, number_all = 0; //重复单词数、合计单词数 //set 容器迭代器,查找时用 //文件单词表,使用 HASH 思想实现 cin >> count; for (int i = 1; i <= count; i++) { cin >> str; while (str != "#") { for (int j = 0; j <= str.size(); j++) { if (isalpha(str[j]) != 0) //判断是否是字母 { if (a_word.size() < 10) //限制单词长度上限 { a_word += tolower(str[j]); //把单个字母加到末尾 } } else //遇到符号,分片单词 { if (a_word.size() > 2) //限制单词下限 { Index_table[a_word][i] = 1; //对单词构建映射 files[i].insert(a_word); //将单词插入对于文件的 set 容器中 } a_word.clear(); //清空字符串 } } cin >> str; } } cin >> fre; for (int i = 0; i < fre; i++) { cin >> files_a >> files_b; if (files[files_a].size() > files[files_b].size()) //选择单词数较小的文件为基准 { count = files_a; files_a = files_b; files_b = count; } number_all = files[files_a].size() + files[files_b].size(); number_same = 0; for (a_iterator = files[files_a].begin(); a_iterator != files[files_a].end(); a_iterator++) { //遍历其中一个文件 if (Index_table[*a_iterator][files_a] == 1 && Index_table[*a_iterator][files_b] == 1) //找到重复单词 { number_same++; number_all--; } } printf("%.1f%%\n", 100.0 * number_same / number_all); } return 0; }

调试遇到的问题#


调试中主要遇到了 2 个技术性问题:
Q1:手法一中,vector 引发的去重问题。
A1:由于一开始没有考虑一个文件中重复单词的问题,因此选择了 vector 来动态存储单词,这时有重复单词就会被多次计数,那就会使答案出错。如果用泛型算法 find() 来处理的话,那就每次添加单词都要搞一遍,效率太低了,必超时,而且用 find() 来确定单词的存在也会超时。最后将 vector 统统改成 set 容器,直接实现去重功能解决这个问题。
Q2:手法二中,vector 引发的哈希表查找问题。
A2:原本我 map 容器内的值是一个 vector 容器,由于 vector 是动态往里面添加空间,因此得到的是哈希链而不是哈希表。这就说明每次查找还是要用 find() 函数,这就造成了超时问题,因此就要对容器提前动态分配一些空间。不过这个用数组来做就绰绰有余了,因此改成数组,迭代器的类型相应地修改就好。
还遇到了 1 个非技术性问题:
Q3:出现了内存超限问题;
A3:用结构体或容器作为 map 的值的用法我很久没写了,因此忘记了不需要另外申请空间,每次构建映射时我都申请一个很大的数组。最后调试时,我忘记删掉了这个操作,这就导致了每处理一个单词,就要申请一大堆空间,这样空间就被快速地消耗了。删除这个操作就可以解决内存超限的问题,这还是我第一次遇到。

posted @   乌漆WhiteMoon  阅读(1002)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
CONTENTS