- 哈希是一種可以接受各種類型、大小的輸入,輸出一個固定長度整數的過程。
- 你可以將哈希理解成一種特殊的映射,哈希映射,將一個理論無限的集合A映射到有限整數集合B上。
哈希函數:哈希函數是哈希過程的核心,它決定了哈希映射過程的規則。
哈希沖突:哈希是一種化無限為有限的映射,允許出現多對一,但絕不允許出現一對多。若映射中出現多對一,就是哈希沖突。哈希沖突可以減少,但絕不可能沒有。
哈希函數
/* murmur_hash2 */ static uint32_t hash(const void* key, int len, uint32_t seed) {const uint32_t m = 0x5bd1e995;const int r = 24;uint32_t h = seed ^ len;const unsigned char* data = (const unsigned char*)key;while (len >= 4) {uint32_t k = *(uint32_t*)data;k *= m;k ^= k >> r;k *= m;h *= m;h ^= k;data += 4;len -= 4;}switch (len) {case 3: h ^= data[2] << 16;case 2: h ^= data[1] << 8;case 1: h ^= data[0];h *= m;};h ^= h >> 13;h *= m;h ^= h >> 15;return h; }
?
MurmurHash 是一種非加密哈希函數,適用于一般的哈希檢索操作。它以高運行速度和分布均勻性而聞名。其中uint32_t表示無符號 32 位整型,要想使用它需要包含頭文件<stdint.h>。
該函數調用需要三個參數:
void* key,也就是需要計算哈希值的key元素。此形參的類型是void*,這意味著它可以傳入任何類型的數據,提高了函數的通用性。
- 如果key是基本數據類型,那就傳入指向基本數據類型的指針
- 如果key本身就是一個指針類型,比如字符串,那么就直接傳入這個指針就可以了。
- int len,表示哈希函數要處理的key的大小長度。key若是字符串可以使用函數strlen計算,若是其它類型可以使用sizeof計算。
uint32_t seed,種子值。
- 此哈希函數會根據key和seed共同生成一個哈希值
- 不同的種子值會導致相同的數據產生不同的哈希值。需要注意的是,在程序一次運行中,同一個哈希表應該具有相同的種子值!
- 最常見的設置種子值的方式就是用時間戳,也就是
time(NULL);
函數調用。這對于大家而言,應該都比較熟悉了。
?拉鏈法實現固定容量的哈希表
#ifndef HASH_MAP_H #define HASH_MAP_H#include <stdint.h> // 包含它是為了使用別名uint32_t #include <stdbool.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #define HASHMAP_CAPACITY 10 // 哈希表中數組的長度固定是10// 此時哈希表用于存儲字符串類型的鍵值對 typedef char *KeyType; typedef char *ValueType;// 鍵值對結點 typedef struct node_s {KeyType key;ValueType val;struct node_s *next; } KeyValueNode;typedef struct {// 哈希桶KeyValueNode *buckets[HASHMAP_CAPACITY]; // 直接給定哈希桶的數量是10個// 哈希函數需要的種子值uint32_t hash_seed; } HashMap;// 創建一個固定容量的哈希表 HashMap *hashmap_create(); // 銷毀一個哈希表 void hashmap_destroy(HashMap *map); // 插入一個鍵值對 ValueType hashmap_put(HashMap *map, KeyType key, ValueType val); // 查詢一個鍵值對 ValueType hashmap_get(HashMap *map, KeyType key); // 刪除某個鍵值對 bool hashmap_remove(HashMap *map, KeyType key);#endif // !HASH_MAP_H
?
實現創建哈希表
// 創建一個固定容量的哈希表 HashMap *hashmap_create() {HashMap *map = calloc(1, sizeof(HashMap));if (map == NULL) {printf("error: calloc failed in hashmap_create.\n");exit(-1);}map->hash_seed = time(NULL);return map; }
?
實現哈希表銷毀
// 銷毀一個哈希表 void hashmap_destroy(HashMap *map) {// 遍歷數組,然后銷毀哈希桶中的鏈表,最后銷毀HashMap結構體for (size_t i = 0; i < HASHMAP_CAPACITY; i++) {KeyValueNode *curr = map->buckets[i];while (curr != NULL) {KeyValueNode *tmp = curr->next;free(curr);curr = tmp;}}free(map); }
?
實現插入鍵值對
// 插入一個鍵值對 ValueType hashmap_put(HashMap *map, KeyType key, ValueType val) {// 1.計算哈希值確定哈希桶的位置int idx = hash(key, strlen(key), map->hash_seed) % HASHMAP_CAPACITY;// 2.遍歷哈希桶,查找Key是否重復KeyValueNode *curr = map->buckets[idx];while (curr != NULL) {// 比較字符串if (strcmp(key, curr->key) == 0) {// key已存在,更新value,并返回舊valueValueType old_val = curr->val;curr->val = val;return old_val;}curr = curr->next;}// 3.只要Key不重復肯定都要插入,于是無腦使用鏈表頭插法插入結點KeyValueNode *new_node = malloc(sizeof(KeyValueNode));if (new_node == NULL) {printf("Error: malloc failed in hashmap_put.\n");exit(1);}new_node->key = key; // key和val都是指針類型,可以直接"="賦值new_node->val = val;// 鏈表頭插法new_node->next = map->buckets[idx]; // 新結點指向原本的第一個結點map->buckets[idx] = new_node; // 更新頭指針// 沒有更新鍵值對返回空指針return NULL; }
?
實現鍵值對查詢
// 查詢一個鍵值對 ValueType hashmap_get(HashMap *map, KeyType key) {// 1.計算哈希值確定哈希桶的位置int idx = hash(key, strlen(key), map->hash_seed) % HASHMAP_CAPACITY;// 2.遍歷哈希桶,查找Key是否存在KeyValueNode *curr = map->buckets[idx];while (curr != NULL) {// 比較字符串if (strcmp(key, curr->key) == 0) {// 已找到目標鍵值對return curr->val;}curr = curr->next;}// 目標鍵值對不存在return NULL; }
?
實現鍵值對刪除
// 刪除某個鍵值對 bool hashmap_remove(HashMap *map, KeyType key) {// 1.計算哈希值確定哈希桶的位置int idx = hash(key, strlen(key), map->hash_seed) % HASHMAP_CAPACITY;// 2.初始化兩個指針,用于刪除結點KeyValueNode *curr = map->buckets[idx];KeyValueNode *prev = NULL;// 3.遍歷鏈表查找目標Keywhile (curr != NULL) {// 比較字符串if (strcmp(key, curr->key) == 0) {// 已找到目標鍵值對if (prev == NULL) {// 刪除的是第一個結點map->buckets[idx] = curr->next;}else{// 刪除的不是第一個結點prev->next = curr->next;}// free結點free(curr);return true; // 刪除成功}prev = curr; // 更新prev為當前節點curr = curr->next; // 當前結點向后移動}// 沒找到目標鍵值對,刪除失敗return false; }
?
負載因子(Load Factor )
?一般來說,負載因子在0.7/0.75以下時,哈希表處在健康、性能優秀的狀態。但一旦超出這個閾值,就意味著哈希沖突會增多,鏈表平均長度變長,從而導致性能下降。
- 在最佳情況下,即哈希函數均勻分布且沖突較少時,哈希表的插入、查詢和刪除操作的效率非常高,接近 O(1)。
- 在最壞情況下,特別是哈希沖突很多導致鏈表過長時,這些操作的效率會降低,接近 O(L),因為此時的哈希表訪問幾乎相當于鏈表的訪問。
?實現動態哈希表
頭文件
#ifndef DYNAMIC_HASHMAP_H #define DYNAMIC_HASHMAP_Htypedef char *KeyType; typedef char *ValueType;#include <stdint.h>typedef struct node_s {KeyType key;ValueType val;struct node_s *next; } KeyValueNode;typedef struct {KeyValueNode **buckets; // 此時哈希桶是一個動態數組,指向char*元素的指針,所以是一個二級指針int size; // 鍵值對的個數int capacity; // 數組的長度uint32_t hash_seed; // 哈希函數需要的種子值 } DynamicHashMap;// 創建一個固定容量的哈希表 DynamicHashMap *hashmap_create(); // 銷毀一個哈希表 void hashmap_destroy(DynamicHashMap *map); // 插入一個鍵值對 ValueType hashmap_put(DynamicHashMap *map, KeyType key, ValueType val); // 查詢一個鍵值對 ValueType hashmap_get(DynamicHashMap *map, KeyType key); // 刪除某個鍵值對 bool hashmap_remove(DynamicHashMap *map, KeyType key);#endif // !DYNAMIC_HASHMAP_H
?
實現創建和銷毀動態哈希表
#include "dynamic_hashmap.h" #define DEFAULT_CAPACITY 8 // 哈希表的初始默認容量是8// 創建一個固定容量的哈希表 DynamicHashMap *hashmap_create() {DynamicHashMap *map = malloc(sizeof(DynamicHashMap));if (map == NULL) {printf("Error: malloc failed in hashmap_create\n");exit(1);}map->buckets = calloc(DEFAULT_CAPACITY, sizeof(KeyValueNode *));if (map->buckets == NULL) {// 不要忘記先free結構體free(map);printf("Error: calloc failed in hashmap_create\n");exit(1);}// 初始化其它成員map->size = 0;map->capacity = DEFAULT_CAPACITY;map->hash_seed = time(NULL);return map; }// 銷毀一個哈希表 void hashmap_destroy(DynamicHashMap *map) {// 1.先遍歷動態數組銷毀鏈表的每一個結點for (int i = 0; i < map->capacity; i++) {KeyValueNode *curr = map->buckets[i];while (curr != NULL) {KeyValueNode *tmp = curr->next;free(curr);curr = tmp;}}// 2.再銷毀動態數組free(map->buckets);// 3.最后銷毀哈希表結構體free(map); }
?
實現插入和擴容功能
#define LOAD_FACTOR_THRESHOLD 0.75 // 負載因子的閾值 #define CAPACITY_THRESHOLE 1024 // 數組容量的閾值/* murmur_hash2 */ static uint32_t hash(const void* key, int len, uint32_t seed) {const uint32_t m = 0x5bd1e995;const int r = 24;uint32_t h = seed ^ len;const unsigned char* data = (const unsigned char*)key;while (len >= 4) {uint32_t k = *(uint32_t*)data;k *= m;k ^= k >> r;k *= m;h *= m;h ^= k;data += 4;len -= 4;}switch (len) {case 3: h ^= data[2] << 16;case 2: h ^= data[1] << 8;case 1: h ^= data[0];h *= m;};h ^= h >> 13;h *= m;h ^= h >> 15;return h; }static void rehash(KeyValueNode *curr, KeyValueNode **new_table, int new_capacity, uint32_t seed) {// 計算 key 的哈希值int len = strlen(curr->key);int idx = hash(curr->key, len, seed) % new_capacity;// 頭插法curr->next = new_table[idx];new_table[idx] = curr; }// 對哈希表進行擴容操作 static void grow_capacity(DynamicHashMap *map) {/** 擴容策略:* 1.如果容量沒有達到閾值,那就每次將長度擴大為原先的2倍* 2.如果容量達到閾值,此時哈希表已經很長了,為了避免擴容過程性能損耗過大* 所以擴容保守一些,每次只擴容閾值長度的容量** 擴容的過程:* 1.每個鍵值對結點都要重新計算哈希值,重新映射到新哈希桶中(新數組)* 2.原先的動態數組的鏈表很復雜,難以進行重哈希操作,建議直接丟棄它* 重新創建一個新動態數組*/int new_capacity =(map->capacity <= CAPACITY_THRESHOLE) ?(map->capacity << 1) :(map->capacity + CAPACITY_THRESHOLE);// 動態數組擴容需要使用 callocKeyValueNode **new_buckets = calloc(new_capacity, sizeof(KeyValueNode *));if (new_buckets == NULL) {printf("Error: calloc failed in grow_capacity\n");exit(1);}// 每次擴容都更改一次哈希種子,提高安全性uint32_t seed = time(NULL);// 將所有鍵值對重新映射到新數組中for (int i = 0; i < map->capacity; i++) {KeyValueNode *curr = map->buckets[i];while (curr != NULL) {KeyValueNode *next = curr->next;// 重新進行哈希映射rehash(curr, new_buckets, new_capacity, seed);curr = next;}}// 將舊動態數組free,但是注意不要free鍵值對結點,結點已經被鏈接到新數組中了。free(map->buckets);// 更新 HashMap 的信息map->buckets = new_buckets;map->capacity = new_capacity;map->hash_seed = seed; }// 插入一個鍵值對 // 1. 如果key不存在,則添加鍵值對結點 // 2. 如果key存在,則更新val,將舊的val返回 ValueType hashmap_put(DynamicHashMap *map, KeyType key, ValueType val) {// 計算key的哈希值確定存儲位置int idx = hash(key, strlen(key), map->hash_seed) % (map->capacity);// 遍歷鏈表KeyValueNode *curr = map->buckets[idx];while (curr != NULL) {if (strcmp(key, curr->key) == 0) {// 更新 key 關聯的 val, 并將舊的value返回ValueType old_val = curr->val;curr->val = val;return old_val;}curr = curr->next;} // while循環結束時, curr是一個NULL// 鍵值對不存在,即需要將鍵值對插入// 插入操作前需要計算當前哈希表的負載因子double load_factor = (1.0 * map->size) / (map->capacity);if (load_factor >= LOAD_FACTOR_THRESHOLD) {// 當前哈希表負載因子已達到閾值,將動態數組進行擴容grow_capacity(map);// 數組長度已變,需要再哈希確定當前鍵值對的存儲位置idx = hash(key, strlen(key), map->hash_seed) % (map->capacity);}// 開始插入新鍵值對KeyValueNode *new_node = malloc(sizeof(KeyValueNode));if (new_node == NULL) {printf("Error: malloc failed in hashmap_put\n");exit(1);}// 初始化結點new_node->key = key;new_node->val = val;// 鏈表的頭插法new_node->next = map->buckets[idx];map->buckets[idx] = new_node;// 不要忘記更新sizemap->size++;return NULL; }
?
訪問和刪除鍵值對
// 查詢一個鍵值對 ValueType hashmap_get(DynamicHashMap *map, KeyType key) {// 計算key的哈希值,從而確定哈希桶int idx = hash(key, strlen(key), map->hash_seed) % map->capacity;// 遍歷哈希桶的鏈表,判斷 key 是否存在KeyValueNode *curr = map->buckets[idx];while (curr != NULL) {if (strcmp(key, curr->key) == 0) {// 找到目標鍵值對return curr->val;}curr = curr->next;} // curr == NULL// 沒找到目標鍵值對return NULL; }// 刪除某個鍵值對 bool hashmap_remove(DynamicHashMap *map, KeyType key) {// 計算key的哈希值,從而確定哈希桶int idx = hash(key, strlen(key), map->hash_seed) % map->capacity;// 初始化兩個指針,一個指向鏈表當前結點,一個指向當前結點的前驅KeyValueNode *prev = NULL;KeyValueNode *curr = map->buckets[idx];while (curr != NULL) {if (strcmp(key, curr->key) == 0) {// 找到了要刪除的節點if (prev == NULL) {// 刪除的是哈希桶的第一個節點,于是更新頭指針map->buckets[idx] = curr->next;}else {// 刪除的是哈希桶中的非第一個節點,只需要讓當前結點的前驅結點指向當前結點的后繼prev->next = curr->next;}free(curr);map->size--;return true;}// 繼續遍歷鏈表prev = curr;curr = curr->next;}// 沒找到指定key的節點return false; }
?
擴展:利用哈希表判斷單鏈表有環
頭文件
#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include <stdbool.h>#define CAPACITY 10 // 哈希表的底層數組長度是10typedef struct node {int data;struct node *next; }Node; // 哈希表當中存儲的鏈表結點類型// 此哈希表只存儲鍵,不存儲value // key值存儲鏈表結點的指針 typedef Node *KeyType;// 鍵值對結點,此時無需存儲值了 typedef struct node_s {KeyType key; // 鍵struct node_s *next; } HashNode; // 鍵值對結點類型typedef struct {HashNode *buckets[CAPACITY];uint32_t hash_seed; } HashMap; // 哈希表結構體類型// 創建一個固定容量的哈希表 HashMap *hashmap_create(); // 銷毀哈希表 void hashmap_destroy(HashMap *map);/*嘗試將鏈表結點的指針(地址)插入哈希表若發現結點已存入哈希表, 則函數返回true若結點未存入, 則將結點地址存入哈希表并返回false */ bool hashmap_put(HashMap *map, KeyType key);
該哈希表的實現代碼如下所示:
#include "hash_map.h"/* murmur_hash2 */ static uint32_t hash(const void *key, int len, uint32_t seed) {const uint32_t m = 0x5bd1e995;const int r = 24;uint32_t h = seed ^ len;const unsigned char *data = (const unsigned char *)key;while (len >= 4) {uint32_t k = *(uint32_t *)data;k *= m;k ^= k >> r;k *= m;h *= m;h ^= k;data += 4;len -= 4;}switch (len) {case 3: h ^= data[2] << 16;case 2: h ^= data[1] << 8;case 1: h ^= data[0];h *= m;};h ^= h >> 13;h *= m;h ^= h >> 15;return h; }// 創建一個固定容量的哈希表 HashMap *hashmap_create() {HashMap *map = calloc(1, sizeof(HashMap));if (map == NULL) {printf("error: calloc failed in hashmap_create.\n");return NULL;}map->hash_seed = time(NULL);return map; }// 銷毀哈希表 void hashmap_destroy(HashMap *map) {// 遍歷每一個哈希桶(也就是遍歷數組的每一個元素),逐一銷毀鏈表結點,最后銷毀結構體自身for (int i = 0; i < CAPACITY; i++) {HashNode *curr = map->buckets[i];while (curr != NULL) {HashNode *tmp = curr->next;free(curr);curr = tmp;}}free(map); }/*該函數會將鏈表結點的地址作為一個結點key值,存入哈希表當中在存入的過程中,如果發現key值重復,說明有環,直接返回true如果key值不重復,那就將key結點正常存入哈希表,并且返回false */ bool hashmap_put(HashMap *map, KeyType key) {// 1.根據key這個結點地址數據來計算哈希值,確定哈希桶的位置int idx = hash(key, sizeof(KeyType), map->hash_seed) % CAPACITY;// 2.遍歷哈希桶,確定這個結點地址是否已經出現HashNode *curr = map->buckets[idx];while (curr != NULL) {if (key == curr->key) {// 當前結點已經重復出現了,說明有環return true;}curr = curr->next;}// 3.key是不存在的,于是新增這個結點HashNode *new_node = malloc(sizeof(HashNode));if (new_node == NULL) {printf("error: malloc failed in hashmap_put.\n");exit(-1);}new_node->key = key; // 由于key是Node結點指針類型,是地址,所以可以直接用=賦值// 4.執行鏈表的頭插法,將新結點鏈接到鏈表中new_node->next = map->buckets[idx];map->buckets[idx] = new_node;return false; // 表示當前結點還沒有重復,所以插入哈希表成功了 }
// 利用哈希表來判斷單鏈表有環 bool has_circle(Node *head) {// 1.創建一個哈希表HashMap *map = hashmap_create();// 2.遍歷整條單鏈表,并且將每一個結點的地址存入哈希表Node *curr = head;while (curr != NULL) {if (hashmap_put(map, curr)) {// 已經存儲了重復結點,有環hashmap_destroy(map);return true;}curr = curr->next;}// 如果while循環能夠結束,且能夠執行到這里,說明鏈表沒環hashmap_destroy(map);return false; }