哈希表基本概念介紹及哈希衝突的處理方法(附源碼)

@[TOC]算法

哈希表和哈希函數的概念

  哈希表(散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作哈希(散列)函數,存放記錄的數組叫作哈希(散列)表。數組

  給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能獲得包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。數據結構

  數據的哈希地址=f(關鍵字的值)dom

  哈希地址只是表示在查找表中的存儲位置,而不是實際的物理存儲位置。f()是一個函數,經過這個函數能夠快速求出該關鍵字對應的的數據的哈希地址,稱之爲「哈希函數」。函數

哈希函數的構造

直接定址法

  取關鍵字或關鍵字的某個線性函數值爲散列地址。即H(key)=keyH(key) = a·key + b,其中a和b爲常數(這種散列函數叫作自身函數)。若其中H(key)中已經有值了,就往下一個找,直到H(key)中沒有值了,就放進去。
  例若有一個從 1 歲到 100 歲的人口數字統計表
在這裏插入圖片描述
  假設其哈希函數爲第一種形式,其關鍵字的值表示最終的存儲位置。若須要查找年齡爲 25 歲的人口數量,將年齡 25 帶入哈希函數中,直接求得其對應的哈希地址爲 25(求得的哈希地址表示該記錄的位置在查找表的第 25 位)。通常用數組實現。spa

數字分析法

  若是關鍵字由多位字符或者數字組成,就能夠考慮抽取其中的 2 位或者多位做爲該關鍵字對應的哈希地址,在取法上儘可能選擇變化較多的位,避免衝突發生。
  好比一組員工的出生年月日,這時咱們發現出生年月日的前幾位數字大致相同,這樣的話,出現衝突的概率就會很大,可是咱們發現年月日的後幾位表示月份和具體日期的數字差異很大,若是用後面的數字來構成散列地址,則衝突的概率會明顯下降。所以數字分析法就是找出數字的規律,儘量利用這些數據來構造衝突概率較低的散列地址。指針

平方取中法

  對關鍵字作平方操做,取中間得幾位做爲哈希地址。此方法也是比較經常使用的構造哈希函數的方法。code

  例如關鍵字序列爲{421,423,436},對各個關鍵字進行平方後的結果爲{177241,178929,190096},則能夠取中間的兩位{72,89,00}做爲其哈希地址。blog

摺疊法

  例如,在圖書館中圖書都是以一個 10 位的十進制數字爲關鍵字進行編號的,若對其查找表創建哈希表時,就可使用摺疊法。圖片

  若某書的編號爲:0-442-20586-4,分割方式如圖 1 中所示,在對其進行摺疊時有兩種方式:一種是移位摺疊,另外一種是間界摺疊:

  • 移位摺疊是將分割後的每一小部分,按照其最低位進行對齊,而後相加,以下圖 (a);
  • 間界摺疊是從一端向另外一端沿分割線來回摺疊,以下圖(b)。

在這裏插入圖片描述

除留餘數法(經常使用)

  取關鍵字被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即 H(key) = key MOD p,p<=m。不只能夠對關鍵字直接取模,也可在摺疊、平方取中等運算以後取模。
  對p的選擇很重要,通常取素數或m,若p選的很差,容易產生同義詞(即衝突)。
  由經驗得知 p 能夠爲不大於 m 的質數或者不包含小於 20 的質因數的合數

隨機數法

  取關鍵字的一個隨機函數值做爲它的哈希地址,即:
H(key)=random(key),此方法適用於關鍵字長度不等的狀況。

  注意:這裏的隨機函數實際上是僞隨機函數,隨機函數是即便每次給定的 key 相同,可是 H(key)都是不一樣;而僞隨機函數正好相反,每一個 key 都對應的是固定的 H(key)

哈希函數的選擇

  如此多的構建哈希函數的方法,在選擇的時候,須要根據實際的查找表的狀況採起適當的方法。一般考慮的因素有如下幾方面:

  • 關鍵字的長度。若是長度不等,就選用隨機數法。若是關鍵字位數較多,就選用摺疊法或者數字分析法;反之若是位數較短,能夠考慮平方取中法;
  • 哈希表的大小。若是大小已知,能夠選用除留餘數法;
  • 關鍵字的分佈狀況;
  • 查找表的查找頻率;
  • 計算哈希函數所需的時間(包括硬件指令的因素)

處理衝突的方法

  哈希衝突只能儘可能減小可是不能徹底避免了,一般處理哈希衝突的方法有如下幾種

開放定址法

  H(key)=(H(key)+ d)MOD m(其中 m 爲哈希表的表長,d 爲一個增量)
  當得出的哈希地址產生衝突時,選取如下 3 種方法中的一種獲取 d 的值,而後繼續計算,直到計算出的哈希地址不在衝突爲止,這 3 種方法爲:

  • 線性探測法:d=1,2,3,…,m-1
  • 二次探測法:d=12,-12,22,-22,32,…
  • 僞隨機數探測法:d=僞隨機數

  例如,在長度爲 11 的哈希表中已填寫好 1七、60 和 29 這 3 個數據(如圖(a) 所示),其中採用的哈希函數爲:H(key)=key MOD 11,現有第 4 個數據 38 ,當經過哈希函數求得的哈希地址爲 5,與 60 衝突,則分別採用以上 3 種方式求得插入位置如圖 (b)所示:
在這裏插入圖片描述
  註釋:在線性探測法中,當遇到衝突時,從發生衝突位置起,每次 +1,向右探測,直到有空閒的位置爲止;二次探測法中,從發生衝突的位置起,按照 +12,-12,+22,…如此探測,直到有空閒的位置;僞隨機探測,每次加上一個隨機數,直到探測到空閒位置結束。

再哈希法

  當經過哈希函數求得的哈希地址同其餘關鍵字產生衝突時,使用另外一個哈希函數計算,直到衝突再也不發生。

鏈地址法

  將全部產生衝突的關鍵字所對應的數據所有存儲在同一個線性鏈表中。例若有一組關鍵字爲{19,14,23,01,68,20,84,27,55,11,10,79},其哈希函數爲:H(key)=key MOD 13,使用鏈地址法所構建的哈希表以下圖 所示:
在這裏插入圖片描述

創建一個公共溢出區

  創建兩張表,一張爲基本表,另外一張爲溢出表。基本表存儲沒有發生衝突的數據,當關鍵字由哈希函數生成的哈希地址產生衝突時,就將數據填入溢出表。

代碼實現

  在哈希表中進行查找的操做同哈希表的構建過程相似,其具體實現思路爲:對於給定的關鍵字K,將其帶入哈希函數中,求得與該關鍵字對應的數據的哈希地址,若是該地址中沒有數據,則證實該查找表中沒有存儲該數據,查找失敗:若是哈希地址中有數據,就須要作進一步的證實(排除衝突的影響),找到該數據對應的關鍵字同K 進行比對,若是相等,則查找成功;反之,若是不相等,說明在構造哈希表時發生了衝突,須要根據構造表時設定的處理衝突的方法找到下一個地址,同地址中的數據進行比對,直至遇到地址中數據爲NULL(說明查找失敗),或者比對成功

/*
 * @Author: Carlos
 * @Date: 2020-07-2 23:48:50
 * @LastEditTime: 2020-07-2 23:48:50
 * @LastEditors: Carlos
 * @Description: Hash
 */
#include "stdio.h"
#include "stdlib.h"
#define HASHSIZE 7 //定義散列表長爲數組的長度
#define NULLKEY -1
typedef struct
{
    int *elem; //數據元素存儲地址,動態分配數組
    int count; //當前數據元素個數
} HashTable;
/**
 * @Description: 哈希函數初始化
 * @Param: HashTable *hashTable 結構體指針
 * @Return: 無
 * @Author: Carlos
 */
void Init(HashTable *hashTable)
{
    int i;
    hashTable->elem = (int *)malloc(HASHSIZE * sizeof(int));
    hashTable->count = HASHSIZE;
    for (i = 0; i < HASHSIZE; i++)
    {
        hashTable->elem[i] = NULLKEY;
    }
}
/**
 * @Description: 哈希函數(除留餘數法)
 * @Param: int data 哈希的數據
 * @Return: 哈希後data存儲的地址
 * @Author: Carlos
 */
int Hash(int data)
{
    return data % HASHSIZE;
}
/**
 * @Description: 哈希表的插入函數,可用於構造哈希表
 * @Param: HashTable *hashTable 結構體指針,int data 哈希的數據
 * @Return: 無
 * @Author: Carlos
 */
void Insert(HashTable *hashTable, int data)
{
    int hashAddress = Hash(data); //求哈希地址
    //發生衝突
    while (hashTable->elem[hashAddress] != NULLKEY)
    {
        //利用開放定址法解決衝突
        hashAddress = (++hashAddress) % HASHSIZE;
    }
    hashTable->elem[hashAddress] = data;
}
/**
 * @Description: 哈希表的查找算法
 * @Param: HashTable *hashTable 結構體指針,int data 哈希的數據
 * @Return: 無
 * @Author: Carlos
 */
int Search(HashTable *hashTable, int data)
{
    int hashAddress = Hash(data); //求哈希地址
    while (hashTable->elem[hashAddress] != data)
    { //發生衝突
        //利用開放定址法解決衝突
        hashAddress = (++hashAddress) % HASHSIZE;
        //若是查找到的地址中數據爲NULL,或者通過一圈的遍歷回到原位置,則查找失敗
        if (hashTable->elem[hashAddress] == NULLKEY || hashAddress == Hash(data))
        {
            return -1;
        }
    }
    return hashAddress;
}
int main()
{
    int i, result;
    HashTable hashTable;
    int arr[HASHSIZE] = {13, 29, 27, 28, 26, 30, 38};
    //初始化哈希表
    Init(&hashTable);
    //利用插入函數構造哈希表
    for (i = 0; i < HASHSIZE; i++)
    {
        Insert(&hashTable, arr[i]);
    }
    //調用查找算法
    result = Search(&hashTable, 29);
    if (result == -1)
        printf("查找失敗");
    else
        printf("29 在哈希表中的位置是:%d", result + 1);
    return 0;
}

如遇到排版錯亂的問題,能夠經過如下連接訪問個人CSDN。

**CSDN:[CSDN搜索「嵌入式與Linux那些事」]

相關文章
相關標籤/搜索