数据结构之哈希表

1,什么是哈希表?
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

2,使用哈希查找有两个步骤
(1)使用哈希函数将被查找的键转换为数组的索引。在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况 ,
(2)哈希查找的第二步骤就是处理冲突,有很多处理哈希碰撞冲突的方法,本文后面会介绍拉链法和线性探测法。

3,哈希函数的设计
一个好的哈希函数应满足假设:每个关键字都等可能地被哈希到 m 个槽位的任何一个之中,并且与其他的关键字已被哈希到哪一个槽位中无关。不幸的是,通常情况下不太可能检查这一条件是否成立,因为人们很少能知道关键字所符合的概率分布,而各关键字可能并不是完全互相独立的。在实践中,常常运用启发式技术来构造好的哈希函数。

除法哈希法和乘法哈希法属于启发式的方法,而全域哈希法则采用了随机化技术来获取良好的性能。
(1)除法哈希法(The Division Method)
一种好的哈希做法是以独立于数据中可能存在的任何模式的方式导出哈希值。例如,除法哈希法用一个特定的质数来除所给的关键字,所得的余数即为该关键字的哈希值,这种方法最常用。
除法哈希函数可表示为:
hash(key) = key mod m

其中 key 表示被哈希的关键字,m 表示哈希表的大小,mod 为取余操作。假定所选择的质数与关键字分布中的任何模式都是无关的,这种方法常常可以给出很好的结果。

(2)乘法哈希法(The Multiplication Method)
乘法哈希函数可表示为:
hash(key) = floor( m * ( A * key mod 1) )

其中 floor 表示对表达式进行下取整,常数 A 取值范围为(0<A<1),m 表示哈希表的大小,mod 为取余操作。[A * key mod 1] 表示将 key 乘上某个在 0~1 之间的数并取乘积的小数部分,该表达式等价于 [A*key - floor(A * key)]。

乘法哈希法的一个优点是对 m 的选择没有什么特别的要求,一般选择它为 2 的某个幂次,这是因为我们可以在大多数计算机上更方便的实现该哈希函数。

虽然这个方法对任何的 A 值都适用,但对某些值效果更好,最佳的选择与待哈希的数据的特征有关。Don Knuth 认为 A ≈ (√5-1)/2 = 0.618 033 988… 比较好,可称为黄金分割点。

(3)全域哈希法(Universal Hashing)
在向哈希表中插入元素时,如果所有的元素全部被哈希到同一个桶中,此时数据的存储实际上就是一个链表,那么平均的查找时间为 Θ(n) 。而实际上,任何一个特定的哈希函数都有可能出现这种最坏情况,唯一有效的改进方法就是随机地选择哈希函数,使之独立于要存储的元素。这种方法称作全域哈希(Universal Hashing)。

全域哈希的基本思想是在执行开始时,从一组哈希函数中,随机地抽取一个作为要使用的哈希函数。就像在快速排序中一样,随机化保证了没有哪一种输入会始终导致最坏情况的发生。同时,随机化也使得即使是对同一个输入,算法在每一次执行时的情况也都不一样。这样就确保了对于任何输入,算法都具有较好的平均运行情况。

hasha,b(key) = ((a*key + b) mod p) mod m

其中,p 为一个足够大的质数,使得每一个可能的关键字 key 都落在 0 到 p - 1 的范围内。m 为哈希表中槽位数。任意 a∈{1,2,3,…,p-1},b∈{0,1,2,…,p-1}。
完美哈希(Perfect Hashing)

当关键字的集合是一个不变的静态集合(Static)时,哈希技术还可以用来获取出色的最坏情况性能。如果某一种哈希技术在进行查找时,其最坏情况的内存访问次数为 O(1) 时,则称其为完美哈希(Perfect Hashing)。

4,哈希冲突的解决方法

1.开发定址法
  如果遇到冲突的时候怎么办呢?就找hash表剩下空余的空间,找到空余的空间然后插入。就像你去商店买东西,发现东西卖光了,怎么办呢?找下一家有东西卖的商家买呗。

由于我没有深入试验过,所以贴上在书上的解释:
在这里插入图片描述

