当你看这篇的时候我认为你已经懂得哈希表的基本原理和一些具体方法实现了,如果你是想清晰的理解哈希表原理,点这个哈希表(散列表)原理详解
闭散列
- 我们往哈希表中插入数据时往往会发生哈希冲突,即两个不一样的
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;
}