查找算法系列之复杂算法:哈希查找

目前为止已经介绍了顺序查找、二分查找、分块查找、二叉排序树,见作者之前的文章:

http://blog.csdn.net/u010025211/article/details/46635325

http://blog.csdn.net/u010025211/article/details/46635183

今天这篇博文将介绍哈希查找。

1.为什么要用哈希查找

之前所讲的查找方法都是需要比较待查找元素与线性表或者树中的元素才能实现。

这样的时间复杂度为O(n)或者O(log n),那么有没有可能当给定一个待查找元素x,我们通过一种特殊的计算,计算出该元素在数组A的位置i,那么就可以直接找到该元素A[i]

哈希函数就是这种特殊的计算,能够降低时间复杂度。

2.特殊计算的定义——哈希函数(散列函数)

STL中就有hashtable这种类可以直接使用,那么哈希查找是怎么实现的呢?

哈希函数的构造方法: 
   2.1 直接定址法:
   取关键字或关键字的某个线性函数值为哈希地址。即: 地址H(key) = key 或 H(key) = a*key+b 实例:某大学从1960年开始招生,有历届招生人数统计表A,其中以年份为关键字。则哈希函数可设计为:H(key) = key – 1959 直接定址法由于关键字与存储地址存在一一对应关系,因此,不会 发生冲突现象。

Key

1959

1960

1961

1962

H(key)

0

1

2

3

若此时需要查询1961年得招生人数,那么得到其地址 H(1961) = 2,然后直接取A[2],A[2]中存的就是1961年的招生人数。

   2.2 除余法:
   选择一个适当的正整数P(P≤表长),用P 去除关键字,取所得余数作为哈希地址。即:H(key) = key % P (P ≤ 表长) 除余法的关键是选取适当的P,一般选P为小于或等于哈希表的长 度m的某个素数为好
   例: m = 8,16,32,128,256,512 P = 7,13,31,127,251,503 除余法不仅可以直接对关键字取模,也可在折叠、平方取中等运算 之后取模。
 
  2.3 平方取中法:

   取关键字平方后的中间几位为哈希地址。由于一个数的平方的中间几位与这个数的每一位都有关,因而,平方取中法产生冲突的机会相对较小。平方取中法中所取的位数由表长决定。
   例: K = 456 , K2 = 207936 若哈希表的长度m=102,则可取79(中间两位)作为哈希函数值。
   2.4 折叠法:
   把一个关键码分成位数相同的几段(最后一段的位数可以, 不同),段的长度取决于哈希表的地址位数,然后将各段的 叠加和(舍去进位)作为哈希地址。
   折叠法又分为移位叠加和边界叠加两种。其中,移位叠加是将 各段的最低位对齐,然后相加;而边界叠加则是两个相邻的段沿边界来回折叠,然后对齐相加。
   例:关键字K=58242324169,哈希表长度为1000,则将此关键字分成三位一段,两种叠加结果如下:582+ 423+ 241+69=315,582+324+ 241+96= 243 
   当关键字位数很多,而且关键字中每一位上数字分布大致均匀时,可以使用折叠法。 
   2.5 数字分析法:
   假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字中的若干位组成哈希地址。

但是在以上的方法中可能存在冲突,比如用取余法时,15%13 =2, 28%13=2,有两个关键字对应同一地址,就会发生冲突。

3.冲突的解决办法

     对不同关键字可能得到同一哈希地址,这一现象称为”冲突”,而发生冲突的关键字对于该哈希函数来说,称为”同义词”。因关键字集合比哈希表长度大,故冲突不可避免。

3.1 开放定址法

  基本做法:当冲突发生时,使用某种方法在哈希表中形成一探查序列,然后沿着此探查序列逐个单元地查找,直到碰到一个开放的地址(即该地址单元为空)为止。

  在哈希表中形成一探查序列时,可有三种不同的方法:
  ⑴ 线性探测法:
  基本思想:将散列看成是一个环形表,探测序列是(假设表长为m):
H(k),H(k)+1,H(k)+2,…,m-1,0,1,…,H(k)-1
  用线性探法解决冲突时,求下一个开放地址的公式为:
