搜索结构之哈希

哈希表:哈希表是一种可以在O(1)时间内查找到某个元素的数据结构,它是用数组实现的,这依赖于内存的随机访问特性。那哈希表到底是什么样呢?哈希表是通过哈希函数(hashfunc)使元素的存储位置与关键码一一对应的关系,所以在进行元素查找的时候可以通过该函数快速的找到该元素。若结构中存在关键字和key相等的元素,则必然存储在f(k)的位置上。由此,不需要进行比较便可以直接取得所查记录。这个对应函数就被称为散列函数(哈希函数),依照这个思想所建立出来的表就被称为散列表(哈希表)。

常见的哈希函数有很多种,在这里我先用除留余数法简单的画一下哈希表是怎样存储元素的。


但是有没有发现一个问题,如果我们想再存入一个值为16的元素,应该把它存在哪里,这就是哈希冲突问题;所谓哈希冲突就是对于两个不同的数据元素m和n,存在HashFunc(m)==HashFunc(n);即不同的关键字计算出相同的哈希地址,这种现象称为哈希冲突或者哈希碰撞,把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

那我们应该怎样去很好的解决掉哈希冲突问题呢?

一般来说有三种解决方式:

第一种:引起哈希冲突的一个很可能的因素就是哈希函数设计的不够合理,哈希函数设计时应遵从下面几条原则:

1.哈希函数的定义域必须包含需要存储的所有关键码,即如果哈希表中有m个地址时,其值域必须在0~m-1之间。

2.哈希函数计算出来的哈希地址应该均匀的分布在整个哈希表中。

3.哈希函数的设计应该进可能的简单一点。

常见的哈希函数可自行百度。

第二种:闭散列

闭散列也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明哈希表中还有位置,可以把key放在下一个空位中去,那么如何去寻找下一个空余位置呢?

线性探测:举个例子:


根据闭散列线性的处理哈希冲突的方法,我们不难发现,不能随便删除哈希表中的元素,如果直接删除会影响其他元素的搜索。假设我们在上面例子中的哈希表中删除了元素32,当我们再想去查找元素12的时候,我们会首先去位置2的地方去找,此时我们发现位置2是空着的,我们就会认为12不存在于哈希表中,我们肯定就不再去向后进行查找了。(只有当位置2的不为空的时候,我们才会认为有可能发生了哈希冲突,才会去向后继续查找)。

采用线性探测的方法非常简单,但是当大量的哈希冲突聚集在一起,容易产生“数据堆积”,不同的关键码占据了可利用的空位置,使得寻找某一个关键字可能需要进行多次的比较,降低了搜索效率。那么如何缓解这种问题呢?我们引入了散列表的负载因子a=插入表中的元素个数/散列表的长度;a是散列表装满程度的标志因子,a越大,表明表中的元素越多,产生冲突的可能性就越大;反之,a越小,表明插入表中的元素就越少,产生哈希冲突的可能性就越小,实际上,散列表的平均查找长度是负载因子a的函数,只是不同处理冲突的方法有不同的函数。对于开放定址法,a非常重要,一般控制在0.7-0.8以下,当插入表中的元素个数=a*散列表长度时,我们就认为该哈希表已经满了,不可以再进行插入元素了。a超过0.8,查表时的cpu缓存命不中的概率呈指数上升趋势。

二次探测:发生哈希冲突时,二次探测法在表中查找下一个空位置的计算公式为: Hi=(H0+i^2)%m;Hi=(H0-i^2)%m;i=1,2,3...

m为哈希表的大小。


在上面的例子中,哈希冲突有三个元素:25,35和55,当插入5的时候直接存在了位置5处,插入35的时候发现位置5被占用了,所以就不能根据公式Hi=(H0+i^2)%m=(5+1)%10=6;尝试去往位置6出插入,发现位置6也被占用了,于是根据公式Hi=(H0-i^2)%m=(5-1)%10=4;尝试往位置4进行插入,位置4为空,所以将35插在了位置4处。接下来插入最后一个元素55时发现4和6的位置都被占用了,于是根据公式Hi=(H0+i^2)%m=(5+2^2)%10=9(此时i=2);尝试向位置9进行插入,并插入成功。

研究表明:当表的程度为质数且表的负载因子不超过0.5时,新的元素一定能够插入,而且任意一个位置都不会被探查两次,因此表中只要有一半的空位置,就不存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入元素的时候必须保证负载因子a不超过0.5,如果超出必须对哈希表进行增容。

用闭散列实现哈希结构的代码如下:

