C語言變量名必須:先定義(或者聲明)後再使用
編譯處理時,涉及變量及屬性(如:變量類型)的管理:
※ 插入:新變量定義
※ 查找:變量的引用
編譯處理中對變量管理:動態查找問題
利用查找樹(搜索樹)進行變量管理? 兩個變量名(字符串)比較效率不高
是否可以先把字符串轉換爲數字,再處理?
已知的幾種查找方法:
- 順序查找 O(N) ---要找的對象放在數組或鏈表裏,從頭到尾一個一個地找
- 二分查找(靜態查找) ---先排好序,再查找
- 二叉搜索樹 O(h) h爲二叉查找樹的高度
平衡二叉樹
二叉搜索樹或平衡二叉樹涉及到關鍵字的比較,對變量名來講是一個字符一個字符的比較,所以AVL樹用在剛剛的場景上,不是特別合適。
【例】在登錄QQ的時候,QQ服務器是如何覈對你的身份?面對龐大的用戶羣,如何快速找到用戶信息?
【分析】看看是否可以用二分法查找
【問題】如何快速搜索到需要的關鍵詞?如果關鍵詞不方便比較怎麼辦?
查找的本質:已知對象找位置。
□ 有序安排對象:全序、半序(某些關鍵字有序)
□ 直接“算出”對象位置:散列
☆ 散列查找法的兩項基本工 作:
□ 計算位置:構造散列函數確定關鍵詞存儲位置;
□ 解決衝突:應用某種策略解決多個關鍵詞位置相同的問題
☆ 時間複雜度幾乎是常量:O(1),即查找時間與問題規模無關!
1. 散列表(哈希表)
抽象數據結構描述
類型名稱: 符號表(SymbolTable)
數據對象集:符號表是“名字(Name)-屬性(Attribute)”對的集合
操作集:Table SymbolTable,Name NameType,Attr AttributeType 1、SymbolTable InitializeTable(int TableSize): 創建一個長度爲TableSize的符號表;
2、 Boolean IsIn( SymbolTable Table, NameType Name): 查找特定的名字Name是否在符號表Table中;
3、 AttributeType Find( SymbolTable Table, NameType Name):獲取Table中指定名字Name對應的屬性;
4、 SymbolTable Modify( SymbolTable Table, NameType Name, AttributeType Attr): 將Table中指定名字Name的屬性修改爲Attr;
5、SymbolTable Insert( SymbolTable Table, NameType Name, AttributeType Attr):向Table中插入一個新名字Name及其屬性Attr;
6、SymbolTable Delete( SymbolTable Table, NameType Name): 從Table中刪除一個名字Name及其屬性。 |
【例1】有n = 11個數據對象的集合{18, 23, 11, 20, 2, 7, 27, 30, 42, 15, 34}。
【處理】符號表的大小用TableSize = 17,選取散列函數h如下:h(key) = key mod TableSize (求餘)
□ 存放:
h(18) = 1,h(23) = 6,h(11) = 11,h(20) = 3,h(2) = 2,......
如果新插入35, h(35) = 1,該位置已有對象(18)!衝突!!
□ 查找:
※ key = 22,h(22) = 5,該地址空,不在表中
※ key = 30,h(30) = 13,該地址存放是30,找到!
裝填因子(Loading Factor):設散列表空間大小爲m,填入表中元素個數是n,則稱α = n / m爲散列表的裝填因子
本例中:α = 11 / 17 ≈ 0.65
【例2】將acos、define、float、exp、char、atan、ceil、floor、clock、ctime, 順次存入一張散列表中。
【處理】散列表設計爲一個二維數組Table[26][2],2列分別表示2個槽。
(如果有衝突,先來的放第一列,後來的放第二列)
如何設計散列函數h(key) = ?
h(key) = key[0] - 'a' (結果就得到0~25的整數)
按照字符串的首字母經過散列函數得到的結果放入Table中:
clock和ctime都無法放入,出現了溢出。可以發現:
散列的基本思想
“散列(Hashing)”的基本思想是:
(1)以關鍵字key爲自變量,通過一個確定的函數h(散列函數),計算出對應的函數值h(key),作爲數據對象的存儲地址。
(2)可能不同的關鍵字互映射到同一個散列地址上,即(當),稱爲“衝突(Collision)”。-------需要某種衝突解決策略
2.散列函數的構造方法
- 一個“好”的散列函數一般應考慮下列兩個因素:
- 計算簡單,以便提高轉換速度;
- 關鍵詞對應的地址空間分佈均勻,以儘量減少衝突。
- 數字關鍵詞的散列函數構造
- 直接定址法:取關鍵詞的某個線性函數值爲散列地址,即h(key) = a x key + b (a、b爲常數)
- 除留餘數法:散列函數爲:h(key) = key mod p
3. 數字分析法:分析數字關鍵字在各位上的變化情況,取比較隨機的位作爲散列地址
[例1]
[例2] 表格中藍色的數字更容易發生變化
4. 摺疊法:把關鍵字分割成位數相同的幾個部分,然後疊加
5. 平方取中法:希望結果能夠被更多的位數所影響
- 字符關鍵詞的散列函數構造
- 一個簡單的散列函數-----ASCII碼加和法
對字符型關鍵詞key定義散列函數如下:h(key) = (∑key[i]) mod TableSize,
【缺點】衝突嚴重:a3、b2、c1、eat、tea
2. 簡單的改進------前3個字符移位法
h(key) = (key[0] x 27² + key[1] x 27 + key[2]) mod TableSize
【缺點】仍然衝突:string、street、strong、structure等等;空間浪費:3000/ 26³ ≈ 30%
3. 好的散列函數----移位法
涉及關鍵詞所有n個字符,並且分佈得很好:
如何快速計算:
‘x’ * 32: 也就是x << 5
Index Hash(const char *Key, int TableSize) { unsigned int h = 0; /*散列函數值,初始化爲0*/ while ( *Key != '\0') /*位移映射*/ h = (h << 5) + *Key++; return h % TableSize; } |
【函數執行流程】
一開始,key指向a,h = 0 + ‘a’;然後key指向b,h = ‘a’ << 5 + b;接着key指向c,h = (‘a’<<5 + b)<<5 + c,......
3.衝突處理方法
常用處理衝突的思路:
- 換個位置:開放地址法
- 同一位置的衝突對象組織在一起:鏈地址法
3.1 開放地址法(Open Addressing)
一旦產生了衝突(該地址已有其他元素),就按某種規則去尋找另一空地址。
3.1.1 線性探測(Linear Probing)
線性探測法:以增量序列1,2,......,(TableSize - 1)循環試探下一個存儲地址。
【例1】設關鍵詞序列爲{47, 7, 29, 11, 9, 84, 54, 20, 30},
- 散列表表長TableSize = 13(裝填因子α = 9/13 ≈ 0.69)
- 散列函數爲:h(key) = key mod 11
用線性探測法處理衝突,列出依次插入後的散列表,並估算查找性能
從上面可以發現,但某個地方出現衝突的時候,衝突會越來越多,這就是線性探測的一個問題:“聚集”現象:
散列表查找性能分析
- 成功平均查找長度(ASLs)
- 不成功平均查找長度(ASLu)
散列表:
【分析】
ASLs:查找表中關鍵詞的平均查找比較次數(其衝突次數加1)
ASL s = (1 + 7 + 1 + 1 + 2 + 1 + 4 + 2 + 4)/ 9 = 23 / 9 ≈ 2.56 ---- 比如查找11,取餘結果爲0,一次就找到了;查找30,衝突了6次,第7次找到
ASLu:不在散列表中的關鍵詞的平均查找次數(不成功)
一般方法:將不在散列表中的關鍵詞分若干類。
如:根據H(key)值分類 (比如22,33,取餘結果爲0,但是現在查找表中這個位置的值爲11,並不表示22不在表中,可能因爲衝突在其他位置上,要按照衝突解決策略找下一個位置;下一個位置是1,值爲30,不是22,繼續往後挪發現是空位,就斷定22不在表中,所以餘數爲0的比較次數和過程是一樣的)
ASL u = (3 + 2 + 1 + 2 + 1 + 1 + 1 + 9 + 8 + 7 + 6)/ 11 = 41 / 11 ≈ 3.73
(括號中的加數依次表示餘數爲:0,1,2,3,4,5,6,7,8,9,10的同類型的數需要查找的次數)
散列函數爲:h(key) = key mod 11,因此分爲11類。
【例2】將acos、define、float、exp、char、atan、ceil、floor,順次存入一張大小爲26的散列表中。
H(key) = key[0] - 'a',採用線性探測d = i.
【處理】前5個都很順利放到了散列表中,沒有衝突。
接下來放第6個atan,應該放在0位置,但是已經有元素了,於是放到下一個位置:
接下來是ceil,計算之後應該放在2位置處,有衝突,於是不斷找下一個位置,一直到6位置:
接下來是floor,計算之後應該放在5處,有衝突,於是找下一個也有衝突,繼續找下一個,是空的,於是放在7處:
【分析】
ASLs:表中關鍵詞的平均查找比較次數
ASL s = (1 + 1 + 1 + 1 + 1 + 2 + 5 + 3) / 8 = 15 / 8 ≈ 1.87
ASLu:不在散列表中的關鍵詞的平均查找次數(不成功)
根據H(key)值分爲26種情況:H值爲0,1,2,...,25
ASL u = (9 + 8 + 7 + 6 + 5 +4 + 3 + 2 + 1*18) / 26 = 62 / 26 ≈ 2.38
(依次爲變量名以a、b、...、z開頭的關鍵詞要查找的次數)
3.1.2 平方探測法(Quadratic Probing) --- 二次探測
平方探測法:以增量序列1²,-1²,2²,-2²,......,q²,-q²且循環試探下一個存儲地址。
【例】設關鍵詞序列爲{47, 7, 29, 11, 9, 84, 54, 20, 30},
- 散列表表長TableSize = 11
- 散列函數爲:h(key) = key mod 11
用平方探測法處理衝突,列出一次插入後的散列表,並估算ASLs。
是否有空間,平方探測(二次探測)就能找得到?
結果就在0和2之間跳來跳去,就是找不到空位。這就是相比線性探測的不足之處,但是它比線性探測優在不太會出現“聚集”現象。
有定理顯示:如果散列表長度TableSize是某個4k + 3(k是正整數)形式的素數時,平方探測法就可以探查到整個散列表空間。
代碼實現
【1.表的初始化】
typedef struct HashTbl *HashTable;
struct HashTbl { /*散列表*/
int TableSize; //當前表的實際大小
Cell *TheCells; //數組
};
HashTable InitializeTable(int TableSize)
{
HashTable H;
int i;
if (TableSize < MinTableSize) {
Error("散列表太小");
return NULL;
}
/*分配散列表*/
H = (HashTable)malloc(sizeof(struct HashTbl));
if (H == NULL)
FatalError("空間溢出!!!");
H->TableSize = NextPrime(TableSize); //根據TableSize找到一個素數,例size = 12,就找到一個比它大的素數13
/*分配散列表Cells*/
H->TheCells = (Cell*)malloc(sizeof(Cell) * H->TableSize);
if (H->TheCells == NULL)
FatalError("空間溢出!!!");
for (i = 0; i < H->TableSize; i++)
H->TheCells[i].Info = Empty;
return H;
}
Cell數組中的每個元素也是一個結構體,包含Element、Info。Info就表示每個元素的狀態。
之所以要設計成一個結構,是因爲前面討論了往表中插入元素、查找元素,但是沒有說到過刪除元素。刪除元素不能真的從表裏把這元素移除,而是將該元素仍然放在表中,但是做個記號(Deleted),這樣對查找和插入的好處就出現了:查找的時候碰到被刪除的元素(因爲做了記號Deleted),就知道現在這個位置不是空位,還可以繼續往下找,但是如果真的移除了,變成空位就會產生誤判;插入的時候發現元素被刪掉了,不是空位,就可以直接替代原來的元素。
【2.平方探測查找】
/*平方探測*/
Position Find(ElementType Key, HashTable H)
{
Position CurrentPos, NewPos;
int CNum; /*記錄衝突次數*/
CNum = 0;
NewPos = CurrentPos = Hash(Key, H->TableSize); //Hash計算出應該放在什麼位置
while ( H->TheCells[NewPos].Info != Empty && H->TheCells[NewPos].Element != Key) {
/*字符串類型的關鍵詞需要strcmp函數!!*/
if (++CNum % 2) { /*判斷衝突的奇偶次*/
NewPos = CurrentPos + (CNum + 1)/2*(CNum + 1)/2;
while(NewPos >= H->TableSize) /*使NewPos落在TableSize中,也可以使用求餘的方法*/
NewPos -= H->TableSize;
} else {
NewPos = CurrentPos - CNum/2*CNum/2;
while(NewPos < 0) /*使NewPos落在TableSize中*/
NewPos += H->TableSize;
}
}
return NewPos;
}
其中的“(CNum + 1)/2*(CNum + 1)/2” 和 “CNum/2*CNum/2”是CNum和d之間的映射:
【3. 插入操作】
void Insert(ElementType Key, HashTable H)
{
Position Pos;
Pos = Find(Key, H);
if (H->TheCells[Pos].Info != Legitimate) { //空位或者被刪除
/*確認在此插入*/
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Element = Key;
/*字符串類型的關鍵詞需要strcpy函數!!*/
}
}
3.1.2 雙散列探測法(Double Hashing)
雙散列探測法:爲是另一個散列函數,探測序列成:
1. 對任一的key,
2. 探測序列還應該保證所有的散列存儲單元都應該能夠被探測到。選擇以下形式有良好的效果:
其中:p < TableSize, p、TableSize都是素數。
3.1.3 再散列(Rehashing)
- 當散列表元素太多(即裝填因子α太大)時,查找效率會下降;
(例如開始散列表爲11,已經裝了9個元素了,這時將散列表擴大到23,那麼原先的9個元素要重新根據23重新計算)
實用最大裝填因子一般取0.5 <= α <= 0.85
- 當裝填因子過大時,解決的方法是加倍擴大散列表,這個過程叫做“再散列(Rehashing)”
完整代碼實現
-
創建開放定址法的散列表
#define MAXTABLESIZE 100000 /* 允許開闢的最大散列表長度 */
typedef int ElementType; /* 關鍵詞類型用整型 */
typedef int Index; /* 散列地址類型 */
typedef Index Position; /* 數據所在位置與散列地址是同一類型 */
/* 散列單元狀態類型,分別對應:有合法元素、空單元、有已刪除元素 */
typedef enum { Legitimate, Empty, Deleted } EntryType;
typedef struct HashEntry Cell; /* 散列表單元類型 */
struct HashEntry{
ElementType Data; /* 存放元素 */
EntryType Info; /* 單元狀態 */
};
typedef struct TblNode *HashTable; /* 散列表類型 */
struct TblNode { /* 散列表結點定義 */
int TableSize; /* 表的最大長度 */
Cell *Cells; /* 存放散列單元數據的數組 */
};
int NextPrime( int N )
{ /* 返回大於N且不超過MAXTABLESIZE的最小素數 */
int i, p = (N%2)? N+2 : N+1; /*從大於N的下一個奇數開始 */
while( p <= MAXTABLESIZE ) {
for( i=(int)sqrt(p); i>2; i-- )
if ( !(p%i) ) break; /* p不是素數 */
if ( i==2 ) break; /* for正常結束,說明p是素數 */
else p += 2; /* 否則試探下一個奇數 */
}
return p;
}
HashTable CreateTable( int TableSize )
{
HashTable H;
int i;
H = (HashTable)malloc(sizeof(struct TblNode));
/* 保證散列表最大長度是素數 */
H->TableSize = NextPrime(TableSize);
/* 聲明單元數組 */
H->Cells = (Cell *)malloc(H->TableSize*sizeof(Cell));
/* 初始化單元狀態爲“空單元” */
for( i=0; i<H->TableSize; i++ )
H->Cells[i].Info = Empty;
return H;
}
- 平方探測法的查找與插入
Position Find( HashTable H, ElementType Key )
{
Position CurrentPos, NewPos;
int CNum = 0; /* 記錄衝突次數 */
NewPos = CurrentPos = Hash( Key, H->TableSize ); /* 初始散列位置 */
/* 當該位置的單元非空,並且不是要找的元素時,發生衝突 */
while( H->Cells[NewPos].Info!=Empty && H->Cells[NewPos].Data!=Key ) {
/* 字符串類型的關鍵詞需要 strcmp 函數!! */
/* 統計1次衝突,並判斷奇偶次 */
if( ++CNum%2 ){ /* 奇數次衝突 */
NewPos = CurrentPos + (CNum+1)*(CNum+1)/4; /* 增量爲+[(CNum+1)/2]^2 */
if ( NewPos >= H->TableSize )
NewPos = NewPos % H->TableSize; /* 調整爲合法地址 */
}
else { /* 偶數次衝突 */
NewPos = CurrentPos - CNum*CNum/4; /* 增量爲-(CNum/2)^2 */
while( NewPos < 0 )
NewPos += H->TableSize; /* 調整爲合法地址 */
}
}
return NewPos; /* 此時NewPos或者是Key的位置,或者是一個空單元的位置(表示找不到)*/
}
bool Insert( HashTable H, ElementType Key )
{
Position Pos = Find( H, Key ); /* 先檢查Key是否已經存在 */
if( H->Cells[Pos].Info != Legitimate ) { /* 如果這個單元沒有被佔,說明Key可以插入在此 */
H->Cells[Pos].Info = Legitimate;
H->Cells[Pos].Data = Key;
/*字符串類型的關鍵詞需要 strcpy 函數!! */
return true;
}
else {
printf("鍵值已存在");
return false;
}
}
3.2 分離鏈接法(Separate Chaining)
分離鏈接法:將相應位置上衝突的所有關鍵詞存儲在同一個單鏈表中
【例】設關鍵字序列爲47, 7, 29, 11, 16, 92, 22, 8, 3, 50, 37, 89,94,21;
散列函數取爲:h(key) = key mod 11;
用分離鏈接法處理衝突
- 表中有9個結點只需1次查找
- 5個結點需要2次查找
- 查找成功的平均查找次數:ASL s = (9+5*2) / 14 ≈ 1.36
實現
【1.結構體定義】
typedef struct ListNode *Position, *List;
struct ListNode
{
ElementType Element;
Position Next;
};
typedef struct HashTbl *HashTable;
struct HashTbl {
int TableSize;
List TheLists;
};
【2.查找操作】
Position Find(ElementType Key, HashTable H)
{
Position P;
int Pos;
Pos = Hash(Key, H->TableSize); /*初始散列位置*/
P = H->TheLists[Pos].Next; /*獲得鏈表頭,鏈表的第一個元素*/
while(P != NULL && strcmp(P->Element, Key))
P = P->Next;
return P;
}
完整代碼
- 分離鏈接法的散列表實現
#define KEYLENGTH 15 /* 關鍵詞字符串的最大長度 */
typedef char ElementType[KEYLENGTH+1]; /* 關鍵詞類型用字符串 */
typedef int Index; /* 散列地址類型 */
/******** 以下是單鏈表的定義 ********/
typedef struct LNode *PtrToLNode;
struct LNode {
ElementType Data;
PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;
/******** 以上是單鏈表的定義 ********/
typedef struct TblNode *HashTable; /* 散列表類型 */
struct TblNode { /* 散列表結點定義 */
int TableSize; /* 表的最大長度 */
List Heads; /* 指向鏈表頭結點的數組 */
};
HashTable CreateTable( int TableSize )
{
HashTable H;
int i;
H = (HashTable)malloc(sizeof(struct TblNode));
/* 保證散列表最大長度是素數,具體見代碼5.3 */
H->TableSize = NextPrime(TableSize);
/* 以下分配鏈表頭結點數組 */
H->Heads = (List)malloc(H->TableSize*sizeof(struct LNode));
/* 初始化表頭結點 */
for( i=0; i<H->TableSize; i++ ) {
H->Heads[i].Data[0] = '\0';
H->Heads[i].Next = NULL;
}
return H;
}
Position Find( HashTable H, ElementType Key )
{
Position P;
Index Pos;
Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */
P = H->Heads[Pos].Next; /* 從該鏈表的第1個結點開始 */
/* 當未到表尾,並且Key未找到時 */
while( P && strcmp(P->Data, Key) )
P = P->Next;
return P; /* 此時P或者指向找到的結點,或者爲NULL */
}
bool Insert( HashTable H, ElementType Key )
{
Position P, NewCell;
Index Pos;
P = Find( H, Key );
if ( !P ) { /* 關鍵詞未找到,可以插入 */
NewCell = (Position)malloc(sizeof(struct LNode));
strcpy(NewCell->Data, Key);
Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */
/* 將NewCell插入爲H->Heads[Pos]鏈表的第1個結點 */
NewCell->Next = H->Heads[Pos].Next;
H->Heads[Pos].Next = NewCell;
return true;
}
else { /* 關鍵詞已存在 */
printf("鍵值已存在");
return false;
}
}
void DestroyTable( HashTable H )
{
int i;
Position P, Tmp;
/* 釋放每個鏈表的結點 */
for( i=0; i<H->TableSize; i++ ) {
P = H->Heads[i].Next;
while( P ) {
Tmp = P->Next;
free( P );
P = Tmp;
}
}
free( H->Heads ); /* 釋放頭結點數組 */
free( H ); /* 釋放散列表結點 */
}
4.散列表的性能分析
- 平均查找長度(ASL)用來衡量散列表的查找效率:成功、不成功(成功:查找的元素在散列表裏;不成功:查找的元素不在散列表裏)
- 關鍵詞的比較次數,取決於產生衝突的多少。影響產生衝突多少有以下3個因素:
- 散列函數是否均勻;
- 處理衝突的方法;
- 散列表的裝填因子α。
分析:不同衝突處理方法、裝填因子對效率的影響
4.1 線性探測法的查找性能
可以證明,線性探測法的期望探測次數滿足下列公式:
當α = 0.5時,
- 插入操作和不成功查找的期望ASLu = 0.5 * (1 + 1/(1 - 0.5)²) = 2.5次
- 成功查找的期望ASLs = 0.5 * (1 + 1/(1-0.5)) = 1.5次
4.2 平方探測法和雙散列探測法的查找性能
可以證明,平方探測法和雙散列探測法探測次數滿足下列公式(理論值):
當α = 0.5時,
- 插入操作和不成功查找的期望ASLu = 1/(1-0.5) = 2次
- 成功查找的期望ASLs = - 1/0.5 * ln(1-0.5) ≈ 1.39次
4.3 線性和雙散列期望探測次數與裝填因子α的關係
4.4 分離鏈接法的查找性能
所有地址鏈表的平均長度定義成裝填因子α,α有可能超過1。
不難證明:其期望探測次數p爲:
當α = 1時,
- 插入操作和不成功查找的期望ASLu = 1 + = 1.37次;
- 成功查找的期望ASLs = 1 + 1/2 = 1.5次。
4.5 總結
選擇適合的h(key),散列法的查找效率期望是常數O(1),它幾乎與關鍵字的空間的大小n無關!也適合於關鍵字直接比較計算量大(如字符串比較)的問題
它是以較小的α爲前提。因此,散列方法是一個以空間換時間
散列方法的存儲對關鍵字是隨機的,不便於順序查找關鍵字,也不適合於範圍查找,或最大最小值查找。
4.5.1 開放地址法:
散列表是一個數組,存儲效率高,隨機查找。
散列表有“聚集”現象
4.5.3 分離鏈接法
散列表是順序存儲和鏈式存儲的結合,鏈表部分的存儲效率和查找效率都比較低。
關鍵字刪除不需要“懶惰刪除”法,從而沒有存儲“垃圾”。
太小的α可能導致空間浪費,大的α又將付出更多的時間代價。不均勻的鏈表長度導致時間效率的嚴重下降。
5. 應用實例:詞頻統計
應用:文件中單詞詞頻統計
【例】給定一個英文文本文件,統計文件中所有單詞出現的頻率,並輸出詞頻最大的前10%的單詞及其詞頻。
假設單詞字符定義爲大小寫字母、數字和下劃線(組成的序列),其他字符均認爲是單詞分隔符,不予考慮。
【分析】關鍵:對新讀入的單詞在已有單詞表中查找,如果已經存在,則將該單詞的詞頻加1,如果不存在,則插入該單詞並記詞頻爲1。
【問題】如何設計該單詞表的數據結構纔可以進行快速地查找和插入?
答:可以構造散列表。
【代碼實現】
- 程序框架
int main()
{
int TableSize = 10000; /*散列表的估計大小*/
int wordcount = 0, length;
ElementType word;
FILE *fp;
char document[30] = "HarryPotter.txt"; /*要被統計詞頻的文件名*/
HashTable H = InitializeTable(TableSize); /*建立散列表*/
if ((fp = fopen(document, "r")) == NULL)
FatalError("無法打開文件\n");
while (!feof(fp)) { //文件沒有結束
length = GetAWord(fp, word); /*從文件中讀取一個單詞,不斷讀取字符,直到它不是字母、數字和下劃線爲止*/
if (length > 3) { /*只考慮適當長度的單詞*/
wordcount++; /*統計文件中單詞總數*/
InsertAndCount(word, H); /*先查找,再插入*/
}
}
fclose(fp);
printf("該文檔共出現%d個有效單詞,", wordcount);
Show(H, 10.0/100); /*顯示詞頻前10%的所有單詞*/
DestroyTable(H); /*銷燬散列表*/
return 0;
}
其中Show(H, 10.0/100) 主要完成4件事:
(1)統計最大詞頻;(掃描散列表,找到出現次數最多的單詞)
(2)用一組數統計從1到最大詞頻的單詞數;(假設單詞出現次數最多的是100次,那就需要一個大小爲100的數組)
(3)計算前10%的詞頻應該是多少;
(4)輸出前10%詞頻的單詞。