實驗——散列表(基於詞頻的文件相似度)詳細過程


一、  實驗目的   

1. 掌握散列表相關內容

2. 掌握倒排索引表的應用

二、  實驗內容和要求  

1. 問題描述   

      實現一種簡單原始的文件相似度計算,即以兩文件的公共詞匯占總詞匯的比例來定義相似度。為簡化問題,這里不考慮中文(因為分詞太難了),只考慮長度不小於3、且不超過10 的英文單詞,長度超過10的只考慮前10個字母。

2. 輸入格式                                                                        

      輸入首先給出正整數N(≤100),為文件總數。隨后按以下格式給出每個文件的內容:首先給出文件正文,最后在一行中只給出一個字符'#',表示文件結束。在N個文件內容結束之后,給出查詢總數M(≤10^4 ),隨后M行,每行給出一對文件編號,其間以空格分隔。這里假設文件按給出的順序從1到N編號。

3. 輸出格式                                                                        

       針對每一條查詢,在一行中輸出兩文件的相似度,即兩文件的公共詞匯量占兩文件總詞匯量的百分比,精確到小數點后1位。注意這里的一個“單詞”只包括僅由英文字母組成的、長度不小於3、且不超過10的英文單詞,長度超過10的只考慮前10個字母。單詞間以任何非英文字母隔開。另外,大小寫不同的同一單詞被認為是相同的單詞,例如“You”和“you”是同一個單詞。

4. 輸入樣例

 

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

5. 輸出樣例

50.0%
33.3%

三、算法設計

1. 主流程設計

int main 
{ 輸入文件總數; 讀入並存儲單詞,建立詞匯索引表; 輸入查詢次數; 輸入要查詢的文件編號; 輸出文件相似度;
return 0; }

 

2. 散列表的創建

    設計思路:存儲單詞以及對應所在的文件的索引,即倒排索引,通過構建針對字符串關鍵 字的散列表,來快速查找到某個單詞。

    具體實現:由於采用每個字母占5位的方法進行移位且輸入總規模不超過2MB,按每個單詞 最少占4 字節(3個字母+1個分隔符)計算,總詞匯表中最多要存50萬個單詞,則可以建 立一個散列表,規模為500009(大於50萬的最小素數),可保證插入不會失敗,並初始化 散列表。

 

