转载自:https://www.cnblogs.com/niceforbear/p/4533701.html
大话数据结构这本书中的散列查找讲的很简单,这几简单记录一下。
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置。
存储位置=f(关键字)
对应关系f称为散列函数,又称哈希函数,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或者哈希表。
散列技术既是一种存储方法,又是一种查找方法。
散列不适合范围查找,排序等。
两个关键字key1不等于key2,但是f(key1)=f(key2),这种现象称为冲突。
散列函数的构造
构造哈希表的前提是要有哈希函数,并且这个函数尽可能地减小冲突
(1)直接定址法
可以取关键字的某个线性函数值为散列地址,即
f(key) = a*key + b
这样的哈希函数简单均匀,不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。
(2)数字分析法
该方法在知道关键字的情况下,取关键字的尽量不重复的几位值组成散列地址。
(3)平方取中法
取关键字平方后的中间几位为散列地址
(4)折叠法
将关键字分为位数相等的几部分,最后一部分的位数可以不等,然后把这几部分的值(舍去进位)相加作为散列地址。
(5)除留余数法
该方法为最常用的构造哈希函数方法,对于散列表长为m的散列函数公式为
f(key) = key mod p (p<=m)
使用除留余数法的一个经验是,若散列表表长为m,通常p为小于或等于表长的最小质数或不包含小于20质因子的合数。
实践证明,当p取小于散列表长的最大质数时,函数较好。
(6)随机数法
选择一个随机函数,取关键字的随机函数值作为散列地址。
=======================================================
处理冲突
哈希函数的构造可能导致散列地址会产生冲突,通常处理冲突的方法有下面几种:
(1)开发定址法
一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入,公式:
fi(key) = (f(key)+di) mod m (di=1,2,3...m-1)
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列,沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(该地址单元为空)为止(若要插入,在探查到开放的地址,则可将带插入的新节点存入该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。
比如说,我们的关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12。 我们用散列函数f(key) = key mod l2。
当计算前S个数{12,67,56,16,25}时,都是没有冲突的散列地址,直接存入:
计算key = 37时,发现f(37) = 1,此时就与25所在的位置冲突。
于是我们应用上面的公式f(37) = (f(37)+1) mod 12 = 2。于是将37存入下标为2的位置。这其实就是房子被人买了于是买下一间的作法:。
接下来22,29,15,47都没有冲突,正常的存入:
到了 key=48,我们计算得到f(48) = 0,与12所在的0位置冲突了,不要紧,我们f(48) = (f(48)+1) mod 12 = 1,此时又与25所在的位置冲突。于是f(48) = (f(48)+2) mod 12=2,还是冲突……一直到 f(48) = (f(48)+6) mod 12 = 6时,才有空位,机不可失,赶快存入:
我们把这种解决冲突的开放定址法称为线性探测法。
二次探测法
考虑深一步,如果发生这样的情况,当最后一个key=34,f(key)=10,与22所在的位置冲突,可是22后面没有空位置了,反而它的前面有一个空位置,尽管可以不断地求余数后得到结果,但效率很差。
因此我们可以改进di = 12, -12, 22, -22,……, q2, -q2 (q <= m/2),这样就等于是可以双向寻找到可能的空位置。
对于34来说,我们取di即可找到空位置了。另外增加平方运算的目的是为了不让关键字都聚集在某一块区域。我们称这种方法为二次探测法。
fi(key) = (f(key)+di) MOD m (di = 12, -12, 22, -22,……, q2, -q2, q <= m/2)
随机探测法
还有一种方法,是在冲突时,对于位移量di采用随机函数计算得到,我们称之为随机探测法。
此时一定会有人问,既然是随机,那么查找的时候不也随机生成吗?如何可以获得相同的地址呢?这是个问题。这里的随机其实是伪随机数。
伪随机数是说,如果我们设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,我们在査找时,用同样的随机种子,它每次得到的数列是相同的,相同的di当然可以得到相同的散列地址。
fi(key) = (f(key)+di) MOD m (di是一个随机数列)
总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是我们常用的解决冲突的办法。
(2) 再哈希法
再哈希法是当散列地址冲突时,用另外一个散列函数再计算一次,这种方法减少了冲突,但增加了计算的时间。
(3) 链地址法
前面我们谈到了散列冲突处理的开放定址法,它的思路就是一旦发生了冲突,就去寻找下一个空的散列地址。那么,有冲突就非要换地方吗?我们直接就在原地处理行不行呢?
可以的,于是我们就有了链地址法。
将所有关键字散列地址相同的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。
对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用12为除数,进行除留余数法:
此时,已经不存在什么冲突换址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。链地址法解决冲突的做法是:将所有关键字散列地址相同的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。
链地址法的优势是对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了査找时需要遍历单链表的性能损耗,不过性能损耗在很多场合下也不是什么大问题。
(4) 建立公共溢出区
这种方法的基本思想是:将散列表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
//哈希查找
#include<stdio.h>
#include<stdlib.h>
#define bool int
#define true 1
#define false 0
#define NULLKEY INT_MIN
#define HASHSIZE 12
typedef struct
{
int *data;
int count;
}HashTable;
int m = 0;
//初始化哈希表
bool InitHashTable(HashTable *h)
{
m = HASHSIZE;
h->count = m;
h->data = (int*)malloc(sizeof(int)*HASHSIZE);
for (int i = 0; i < HASHSIZE; i++)
{
h->data[i] = NULLKEY;
}
return true;
}
//hash函数
int Hash(int key)
{
return key%m;
}
//插入
void InsertHash(HashTable * h,int key)
{
int adr = Hash(key);
while (h->data[adr] != NULLKEY)
{
adr = (adr + 1) % m;
}
h->data[adr] = key;
}
//查找
bool SearchHashTable(HashTable *h, int key, int *adr)
{
*adr = Hash(key);
while (h->data[*adr] != key)
{
*adr = (*adr + 1)%m;
if (h->data[*adr] == NULLKEY || *adr == Hash(key))
{
return false;
}
}
return true;
}
int main(void)
{
int a[12] = { 12,67,56,16,25,37,22,29,15,47,48,34 };
HashTable h;
InitHashTable(&h);
for (int i = 0; i < 12; i++)
{
InsertHash(&h, a[i]);
}
int key = 39;
int adr;
bool status = SearchHashTable(&h, key, &adr);
if (status)
printf("查找成功,位置为:%d\n", adr);
else
printf("查找失败\n");
for (int i = 0; i < 12; i++)
{
status = SearchHashTable(&h, a[i], &adr);
if (status)
printf("查找到%d,位置为%d\n", a[i], adr);
}
return 0;
}
时间复杂度O(1)
查找,插入,删除都是O(1)