C
实现顺序表查找时,可以使用 = = 或 ≠ ≠ 来遍历比较元素与查找值,有相等则查找成功; 有序表查找时, 可以使用 < < 和 > > , 来折半查找,相等时则查找成功. 最终得到元素的存储位置, 但有没有直接通过关键字key
得到要查找的记录内存存储位置呢?
f
, 使得每个关键字key
对应一个存储位置f(key)
。 f
, 又称为 哈希(hash
)函数。Hash table
):采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表collision
): 理想状态下,不同的关键字通过散列函数计算的地址也不同。但现实中,时常会碰到两个关键字 key1≠key2 k e y 1 ≠ k e y 2 , 但却有 f(key1)==f(key2) f ( k e y 1 ) == f ( k e y 2 ) , 这种现象被称为 冲突(collision
), 并把 key1 k e y 1 和 key2 k e y 2 称为这个散列函数的同义词。故如何处理冲突也是很重要的问题散列函数选择的两个原则
取关键字的某个线性函数值作为散列地址
即抽取关键字的一部分计算散列存储地址,比如手机号码11
位,取中间若干位计算。适合处理关键字位数比较大得到情况,但需要事先知道关键字的分布以及关键字的若干位分布均匀
假如关键字为整数,可以对其平方,然后取若干位作为散列地址,适合于不知道关键字的分布,而位数又不是很大得到情况
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够可以短些),然后将这几部分叠加求和,并按散列表表长,去后几位作为散列地址。 适合于不知道关键字的分布,且关键字位数较多的情况
此方法为最常用的构造散列函数方法,对于散列表长为m
的散列函数公式为
mod
即
取模, 当然也可与对折叠、平方取中后再取模
本方法的关键在于选择合适的p
, 否则容易产生同义词。 比如下图,有12
个记录的关键字,散列函数为 f(key)=key mod 12 f ( k e y ) = k e y m o d 12 , 比如29 mod 12 = 5
, 故存储在下标为5
的位置
对于上述散列函数,若关键字存在18
等数字,余数为6
,则与78
的存储位置冲突
选择一个随机数,取关键字的随机函数值为它的散列地址,散列函数为 f(key)=random (key) f ( k e y ) = r a n d o m ( k e y ) , random
为随机函数, 当关键字的长度不等时,比较适合
开放定址法即一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入, 公式为
冲突后,寻找下一位置, 这种解决冲突的开放定址法称为线性探测法
比如关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34}
, 表长为12
, 则散列函数为 f(key)=key mod 12 f ( k e y ) = k e y m o d 12
计算前5
个数{12, 67, 56, 16, 25}
, 都没有冲突,直接存入
计算37
, 余数为1
,与25
冲突, 故使用公式 f(37)=(f(37)+1)) mod 12=2 f ( 37 ) = ( f ( 37 ) + 1 ) ) m o d 12 = 2 , 故存储位置为2
接下来{22, 29, 15, 47}
都没有冲突, 正常存入
对于关键字48
, 计算得到 f(48)=0 f ( 48 ) = 0 , 与12
冲突,然后寻找下一地址 f(48)=(f(48)+1)) mod 12=1 f ( 48 ) = ( f ( 48 ) + 1 ) ) m o d 12 = 1 ,与25
冲突, … , 直到 f(48)=(f(48)+6)) mod 12=6 f ( 48 ) = ( f ( 48 ) + 6 ) ) m o d 12 = 6 , 有空位,存入
与线性探测法类似,只不过是增加平方运算,目的是为了不让关键字都聚焦在某一块区域,公式为
随机探测法 即在冲突时,对于位移量 di d i 采用随机函数计算得到, 这里的随机函数为伪随机函数,即随机种子相同的话,每次得到的数列相同, 公式为
选择多个散列函数, 每当发生散列地址冲突时,就换一个散列函数计算,知道解决冲突。这种方法使得关键字不产生聚焦,但增加了计算时间, 公式为
链地址法即将所有关键字为同义词的记录存储在一个单链表中, 这种表为同义词子表, 在散列表中只存储所有同义词字表的头指针。比如对于关键字集合{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34}
, 进行除留余数法, 可得到如下图所示的结构,此时,不存在冲突换址的问题,因为有冲突,只需要在当前位置给单链表增加节点即可
链地址法对于可能造成很多冲突的散列函数来说,保证了绝不会找不到地址,但也出现了查找是需要的便利单链表的性能损耗
下列代码为线性探测法实现的散列表查找
#define HASHSIZE 12 /* 定义散列表长为数组的长度 */
#define NULLKEY (-32768)
typedef int Status;
typedef struct
{
int *elem; // 数据元素存储基址
int count; // 当前数据元素个数
}HashTable;
int m = 0; // 散列表长,全局变量
/*
* 初始化散列表
*/
Status InitHashTable(HashTable *H)
{
m = HASHSIZE;
H->elem = (int *)malloc(m * sizeof(int));
H->count = m;
for(int 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; // 发现空位,插入关键字
}
Status SearchHash(HashTable H, int key, int *addr)
{
*addr = hash(key); // 求散列地址
while(H.elem[*addr] != key) // 如果不为空,则冲突
{
*addr = (*addr + 1) % m; // 开放定址法的先行探测
if(H.elem[*addr] == NULLKEY || *addr == hash(key)) // 表示通过线性探测法查找冲突新地址,直到为空或循环回到原点
return UNSUCCESS; // 说明关键字不存在
}
return SUCCESS;
}
关于查找的知识点,学习了几下几种
AVL
树才最佳但是还有很多查找的知识点未学习,比如B
树, B+
树,红黑树等等。继续加油, Fighting