程序相似性判斷
一、問題分析
基本任務:
對於兩個C語言的源程序清單,用哈希表的方法分別統計兩程序中使用C語言關鍵字的情況,並最終按定量的計算結果,得出兩份源程序清單的相似性。
任務要求:
C語言關鍵字的Hash表可以自建,也可以如實現提示中那樣構建。此題的主要工作是掃描給定的源程序,累計在每個源程序中C語言關鍵字出現的頻度。在掃描源程序過程中,每遇到關鍵字就查找Hash表,並累加相應關鍵字出現的頻度。為保證查找效率,建議Hash表的平均查找長度ASL不大於2。
二、程序設計
解決方案:
本程序根據要求和實現提示設計思路,首先要實現在哈希表中的關鍵字查找和統計先要設計哈希表的結構以及使用到的操作並進行實現,其次要實現對C語言程序中所有字符串在哈希表中進行查找,會多余出大量的無效查找增加哈希表的查找次數和時間復雜度,所以根據實現提示,采用查找速度更快捷、占用空間更小的鍵樹結構進行過濾,所以下一步設計C語言關鍵字的鍵樹結構並實現過濾操作,程序一邊讀取C程序清單中的字符串,一邊將字符串在鍵樹結構中進行查找,如果存在則繼續在哈希表中進行查找,查詢成功一次則將哈希表結點中的nvalue值加一,記錄頻數,最後,實現主體函數的設計,即整個程序中主調程序的實現過程。
方案理論:
(1) 哈希表構建的具體理念
本程序哈希表結構的構造方法采用較為簡單且常用的除留余數法,以除留余數法構造哈希函數並進行關鍵字尋址,采用較為簡單且方便有效的二次探測再散列的方法處理哈希結點構造中的沖突。
C語言有32個關鍵字,假設以二次探測再散列處理沖突,為達到ASL≤2,則要求裝載因子a≤0.795,因關鍵字個數n=32,應取表長m>40。對於二次探測再散列,應取4j+3型的素數,故設表長m=43。最後根據所設表長設計Hash函數。采用除留余數法,並取p=41,設
hash(key)=[(key的首字符序號)*100+(key的尾字符序號)] mod 41
沖突處理方法為
Hi+1,
(2)鍵樹構建的具體理念
本題很大的工作量將是對源程序掃描,區分出C程序的每一關鍵字。可以為C語言關鍵字建一棵鍵樹,掃描源程序和在鍵樹中查找同步進行,以取得每一個關鍵字。使用查找速度較快的順序數組存儲結構建立Trie樹,在鍵樹結構中設置根節點並其中記錄孩子結點數目和孩子結點的數組,分支結點的構造類似根節點,在葉子結點中設置指向存儲關鍵字字符串的指針,從而構建起字典查詢鍵樹。
(3)程序相似性判斷的具體理念
根據程序1和程序2中關鍵字出現的頻度,可提取到兩個程序的特征向量X1和X2。
一般情況下,可以通過計算向量
其中 。 的值介於[0, 1]之間,也稱廣義余弦,即 ,當 時,顯見 ;當 和 差別很大時, 的值近似為0,θ就接近於∏/2。
盡管 和 的值是一樣的,但直觀上Xi與Xj更相似。因此當S值接近於1時,為避免誤判相似性(可能是夾角很小,模值差很大的向量),應當再次計算Xi與Xj之間的“幾何距離” 。其計算公式為
最後的相似性判別計算可分兩步完成:
① 用公式1計算S,把接近1的保留,拋棄接近0的情況(即排除不相似者)
② 對保留下來的特征向量,再用公式2計算D,如D值也比較小,說明兩者對應的程序確實可能相似。並根據實際經驗給出S>0.9,D<10時說明兩個程序的相似程度較高。
(4) 設計理論誤差說明:
這種判斷方法只是提供一種輔助手段,即便S=1也可能不是同一個程序,S的值很小,也可能算法完全是一樣的。例如一個程序使用while語句,另一個使用for語句,但功能完全相同。事實上,當發現S的值接近於1且D又很小時,就應該以人工幹預來區分。
抽象數據定義:
(1)Trie樹:
ADT List{
數據對象:D={}
數據關系:RI={,i=2,….n}
基本操作:
Node* CreateTrie();
操作結果:構造一個Trie樹。
void insert_node(Trie_node root, char *str) ;
初始條件:Trie樹已經存在。
操作結果:在Trie樹中插入結點,結點字符串為str。
int search_str(Trie_node root, char *str);
初始條件:Trie樹已經存在。
操作結果:在Trie樹中查找字符串str,存在返回1,否則返回0。
void del(Trie_node root) ;
初始條件:Trie樹已經存在。
操作結果:釋放整個Trie樹的內存空間。
}
(1)哈希表:
ADT List{
數據對象:D={}
數據關系:RI={,i=2,….n}
基本操作:
void hash_table_init();
操作結果:初始化哈希表,並為其分配內存空間。
int hash_table_hash_str(char* skey);
初始條件:哈希表已經存在。
操作結果:構造哈希函數,返回值為哈希函數計算的地址。
HashNode* hash_table_lookup(char* skey) ;;
初始條件:哈希表已經存在。
操作結果:在哈希表中查找字符串str 。如果查找成功則將字符串的nvalue值加1。
void hash_table_insert(char* skey);
初始條件:哈希表已經存在。
操作結果:向哈希表中插入字符串skey。
void hash_table_print();
初始條件:哈希表已經存在。
操作結果:打印哈希表中的結點。
void hash_table_release();
初始條件:哈希表已經存在。
操作結果:釋放哈希表的存儲空間。
void hash_table_revalue();
初始條件:哈希表已經存在。
操作結果:使哈希表中所有結點nvalue值為0。
void hash_table_nvalue();
初始條件:哈希表已經存在。
操作結果:將哈希表中的所有nvalue輸入到value1.txt文件中。
}
函數調用圖:
三、程序源碼(偽代碼,具體代碼實現可以參考github地址:)
Trie樹:
#define定義分支結點的最大個數
typedef struct構建Trie樹的模板
{
int 標記該節點是否可以形成一個單詞
struct Tree *聲明它的子樹結點
}Node, *Trie_node;
Node* CreateTrie()創建Trie樹
{
為Trie樹的葉子結點分配空間
分配空間
返回葉子結點指針
}
//Trie樹的結點插入
void insert_node(Trie_node root, char *str)
{
if根節點為NULL或者字符串為空
中斷插入並返回
聲明一個指向根結點的指針
聲明一個指向字符串的指針
while被指向的字符不為結尾空字符
{
if根結點的孩子中這個字母為首的結點為空
{
創建一個結點
將孩子結點指向這個所建立的葉子結點
}
指針指向子節點
字符串指針後移
}
標記該結點為1,即存在指向字符串的指針
}
int search_str(Trie_node root, char *str)查找串是否在該trie樹中
{
if如果Trie樹為空或者待查找的字符串為空
{
中斷並返回
}
否則聲明指針指向字符串即首個字符
/聲明指針指向Trie樹根節點;
while 不到到字符串尾
// 依次逐層查找
}
void del(Trie_node root)釋放整個Trie樹占的堆空間
{
//循環不為空,直接釋放
free(root);釋放結點的空間
}
Hash表:
#define哈希表的數組總個數
typedef struct HashNode_Struct HashNode;/聲明哈希表的結點結構
struct HashNode_Struct//寫出哈希表的結點模板
{
哈希結點中的關鍵字
哈希結點中的權值
記錄沖突的次數
};
//定義哈希表
//定義哈希表中實際數組的大小
//初始化哈希表
void hash_table_init()
//構造哈希函數
int hash_table_hash_str(char* skey)
{
設置指針指向要傳遞的字符串
聲明一個新的int值
循環算一下字符的長度並依次向後移動指針
if如果指針指向了空的地方
break;//跳出循環
計算哈希值
返回計算出的數組序號
}
//將結點插入哈希表
void hash_table_insert(char* skey)
{
if判斷哈希表的實際元素數目是否已經超過了哈希表的規定數組長度
{
並中斷返回
}
通過哈希函數計算出數組的序號
聲明一個指針指向待插向的結點
while這個結點已經沒被占用了
{
if這個結點的值與帶插入的結點值相等
{ return;//中斷並返回
}
else //繼續進行二測探測
}
聲明一個新的結點並為其分配向量空間
分配指針空間
為結點的關鍵字分配動態存儲空間
復制待插入字符串到剛剛分配的結點的關鍵字
將待出入的value值付給剛剛建立的結點的value
hash_table_size++;//哈希表的實際長度加一
}
//在哈希表中查找一個關鍵字
HashNode* hash_table_lookup(char* skey)
{
通過哈希函數計算出數組的序號;
if對應結點存在值
{
聲明一個新的結點指針指向該結點
while新的結點指針指向的結點的值存在
{
if
{//如果要查找的關鍵字與指向結點道德關鍵字相等
nvalue+1;
返回該節點
}
//繼續進行二測探測
}
如果對應結點值不存在,則返回空值
}
//打印哈希表中的存在的結點
void hash_table_print()
{
for從頭遍歷哈希表的結點數組
{
聲明新的結點指針指向該結點
if 結點1存在
{
/打印結點
}
}
}
//釋放整個哈希表的內存空間
void hash_table_release()
{
for從頭遍歷哈希表的結點數組
{
if如果結點不為NULL
{
釋放這個結點的空間吧
}
}
}
//使哈希表中的所有權值為0
void hash_table_revalue()
{ for//從頭遍歷哈希表的結點數組
{
//使結點的權值為0
}
}
void hash_table_nvalue()
{
//從頭遍歷哈希表的結點數組
{
輸入文件中
}
主程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "Hash.h" //調用哈希結構頭文件
#include "Trie.h" //調用鍵樹結構頭文件
聲明chengxu函數
int main()
{
聲明一個存儲關鍵字的二維數組
將keywords文件讀入並打印
//對C語言中的關鍵字建立鍵樹
聲明字符數組存儲字符串
聲明一個鍵樹的根節點
初始化一個鍵樹
循環在鍵樹中依次插入之前數組存儲的關鍵字
//對C語言中的關鍵字建立哈希表
初始化哈希表
循環在哈希表中依次插入關鍵字
輸出哈希表
//讀取並計算第一個源程序
打印提示操作的字符
聲明存儲程序1文件名的數組
獲取輸入,並將其存儲到chengxu1數組中
聲明打開程序1文件的指針
聲明一個char字符,用於讀取傳遞
聲明一個words數組,存儲從chegxu1文件中讀取的字符
while (循環到文件尾
{
if如果ch1是小寫英文字母,words依次數組讀取
else if ((如果ch1不是英文字母判斷words首個字符是否為英文字符
{
if (如果是則查詢鍵樹
h鍵樹存在則查詢哈希
將i值重新賦值為0
將words字符數組重新賦值為‘\n‘
}
else如果words數組首字符不是英文字母,將words數組重新歸為空字符串
}
關閉文件
打印第一次查詢後的哈希表狀態
//調用頭文件中函數,將哈希表中的頻數寫入value1.txt文件
聲明int數組
將value1.txt文件中的值讀入value1數組
將哈希表中的所有頻數歸為0
將釋放Trie樹空間
釋放哈希表的存儲空間
//對C源程序2進行查詢計算
調用函數讀取下一個程序文件,並將頻數寫入value1.txt文件
聲明數組value2
將value1.txt文件中的頻數寫入value2數組中
//進行相似度計算求解
//第一步進行第一個公式計算
//第二步進行第二個公式計算
//給出結論
return 0;
}
四、程序分析
算法分析:
(1)程序算法的特性分析
本程序滿足算法的基本特性要求,具備有窮性、確定性、可行性且輸入輸出合理。
(2)程序算法的設計要求分析
本程序滿足程序算法的設計要求,具備正確性、可讀性、健壯性和效率與低存儲量需求。
(3)程序算法的時間復雜度分析。
Trie的核心思想是空間換時間,利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的,假設字符的種數有m個,有若幹個長度為n的字符串構成了一個Trie樹,則每個節點的出度為m(即每個節點的可能子節點數量為m),Trie樹的高度為n。很明顯我們浪費了大量的空間來存儲字符,此時Trie樹的最壞空間復雜度為O(m^n)。也正由於每個節點的出度為m,所以我們能夠沿著樹的一個個分支高效的向下逐個字符的查詢,而不是遍歷所有的字符串來查詢,此時Trie樹的最壞時間復雜度為O(n)。這正是空間換時間的體現,也是利用公共前綴降低查詢時間開銷的體現。由於本程序中一共有C語言關鍵字32個,每個關鍵字長度不超過10,所以本程序中每次查詢Trie樹的時間復雜度最壞不超10,平均查找長度為5*26=130。
Hash表的時間復雜度較小可以達到常數級,在本程序中哈希表的裝填因子為α,≈0.744,平均查找長度ASL≈1.84。查找速度較快,時間復雜度較低。
(4)程序算法的空間復雜度分析
本程序的算法中,通常來說Trie樹的空間的復雜度較高,但是本程序為分支結點僅開辟一個標誌字符的空間,利用指針指向字符串所在實際空間,所以本程序中的Trie樹空間復雜度較為理想,而Hash表中開辟了等於表長個數的結點,即為43個結點空間,所以哈希表中的空間復雜度較Trie樹大一些,但哈希表的查找時間效率更高。
改進設想:
本程序雖然能夠準確統計兩個C語言源程序的關鍵字出現頻數,並根據公式計算得出對兩個C語言程序相似度的判斷,但是仍然存在著較大的改進空間。
(1)對於Trie樹結構的改進設想:Trie樹本身的結構就是一個較為理想的字典查詢結構,但是由於分支結點的設置會造成較多的未利用空間,而之前在一片博客了解到了DAT(Double-Array Tire)雙數組Trie樹的存在,在DAT中用的就是雙數組:base數組和check數組。雙數組的分工是:base負責記錄狀態,用於狀態轉移;check負責檢查各個字符串是否是從同一個狀態轉移而來,當check[i]為負值時,表示此狀態為字符串的結束。這種結構能夠較為有效的解決Trie樹浪費空間的問題。
(2)對於哈希表結構的改進設想:當關鍵字的集合是一個不變的靜態集合時,哈希技術還可以用來獲取出色的最壞情況性能。如果某一種哈希技術在進行查找時,其最壞情況的內存訪問次數為 O(1) 時,則稱其為完美哈希(Perfect Hashing)。設計完美哈希的基本思想是利用兩級的哈希策略,而每一級上都使用全域哈希。完美哈希的主要思想是提供一種避免哈希沖突的解決思路與方法。
(3)對於本程序中的主體程序函數以及算法思想的改進設想:本程序能夠較為準確高效的完成對兩個C語言程序代碼相似性進行判斷的任務,但仍然存在一定改進空間,我對本程序的主體程序提出了一下三點改進設想:
① 在解決進行兩次源程序文件查詢統計產生的沖突時,如果能夠設計更理想的查詢統計函數,即能夠將文件名作為參數傳遞給求解函數能夠大大減少代碼的數。
② 而且如果能夠舍去value1.txt文件的使用,直接將頻數數據分別傳入到value1和value2數組中,會省去寫入與讀取文件的函數操作,會使程序更為簡潔。
③ 如果能夠將對於相似性判斷的S、D值求解過程重新設計外部函數,通過調用實現計算並給出合理結論,能夠減輕主程序的代碼量負擔。使程序更為美觀。
五、程序截圖
程序相似性判斷