PTA习题解析与思考——基于词频的文件类似度

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

基于词频的文件类似度

情景需求

测试样例

输入样例

3
Aaa Bbb Ccc
#
Bbb Ccc Ddd
#
Aaa2 ccc Eee
is at Ddd@Fff
#
2
1 2
1 3

输出样例

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()方法。数组

伪代码

代码实现

#include <iostream>
#include <string>
#include <set>
using namespace std;
#define MAXSIZE 101
int main()
{
    int count, fre;      //文件数、查找次数
    string str;      //单次输入的单词
    string a_word;      //单个分片的单词
          //文件单词表,使用 HASH 思想实现
    int files_a, files_b;      //待查找的文件编号
    int number_same = 0, number_all = 0;      //重复单词数、合计单词数
          //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)      //限制单词下限
                    {
                              //将单词插入对于文件的 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 ( )
        {                                                                      //遍历其中一个文件
            if ()      //找到重复单词
            {
                number_same++;
                number_all--;
            }
        }
        printf("%.1f%%\n", 100.0 * number_same / number_all);
    }
    return 0;
}

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

思路分析

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

伪代码

代码实现

#include <iostream>
#include <string>
#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;     //重复单词数、合计单词数
          //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)      //限制单词下限
                    {
                              //对单词构建映射
                    }
                    a_word.clear();      //清空字符串
                }
            }
        }
    }

    cin >> fre;
    for (int i = 0; i < fre; i++)
    {
        cin >> files_a >> files_b;
        number_all = number_same = 0;
        for ()
        {                                                          //遍历 Index_table 中全部单词
            if ()
            {                                                      //单词在 2 个文件中都出现
                number_same++;
                number_all++;
            }
            else if()
            {                                                      //单词出如今 2 个文件其中之一
                number_all++;
            }
        }
        printf("%.1f%%\n", 100.0 * number_same / number_all);
    }
    return 0;
}

文件单词表、单词索引表协同工做

思路分析

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

伪代码

代码实现

#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;
                             //将单词插入对于文件的 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 ()
        {                                                                      //遍历其中一个文件
            if ()      //找到重复单词
            {
                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 的值的用法我好久没写了,所以忘记了不须要另外申请空间,每次构建映射时我都申请一个很大的数组。最后调试时,我忘记删掉了这个操做,这就致使了每处理一个单词,就要申请一大堆空间,这样空间就被快速地消耗了。删除这个操做就能够解决内存超限的问题,这仍是我第一次遇到。网站

知识点总结

上面已经说得够明白了,不须要再多说一遍。spa

相关文章
相关标签/搜索