数据结构:哈希散列结构理解及位图的使用

目录

哈希概念

哈希冲突

哈希函数

常见哈希算法

处理哈希冲突 

闭散列

线性探测 

 二次探测

开散列

哈希变形—位图

布隆过滤器 


哈希概念

顺序搜索以及二叉树搜索树中,元素存储位置和元素各关键码之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数。  


理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。  
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。


 当向该结构中:
插入元素时:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素时:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键
码相等,则搜索成功  
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者
称散列表)

哈希冲突

HashFun(Ki) == HashFun(Kj)  
即不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

数据结构:哈希散列结构理解及位图的使用_第1张图片

哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。  
哈希函数设计原则

  1. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  2. 哈希函数计算出来的地址能均匀分布在整个空间中
  3. 哈希函数应该比较简单 

常见哈希算法

  1. 直接定制法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B  
    优点:简单、均匀  
    缺点:需要事先知道关键字的分布情况  
    适合查找比较小且连续的情况  
    面试题:找出一个字符串中第一个只出现一次的字符,要求:时间复杂度O(N),空间复杂度O(1)  
  2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key
    % p(p<=m),将关键码转换成哈希地址 
  3. 平方取中发
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;  
    再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址  
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况  
  4. 折叠法
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表
    长,取后几位作为散列地址  
    折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况  
  5. 随机数法
    择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数  
    通常应用于关键字长度不等时采用此法  
  6. 数学分析法 
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均
    匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分
    布均匀的若干位作为散列地址。例如:  
      
    假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我们可以选择后面的四位
    作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如
    1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法  
    数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况  

     

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

处理哈希冲突 

解决哈希冲突两种常见的方法是:闭散列和开散列

闭散列

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


线性探测 

:(一旦冲突必须要找出下一个空余位置  )从发生冲突的位置开始,依次继续向后探测,直到找到空位置为止

【插入】  
1. 使用哈希函数找到待插入元素在哈希表中的位置  
2. 如果该位置中没有元素则直接插入新元素;如果该位置中有元素且和待插入元素相同,则不用插入;如果该位置中有元素但
不是待插入元素则发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。为什么?那该

采用线性探测,实现起来非常简单,缺陷是:  
一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键
码的位置需要许多次比较,导致搜索效率降低。
  
如何缓解呢?负载因子

数据结构:哈希散列结构理解及位图的使用_第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  
使用哈希函数计算出每个元素所在的桶号,同一个桶的链表中存放哈希冲突的元素。

数据结构:哈希散列结构理解及位图的使用_第3张图片

 以搜索平均长度为 的链表代替了搜索长度为 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就可以了。
 

 

 

你可能感兴趣的:(数据结构)