//hash_table.h
#pragma once 
#include<stddef.h>
#include<Windows.h>
#include<stdio.h>
#define Header printf("=============%s===============\n",__FUNCTION__);
#define HashMaxSize 1000 

typedef enum Stat { 
 Empty, 
 Valid, 
 Invalid // 当前元素被删除了 
} Stat; 

typedef int KeyType; 
typedef int ValType; 

typedef size_t (*Hashfunc)(KeyType); 

typedef struct HashElem { 
 KeyType key; 
 ValType value; 
 Stat stat; // 引入一个 stat 标记来作为是否有效的标记 
} HashElem; 

typedef struct HashTable { 
 HashElem data[HashMaxSize]; 
 size_t size; 
 Hashfunc hash_func; 
} HashTable; 

void HashInit(HashTable* ht, Hashfunc hash_func); 
size_t HashfuncDefault(KeyType key);
void HashPrint(HashTable* ht,const char* msg);
int HashInsert(HashTable* ht, KeyType key, ValType value); 
// 输入key, 查找对应key的value. 
int HashFind(HashTable* ht, KeyType key, ValType* value); 
void HashRemove(HashTable* ht, KeyType key); 
int HashEmpty(HashTable* ht); 
size_t HashSize(HashTable* ht); 
void HashDestroy(HashTable* ht); 
void CountNum(HashTable* ht);//统计每个元素出现了几次
//hash_table.c
#include "hash_table.h"
size_t HashfuncDefault(KeyType key)
{
	return key%HashMaxSize;
}
void HashInit(HashTable* ht, Hashfunc hash_func)
{
	size_t i=0;
	if(ht==NULL)//非法输入
	{
		return;
	}
	ht->hash_func=hash_func;
	ht->size=0;
	for(; i<HashMaxSize; ++i)
	{
		ht->data[i].stat=Empty;
	}
	return;
}
int HashInsert(HashTable* ht, KeyType key, ValType value)
{
	size_t offset;
	if(ht==NULL)
	{
		return 0;
	}
	if(ht->size>HashMaxSize*0.8)//负载因子设为0.8
	{
		return 0;
	}
	offset=ht->hash_func(key);
	while(1)
	{
		if(ht->data[offset].stat==Valid)
		{
			if(ht->data[offset].key==key)
			{
				return 0;
			}
			++offset;
		   if(offset>=HashMaxSize)
		   {
			offset-=HashMaxSize;
		   }
		}
		else
		{
			ht->data[offset].key=key;
			ht->data[offset].value=value;
			ht->data[offset].stat=Valid;
			++ht->size;
			break;
		}
	}
	return 1;
}
void HashPrint(HashTable* ht,const char* msg)
{
	size_t i=0;
	printf("%s>\n",msg);
	for(; i<HashMaxSize; ++i)
	{
		if(ht->data[i].stat!=Empty)
		{
		printf("[%lu] key:[%d]       value[%d]   stat[%d]",i,ht->data[i].key,
			ht->data[i].value,ht->data[i].stat);
		printf("\n");
		}
		
	}
}
int HashFind(HashTable* ht, KeyType key, ValType* value)
{
	size_t offset;
	if(ht==NULL)
	{
		return 0;
	}
	offset=ht->hash_func(key);
	while(1)
	{
	if(key==ht->data[offset].key&&ht->data[offset].stat==Valid)//找到了
	{
		*value=ht->data[offset].value;
		return 1;
	}
	else if(ht->data[offset].stat==Empty)
	{
		return 0;
	}
	else
	{
		++offset;
		if(offset>=HashMaxSize)
		{
			offset-=HashMaxSize;
		}
	}
	}
	return 0;
}
void HashRemove(HashTable* ht, KeyType key)
{
	size_t offset;
	if(ht==NULL)
	{
		return;
	}
	offset=ht->hash_func(key);
	while(1)
	{
		if(key==ht->data[offset].key&&ht->data[offset].stat==Valid)//找到了
		{
			ht->data[offset].stat=Invalid;
			--ht->size;
			return;
		}
		else if(ht->data[offset].stat==Empty)
		{
			break;
		}
		else
		{
			++offset;
			if(offset>=HashMaxSize)
			{
				offset-=HashMaxSize;
			}
		}
	}
}
void HashDestroy(HashTable* ht)
{
	size_t i=0;
	if(ht==NULL)
	{
		return;
	}
	for(; i<HashMaxSize; ++i)
	{
		ht->data[i].stat=Empty;
	}
	ht->size=0;
	ht->hash_func=NULL;
	return;
}
int HashEmpty(HashTable* ht)
{
	if(ht==NULL)
	{
		return 1;
	}
	return ht->size==0?1:0;
}
size_t HashSize(HashTable* ht)
{
	if(ht==NULL)
	{
		return 0;
	}
	return ht->size;
}