Hi = (H(k)+i) MOD m 
  ⑵ 二次探测法:
  二次探测法的探测序列依次是12,-12,22,-22,…等,当发生冲突时,求下一个开放地址的公式为: 
  H2i-1 = (H(k)+i2) MOD m
  H2i = (H(k)-i2) MOD m (1=< i <= (m-1)/2 )
  优点:减少了堆集发生的可能性。
  缺点:不容易探测到整个哈希表空间。
  ⑶ 伪随机探测法:
  采用随机探查法解决冲突时,求下一个开放地址的公式为:
  Hi = (H(k)+Ri) MOD m
  其中:R1,R2,…,Rm-1是1,2,…,m-1的一个随机排列。如何得随机排列,涉及到随机数的产生问题。

  3.2 再哈希法:
  基本做法:当冲突发生时,使用另一个哈希函数计算得到一个新的哈希地址,直到冲突不再发生时为止,即
  Hi = RHi(key) i = 1,2,…,k
  其中,RHi均是不同的哈希函数。
  这种方法的优点是不易产生”堆集”,但缺点是增加了计算时间。

 3.3 链地址法:
  基本做法:将所有关键字为同义词的结点链接在同一个单链表中。若选定的哈希函数所产生的哈希地址为0~m-1,则可将哈希表定义成一个由m个链表头指针组成的指针数组。

 这种方法的优点是:
    ① 不产生”堆集”。
    ② 由于结点空间是动态申请的,故更适合于造表前无法确定表长的情况。
    ③ 从表中删除结点容易。

 3.4 公共溢出区法:
  基本做法:假设哈希函数的值域为[0..m-1],则设向量HashTable[0..m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0..v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都被填入溢出表中。
在哈希表上进行查找的过程和建表的过程基本一致。假设给定的值为K,根据建表时设定的哈希函数H,计算出哈希地址H(K),若表中该地址对应的空间未被占用,则查找失败,否则将该地址中的结点与给定值K比较,若相等则查找成功,否则按建表时设定的处理冲突方法找下一个地址,如此反复下去,直到找到某个地址空间未被占用(查找失败)或者关键字比较相等(查找成功)为止。 

4.哈希查找的实现

    hash是一种典型以空间换时间的算法

  在哈希表上进行查找的过程和建表的过程基本一致。假设给定的值为K,根据建表时设定的哈希函数H,计算出哈希地址H(K),若表中该地址对应的空间未被占用,则查找失败,否则将该地址中的结点与给定值K比较,若相等则查找成功,否则按建表时设定的处理冲突方法找下一个地址,如此反复下去,直到找到某个地址空间未被占用(查找失败)或者关键字比较相等(查找成功)为止。

  虽然哈希表是在关键字和存储位置之间建立了对应关系,但是由于冲突的发生,哈希表的查找仍然是一个和关键字比较的过程,不过哈希表平均查找长度比顺序查找要小得多,比二分查找也小。

  查找过程中需和给定值进行比较的关键字个数取决于下列三个因素:哈希函数、处理冲突的方法和哈希表的装填因子。
哈希函数的”好坏”首先影响出现冲突的频繁程度,但如果哈希函数是均匀的,则一般不考虑它对平均查找长度的影响。
  对同一组关键字,设定相同的哈希函数,但使用不同的冲突处理方法,会得到不同的哈希表,它们的平均查找长度也不同。
  一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于哈希表的装填因子α。
  显然,α越小,产生冲突的机会就越小,但α过小,空间的浪费就过多。通过选择一个合适的装填因子α,可以将平均查找长度限定在一个范围内。

后续将给出参考代码

———————–修改于20150626—————————–

几种解决冲突方法的详细解释

对于开放定址法:只能存储和hash表同样长度的数据,因此具有很大的局限性,而且hash表一旦满了则不能再存。

对于链地址法:其实就是一个链表数组,不同的关键词对应与不同的链表头,将所有关键字为同义词的结点链接在同一个单链表中。

《查找算法系列之复杂算法:哈希查找》

左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
元素特征转变为数组下标的方法就是散列法,也就是hash函数。

适用范围:快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。

 公共溢出区法也是同样的道理,只是将链表改为了一个溢出数组。

/*开放定址法*/
#define tablesize 10//定义hash表的长度

typedef int  HashTable[10];
typedef int  KeyType;

#include <iostream>
using namespace std;

//Search
int Search_HashTable(HashTable ht, KeyType key)
{
	int address = key % tablesize;
	int comparetimer = 0; //aviod the loop of death. 

	while(comparetimer < tablesize && ht[address] != key && ht[address] != -1)
	{
		comparetimer ++; 
		address = (address + 1) % tablesize;  // *****sequence probing. *****
	}
	if(comparetimer == tablesize)
		return -1;
	return address;   // no match if ht[address] = -1.
}

//Insert
int Insert_HashTable(HashTable ht, KeyType key)
{
	int address;
	address = Search_HashTable(ht, key);
	if(ht[address] == -1)
	{
		ht[address] = key;
		return 1; //insert success.
	}
	else
		return -1;  //the key has been insert into the hashtable yet,or the HashTable is full.
}

//initialization
void Initial_HashTable(HashTable ht)
{
	for(int i = 0; i < tablesize; i++)
	{
		ht[i] = -1;
	}
}

/* 链地址法*/
#include<stdio.h>
#include<stdlib.h>

#define tablesize 5
typedef int ElemType;  
typedef struct HashNode  
{  
	ElemType elem;  
	struct HashNode *next;  
}HashNode;  


typedef struct  
{  
	HashNode ChainHash[tablesize];  
	int  count;  
}HashTable;  


int hash_mod(ElemType key)  
{  
	return key % tablesize;  
}  


void InsertHash(HashTable *h, int key)  
{  
	HashNode *p;  
	int index;  
	p = (HashNode*)malloc(sizeof(HashNode));  
	p->elem = key;  
	index = hash_mod(key);  
	p->next = h->ChainHash[index].next;  
	h->ChainHash[index].next = p;  
	h->count++;  
}  


void CreateHashTable(HashTable *h, int n)  
{  
	int key;  
	int i;  
	for(i = 0; i < n; i++)  
	{  
		printf("Input the  %d key :", i+1);  
		scanf_s("%d", &key);  
		InsertHash(h, key);  
	}	
}  


void PrintHashTable(HashTable *h)  
{  
	int i;  
	HashNode *p;  
	for(i = 0;i <= tablesize; i++)  
	{  
		p = h->ChainHash[i].next;  
		while(p)  
		{  
			printf("%-5d", p->elem);  
			p = p->next;  
		}  
	}  
}  


int SearchHash(HashTable *h, int key)  
{  
	HashNode *p;  
	int index;  
	int counter = 0;  
	index = hash_mod(key);  
	p = h->ChainHash[index].next;  
	while(p)  
	{  
		if(p->elem == key)  
			return 1;  
		else   
			p = p->next;  
	}  
	return 0;  
}  

void main()  
{  
	int n ,key;  
	int i;  
	HashTable H;  
	printf("input the length of the Hash that we want to build:");  
	scanf_s("%d", &n);  
	for(i = 0;i <= tablesize; i++)  
		H.ChainHash[i].next = NULL;  
	H.count = 0;  
	CreateHashTable(&H,n);  


	printf("The hash table that we build is:");  
	PrintHashTable(&H);  


	printf("\nInput the key that we want to search(-1 for exit):");  
	scanf_s("%d", &key);  
	while(key != -1)  
	{  
		if(SearchHash(&H, key))  
			printf("There is a %d record in the Hash Table!\n", key);  
		else  
			printf("There is not a %d record in the Hash Table!\n", key);  


		printf("\nInput the key that we want to search(-1 for exit):");  
		scanf_s("%d", &key);  
	}  

	free(&H);
	return;
}  

完整代码下载点击:

查找算法代码C++——包括顺序、二分、BST、哈希

点击打开链接


参考文章:http://www.cnblogs.com/li-hao/archive/2011/10/16/2214017.html

完整代码下载点击:

查找算法代码C++——包括顺序、二分、BST、哈希

http://download.csdn.net/detail/u010025211/8841123

    原文作者:查找算法
    原文地址: https://blog.csdn.net/u010025211/article/details/46636299
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