一、 實驗目的
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; }
以上均為原創作品,歡迎大佬前來指正!