离散哈希表实现
前言:
哈希表也叫离散表,在PHP内核中常用于记录变量值,其存储思路就是通过哈希算法计算Key值,然后取余,将余作为索引下标放到指定列表里去!
这样的话极大降低了查找速度,因为链表是关系型存储结构,需要一步一步的Next才可以得到想要查找的数据,而哈希表则可以直接通过关联Key计算出数据的下标然后直接获取!
就好像变量一样: int Test = 5;
其中Test就是关联的Key,5就是关联的数据,可以通过Key找到数据!而不像链表一样一步一步Next寻找索引!
但缺点是大小在一开始就会被固定,因为插入时计算的Key值不是按顺序来的,所以插入时大小要在一开始固定好
比如Key是7那么就插入第7个索引里去,所以大小在一开始就需要固定好!不过不用担心大小一开始被固定好后浪费内存的事情,因为这里我们用的是指针数组,然后在插入时在分配内存即可,这里就用了少量的内存,换取查找速度!
这样的话大家肯定会想,和数组有什么区别呢?
第一这里我们用的是结构体可以额外增加很多个数据类型在里面,然后在堆中分配内存就实现了动态内存的增长!
比如:
普通数组:
int a[1024];
哈希数组:
struct a{
int b;
int y;
};
a a1[1024];
可以看到哈希数组可以存放更多的数据且用少量栈内存换取大量的堆存储空间,且查找速度快于链式结构!
实现:
第一步:
新建一个C++程序
#include <stdio.h>//标准输入输出
#include <stdlib.h>//malloc
int main(){
return 1;
}
第二步:
新建一个结构体用于作为哈希表,其次在定义一个宏作为哈希表的大小便于修改:
//哈希数组大小
#define HASH_ARR_MAX 1024
//哈希节点
struct Hash{
char *Key; //关联Key
int Data; //数据
bool State; //特征值用来表示此空间是否被赋予值
};
注意这里的特征值用来判断此Data是否被赋予值,当然有人会说不可以通过Data是否为0判断?不可以因为有的时候Data会被显示的赋予0作为数据!
第二步:
定义哈希表
//定义哈希数组
struct Hash *Node[HASH_ARR_MAX];
第三步:
初始化,将哈希指针数组全部指向NULL,便于后面判断是否被开辟空间
//初始化
int InI(){
for (int i = 0; i <= HASH_ARR_MAX - 1; ++i){
Node[i] = NULL;
}
return 1;
}
第四步:
计算Key的哈希值,这里使用的哈希算法的思路是33*字符数组的里的每个字符的ASCII码,然后让哈希值加上乘出来的值!
返回的时候返回取最大值的余就可以了,可以避免溢出界!
//计算哈希
signed int SetHashIndex(char* Key){
if (Key == NULL){
return -1;
}
signed int HashData = 0;//哈希值
for (int i = 0; Key[i] != '\0'; ++i){
HashData = HashData+33 * Key[i];//计算
}
return HashData%HASH_ARR_MAX;//取余返回
}
第五步:
查找哈希,其实也就是判断哈希值是否存在或者被赋值!
没有被赋予值返回-1或者等于此链表还没有malloc也返回0
通过获取Key的索引然后在Node指针数组里判断就好了
//查找
signed int GetIndex(char* Key){
if (Key == NULL){
return -2;
}
signed int HashIndex = SetHashIndex(Key);
//没有计算出哈希值
if (HashIndex == -1){
return -1;
}
//判断是否为NULL
if (Node[HashIndex] == NULL){
return -1;
}
//判断是否赋予值
if (Node[HashIndex]->State == 0){
return -1;
}
return HashIndex; //返回已经开辟空间了得值
}
第五步:
压入,压入其实很简单只需要调用GetIndex判断此空间是否赋予值就好了,也就是在返回-1的时候才是我们想要的结果
在分配内存时做一下判断,若NULL则开辟空间否则就修改值,相当于实现了压入和修改两个功能
//压入
int Input(char* Key,int Data){
if (Key == NULL || Data < 0){
return 0;
}
signed int Yoi = GetIndex(Key);
//此哈希空间没有被赋予值或者开辟空间
if (Yoi == -1){
signed int Ind = SetHashIndex(Key);
//没有开辟空间则开辟空间,否则修改值
if(Node[Ind] == NULL){
Node[Ind] = (struct Hash*)malloc(sizeof(struct Hash));
}
Node[Ind]->Data = Data;
Node[Ind]->State = true;
return 1;
}
return 0;
}
第六步:
删除:
删除非常简单,只需要稍微判断一下即可
//删除
void Delete(char* Key){
if (Key == NULL){
return;
}
//获取哈希值然后判断是否为NULL不为NULL则释放并指向NULL
signed int Ind = SetHashIndex(Key);
if (Node[Ind] != NULL){
free(Node[Ind]);
Node[Ind] = NULL;
}
return;
}
第七步:
获取:
获取哈希值然后判断不等于-1的情况下就是正确的被赋予过值得被malloc的,然后返回Data就可以了!
//获取
signed int GetData(char* Key){
if (Key == NULL){
return -1;
}
signed int Yoi = GetIndex(Key);
if (Yoi != -1){
return Node[Yoi]->Data;
}
return -1;
}
好了,七步实现一个离散哈希表,非常简单!
下面是示例:
int main(){
InI();
Input("test", 2);
Input("test1", 4);
printf("%d", GetData("test1"));
getchar();
return 0;
}
运行示例:
可以看到运行结果是OK的!
这里肯定有人会问了,每次获取索引都要计算一下,那这样时间上是不是就慢了,这和链表Next查找有什么区别吗?
答:这些哈希算法都是数学科学家写出来的,属于快速算法,CPU在计算起来速度要快于一般的一步一步的Next取值运算!
因为一步一步Next需要取地址,然后通过地址总线取出地址里的值,然后再跳到另外一个堆中的结构体中去在寻找Next,这样过于麻烦,而直接计算出下标然后直接地址访问速度要明显快,但是这样会浪费不少栈内存,两个都有优缺点,哈希是空间换取时间,链表是时间换取空间!
完整代码:
#include <stdio.h>//标准输入输出
#include <stdlib.h>//malloc
//哈希数组大小
#define HASH_ARR_MAX 1024
//哈希节点
struct Hash{
char *Key; //关联Key
int Data; //数据
bool State; //特征值用来表示此空间是否被赋予值
};
//定义哈希数组
struct Hash *Node[HASH_ARR_MAX];
//初始化
int InI(){
for (int i = 0; i <= HASH_ARR_MAX - 1; ++i){
Node[i] = NULL;
}
return 1;
}
//计算哈希
signed int SetHashIndex(char* Key){
if (Key == NULL){
return -1;
}
signed int HashData = 0;//哈希值
for (int i = 0; Key[i] != '\0'; ++i){
HashData = HashData+33 * Key[i];//计算
}
return HashData%HASH_ARR_MAX;//取余返回
}
//查找
signed int GetIndex(char* Key){
if (Key == NULL){
return -2;
}
signed int HashIndex = SetHashIndex(Key);
//没有计算出哈希值
if (HashIndex == -1){
return -1;
}
//判断是否为NULL
if (Node[HashIndex] == NULL){
return -1;
}
//判断是否赋予值
if (Node[HashIndex]->State == 0){
return -1;
}
return HashIndex; //返回已经开辟空间了得值
}
//压入
int Input(char* Key,int Data){
if (Key == NULL || Data < 0){
return 0;
}
signed int Yoi = GetIndex(Key);
//此哈希空间没有被赋予值或者开辟空间
if (Yoi == -1){
signed int Ind = SetHashIndex(Key);
//没有开辟空间则开辟空间,否则修改值
if(Node[Ind] == NULL){
Node[Ind] = (struct Hash*)malloc(sizeof(struct Hash));
}
Node[Ind]->Data = Data;
Node[Ind]->State = true;
return 1;
}
return 0;
}
//删除
void Delete(char* Key){
if (Key == NULL){
return;
}
//获取哈希值然后判断是否为NULL不为NULL则释放并指向NULL
signed int Ind = SetHashIndex(Key);
if (Node[Ind] != NULL){
free(Node[Ind]);
Node[Ind] = NULL;
}
return;
}
//获取
signed int GetData(char* Key){
if (Key == NULL){
return -1;
}
signed int Yoi = GetIndex(Key);
if (Yoi != -1){
return Node[Yoi]->Data;
}
return -1;
}
int main(){
InI();
Input("test", 2);
Input("test1", 4);
printf("%d", GetData("test1"));
getchar();
return 1;
}
二. 开放地址法
当在计算Key值时并不能保证是否有重复,所以为了解决这一问题,推出了开放地址法
思路就是:
记录每个插入的值,保存到链表里,然后插入时循环一遍看一下是否重复,如果重复则插入到+1节点如果+1节点也重复,则继续+1直到哈希表爆满才返回NULL
这种方法虽然解决了重复问题但是速度上大大降低了
1.修改代码
我们将上面的代码稍作修改,这里我选择使用线性存储结构来保存每个哈希值来遍历插入
新建一个结构体用于存储哈希的Key值和插入的索引(这样不需要二次计算)
//存储哈希值
struct HashKey{
char* Key; //Key
int Index; //索引
};
struct HashKey *StorNode[HASH_ARR_MAX];
2.修改InI函数
将StorNode也指向NULL
//初始化
int InI(){
for (int i = 0; i <= HASH_ARR_MAX - 1; ++i){
Node[i] = NULL;
StorNode[i] = NULL;
}
return 1;
}
3.修改压入
这里每次压入时做一次判断,压入之后将key值copy到记录key的表里去
这里唯一的遗憾是没有用c++11的小型函数,不然的话可以极大化的降低代码量
//压入
int Input(char* Key, int Data){
if (Key == NULL || Data < 0){
return 0;
}
signed int Yoi = GetIndex(Key);
//此哈希空间没有被赋予值或者开辟空间
if (Yoi == -1){
//计算哈希
signed int Ind = SetHashIndex(Key);
//遍历一遍是否重复
for (int i = 0; StorNode[i] != NULL; ++i){
if (StorNode[i]->Index == Ind){
Ind++; //插入递增1
}
}
//注意这里不能判断开辟空间,因为我们记录key值了,直接修改会导致冲突需要编写额外的修改函数
if (Node[Ind] == NULL){
Node[Ind] = (struct Hash*)malloc(sizeof(struct Hash));
Node[Ind]->Key = (char*)malloc(strlen(Key) + 1);
memset(Node[Ind]->Key, 0, strlen(Key) + 1);
strcpy(Node[Ind]->Key, Key);//此key需要保存,因为上面会用于比对
Node[Ind]->Data = Data;
Node[Ind]->State = true;
}
//将值压入到Key记录里
//循环可用的空间然后插入
for (int i = 0; i <= HASH_ARR_MAX - 1; ++i){
if (StorNode[i] == NULL){
StorNode[i] = (struct HashKey*)malloc(sizeof(struct HashKey));
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
if (*StorNode[i]->Key == 0/*取头字符*/){
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
}
return 1;
}
else{//哈希值算出且此空间不等于NULL也被赋予值了也就表明直接冲突了
//计算哈希
signed int Ind = SetHashIndex(Key);
//将冲突值后移动
for (int i = 0; StorNode[i] != NULL; ++i){
if (StorNode[i]->Index == Ind){
Ind++; //插入递增1
}
}
//注意这里不能判断开辟空间,因为我们记录key值了,直接修改会导致冲突需要编写额外的修改函数
if (Node[Ind] == NULL){
Node[Ind] = (struct Hash*)malloc(sizeof(struct Hash));
Node[Ind]->Key = (char*)malloc(strlen(Key) + 1);
memset(Node[Ind]->Key, 0, strlen(Key) + 1);
strcpy(Node[Ind]->Key, Key);//此key需要保存,因为上面会用于比对
Node[Ind]->Data = Data;
Node[Ind]->State = true;
}
//将值压入到Key记录里
//循环可用的空间然后插入
for (int i = 0; i <= HASH_ARR_MAX-1; ++i){
if (StorNode[i] == NULL){
StorNode[i] = (struct HashKey*)malloc(sizeof(struct HashKey));
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
if (*StorNode[i]->Key == 0/*取头字符*/){
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
}
return 1;
}
return 0;
}
4.删除
修改上面的删除代码,在删除时做一下判断,寻找正确的Key值,避免删除错误
其次这里说一下指针数组释放顺序的问题
比如有个列表
1 2 3 4 5
都有数据
那么 2被清空了 free了
下次根据是否为NULL判断时,走到2就会停止,3 4 5 还有数据的
//删除
void Delete(char* Key){
if (Key == NULL){
return;
}
//获取哈希值然后判断是否为NULL不为NULL则释放并指向NULL
signed int Ind = SetHashIndex(Key);
//循环遍历找到真正的对应的Key索引
for (int i = 0; StorNode[i] != NULL; ++i){
if (strcmp(StorNode[i]->Key, Key) == 0){ //寻找Key
if (StorNode[i]->Index == Ind){ //二次判断索引
if (Node[Ind] != NULL){ //释放
free(Node[Ind]);
Node[Ind] = NULL;
}
//这里不能释放StorNode不然会导致出顺序出问题不能用于某些循环了,但插入里的循环没有什么问题,可以稍微修改代码达到省空间作用
//直接将里面的值初始化即可,这样判断时就不会出现NULL而终止继续循环判断
free(StorNode[i]->Key);
StorNode[i]->Key = NULL;
//这里分配一个空间,因为有些比对函数给NULL会报中断,这里随便分配一点内存,然后字符初始化为0
StorNode[i]->Key = (char*)malloc(2);
memset(StorNode[i]->Key, 0, 2);//给0便于判断
StorNode[i]->Index = 0;
--KeyIndex;//key记录值减少
}
}
}
return;
}
5.获取
只需要增加比对Key即可
//获取
signed int GetData(char* Key){
if (Key == NULL){
return -1;
}
signed int Yoi = GetIndex(Key);
if (Yoi != -1){
for (int i = 0; StorNode[i] != NULL; ++i){
if (strcmp(StorNode[i]->Key, Key) == 0){ //寻找Key
signed int Ind = SetHashIndex(Key);
if (StorNode[i]->Index == Ind){ //二次判断索引
return Node[StorNode[i]->Index]->Data;
}
}
}
}
return -1;
}
6.修改
由于上面的Input函数被修改了,不能直接修改数据,所以这里我们需要额外增加一个修改函数
修改思路和获取一样
//修改
signed int SetHashData(char* Key, int Data){
if (Key == NULL){
return -1;
}
signed int Yoi = GetIndex(Key);
if (Yoi != -1){
for (int i = 0; StorNode[i] != NULL; ++i){
if (strcmp(StorNode[i]->Key, Key) == 0){ //寻找Key
signed int Ind = SetHashIndex(Key);
if (StorNode[i]->Index == Ind){ //二次判断索引
Node[StorNode[i]->Index]->Data = Data;
}
}
}
}
}
完整代码:
#include <stdio.h>//标准输入输出
#include <stdlib.h>//malloc
//哈希数组大小
#define HASH_ARR_MAX 1024
//哈希节点
struct Hash{
char *Key; //关联Key
int Data; //数据
bool State; //特征值用来表示此空间是否被赋予值
};
//定义哈希数组
struct Hash *Node[HASH_ARR_MAX];
//存储哈希值
struct HashKey{
char* Key; //Key
int Index; //索引
};
struct HashKey *StorNode[HASH_ARR_MAX];
int KeyIndex; //开放地址索引
//初始化
int InI(){
for (int i = 0; i <= HASH_ARR_MAX - 1; ++i){
Node[i] = NULL;
StorNode[i] = NULL;
}
KeyIndex = 0;
return 1;
}
//计算哈希
signed int SetHashIndex(char* Key){
if (Key == NULL){
return -1;
}
signed int HashData = 0;//哈希值
for (int i = 0; Key[i] != '\0'; ++i){
HashData = HashData + 33 * Key[i];//计算
}
return HashData%HASH_ARR_MAX;//取余返回
}
//查找
signed int GetIndex(char* Key){
if (Key == NULL){
return -2;
}
signed int HashIndex = SetHashIndex(Key);
//没有计算出哈希值
if (HashIndex == -1){
return -1;
}
//判断是否为NULL
if (Node[HashIndex] == NULL){
return -1;
}
//判断是否赋予值
if (Node[HashIndex]->State == 0){
return -1;
}
return HashIndex; //返回已经开辟空间了得值
}
//压入
int Input(char* Key, int Data){
if (Key == NULL || Data < 0){
return 0;
}
signed int Yoi = GetIndex(Key);
//此哈希空间没有被赋予值或者开辟空间
if (Yoi == -1){
//计算哈希
signed int Ind = SetHashIndex(Key);
//遍历一遍是否重复
for (int i = 0; StorNode[i] != NULL; ++i){
if (StorNode[i]->Index == Ind){
Ind++; //插入递增1
}
}
//注意这里不能判断开辟空间,因为我们记录key值了,直接修改会导致冲突需要编写额外的修改函数
if (Node[Ind] == NULL){
Node[Ind] = (struct Hash*)malloc(sizeof(struct Hash));
Node[Ind]->Key = (char*)malloc(strlen(Key) + 1);
memset(Node[Ind]->Key, 0, strlen(Key) + 1);
strcpy(Node[Ind]->Key, Key);//此key需要保存,因为上面会用于比对
Node[Ind]->Data = Data;
Node[Ind]->State = true;
}
//将值压入到Key记录里
//循环可用的空间然后插入
for (int i = 0; i <= HASH_ARR_MAX - 1; ++i){
if (StorNode[i] == NULL){
StorNode[i] = (struct HashKey*)malloc(sizeof(struct HashKey));
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
if (*StorNode[i]->Key == 0/*取头字符*/){
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
}
return 1;
}
else{//哈希值算出且此空间不等于NULL也被赋予值了也就表明直接冲突了
//计算哈希
signed int Ind = SetHashIndex(Key);
//将冲突值后移动
for (int i = 0; StorNode[i] != NULL; ++i){
if (StorNode[i]->Index == Ind){
Ind++; //插入递增1
}
}
//注意这里不能判断开辟空间,因为我们记录key值了,直接修改会导致冲突需要编写额外的修改函数
if (Node[Ind] == NULL){
Node[Ind] = (struct Hash*)malloc(sizeof(struct Hash));
Node[Ind]->Key = (char*)malloc(strlen(Key) + 1);
memset(Node[Ind]->Key, 0, strlen(Key) + 1);
strcpy(Node[Ind]->Key, Key);//此key需要保存,因为上面会用于比对
Node[Ind]->Data = Data;
Node[Ind]->State = true;
}
//将值压入到Key记录里
//循环可用的空间然后插入
for (int i = 0; i <= HASH_ARR_MAX-1; ++i){
if (StorNode[i] == NULL){
StorNode[i] = (struct HashKey*)malloc(sizeof(struct HashKey));
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
if (*StorNode[i]->Key == 0/*取头字符*/){
StorNode[i]->Key = (char*)malloc(strlen(Node[Ind]->Key) + 1);
memset(StorNode[i]->Key, 0, strlen(Node[Ind]->Key) + 1);
strcpy(StorNode[i]->Key, Node[Ind]->Key);
StorNode[i]->Index = Ind; //索引交换
++KeyIndex;
break;
}
}
return 1;
}
return 0;
}
//删除
void Delete(char* Key){
if (Key == NULL){
return;
}
//获取哈希值然后判断是否为NULL不为NULL则释放并指向NULL
signed int Ind = SetHashIndex(Key);
//循环遍历找到真正的对应的Key索引
for (int i = 0; StorNode[i] != NULL; ++i){
if (strcmp(StorNode[i]->Key, Key) == 0){ //寻找Key
if (StorNode[i]->Index == Ind){ //二次判断索引
if (Node[Ind] != NULL){ //释放
free(Node[Ind]);
Node[Ind] = NULL;
}
//这里不能释放StorNode不然会导致出顺序出问题不能用于某些循环了,但插入里的循环没有什么问题,可以稍微修改代码达到省空间作用
//直接将里面的值初始化即可,这样判断时就不会出现NULL而终止继续循环判断
free(StorNode[i]->Key);
StorNode[i]->Key = NULL;
//这里分配一个空间,因为有些比对函数给NULL会报中断,这里随便分配一点内存,然后字符初始化为0
StorNode[i]->Key = (char*)malloc(2);
memset(StorNode[i]->Key, 0, 2);//给0便于判断
StorNode[i]->Index = 0;
--KeyIndex;//key记录值减少
}
}
}
return;
}
//获取
signed int GetData(char* Key){
if (Key == NULL){
return -1;
}
signed int Yoi = GetIndex(Key);
if (Yoi != -1){
for (int i = 0; StorNode[i] != NULL; ++i){
if (strcmp(StorNode[i]->Key, Key) == 0){ //寻找Key
signed int Ind = SetHashIndex(Key);
if (StorNode[i]->Index == Ind){ //二次判断索引
return Node[StorNode[i]->Index]->Data;
}
}
}
}
return -1;
}
//修改
signed int SetHashData(char* Key, int Data){
if (Key == NULL){
return -1;
}
signed int Yoi = GetIndex(Key);
if (Yoi != -1){
for (int i = 0; StorNode[i] != NULL; ++i){
if (strcmp(StorNode[i]->Key, Key) == 0){ //寻找Key
signed int Ind = SetHashIndex(Key);
if (StorNode[i]->Index == Ind){ //二次判断索引
Node[StorNode[i]->Index]->Data = Data;
}
}
}
}
}
int main(){
InI();
Input("test", 2);
Input("test1", 4);
printf("%d", GetData("test1"));
getchar();
}
运行: