hash表的實現和hash桶的示例(c實現)

hash是以空間換時間的結構,現在空間越來越大,而且對性能要求越來越高的年代,這絕對是值得的。

hash含義就是散列,就是把我們本來想​查找的一大羣結構體數據分散開,更容易查找。一個好的hash函數應該做到對所有元素平均分散排列,儘量避免或者降低他們之間的衝突(Collision)。hash函數的選擇必須慎重,如果不幸所有的元素之間都產生了衝突,那麼hash表將退化爲鏈表,其性能會大打折扣,時間複雜度迅速降爲O(n),絕對不要存在任何僥倖心理,因爲那是相當危險的。歷史上就出現過利用Linux內核hash函數的漏洞,成功構造出大量使hash表發生碰撞的元素,導致系統被DoS,所以目前內核的大部分hash函數都有一個隨機數作爲參數進行摻雜,以使其最後的值不能或者是不易被預測。這又對 hash函數提出了第二點安全方面的要求:hash函數最好是單向的,並且要用隨機數進行摻雜。提到單向,你也許會想到單向散列函數md4和md5,很不幸地告訴你,他們是不適合的,因爲hash函數需要有相當好的性能。

散列函數: 將字符串等傳入我們的散列函數,而散列函數的職責就是給我們返回一個value值,我們通過這個值引做hash表下標去訪問我們想要查找的數據;例如這樣的函數:

int Hash(char *key, int TableSize)
{
     unsigned int HashVal = 0;
     while(*key != '\0')
             HashVal += *key++;
     return HashVal % TableSize;
}

這就是一個簡單的hash函數,就是把我們傳入過來的key(由我們的數據中一個或者多個結構體成員的成員來作爲key)來得到一個返回值,這個返回值就是我們的value值。

一個好的hash​函數就是把我們的說有數據儘可能均勻的分散在我們預設的TableSize大小的hash表中。哈希表的幾種方法:

1、直接定址法:取關鍵字key的某個線性函數爲散列地址,如Hash(key) = key 或 Hash(key) = A*key+B;A,B爲常數

2、除留取餘法:關鍵值除以比散列表長度小的素數所得的餘數作爲散列地址。Hash(key) = key % p;

3、平均取中法:先計算構成關鍵碼的標識符的內碼的平方,然後按照散列表的大小取中間的若干位作爲散列地址。

4、摺疊法:把關鍵碼自左到右分爲位數相等的幾部分,每一部分的位數應與散列表地址位數相同,只有最後一部分的位數可以短一些。把這些部分的數據疊加起來,就可以得到具有關鍵碼的記錄的散列地址。分爲移位法和分界法。

5、隨機數法:選擇一個隨機函數,取關鍵字的隨機函數作爲它的哈希地址。

6、數學分析法:設有N個d位數,每一位可能有r種不同的符號。這r種不同的符號在各位上出現的頻率不一定相同,可能在某些位上分佈均勻些,每種符號出現的機會均等;在某些位上分佈不均勻,只有某幾種符號經常出現。可根據散列表的大小,選取其中各種符號分佈均勻的若干位作爲散列地址。

但是無論我們怎麼樣去選擇這個函數,都不可能完全避免不同key值的hash[value]​指向會映射到同一散列地址上。這樣就會造成哈希衝突/哈希碰撞。所以我們需要找到處理這種衝突的方法,大概分爲這兩種:分離鏈接法和開放定址法。

分離鏈接法:其實就是我們說的hash桶的含義了。哈希桶就是盛放不同key鏈表的容器(即是哈希表),在這裏我們可以把每個key的位置看作是一個桶,桶裏放了一個鏈表

這裏寫圖片描述

相信大家可以看出來,使用一個數組來存放記錄方法的哈希衝突太多,基於載荷因子的束縛,空間利用率不高,在需要節省空間的情況下,我們可以用哈希桶來處理哈希衝突。

哈希桶是使用一個順序表來存放具有相同哈希值的key的鏈表的頭節點,利用這個頭節點可以找到其它的key。

下面把完整的一套函數分開講:

1、首先是創建我們需要的結構體:

數據的結構體(也就是我們表中需要存放的數據):

 typedef struct node_s
{
        int key;    //這個值是我們得到我們的value值的依據,當然也可以能使字符串等,看你的數據類型了;

       struct node *next;

}NODE;

hash表的節點:​

typedef struct hash_s
{
        NODE **list;   //這個就是我們所有的hash桶的首個節點了,我們用它來查找到我們的桶的位置,爲什麼是**類型呢 ,因爲這是地址的地址,例如某個桶 i 的位置*list[i];這樣就找到我們的桶了;然後再在桶下面看看有沒有我們要查找的NODE節點了

}HASH;

2、初始化hash表:

HashTable InitializeTable(int TableSize)
{
        Hash H;
        int i = 0;
        H = calloc(1, sizeof(HASH));
        if (H ==  NULL)
                return -1;

        H->TableSize = NextPrime();   //就是和TableSize最近的下一個素數;
        H->hlist = calloc(1, sizeof(&NODE) * H->TableSize);
        if (H->hlist == NULL)
                return -1;

        for (i = 0; i < H->TableSize; i ++)     
        {
                *(H->hlist[i)] = calloc(1, sizeof(NODE));
                if (*hlist[i] == NULL)
                        return -1;
                else
                        *(H->hlist[i])->next = NULL;
        }
}

3、查找NODE:

NODE *Find(int key, HASH H)
{
        NODE *p;
        NODE *list;

        list = H->hlist[Hash(key, H->TableSize)];
        p = list->next;
        while(p != NULL && p->key != key)
                p = p->next;
        return p;

}    //先找到這個桶的頭結點list,然後再往後面遍歷查找,這時候基本是鏈表查找操作了;

4、插入NODE:

void Insert(int key, HASH H)
{
        NODE *p;
        NODE *list;
        NODE *tmp;

        if (Find(key, H) == NULL)
        {
                tmp = calloc(1, sizeof(NODE));
                if (tmp == NULL)
                        return -1;

                list = H->hlist[Hash(key, H->TableSize)];
                tmp->key = key;
                tmp->next = list->next;//這裏是直接把我們的節點插入到桶的首節點;
                list->next = tmp;
        }
        return;
}

以上基本完成以hash桶​爲處理衝突的hash表的實現

發佈了158 篇原創文章 · 獲贊 134 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章