2.链地址法
  上面所说的开发定址法的原理是遇到冲突的时候查找顺着原来哈希地址查找下一个空闲地址然后插入,但是也有一个问题就是如果空间不足,那他无法处理冲突也无法插入数据,因此需要装填因子(插入数据/空间)<=1。
  那有没有一种方法可以解决这种问题呢?链地址法可以,链地址法的原理时如果遇到冲突,他就会在原地址新建一个空间,然后以链表结点的形式插入到该空间。我感觉业界上用的最多的就是链地址法。下面从百度上截取来一张图片,可以很清晰明了反应下面的结构。比如说我有一堆数据{1,12,26,337,353…},而我的哈希算法是H(key)=key mod 16,第一个数据1的哈希值f(1)=1,插入到1结点的后面,第二个数据12的哈希值f(12)=12,插入到12结点,第三个数据26的哈希值f(26)=10,插入到10结点后面,第4个数据337,计算得到哈希值是1,遇到冲突,但是依然只需要找到该1结点的最后链结点插入即可,同理353。
   
哈希表的拉链法实现 在这里插入图片描述
5,除法哈希法和开放地址法的代码实现:

#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义散列表表未数组的长度
#define NULLKEY -32768
 
typedef struct
{
    int *elem;  //数据元素存储基地址,动态分配数组
    int count;  //当前数据元素个数
}HashTable;     
 int m = 0;     //散列表长,全局变量
 
 //初始化散列表
 int InitHashTable(HashTable *h)
 {
     int i;
     m = HASHSIZE;
     h->elem = (int *)malloc(sizeof(int) * m );
     if(h->elem == NULL)
     {
 
         fprintf(stderr, "malloc() error.\n");
         return ERROR;
     }
     for(i = 0; i < m; i++)
     {
         h->elem[i] = NULLKEY;
     }
 
     return OK;
 }
 
 //散列函数
 int Hash(int key)
 {
     return key % m;    //除留余数法
 }
 
 //插入关键字进散列表
 void InsertHash(HashTable *h, int key)
 {
     int addr = Hash(key);              //求散列地址
     while(h->elem[addr] != NULLKEY) //如果不为空,则冲突
     {
         addr = (addr + 1) % m;         //开放地址法的线性探测
     }
 
     h->elem[addr] = key;                //直到有空位后插入关键字
 }
 
 //散列表查找关键字
 int  SearchHash(HashTable h, int key)
 {
     int addr = Hash(key);                  //求散列地址
     while(h.elem[addr] != key)     //如果不为空,则冲突
     {  
         addr = (addr + 1) % m;     //开放地址法的线性探测
         if(h.elem[addr] == NULLKEY || addr == Hash(key))
         {
             //如果循环回原点
             printf("查找失败, %d 不在Hash表中.\n", key);
             return UNSUCCESS;
         }
     }
     printf("查找成功,%d 在Hash表第 %d 个位置.\n", key, addr);
     return SUCCESS;
 }
  
 int main(int argc, char **argv)
 {
      int i = 0;
      int num = 0;
      HashTable h;
       
      //初始化Hash表
      InitHashTable(&h);
       
      //未插入数据之前,打印Hash表
      printf("未插入数据之前,Hash表中内容为:\n");
      for(i = 0; i < HASHSIZE; i++)
      {
          printf("%d  ", h.elem[i]);
      }
      printf("\\n");
 
      //插入数据
      printf("现在插入数据,请输入(A代表结束哦).\n");
      while(scanf("%d", &i) == 1 && num < HASHSIZE) 
      { 
          if(i == 'a') 
          {
              break;
          }
          num++;
          InsertHash(&h,i); 
          
          if(num > HASHSIZE)
          {
              printf("插入数据超过Hash表大小\n");
              return ERROR;
          }
      } 
      
 
      //打印插入数据后Hash表的内容
      printf("插入数据后Hash表的内容为:\n");
      for(i = 0; i < HASHSIZE; i++)
      {
          printf("%d  ", h.elem[i]);
      }
      printf("\n");
 
     printf("现在进行查询.\n");
     SearchHash(h, 12);
     SearchHash(h, 100); 
 
       
     return 0;
 } 

6,除法哈希法和链地址哈希冲突解决方法的算法实现
(1)哈希表的定义:

typedef int KeyType;
typedef int ValueType;
typedef struct HashNode
{
	KeyType _key; //下标
	ValueType _value; //关键字(内容)
	struct HashNode* _next;
}HashNode, *pHashNode;
//注意区分HashNode和*pHashNode的区别
typedef struct HashTable
{
	pHashNode *_tables;
	size_t _size;
	size_t _N;
}HashTable, *pHashTable;