void CountNum(HashTable* ht)
{
	int arr[]={1,2,4,2,1,4,1};
	int i=0;
	int ret=0;
	int value=0;
	int sz=sizeof(arr)/sizeof(arr[0]);
	Header;
	HashInit(ht,HashfuncDefault);
	for(; i<sz; ++i)
	{
		ret=HashFind(ht,arr[i],&value);
		if(ret==0)
		{
			HashInsert(ht,arr[i],1);
		}
		else
		{
			HashRemove(ht,arr[i]);
			HashInsert(ht,arr[i],value+1);
		}
	}
	for(i=0; i<sz; i++)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	HashPrint(ht,"统计之后的结果为");
}
//测试用例test.c
#include "hash_table.h"
void TestInit()
{
	HashTable ht;
	size_t i=0;
	Header;
	HashInit(&ht,HashfuncDefault);
	printf("size expect 0,actual %d\n",ht.size);
	printf("ht->hash_func expect %p,actual %p\n",HashfuncDefault,ht.hash_func);
	for(i=0;i<HashMaxSize; ++i)
	{
		if(ht.data[i].stat!=Empty)
		{
			printf("pos [%lu] elem error!\n",i);
		}
	}
}
void TestInsert()
{
	HashTable ht;
	Header;
	HashInit(&ht,HashfuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,1001,200);
	HashInsert(&ht,1002,300);
	HashInsert(&ht,1003,400);
	HashInsert(&ht,2,500);
	HashInsert(&ht,5,600);
	HashPrint(&ht,"向哈希表插入六个元素");

}
void TestFind()
{
	HashTable ht;
	int ret;
	int value=0;
	Header;
	HashInit(&ht,HashfuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,1001,200);
	HashInsert(&ht,1002,300);
	HashInsert(&ht,1003,400);
	HashInsert(&ht,2,500);
	HashInsert(&ht,5,600);
	ret=HashFind(&ht,1002,&value);
	printf(" ret expect 1,actual %d\n value expect 300,actual %d\n",ret,value);
	ret=HashFind(&ht,3,&value);
    printf(" ret expect 0,actual %d\n",ret);

}
void TestRemove()
{
	HashTable ht;
	Header;
	HashInit(&ht,HashfuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,1001,200);
	HashInsert(&ht,1002,300);
	HashInsert(&ht,1003,400);
	HashInsert(&ht,2,500);
	HashInsert(&ht,5,600);
	HashRemove(&ht,1002);
	HashPrint(&ht,"删除1002之后的结果为");
}
void TestDestroy()
{
	HashTable ht;
	Header;
	HashInit(&ht,HashfuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,1001,200);
	HashInsert(&ht,1002,300);
	HashInsert(&ht,1003,400);
	HashInsert(&ht,2,500);
	HashInsert(&ht,5,600);
	HashDestroy(&ht);
	HashPrint(&ht,"销毁后的结果为");
}
void TestEmpty()
{
	HashTable ht;
	int ret;
	Header;
	HashInit(&ht,HashfuncDefault);
	ret=HashEmpty(&ht);
	printf("ret expect 1,actual %d\n",ret);
	HashInsert(&ht,1,100);
	HashInsert(&ht,1001,200);
	ret=HashEmpty(&ht);
	printf("ret expect 0,actual %d\n",ret);

}
void TestSize()
{
	HashTable ht;
	size_t size;
	Header;
	HashInit(&ht,HashfuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,1001,200);
	HashInsert(&ht,1002,300);
	HashInsert(&ht,1003,400);
	size=HashSize(&ht);
	printf("size expect 4,actual %lu\n",size);
}
void TestCountNum()
{
	HashTable ht;
	Header;
	HashInit(&ht,HashfuncDefault);
	CountNum(&ht);
}
int main()
{
	
	TestInit();
	TestInsert();
	TestFind();
	TestRemove();
	TestDestroy();
	TestEmpty();
	TestSize();
	TestCountNum();
	system("pause");
	return 0;
}

第三种:开散列

开散列法又叫做链地址法(开链法);首先对每个关键码使用哈希函数求哈希地址,具有相同地址的关键码归于同一个集合,每一个集合称为一个桶,各个桶中的元素通过一个单链表连接起来,各链表的节点存储在一个哈希表中。

那么开散列哈希桶是怎样存储的呢?



通常情况下,每个桶对应的节点很少,将n个关键码通过某一哈希函数,存放到哈希表中的m个桶中,那么每一个链表的平均长度为:n/m;以搜索平均长度为:n/m的链表代替搜索长度为n的顺序表,效率高了很多。

