MOOC 數據結構 | 11. 散列查找

C語言變量名必須:先定義(或者聲明)後再使用

編譯處理時,涉及變量及屬性(如:變量類型)的管理:

              ※ 插入:新變量定義

              ※ 查找:變量的引用

編譯處理中對變量管理:動態查找問題

利用查找樹(搜索樹)進行變量管理? 兩個變量名(字符串)比較效率不高

是否可以先把字符串轉換爲數字,再處理?

已知的幾種查找方法

  • 順序查找     O(N)                                         ---要找的對象放在數組或鏈表裏,從頭到尾一個一個地找
  • 二分查找(靜態查找)  O(log_2N)              ---先排好序,再查找     
  • 二叉搜索樹                O(h)  h爲二叉查找樹的高度

       平衡二叉樹                O(log_2N)   

二叉搜索樹或平衡二叉樹涉及到關鍵字的比較,對變量名來講是一個字符一個字符的比較,所以AVL樹用在剛剛的場景上,不是特別合適。

【例】在登錄QQ的時候,QQ服務器是如何覈對你的身份?面對龐大的用戶羣,如何快速找到用戶信息?

【分析】看看是否可以用二分法查找

【問題】如何快速搜索到需要的關鍵詞?如果關鍵詞不方便比較怎麼辦?

   查找的本質:已知對象找位置。

                 □ 有序安排對象:全序、半序(某些關鍵字有序)

                 □ 直接“算出”對象位置:散列              

☆ 散列查找法的兩項基本工 作:

                  □ 計算位置:構造散列函數確定關鍵詞存儲位置;

                  □ 解決衝突:應用某種策略解決多個關鍵詞位置相同的問題 

☆ 時間複雜度幾乎是常量:O(1),即查找時間與問題規模無關!

1. 散列表(哈希表)

抽象數據結構描述

類型名稱 符號表(SymbolTable)

 

數據對象集符號表是“名字(Name)-屬性(Attribute)”對的集合

 

操作集Table \epsilon SymbolTable,Name \epsilon NameType,Attr \epsilon 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)可能不同的關鍵字互映射到同一個散列地址上,即h(key_i)= h(key_j)(當key_i\neq key_j),稱爲“衝突(Collision)”。-------需要某種衝突解決策略

 

2.散列函數的構造方法

  • 一個“好”的散列函數一般應考慮下列兩個因素
    1. 計算簡單,以便提高轉換速度;
    2. 關鍵詞對應的地址空間分佈均勻,以儘量減少衝突。
  • 數字關鍵詞的散列函數構造
    1. 直接定址法:取關鍵詞的某個線性函數值爲散列地址,即h(key) = a x key + b (a、b爲常數)  
    2. 除留餘數法:散列函數爲:h(key) = key mod p 

                      

                 3. 數字分析法:分析數字關鍵字在各位上的變化情況,取比較隨機的位作爲散列地址

                   [例1]

                                       

                      

                  [例2] 表格中藍色的數字更容易發生變化

                    

                 4. 摺疊法:把關鍵字分割成位數相同的幾個部分,然後疊加

                 

                5. 平方取中法:希望結果能夠被更多的位數所影響

                

  • 字符關鍵詞的散列函數構造
    1. 一個簡單的散列函數-----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 = 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²{\color{Blue} q \leq \left \lfloor TableSize / 2 \right \rfloor}循環試探下一個存儲地址。

【例】設關鍵詞序列爲{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_i之間的映射:

【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)

雙散列探測法d_i{\color{Blue} i*h_2(key)},h_2(key)是另一個散列函數,探測序列成:{\color{Blue} h_2(key),2h_2(key),3h_2(key),......}

1. 對任一的key,{\color{Blue} h_2(key)\neq 0}

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個因素
    1. 散列函數是否均勻;
    2. 處理衝突的方法
    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 + e^{-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%詞頻的單詞。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章