(2)代码实现

pHashNode BuyHashNode(KeyType key, ValueType value)
{
	pHashNode  node = (pHashNode)malloc(sizeof(HashNode));
	node->_key = key;
	node->_value = value;
	node->_next = NULL;
	return node;
}
//确定下标
size_t HashFunc(KeyType key, size_t N)
{
	return key % N;
}

size_t GetNextPrimeNum(size_t cur)
{
	const int _PrimeSize = 28;
	static const unsigned long _PrimeList[28] = { 56,24,58,870,12,11 ,1,234,64,  73,90,46,55, 32, 98 , 76,34 ,2, 333,42,111, 999,235 ,45 ,87,15,8,66};
	for (int i = 0; i < _PrimeSize; i++)
	{
		if (_PrimeList[i]  >  cur)
			return _PrimeList[i];
	}
}
//初始化
void HashTableInit(pHashTable ht,size_t x)
{
	assert(ht);
	ht->_tables = (pHashNode*)malloc(sizeof(pHashNode)*x);
	memset(ht->_tables, 0, sizeof(pHashNode)*x);
	ht->_size = 0;
	ht->_N = x;
}
//插入
int HashTableInsert(pHashTable ht, KeyType key, ValueType value)
{
	assert(ht);
	if (ht->_N == ht->_size) //扩容
	{
		HashTable newht;
		HashTableInit(&newht, GetNextPrimeNum(ht->_N));
		for (size_t i = 0; i < ht->_N; i++)
		{
			pHashNode cur = ht->_tables[i];
			size_t count = HashFunc(cur->_key, newht._N);
			while (cur != NULL)
			{
				cur->_value = 0;
				pHashNode next = cur->_next;
				cur->_next = newht._tables[count];
				newht._tables[count] = cur;
				while (cur->_next != NULL)
				{
					cur = cur->_next;
					cur->_value++;
				}
				cur = next;
			}
		}
		free(ht->_tables);
		ht->_tables = newht._tables;
		ht->_N = newht._N;
		ht->_size = newht._size;
	}
	size_t index = HashFunc(key, ht->_N);
	pHashNode node = BuyHashNode(key, value);
	if (HashTableFind(ht, key) != NULL)
		return -1;
	node->_next = ht->_tables[index];
	ht->_tables[index] = node;
	while (node->_next != NULL)
	{
		node = node->_next;
		node->_value++;
	}
	ht->_size++;
	return 0;
}
//查找
pHashNode HashTableFind(pHashTable ht, KeyType key)
{
	assert(ht);
	pHashNode node = ht->_tables[HashFunc(key, ht->_N)];
	while (node != NULL)
	{
		if (node->_key == key)
			return node;
		else
			node = node->_next;
	}
	return NULL;
}
//删除
int HashTableRemove(pHashTable ht, KeyType key)
{
	assert(ht);
	size_t index = HashFunc(key, ht->_N);
	pHashNode cur = ht->_tables[index];
	if (cur->_key == key)
	{
		ht->_tables[index] = ht->_tables[index]->_next;
		free(cur);
		ht->_size--;
		return 0;
	}
	while (cur->_next != NULL)
	{
		if (cur->_next->_key == key)
		{
			pHashNode tmp = cur->_next->_next;
			free(cur->_next);
			cur->_next = tmp;
			ht->_size--;
			return 0;
		}
		else
			cur = cur->_next;
	}
	return -1;
}
//清除
void HashTableDestory(pHashTable ht)
{
	assert(ht);
	for (size_t i = 0; i < ht->_N; i++)
	{
		while (ht->_tables[i])
		{
			pHashNode tmp = ht->_tables[i];
			ht->_tables[i] = ht->_tables[i]->_next;
			free(tmp);
		}
	}
	free(ht->_tables);
	ht->_size = 0;
	ht->_N = 0;
}
//打印
void Print(pHashTable ht)
{
	assert(ht);
	for (size_t i = 0; i < ht->_N; i++)
	{
		pHashNode node = ht->_tables[i];
		printf("table[%d]->", i);
		while (node != NULL)
		{
			printf("%d.%d->", node->_key, node->_value);
			node = node->_next;
		}
		printf("NULL\n");
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陌上花开缓缓归以

你的鼓励将是我创作的最大动力,

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值