应用链地址法需要增加链接指针,这看似增加了存储开销,实际上,由于开地址法必须保证大量的空闲空间以确保搜索效率,如二次探测法要求负载因子a<0.5;而表项所占空间又比指针大的多,所以从本质上来说使用链地址法反而比开地址法更加节省空间。

下面是开散列的代码实现:

//hash_table.h
#pragma once 

#include <stddef.h>
#include<Windows.h>
#include<stdio.h>

#define HashMaxSize 1000 

typedef int KeyType; 
typedef int ValType; 

typedef size_t (*HashFunc)(KeyType key); 

typedef struct HashElem { 
 KeyType key; 
 ValType value; 
 struct HashElem* next; 
} HashElem; 

// 数组的每一个元素是一个不带头结点的链表 
// 对于空链表, 我们使用 NULL 来表示 
typedef struct HashTable { 
 HashElem* data[HashMaxSize]; 
 size_t size; 
 HashFunc hash_func; 
} HashTable; 

void HashInit(HashTable* ht, HashFunc hash_func); 

// 约定哈希表中不能包含 key 相同的值. 
int HashInsert(HashTable* ht, KeyType key, ValType value); 

int HashFind(HashTable* ht, KeyType key, ValType* value); 

void HashRemove(HashTable* ht, KeyType key); 

size_t HashSize(HashTable* ht); 

int HashEmpty(HashTable* ht); 

void HashDestroy(HashTable* ht); 

//hash_table.c
#include "hash_table2.h"
void HashInit(HashTable* ht, HashFunc hash_func)
{
	size_t i=0;
	if(ht==NULL)
	{
		return;
	}
	ht->size=0;
	ht->hash_func=hash_func;
	for(; i<HashMaxSize; ++i)
	{
		ht->data[i]=NULL;
	}
	return;
}

HashElem* HashBucketFind(HashElem* head,KeyType key)
{
	HashElem* cur;
	if(head==NULL)
	{
		return NULL;
	}
	cur=head;
	for(; cur!=NULL; cur=cur->next)
	{
		if(cur->key!=key)
		{
			continue;
		}
		else
		{
			return cur;
		}
	}
	return NULL;
}
HashElem* CreateHashNode(KeyType key,KeyType value)
{
	HashElem* new_node=(HashElem*)malloc(sizeof(HashElem));
	new_node->key=key;
	new_node->value=value;
	new_node->next=NULL;
	return new_node;
}
int HashInsert(HashTable* ht, KeyType key, ValType value)
{
	size_t offset;
	HashElem* res;
	HashElem* new_node;
	if(ht==NULL)
	{
		return 0;
	}
	offset=ht->hash_func(key);
	res=HashBucketFind(ht->data[offset],key);
	if(res!=NULL)
	{
		return 0;
	}
	else
	{
		new_node=CreateHashNode(key,value);
		new_node->next=ht->data[offset];
		ht->data[offset]=new_node;
		++ht->size;
		return 1;
	}
	return 0;
}

int HashFind(HashTable* ht, KeyType key, ValType* value)
{
	size_t offset;
	HashElem* res;
	if(ht==NULL||value==NULL)
	{
		return 0;
	}
	offset=ht->hash_func(key);
	res=HashBucketFind(ht->data[offset],*value);
	if(res==NULL)
	{
		return 0;
	}
	else
	{
		*value=ht->data[offset]->value;
		return 1;
	}
	return 0;
}
int HashBucketFindPrevandcur(HashElem* head,KeyType key,HashElem** cur_output,HashElem** prev_output)
{
	HashElem* pre=NULL;
	HashElem* cur=head;
	if(head==NULL||cur_output==NULL||prev_output==NULL)
	{
		return 0;
	}
	for(; cur!=NULL; pre=cur, cur=cur->next)
	{
		if(cur->key==key)
		{
			*cur_output=cur;
			*prev_output=pre;
			return 1;
		}
	}
	return 0;
}
void DestroyHashNode(HashElem* ptr)
{
	free(ptr);
}
void HashRemove(HashTable* ht, KeyType key)
{
	size_t offset;
    int res;
	HashElem* cur;
	HashElem* pre;
	if(ht==NULL)
	{
		return;
	}
	offset=ht->hash_func(key);
	pre=NULL;
	cur=ht->data[offset];
	res=HashBucketFindPrevandcur(ht->data[offset],key,&cur,&pre);
	if(res==0)
	{
		return;
	}
	if(cur==ht->data[offset])
	{
		ht->data[offset]=cur->next;
	}
	else
	{
		pre->next=cur->next;
	}
	DestroyHashNode(cur);
	--ht->size;
	return;
}
size_t HashSize(HashTable* ht)
{
	if(ht==NULL)
	{
		return 0;
	}
	return ht->size;
}
int HashEmpty(HashTable* ht)
{
	if(ht==NULL)
	{
		return 0;
	}
	return ht->size==0?1:0;
}

