目录
哈希概念
哈希冲突
哈希函数
常见哈希算法
处理哈希冲突
闭散列
线性探测
二次探测
开散列
哈希变形—位图
布隆过滤器
顺序搜索以及二叉树搜索树中,元素存储位置和元素各关键码之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素时:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素时:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键
码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者
称散列表)
HashFun(Ki) == HashFun(Kj)
即不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到表中“下一个” 空位中去
那如何寻找下一个空余位置?
:(一旦冲突必须要找出下一个空余位置 )从发生冲突的位置开始,依次继续向后探测,直到找到空位置为止
【插入】
1. 使用哈希函数找到待插入元素在哈希表中的位置
2. 如果该位置中没有元素则直接插入新元素;如果该位置中有元素且和待插入元素相同,则不用插入;如果该位置中有元素但
不是待插入元素则发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。为什么?那该
采用线性探测,实现起来非常简单,缺陷是:
一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键
码的位置需要许多次比较,导致搜索效率降低。
如何缓解呢?负载因子
二次探测
发生哈希冲突时,二次探查法在表中寻找“下一个”空位置的公式为:
Hi = ( Ho + i²) % m, = ( Ho - i²) % m, i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得
到的位置,m是表的大小
当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只
要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5;如果超出必须考虑增容
开散列法又叫链地址法(开链法)。
开散列法:首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
设元素的关键码为37, 25, 14, 36, 49, 68, 57, 11, 散列表为HT[12],表的大小为12,散列函数为Hash(x) = x % 11
Hash(37)=4
Hash(25)=3
Hash(14)=3
Hash(36)=3
Hash(49)=5
Hash(68)=2
Hash(57)=2
Hash(11)=0
使用哈希函数计算出每个元素所在的桶号,同一个桶的链表中存放哈希冲突的元素。
以搜索平均长度为 的链表代替了搜索长度为 n 的顺序表,搜索效率快的多。
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:
由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
1G是约为10亿(1GB=1000*1000*1000B),40亿是4G,一个整型4个字节,所以需要16G,不过我们可以将个元素映射成一个位矩阵的一个点
一个数在不在就是两个状态,在或者不在,就可以用1个位来代表。
每个整数是32位的,那么所有的整数也就2^32种可能个,大概42亿个数左右。
可以申请2^32的位,把每一个整数都覆盖了,40亿个数的位分别为1,剩下的位为0。
新的整数,就可以跟进它的大小来判断相应的位,
2^32个bit位,也就是512M(2^20为1M,2^3为一Byte,2^9为512)
既不浪费空间有可以很快查找
#include
#include
#include
#include
#include
typedef struct BitMap
{
int size;//比特位为1的个数
int capacity;//比特位总个数
int* Bit;
}BitMap;
void InitBitMap(BitMap *bit, int total);
void SetBitMap(BitMap *bit, int which);
//初始化比特位
void InitBitMap(BitMap *bit, int total)
{
assert(bit);
bit->capacity = total;//建立bit位个数
bit->Bit = (int*)malloc( (total/32 + 1)*sizeof(int) );
if (bit->Bit == NULL)
{
assert(0);
return;
}
memset(bit->Bit,0, (total / 32 + 1) * sizeof(int));//将初始化的比特位置0
bit->size = 0;
}
//置1
void SetBitMap(BitMap *bit, int which)
{
int index = 0;
int pos = 0;
assert(bit);
//用哈希散列的方式将每一个整数映射到所对应的比特位上
//相当于一个32位的数字,只需要1位来表示
index = which >> 5;
//右移5次,等于除了32后这个数字代表的就是第几个字节
//使用Bit整形指针,可以找个这个字节(整形指针一次移动四字节就是32位)
pos = which % 32;
//对这个数字取余32,表示在index字节的哪一位,因为其余后是0-31,所以下面左移一位
//与操作,无关为都置0,只留which所在位
if ( 0 != (bit->Bit[index] & (1 << pos)) )
{
printf("%d 重复\n", which);
return;
}
bit->Bit[index] = bit->Bit[index] | (1<size++;
}
int main()
{
BitMap bit;
InitBitMap(&bit,1000);
SetBitMap(&bit, 19);
SetBitMap(&bit, 409);
SetBitMap(&bit, 868);
SetBitMap(&bit, 868);
system("pause");
return 0;
}
原理
如果要判断一个数是不是在一个集合里,一半想到的是将所有的元素保存起来,然后通过比较确定。但是随着集合中元素的增加,需要的存储空间越来越大,检索速度自然会变慢。这时会有人想到使用哈希表,将元素通过哈希函数映射到一个位阵列中,将相应的比特位置为1,这样就可以判断这个元素是不是在集合之中了。
但是哈希有一个很严重的问题,那就是哈希冲突。针对这个问题,我们的解决方法是使用多个哈希函数,用多个位表示一个值,如果它们之中有一个说元素不在集合中,那么这个元素势必就不在;但是反过来,它们都说在,却是不一定在集合之中,因为有可能它们在说谎。
优缺点
布隆过滤器就是用于检索一个元素是否字一个集合之中,它的优点是空间效率和查询时间都由于其他一般的算法,缺点是有一定的几率识别错误,并且删除困难。
之所以会出现删除困难,是因为由于哈希冲突,可能一个位被多次置1,如果我们直接删除,那么就会出现错误。如果一定要实现删除功能的话,可以想到将位数组换成一般的数组。将其初始化为0,然后每增加一个元素,相应的位置加1,删除的时候相应的位置减1就可以了。