當你看這篇的時候我認爲你已經懂得哈希表的基本原理和一些具體方法實現了,如果你是想清晰的理解哈希表原理,點這個哈希表(散列表)原理詳解
閉散列
- 我們往哈希表中插入數據時往往會發生哈希衝突,即兩個不一樣的
key
通過散列函數求出的下標offset
是一樣的,這時候就要給後插入的數據重新找位置,就誕生了兩種形式,閉散列和開散列 - 閉散列又稱線性探測,即如果當前往哈希表中插入數據產生哈希衝突,就需要把新插入的數據另闢地方存儲。
- 舉個例子:把哈希表看成是擺成一排的一個一個的箱子,箱子中存放的是數據,當我們往目標箱子中放新數據時,發現該箱子中已經有數據了,那就看看它旁邊箱子中空着沒,空着的話就存進去數據,沒空的話就再擴大範圍找,不過這個範圍和規律要自己定義,不是隨隨便便的放。
- 我這裏採用的方法是如果發現所求下標
offset
處已經有數據了,則offset++
往後探測,直到找到空箱子。
代碼以及解釋
- 結構體聲明
//鍵值對
typedef int KeyType;
typedef int ValueType;
//哈希函數指針
typedef int (*HashFunc)(KeyType key);
//哈希表中元素的狀態
typedef enum State{
EMPTY,//當前點是空的
VALUE,//當前點是有元素的
DELETED//當前點已被刪除,也可以當做空了
}State;
//將鍵值對存放於結構體中
typedef struct KeyValue{
KeyType key;
ValueType value;
State state;
}KeyValue;
//哈希表
typedef struct HashTable{
KeyValue data[HashMaxSize];
size_t size;
HashFunc func;
}HashTable;
- 初始化哈希表和銷燬哈希表
//哈希函數(取餘散列法)
int HashFunction(KeyType key)
{
return key % HashMaxSize;
}
//哈希表初始化
void HashInit(HashTable* ht ,HashFunc func)
{
if(ht == 0)
{
//非法輸入
return;
}
ht->size = 0;
ht->func = func;
size_t i = 0;
for(; i < HashMaxSize; i++)
{
ht->data[i].state = EMPTY;
}
}
//銷燬哈希表
void HashDestroy(HashTable* ht)
{
if(ht == 0)
{
return;
}
ht->size = 0;
ht->func = NULL;
}
- 插入和刪除操作
//往哈希表中插入數據
void HashInsert(HashTable* ht, KeyType key, ValueType value)
{
if(ht == NULL)
{
return;
}
//負載因子,意思就是該哈希表只能存儲最大容量的百分之八十的數據
size_t size_max = 0.8 * HashMaxSize;
if(ht->size >= size_max)
{
//達到載荷了
return;
}
//1.先通過哈希函數算出 offset, 即當前元素對應到哈希表中的下標
size_t offset = ht->func(key);
//2.如果當前下標已經有了元素,就將新插入的元素後移
while(1)
{
if(ht->data[offset].state != VALUE)
{
ht->data[offset].key = key;
ht->data[offset].value = value;
ht->data[offset].state = VALUE;
ht->size++;
break;
}
//3.如果碰到 key 相同的元素,則表示插入失敗,或者更新 value
else if(ht->data[offset].key == key)
{
//如果需要更新 value ,就放開下面的代碼
//ht->data[offset].value = value;
break;
}
//4.如果到了哈希表的末尾,則將其換到頭部,繼續後移
else if(offset >= HashMaxSize)
{
offset = 0;
}
offset++;
}
}
//刪除指定元素
void HashRemove(HashTable* ht, KeyType key)
{
if(ht == NULL)
{
return;
}
if(ht->size == 0)
{
return;
}
//1.先求出 offset, 拿着 offset 去哈希表中尋找指定元素
size_t offset = ht->func(key);
//2.如果當前下標元素的狀態爲 VALUE 的話,就判斷 key是否相等
while(ht->data[offset].state != EMPTY)
{
// a)相等:將當前元素的狀態設置爲 DELETED,
if(ht->data[offset].state == VALUE && ht->data[offset].key == key)
{
ht->data[offset].state = DELETED;
ht->size--;
}
// b)不相等:offset++,繼續往後查找,直到該元素下標爲 EMPTY
else{
// 如果遇到的狀態是 DELETED, 依舊需要往後找,因爲可能後面還緊接着跟有有效元素
// 如果 offset 已經到了哈希表末尾,則將其置爲 0
if(offset >= HashMaxSize)
{
offset = 0;
}
offset++;
}
}// 尋找 指定元素並刪除
}
- 尋找指定元素
//查找指定元素
KeyValue* HashFind(HashTable* ht, KeyType key)
{
if(ht == NULL)
{
return NULL;
}
//1.先算出 offset,拿着 offset 到哈希表中去查找
size_t offset = ht->func(key);
//2.如果當前下標的元素狀態爲 VALUE ,則去比較 key
while(ht->data[offset].state != EMPTY)
{
// 如果 key 相等的話,就返回結構體指針,
if(ht->data[offset].key == key && ht->data[offset].state == VALUE)
{
return &ht->data[offset];
}
// 如果 key 不相等的話,就offset++,往後查找
else
{
//如果 offset 到了哈希表末尾,則重新從頭開始
if(offset >= HashMaxSize)
{
offset = 0;
}
offset++;
}
}
//3.如果當前下標元素的狀態不爲 VALUE 的話,說明沒找到,返回NULL
return NULL;
}
開散列
- 開散列也叫二次探測,我們這裏用拉鍊法實現開散列
- 拉鍊法是解決哈希衝突的一種行之有效的方法,某些哈希地址可以被多個關鍵字值共享,這樣可以針對每個哈希地址建立一個單鏈表。
- 在拉鍊(單鏈表)的哈希表中搜索一個記錄是容易的,首先計算哈希地址,然後搜索該地址的單鏈表。
- 在插入時應保證表中不含有與該關鍵字值相同的記錄,然後按在有序表中插入一個記錄的方法進行。針對關鍵字值相同的情況,現行的處理方法是更新該關鍵字值中的內容。
- 刪除關鍵字值爲k的記錄,應先在該關鍵字值的哈希地址處的單鏈表中找到該記錄,然後刪除之。
代碼以及解釋
- 結構體聲明
typedef int KeyType;
typedef int ValueType;
//哈希函數
typedef int (*HashFunc)(KeyType key);
//鍵值對的結構體(用鏈表來處理哈希衝突)
typedef struct KeyValue{
KeyType key;
ValueType value;
struct KeyValue* next;
}KeyValue;
//哈希表
typedef struct HashTable{
//哈希表中存放的是鏈表的頭指針
KeyValue* data[HashMaxSize];
size_t size;
HashFunc func;
}HashTable;
- 哈希表初始化與銷燬
//初始化
void HashInit(HashTable* ht, HashFunc func)
{
if(ht == NULL)
{
return;
}
ht->size = 0;
ht->func = func;
size_t i = 0;
for(; i < HashMaxSize; i++)
{
//每個結點都置空
ht->data[i] = NULL;
}
}
//銷燬每個節點的鏈表
void _HashDestroy(KeyValue* to_destroy)
{
KeyValue* cur = to_destroy->next;
free(to_destroy);
if(cur != NULL)
{
_HashDestroy(cur);
}
}
//銷燬哈希表
void HashDestroy(HashTable* ht)
{
if(ht == NULL)
{
return;
}
size_t i = 0;
for(; i < HashMaxSize; i++)
{
if(ht->data[i] != NULL)
{
_HashDestroy(ht->data[i]);
ht->data[i] = NULL;
}
}
}
- 插入和刪除
插入和刪除也就是先找到 key 對應的下標,然後在該處的鏈表中進行頭插,刪除的時候要先在鏈表中找到該元素,才能進行刪除
//創建元素結點(因爲是鏈表存儲)
KeyValue* CreateNode(KeyType key,ValueType value)
{
KeyValue* new_node = (KeyValue*)malloc(sizeof(KeyValue));
if(new_node == NULL)
{
return NULL;
}
new_node->key = key;
new_node->value = value;
new_node->next = NULL;
return new_node;
}
//插入元素
void HashInsert(HashTable* ht, KeyType key, ValueType value)
{
if(ht == NULL)
{
return;
}
if(ht->size >= HashMaxSize*10)
{
return;
}
//1.先通過哈希函數求出當前元素在哈希表中的下標 offset
size_t offset = ht->func(key);
//2.在 offset 處頭插入新元素,也就是新結點
//3.如果當前鏈表中存在與插入元素相同的 key 值,則直接返回,插入失敗
KeyValue* cur = ht->data[offset];
while(cur != NULL)
{
if(cur->key == key)
{
return;
}
cur = cur->next;
}
KeyValue* new_node = CreateNode(key,value);
new_node->next = ht->data[offset];
ht->data[offset] = new_node;
++ht->size;
}
//釋放指定節點
void DestroyNode(KeyValue* node)
{
free(node);
}
//刪除指定元素
void HashRemove(HashTable* ht, KeyType key)
{
if(ht == NULL)
{
return;
}
//1.先通過哈希函數求出哈希表的下標 offset
size_t offset = ht->func(key);
KeyValue* prev = NULL;
KeyValue* cur = ht->data[offset];
while(cur != NULL)
{
//2.在該位置的鏈表中找到該元素
//3.先保存前一個節點,才能刪除當前要刪除的結點
if(cur->key == key && prev == NULL)
{
ht->data[offset] = cur->next;
free(cur);
return;
}
else if(cur->key == key && prev != NULL)
{
prev->next = cur->next;
DestroyNode(cur);
return;
}
prev = cur;
cur = cur->next;
}
return;
}
- 查找指定元素
//查找指定元素
KeyValue* HashFind(HashTable* ht, KeyType key)
{
if(ht == NULL)
{
return NULL;
}
//1.先通過哈希函數找到當前 key 在哈希表中的下標
size_t offset = ht->func(key);
//2.找到以後遍歷鏈表,找到就返回,找不到就退出
KeyValue* cur = ht->data[offset];
while(cur != NULL)
{
if(cur->key == key)
{
return cur;
}
cur = cur->next;
}
return NULL;
}