PTA習題解析——基於詞頻的文件相似度


禁止碼迷,布布扣,豌豆代理,碼農教程,愛碼網等第三方爬蟲網站爬取!

基於詞頻的文件相似度

情景需求

測試樣例

輸入樣例

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;      //單個分片的單詞
    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 個文件中,就修正重復單詞數和重復單詞數,若進出現在一個文件就只修正重復單詞數。這里單詞的規模會對效率進行限制,不過確定單詞存在於那些文件的速度是很快的,可以用下標直接訪問數組。

偽代碼

代碼實現

#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 種不同信息的表,這為我添加更多的功能提供了基礎。例如可以對多個文件進行總體的詞頻統計,這個就可以利用單詞索引表實現,而僅僅有文件單詞表就需要把所有表全部都遍歷一遍,那效率就太低了!

偽代碼

代碼實現

#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 的值的用法我很久沒寫了,因此忘記了不需要另外申請空間,每次構建映射時我都申請一個很大的數組。最后調試時,我忘記刪掉了這個操作,這就導致了每處理一個單詞,就要申請一大堆空間,這樣空間就被快速地消耗了。刪除這個操作就可以解決內存超限的問題,這還是我第一次遇到。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM