哈希概念 :
- 顺序搜索以及二叉树搜索树中,元素存储位置和元素各关键码之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的 多次比较。搜索的效率取决于搜索过程中元素的比较次数。
- 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
- 当向该结构中插入元素时:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放搜索元素时:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
- 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者 称散列表) , 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 .
哈希冲突
- 对于两个数据元素的关键字 Ki和Kj(i != j),有 i! = j ,但有: HashFun(Ki) == HashFun(Kj) 即不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞把具有不同关键码而具有相同哈希地 址的数据元素称为“同义词" .
处理哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列
- 闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到表中 “下一个” 空位中去
那如何寻找下一个空余位置?
- 线性探测 设关键码集合为{37, 25, 14, 36, 49, 68, 57, 11},散列表为HT[12],表的大小m = 12,假设哈希函数为:Hash(x) = x % p(p = 11,是接近m的质数),就有:
Hash(37) = 4
Hash(25) = 3
Hash(14) = 3
Hash(36) = 3
Hash(49) = 5
Hash(68) = 2
Hash(57) = 2
Hash(11) = 0
其中25,14,36以及68,57发生哈希冲突,一旦冲突必须要找出下一个空余位置,线性探测找的处理为:从发生冲突的位置开始,依次继续向后探测,直到找到空位置为止
【插入】
- 使用哈希函数找到待插入元素在哈希表中的位置
- 如果该位置中没有元素则直接插入新元素;如果该位置中有元素且和待插入元素相同,则不用插入;如果该位置中有元素但不是待插入元素则发生哈希冲突,使用线性探测找到下一个空位置, 插入新元素 ;
- 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索(会造成该删除元素后边的元素无法被找到)。这里我们就用枚举法枚举一个结构State, 用来存储元素的状态, EXIST : 存在 , DELETE : 删除 , EMPTY : 为空 ; 这样当我们要删除一个元素时, 只需要将该元素的状态改为 DELETE 即可;
采用线性探测,实现起来非常简单,缺陷是: 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键 码的位置需要许多次比较,导致搜索效率降低。
- 负载因子 : 散列表的载荷因子定义 : α = 填入表中的个数 / 散列表的长度 α是散列表装满程度的标志因子, 由于表长是定值, α与"填入表中的元素个数" 成正比, 所以, α越大, 表明填入表中元素越多, 产生冲突的可能性越大, 反之越小 . 对于开放定制法( 闭散列 ), 负载因子一般控制在0.7 - 0.8以下 .
二次探测寻找下一个空位置 : 每次以成倍的次数去寻找以下为空当位置 .
闭散列
代码实现如下 :
HashTable.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
//typedef int HTKeyType;
//typedef int HTValueType;
typedef char* HTKeyType;
typedef char* HTValueType;
//枚举每个位置所表示的状态
enum State
{
//表示空,该位置无元素
EMPTY=0,
//表示存在,该位置有元素且存在
EXIST=1,
//表示删除,该位置元素有元素但已被删除
DELETE=2
};
//数据结构
typedef struct HashData
{
//元素的状态
enum State _state;
HTKeyType _key;
HTValueType _value;
}HashData;
typedef struct HashTable
{
HashData* _tables;
size_t _len; // 长度
size_t _size; // 有效数据个数
}HashTable;
//初始化
void HTInit(HashTable* ht, size_t len);
//销毁
void HTDestroy(HashTable* ht);
//插入
int HTInsert(HashTable* ht, HTKeyType key, HTValueType value);
//删除
int HTRemove(HashTable* ht, HTKeyType key);
//查找
HashData* HTFind(HashTable* ht, HTKeyType key);
//计算容量
int ExpendCapacity(int len);
//大小
int HTSize(HashTable* ht);
//判断是否为空
int HTEmpty(HashTable* ht);
void TestHash();
HashTable.c
#define _CRT_SECURE_NO_WARNINGS
#include "HashTable.h"
//哈希初始化
void HTInit(HashTable* ht, size_t len)
{
ht->_tables = (HashData*)malloc(sizeof(HashData)*ExpendCapacity(len));
assert(ht->_tables);
ht->_size = 0;
ht->_len = ExpendCapacity(len);
memset(ht->_tables, 0, sizeof(HashData)*ExpendCapacity(len));
}
//哈希销毁
void HTDestroy(HashTable* ht)
{
assert(ht);
free(ht->_tables);
ht->_len = ht->_size = 0;
}
//计算容量
int ExpendCapacity(int len)
{
int i = 0;
// 使用素数表对齐做哈希表的容量,降低哈希冲突
const int _PrimeSize = 28;
//素数表
static const unsigned long _PrimeList[28] = {
53ul, 97ul, 193ul, 389ul, 769ul, 1543ul, 3079ul, 6151ul,
12289ul, 24593ul, 49157ul, 98317ul, 196613ul, 393241ul,
786433ul, 1572869ul, 3145739ul, 6291469ul, 12582917ul,
25165843ul, 50331653ul, 100663319ul, 201326611ul, 402653189ul,
805306457ul, 1610612741ul, 3221225473ul, 4294967291ul };
for (; i < 28; i++)
{
if (_PrimeList[i] > len)
return _PrimeList[i];
}
return _PrimeList[28 - 1];
}
//采用取余数法,计算关键码key在HT中的位置
int HTRemainder(HashTable* ht,const HTKeyType key)
{
//当key为字符串时只需要将字符串中每个字符所代表的ASCII值乘以31,再相加模上HT的长度
int cur = 0;
HTKeyType ret = key;
while (*ret)
{
//使用31作为乘子,可以减少哈希冲突,
cur = *ret*31 + cur;
ret++;
}
return cur%ht->_len;
//当key为数字时,直接模上HT的长度
//return key*31%ht->_len;
}
//负载因子的判断
int JudgeFactor(HashTable* ht)
{
//当负载因子大于0.7时,需要扩容
return ht->_size * 10 / ht->_len > 7 ? 1 : 0;
}
//哈希插入
int HTInsert(HashTable* ht, HTKeyType key, HTValueType value)
{
size_t cur = 1;
size_t size;
//判断是否扩容
if (JudgeFactor(ht))
{
HashTable newht;
HTInit(&newht, ht->_len);
for (size = 0; size < ht->_len; size++)
{
//将原始数据重新插入的新的哈希中
if (ht->_tables[size]._state==EXIST)
HTInsert(&newht, ht->_tables[size]._key, ht->_tables[size]._value);
}
//释放掉原始哈希中的数据,并且重新将新哈希中的数据放到原始哈希中
free(ht->_tables);
ht->_len = newht._len;
ht->_size = newht._size;
ht->_tables = newht._tables;
}
//计算关键码所要插入的位置
size = HTRemainder(ht, key);
//如果要插入的位置已经有元素存在则寻找为空的位置进行插入
while (ht->_tables[size]._state != EMPTY)
{
//插入到元素已经存在则直接返回
if (ht->_tables[size]._key == key)
return 0;
//二次探测
size += cur*cur;
cur++;
//如果要插入的位置大于哈希的长度,则重新取模进行查找
if (size > ht->_len)
size = size%ht->_len;
//线性探测
//size++;
//if (size == ht->_len)
// size = 0;
}
//插入操作
ht->_tables[size]._key = key;
ht->_tables[size]._value = value;
ht->_tables[size]._state = EXIST;
ht->_size++;
return 1;
}
//哈希删除
int HTRemove(HashTable* ht, HTKeyType key)
{
HashData * index;
if (ht == NULL)
return 0;
index = HTFind(ht, key);
//如果删除的位置为空,则直接返回
if (index == NULL)
return -1;
//如果要删除的位置不为空,只需要将该位置的状态改为DELETE,并且将哈希中元素个数减一
index->_state = DELETE;
ht->_size--;
return 1;
}
//哈希查找
HashData* HTFind(HashTable* ht, HTKeyType key)
{
size_t size = HTRemainder(ht, key);
assert(ht);
while (ht->_tables[size]._state != EMPTY)
{
//如果要查找的元素存在且该位置的状态为存在则返回该元素的地址
if (ht->_tables[size]._state == EXIST&&!strcmp(ht->_tables[size]_key,key))
return &ht->_tables[size];
else
size++;
//查找到哈希尾部还没有找到时,则更新size,使其从开始继续进行查找
if (size == ht->_len)
size = 0;
}
return NULL;
}
//哈希大小
int HTSize(HashTable* ht)
{
return ht->_size;
}
//判断是否为空
int HTEmpty(HashTable* ht)
{
return ht->_size == 0;
}
//打印哈希
void HashPrint(HashTable * ht)
{
size_t size;
char *arr[3] = { "EMPTY", "EXIST", "DELETE" };
assert(ht);
for (size = 0; size < ht->_len; size++)
{
//打印int型数据
// printf("[%s]: %2d %2d\n",arr[ht->_tables[size]._state], ht->_tables[size]._key, ht->_tables[size]._value);
//打印char*型数据
printf("[%s]: %s %s\n", arr[ht->_tables[size]._state], ht->_tables[size]._key, ht->_tables[size]._value);
}
}
void TestHash()
{
char str[10];
HashTable ht;
HTInit(&ht, 10);
HTInsert(&ht, "Inert", "插入");
HTInsert(&ht, "Delete", "删除");
HTInsert(&ht, "Empty", "清空");
HTInsert(&ht, "Release", "释放");
HTInsert(&ht, "opoqd", "杜拉拉");
HTInsert(&ht, "dakhj", "喝酒");
HTInsert(&ht, "dad", "回家多好");
HashPrint(&ht);
printf("请输入要删除的数据!\n");
gets(str);
HTRemove(&ht, str);
HashPrint(&ht);
}
Test.c
#include "HashTable.h"
int main()
{
TestHash();
system("pause");
return 0;
}
调试结果如下 :
若有出错或不懂的地方, 欢迎留言, 共同进步 !