HashTable CreateTable(int tableSize)  //建立散列表
{ HashTable H; H = (HashTable)malloc(sizeof(struct TableNode)); //H->tableSize = NextPrime(tableSize); //保證散列表最大長度是素數 H->tableSize = MAXTABLESIZE; H->heads = (HList)malloc(H->tableSize * sizeof(struct HashList));//分配鏈表頭結點數組 for (int i = 0; i < H->tableSize; i++) //並初始化表頭結點 { H->heads[i].data[0] = 0; H->heads[i].count = 0; H->heads[i].invIndex = NULL; } return H; }

 

3. 創建文件詞匯索引表

    設計思路:存儲文件到單詞的索引,可用文件表即為每個文件建立帶頭結點的索引鏈表, 存儲每個文件的詞匯量以及單詞在散列表中的位置。

    具體實現:創建頭結點數組F[]並初始化,這樣可以在在頭結點中存儲文件的詞匯量,在鏈 表結點中存儲詞匯表中單詞在散列表中的位置。

 

FList CreateFileIndex(int size)//初始化文件的詞匯索引表
{
    FList F;
    F = (FList)malloc(sizeof(struct WordList) * size);
    for (int i = 0; i < size; i++)
    {
       F[i].word = 0;
       F[i].next = NULL;
    }
    return F;
}

 

4. 讀入文件中的每個單詞

int Get(ElementType word)
{
    輸入第一個字符(即用於跳過非字母'\n');
    while (字符為非字母)
    {
        if (字符為文件結束符'#')
            文件結束,結束讀入單詞;
        else
            繼續輸入字符(直到輸入字符為字母時跳出循環);
    }
    while (字符為字母)
    {
        if (小寫字母)
        {
           if (單詞中字母個數小於十)//長度超過10的只考慮前10個字母
               將該字母存入數組中;
        }
        else if (大寫字母)
        {
           if (單詞中字母個數小於十)//長度超過10的只考慮前10個字母
               轉換成小寫字母存入數組中;
        }
        繼續輸入字符(直到輸入字符為非字母時跳出循環);
    }
    if (單詞中字母個數小於三)
        該單詞不符合要求,不能存入數組中,都下一個單詞;
    else
    {
        單詞讀入成功,將數組清零;
        成功返回;
    }
}

5. 查詢單詞

    設計思路:用線性探測法處理沖突。

    具體實現:先在文件詞匯索引表中查詢對應的文件名(取文件詞匯量較少的文件名),找 到對應文件名中的詞匯所在位置,根據此單詞的位置到散列表中查找單詞所在文件列表, 從而判斷該單詞是否是兩文件的公共詞匯。

int Find(HashTable H, ElementType key)
{
    初始散列位置;
    while (從該鏈表的第1個結點開始當未到表尾,並且key未找到時)
    {
        線性探測下一個位置;
        if (位置沒有被占用)
            調整為合法地址;
    }
    return 此時指向找到的結點位置,或者為0;
}

6. 自定義strcmp、strcpy函數

    設計思路:根據庫函數以及該題要求、需要用庫函數的內容功能,對字符串單詞進行比較 拷貝。

 

int mystrcmp(char a[], char b[])  //比較
{
     int i;
     for (i = 0; a[i] != '\0' && b[i] != '\0'; i++)
     {
          if (a[i] > b[i])
              return 1;
          else if (a[i] < b[i])
              return -1;
     }
     if (a[i] == '\0' && b[i] == '\0')
         return 0;
     else if (a[i] == '\0')
         return -1;
     else
         return 1;
}

void mystrcpy(char a[], char b[])  //拷貝
{
      int i = 0, j = 0;
      while ((a[i++] = b[j++]) != '\0');
}

 

7. ADT定義

 

#define MAXTABLESIZE 500009  //允許開辟的最大散列表長度
typedef enum { false, true } bool;

typedef struct FileList* FList;
struct FileList {
     int word;    //存儲文件詞匯量
     FList next;  //單詞在散列表中的位置
};

typedef struct WordList* WList;
struct WordList {
     int count;
     WList next;
};

typedef char ElementType[12];  //關鍵詞類型用字符串
typedef struct HashList* HList;
struct HashList {          //鏈表結點定義
     ElementType data;  //存放單詞
     int count;               //單詞個數,為0時表示結點為空
     WList invIndex;      //倒排索引
};

typedef struct TableNode* HashTable;  //散列表類型
struct TableNode {  //散列表結點定義
     int tableSize;     //表的最大長度
     HList heads;      //指向鏈表頭結點的數組
};

//自定義strcmp函數,用於比較兩個單詞(字符串)是否相同
int mystrcmp(char s1[], char s2[]);
//自定義strcpy函數,拷貝單詞(字符串)
void mystrcpy(char a[], char b[]);
bool IsLower(char ch);  //是否為小寫字母
bool IsUpper(char ch);  //是否為大寫字母

/*散列表初始化*/
HashTable CreateTable(int tableSize);

/*建立文件的詞匯索引表*/
FList CreateFileIndex(int size);

/*讀入文件的每個單詞*/
int Get(ElementType word);

/*字符串key移位法映射到整數的散列函數*/
int Hash(const char* key, int tableSize);

/*返回適合key插入的位置*/
int Find(HashTable H, ElementType key);

/*將key插入散列表,同時插入對應的倒排索引表*/
int InsertIndex(int count, HashTable H, ElementType key);

/*將單詞在散列表中的位置pos存入文件count對應的索引表*/
void FileIndex(FList file, int count, int pos);

/*計算文件f1和f2的相似度*/
double Similiar(FList file, int f1, int f2, HashTable H);

 

 

8. 算法示例

索引(文件詞匯索引表)
文件編號 對應文件單詞 對應存儲單詞 對應存儲單詞個數
1 Aaa\Bbb\Ccc aaa\bbb\ccc 3
2 Bbb\Ccc\Ddd bbb\ccc\ddd 3
3 Aaa\ccc\Eee\Ddd\Fff aaa\ccc\eee\ddd\fff 5

 

倒排索引(散列表)
文件單詞 存儲單詞 對應文件編號【該單詞在文件的位置】 對應出現在的文件個數
Aaa aaa 1[1], 3[1] 2
Bbb bbb 1[2], 2[1] 2
Ccc\ccc ccc 1[3], 2[2], 3[2] 3
Ddd ddd 2[3], 3[4] 2
Eee eee 3[5] 1
Fff fff 3[6] 1

 

四、算法分析

       通過算法過程示例發現,時間復雜度與文件單詞的讀入和存儲線性相關,O(n) = N^2,空間復 雜度為構建散列表和文詞匯索引表空間的使用,O(n) = N。

五、總結

       設計算法時,最初嘗試先將一個文件的詞頻統計構建出來,再建立兩個至多個文件的詞頻統計, 但因為一個到多個文件單詞的讀入存儲出現困難、插入單詞后查詢復雜等因素,最終將自己的 思路弄亂了。通過實驗指導書,采用倒排索引的方法重新構建散列表以及文件詞匯索引表,思 路才漸漸清晰,但也出現了一些問題,例如讀入文件問題,是采用一個一個文件通過讀入字符 串即整個文件內容存儲單詞,還是采用對每個文件中一個一個的字符的讀入存儲,后來發現對 於一個文件的詞頻統計,直接讀入文件整個字符串內容進行存儲處理較為容易,但對於多個文 件的詞頻統計,依次讀入文件的字符構建要求單詞進行存儲,為之后的插入索引創造便利,使 整個思路更加簡便清晰。最后是構建散列表規模導致運行超時的問題,知道要保證散列表最大 長度是素數,但是具體的分析數值也是看了實驗指導書得以解決。

 

六、源代碼(主要)

int InsertIndex(int count, HashTable H, ElementType key)  //將key插入散列表,同時插入對應的倒排索引表
{
    WList W;
    int pos = Find(H, key);  //檢查key是否已經存在
    if (H->heads[pos].count != count) {  //插入散列表
        if (!H->heads[pos].count)
            mystrcpy(H->heads[pos].data, key);  //如果單詞沒找到,插入新單詞
        H->heads[pos].count = count;  //更新文件
        W = malloc(sizeof(struct WordList));  //將文件編號插入倒排索引表
        W->count = count;
        W->next = H->heads[pos].invIndex;
        H->heads[pos].invIndex = W;
        return pos;  //插入成功
    }
    else
        return 0;  //重復單詞,不插入
}

void FileIndex(FList file, int count, int pos) {
    FList F;
    if (pos < 0) 
        return ;  //重復的單詞不處理
    F = malloc(sizeof(struct FileList));
    F->word = pos;
    F->next = file[count - 1].next;
    file[count - 1].next = F;
    file[count - 1].word++;  //頭結點累計詞匯量
}

double Similiar(FList file, int f1, int f2, HashTable H)  //計算文件f1和f2的相似度
{
    WList W;
    FList F;
    int i;
    if (file[f1 - 1].word > file[f2 - 1].word)  //使f1的詞匯量較小
    {
        i = f1;
        f1 = f2;
        f2 = i;
    }
    i = 0;  //相同詞匯量
    F = file[f1 - 1].next;
    while (F)
    {
        W = H->heads[F->word].invIndex;  //找到當前單詞在散列表中的位置
        while (W)  //遍歷該單詞的倒排索引表
        {
            if (W->count == f2)  //f2里也有該單詞
                break;
            W = W->next;
        }
        if (W)  //表示f1、f2都有該單詞
            i++;
        F = F->next;
    }
    double d = (i / (double)(file[f1 - 1].word + file[f2 - 1].word - i)) * 100;
    return d;
}

 

以上均為原創作品,歡迎大佬前來指正!


免責聲明!

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



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