线性表的长度为n,每块有i个记录,平均查找次数[log2(n/2+1)]-1+(i+1)/2
如果线性表既希望查找速度快,又需要冬天变化,则可以用索引查找
过程:
首先根据给定的索引值k1,在索引表上查找索引值等于k1的索引项,以确定对应字表在表中的开始位置
和长度,然后关键字等于k2的元素,
设置数组A是具有mainlist类型的一个主表,数组B是具有indexlist类型的在主表A上建立的一个索引表,m为索引
B的实际长度,即所含的索引项的个数,k1和k2分别为给定
算法实现;
int Indsch(mainlist A, indexlist B, int m, IndexKeyType K1, KeyType K2)
{//利用主表A和大小为 m 的索引表B索引查找索引值为K1,关键字为K2的记录
//返回该记录在主表中的下标位置,若查找失败则返回-1
int i, j;
for (i = 0; i < m; i++)
if (K1 == B[i].index)
break;
if (i == m)
return -1; //查找失败
j = B[i].start;
while (j < B[i].start + B[i].length)
{
if (K2 == A[j].key) break;
else
j++;
}
if (j < B[i].start + B[i].length)
return j; //查找成功
else
return -1; //查找失败
}
//若 IndexKeyType 被定义为字符串类型,则算法中相应的条件改为
//strcmp (K1, B[i].index) == 0;
//同理,若KeyType 被定义为字符串类型
//则算法中相应的条件也应该改为
//strcmp (K2, A[j].key) == 0
//若每个子表在主表A中采用的是链接存储,则只要把上面算法中的while循环
//和其后的if语句进行如下修改即可:
while (j != -1)//用-1作为空指针标记
{
if (K2 == A[j].key) break;
else
j = A[j].next;
}
return j;
}
分块查找属于索引查找,其对应的索引表为稀疏索引,具体地说,分块查找要求主表中每个子表(又称为块)之间
是递增(或递减)有序的。即前块中最大关键字必须小于后块中的最小关键字,但块内元素的排列可无序。它还要求索
引值域为每块中的最大关键字。
算法实现:
int Blocksch(mainlist A, indexlist B, int m, KeyType K)
{//利用主表A和大小为m的索引表B分块查找关键字为K的记录
int i, j;
for (i = 0; i < m; i++)
if (K <= B[i].index)
break;
if (i == m)
return -1; //查找失败
j = B[i].start;
while (j < B[i].start + B[i].length)
{
if (K == A[j].key)
break;
else
j++;
}
if (j < B[i].start + B[i].length)
return j;
else
return -1;
}
//若在索引表上不是顺序查找,而是二分查找相应的索引项,则需要把算法中的for循环
//语句更换为如下的程序段:
int low = 0, high = m - 1;
while (low <= high)
{
int mid = (low + high) / 2;
if (K == B[mid].index)
{
i = mid;
break;
}
else if (K < B[mid].index)
high = mid - 1;
else
low = mid + 1;
}
if (low > high)
i = low;
这里当二分查找失败时,应把low的值赋给i,此时b[i].index是刚大于K的索引值
当然若low的值为m,则表示真正的查找失败。
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
它的公式为: fi(key)=(f(key)+di) MOD m(d1=1,2,3,4.....,m-1)
比如说,关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表长为12。散列函数f(key) = key mod 12
当计算前5个数{12, 67, 56, 16, 25}时,都是没有冲突的散列地址,直接存入,如下表所示。
计算key = 37时,发现f(37) = 1,此时就与25所在的位置冲突。于是应用上面的公式f(37) = (f(37) + 1) mod 12 =2,。于是将37存入下标为2的位置。
如下:
下标 0 1 2 3 4 5 6 7 8 9 10 11
关键字12 25 37 16 67 56 22
接下来22,29,15,47都没有冲突,正常的存入,如下
下标 0 1 2 3 4 5 6 7 8 9 10 11
关键字12 25 37 15 16 29 67 56 22 47
到了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时,才有空位,如下表所示。
,还是冲突......一直到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 = -1即可找到空位置了。另外,增加平方运算的目的是为了不让关键字都聚集在某一块区域。称这种方法为二次探测法。
fi(key)=(f(key)+di=1^2,-1^2,2^2,....q<=m/2)
还有一种方法,在冲突时,对于位移量di采用随机函数计算得到,称之为随机探测法。
既然是随机,那么查找的时候不也随机生成di 吗?如何取得相同的地址呢?这里的随机其实是伪随机数。伪随机数就是说,如果设置随机种子相同,则不
断调用随机函数可以生成不会重复的数列,在查找时,用同样的随机种子,它每次得到的数列是想通的,相同的di 当然可以得到相同的散列地址。
fi(key)=(f(key)+di MOD m (di是一个随机数列))
总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是常用的解决冲突的方法。
2 再散列函数法
对于散列表来说,可以事先准备多个散列函数。
fi(key)=RHi(key)(i=1,2,....k)
这里RHi 就是不同的散列函数,可以把前面说的除留余数、折叠、平方取中全部用上。每当发生散列地址冲突时,就换一个散列函数计算。
这种方法能够使得关键字不产生聚集,但相应地也增加了计算的时间。
3 链地址法
将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表前面的指针。对于关键字集合
{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},用前面同样的12为余数,进行除留余数法,可以得到下图结构。
此时,已经不存在什么冲突换地址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。
链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保证。当然,这也就带来了查找时需要遍历单链表的性能损耗。
4 公共溢出区法
这个方法其实更好理解,你冲突是吧?那重新给你找个地址。为所有冲突的关键字建立一个公共的溢出区来存放。
就前面的例子而言,共有三个关键字37、48、34与之前的关键字位置有冲突,那就将它们存储到溢出表中。如下图所示。
在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表中进行顺序查找。
如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
算法实现
#include
#include
#define OK 1
#define ERROR 0
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义散列表表未数组的长度
#define NULLKEY -32768
typedef struct
{
int *elem; //数据元素存储基地址,动态分配数组
int count; //当前数据元素个数
}HashTable;
int m = 0; //散列表长,全局变量
//初始化散列表
int InitHashTable(HashTable *h)
{
int i;
m = HASHSIZE;
h->elem = (int *)malloc(sizeof(int) * m );
if(h->elem == NULL)
{
fprintf(stderr, "malloc() error.\n");
return ERROR;
}
for(i = 0; i < m; i++)
{
h->elem[i] = NULLKEY;
}
return OK;
}
//散列函数
int Hash(int key)
{
return key % m; //除留余数法
}
//插入关键字进散列表
void InsertHash(HashTable *h, int key)
{
int addr = Hash(key); //求散列地址
while(h->elem[addr] != NULLKEY) //如果不为空,则冲突
{
addr = (addr + 1) % m; //开放地址法的线性探测
}
h->elem[addr] = key; //直到有空位后插入关键字
}
//散列表查找关键字
int SearchHash(HashTable h, int key)
{
int addr = Hash(key); //求散列地址
while(h.elem[addr] != key) //如果不为空,则冲突
{
addr = (addr + 1) % m; //开放地址法的线性探测
if(h.elem[addr] == NULLKEY || addr == Hash(key))
{
//如果循环回原点
printf("查找失败, %d 不在Hash表中.\n", key);
return UNSUCCESS;
}
}
printf("查找成功,%d 在Hash表第 %d 个位置.\n", key, addr);
return SUCCESS;
}
int main(int argc, char **argv)
{
int i = 0;
int num = 0;
HashTable h;
//初始化Hash表
InitHashTable(
//未插入数据之前,打印Hash表
printf("未插入数据之前,Hash表中内容为:\n");
for(i = 0; i < HASHSIZE; i++)
{
printf("%d ", h.elem[i]);
}
printf("\\n"
//插入数据
printf("现在插入数据,请输入(A代表结束哦).\n");
while(scanf("%d", &i) == 1 && num < HASHSIZE)
{
if(i == 'a')
{
break;
}
num++;
InsertHash(&h,i);
if(num > HASHSIZE)
{
printf("插入数据超过Hash表大小\n");
return ERROR;
}
}
//打印插入数据后Hash表的内容
printf("插入数据后Hash表的内容为:\n");
for(i = 0; i < HASHSIZE; i++)
{
printf("%d ", h.elem[i]);
}
printf("\n");
printf("现在进行查询.\n");
SearchHash(h, 12);
SearchHash(h, 100);
return 0;
}
散列技术最适合的求解问题是查找与给定值相等的记录。对于查找来说,简化了比较过程,效率会大大提高。