void HashDestroy(HashTable* ht)
{
	size_t i=0;
	HashElem* cur;
	HashElem* to_delete;
	if(ht==NULL)
	{
		return;
	}
	ht->hash_func=NULL;
	ht->size=0;
	
	for(i=0;i<HashMaxSize; ++i)
	{
		cur=ht->data[i];
		while(cur!=NULL)
		{
			to_delete=cur;
			cur=cur->next;
			DestroyHashNode(to_delete);
		}
	}
}

//测试用例test.c
#include "hash_table2.h"
#define HEADER printf("================%s=================\n",__FUNCTION__);
size_t HashFuncDefault(KeyType key)
{
	return key%HashMaxSize;
}
void HashPrint(HashTable* ht,const char* msg)
{
	size_t i=0;
	HashElem* cur;
	if(ht==NULL)
	{
		return;
	}
	printf("[%s]\n",msg);
	
	for(i=0; i<HashMaxSize; ++i)
	{
		if(ht->data[i]!=NULL)
		{
		cur=ht->data[i];
		for(; cur!=NULL; cur=cur->next)
		 {
			printf("[%d]  ",i);
			printf("%d:%d;",ht->data[i]->key,ht->data[i]->value);
		 }
		printf("\n");
		}
	}
}
void TestInit()
{
	HashTable ht;
	size_t i=0;
	HEADER;
	HashInit(&ht,HashFuncDefault);
	printf("size expect 0,actual %d\n",ht.size);
	printf("HashFunc expect %p,actual %p\n",HashFuncDefault,ht.hash_func);
	for(i=0; i<HashMaxSize; ++i)
	{
		if(ht.data[i]!=NULL)
		{
			printf("pos[%lu] error!\n",i);
		}
	}

}
void TestInsert()
{
	HashTable ht;
	HEADER;
	HashInit(&ht,HashFuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,2,200);
	HashInsert(&ht,1001,300);
	HashInsert(&ht,3,400);
	HashPrint(&ht,"插入四个元素");
	printf("size %d\n",ht.size);
	

}
void TestFind()
{
	HashTable ht;
	ValType value=0;
	int ret;
	HEADER;
	HashInit(&ht,HashFuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,2,200);
	HashInsert(&ht,1001,300);
	HashInsert(&ht,3,400);
	ret=HashFind(&ht,3,&value);
	printf("ret expect 1,ectual %d\n",ret);
	printf("value expect 400,actual %d\n",value);

}
void TestRemove()
{
	HashTable ht;
	HEADER;
	HashInit(&ht,HashFuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,2,200);
	HashInsert(&ht,1001,300);
	HashInsert(&ht,3,400);
	HashRemove(&ht,1);
	HashPrint(&ht,"删除1之后的结果为");
}
void	TestSize()
{
	HashTable ht;
	int ret;
	HEADER;
	HashInit(&ht,HashFuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,2,200);
	HashInsert(&ht,1001,300);
	HashInsert(&ht,3,400);
	ret=HashSize(&ht);
	printf("ret expect 4,actual %d\n",ret);
}
void TestEmpty()
{
	HashTable ht;
	int ret;
	HEADER;
	HashInit(&ht,HashFuncDefault);
	ret=HashEmpty(&ht);
	printf("ret expect 1,actual %d\n",ret);
	HashInsert(&ht,1,100);
	HashInsert(&ht,2,200);
	HashInsert(&ht,1001,300);
	HashInsert(&ht,3,400);
	ret=HashEmpty(&ht);
	printf("ret expect 0,actual %d\n",ret);
}
void TestDestroy()
{
	HashTable ht;
	HEADER;
	HashInit(&ht,HashFuncDefault);
	HashInsert(&ht,1,100);
	HashInsert(&ht,2,200);
	HashInsert(&ht,1002,300);
	HashInsert(&ht,3,400);
	HashDestroy(&ht);
	printf("size expect 0,actual %d\n",ht.size);
}

int main()
{
	TestInit();
	TestInsert();
	TestFind();
	TestRemove();
	TestSize();
	TestEmpty();
	TestDestroy();
	system("pause");
	return 0;
}
















猜你喜欢

转载自blog.csdn.net/qq_39344902/article/details/79752510