哈希表:哈希表是一种可以在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 str