6.1 基本概念
什么是查找?
如何评价查找算法?
-
什么是查找?
在给定的数据结构中搜索满足条件的点,又称检索。 -
如何评价查找算法?
衡量一个查找算法好坏的依据主要是查找过程中需要执行的平均比较次数(或称为平均查找长度)。
6.2 顺序查找
什么是顺序查找?
是否一定要顺序储存?
是否要排序?
复杂性如何?
处理数据的动态变化如何?
-
什么是顺序查找?
逐个将每个结点的关键码和待查的关键码值进行比较,知道找到相等的结点或找遍了所有结点。 -
顺序查找对存储方式和排序是否有要求?
被查找的线性表可以是顺序存储或链接存储,对结点没有排序要求。
struct T
{
int key;
}
int SeqSearch(T A[], int key, int n)
{
int i;
for(i = 0; i < n; i++)
{
if(A[i].key == key)
{
return i;
}
}
return -1;
}
- 复杂性如何?
(1) 最好情况,第一个结点就是要查找的结点,时间复杂性O(1).
(2) 最坏情况,最后一个结点是要查找的结点或者表中没有这个结点,时间复杂性O(n).
(3) 一般每个结点都有相同的查找概率,顺序查找的平均长度为n/2, 时间复杂性为O(n).
6.3 折半查找
什么是折半查找?
是否一定要顺序存储?
复杂性如何?
处理数据的动态变化如何?
-
什么是折半查找?
首先找到表的中间结点,将其关键码与要查找的值进行比较,若相等,则查找成功;若大于要查找的值,则继续在表的前半部分折半查找,否则继续在表的后半部分进行折半查找。 -
折半查找对存储和排序是否有要求?
要求顺序存储并且结点排序。
例如:
注意:
(1) 查找成功,low == high
(2) 查找失败,low > high
int BinarySearch(T A[], int key, int n)
{
int low, high, mid;
low = 0;
high = n-1;
while(low <= high)
{
mid = (low + high) / 2;
if(key == A[mid].key;
return mid;
else if(key > A[mid].key)
low = mid + 1;
else
high = mid-1;
}
return -1;
}
-
复杂性如何?
(1) 最好情况
只需比较一次就能找到,时间复杂度为O(1).
(2) 最坏情况
找不到对应结点,需要log2(n)次比较,时间复杂度为O(logn)
(3) 平均时间复杂度为O(logn) -
顺序查找和折半查找的对比
思考:如何在顺序查找和折半查找取得折中效果(又好又快)?
析:如果既要有较快的查找速度,又要满足元素动态变化的要求,可以采用分块查找算法。
6.4 分块查找
什么是分块查找?
是否要排序?如何排序?
采取什么存储方式?
复杂性如何?
处理数据的动态变化如何?
- 什么是分块查找?
(1) 将一个大的线性表划分成若干块(如何分块?),块内不排序,块之间排序(假设非递减)。建立一个索引表,把每块中的最大关键码值作为索引表的关键码值,且非递减排序。
(2) 查找某结点时,先在索引表中顺序查找或者折半查找,找到该结点对应的块,然后在块内顺序查找。
- 复杂性如何?
分块查找的平均查找长度(比较次数)由索引表的平均查找长度和对块的平均查找长度组成。
假设线性表有n个结点,等分成b块,每块有s = n/b个结点。假设对索引表和块都采用顺序查找,假定对每个结点的查找概率相同。
则平均查找长度为b/2+s/2 = (n/s+s)/2.
当s^2 = n时取得最小值O(n ^ 0.5)-----指导分块查找算法如何分块?
思考:若索引表和块都排序,都采用折半查找呢?
- 顺序查找,折半查找,分块查找的比较
注意:
(1) 分块查找中,当增加或减少结点以及结点的关键码改变时,只需要调整结点所在的块即可。
(2) 当结点变化频繁,导致块与块之间结点数相差很大时,查找效率会下降。
思考:
(1) 如何应对结点变化频繁?对块的存储方式有何考虑?
(2) 如何实现复杂度为O(1)的查找算法?
析:散列查找
6.5 散列查找
什么是散列函数?什么是散列表?
什么是冲突?什么是同义词?
什么是负载因子?设计散列函数要考虑什么?
常用的散列函数有哪些?
怎么处理冲突?
什么是开放地址法?什么是链表地址法?
- 什么是散列函数(哈希函数)?
将分散在一个大区间(例如【0,79999】)的关键码值映射到一个较小的区间(例如【0,9】),用映射后的值作为访问结点的下标。
int Hash(int key)
{
return key%10;
}
- 什么是散列表(哈希表)?
与散列函数相关联的是一个长度为n的表,称为散列表或哈希表,用来存放结点的数据或数据的引用。散列函数将关键码值映射到【0,n-1】范围内的整数值。
例如:假设一组结点(10个结点),最小最大关键码分别为0和79999,建立一个长度为80000的数组,每个结点存放在关键码对应的数组位置。
结果会导致空间闲置,内存空间不够。
用长度n为10的数组来存放,关键码为k的结点存放在k%10号数组单元, k=k%10.
思考:会出现什么样的问题?
-
什么是冲突?什么是同义词?
散列函数经常是多对一,导致冲突(碰撞)。具有相同散列值的关键码值称为同义词。 -
什么是负载因子?
两个结点不能占据同一个位置,需要一种冲突解决策略。为了讨论冲突及其解决办法,引入负载因子α:
α = 散列表中的结点数/散列表长度 -
设计散列函数要考虑什么?
(1) 能够有效减少冲突;
(2) 具有很高的执行效率。
任何不是整数的关键码都可以转换为整数,
下面讨论的假定关键码是整数。
- 常用的散列函数有哪些?
(1) 除留余数法
利用余数运算将整数型的关键码值映射到0-n-1的范围内。选一个适当的正整数p,用p去除关键码值,所得余数作为该关键码的散列值。
方法的关键是p的选取:
一般选小于等于n的最大素数。
(2) 数字分析法
当关键码的位数很多时,可以通过对关键码的各位进行分析,丢掉分布不均匀的位,留下分布均匀的位作为散列值。
只能适合静态的关键码值集合。
(3) 平方取中法
计算关键码值的平方,从平方的中间位置取连续若干位,将这些位构成的数作为散列值。
(4) 随机乘数法
使用一个随机实数f(0<=f<1), f*key的小数部分与散列表长度n相乘,将乘积的整数部分作为key的散列值。
(5) 折叠法
将关键码值分成若干段,其中至少有一段的长度等于散列表长度的位数,把这些多段数相加,并舍弃可能产生的进位,所得整数作为散列值。
关键码值的位数比散列表长度值的位数多出很多时,可以采用折叠法。
-
怎么处理冲突?
当冲突发生时,有两种选择:
(1) 开放地址法
将引起冲突的新数据项存放在表中另外的位置
(2) 链表地址法
为每个散列值单独建立一个表,存放具有相同散列值的所有数据项。 -
什么是开放地址法?
散列表的每个表项有一个表示该表项是否被占用的标志,当试图加入新的数据项到散列表中时,首先判断散列值指定的表项是否被占用,如果被占用,则依据一定的规则在表中寻找其它空闲的表项。
如何探测空闲表项?
(1) 线性探测法
(探测空闲表项的最简单的方法)
当冲突发生时,顺序地探测下一个表项是否空闲。若Hash(key)=d, 而第d表项被占用,则依次探测d+1, d+2,……, n-1,0,1,……,d-1.-----------这会导致一个新的问题:堆积
(使用散列函数计算出散列值,散列表的对应表项可能已经被非同义词的结点占用)
思考:如何改善堆积?
(2) 双散列函数探测法(可改善堆积现象)
使用2个散列函数Hash1和Hash2,其中Hash1以关键码值为自变量,产生一个0~n-1之间的数。Hash1用来产生基本的散列值,当发生冲突时,利用Hash2计算探测序列。即Hash1(key)=d冲突时,再计算k=Hash2(key),得到探测序列为(d+k)%n,d+(2k)%n, (d+3k)%n,……
其中对Hash2的要求?
当n为素数时,Hash2(key)可以是1~n-1之间的任何数;
当n为2的幂次时,Hash2(key)可以是1~n-1之间的任何奇数。
这样,探测序列跳跃式地散列到整个存储区域,减少堆积。
- 什么是链表地址法?
为散列表的每个表项建立一个单链表,用于链接同义词子表,每个表项需要增加一个指针域。
(1) 独立链表地址法:在散列表的基本存储区域外开辟一个新的区域用于存储同义词子表
(2) 公共链表地址法:将同义词子表存储在散列表所在基本存储区域里。例如,在基本存储区域从后往前探测空闲表项,找到后将其链接到同义词子表中。
总结:
(1)开放地址法的要求表长是固定的,独立链表地址法的表项是动态分配的。链表法的缺点是要为每个表项设立指针域。
(2) 开放地址法
a. 线性探测法
b. 双散列函数探测法
(3) 链表地址法
a. 独立链表地址法
b. 公共链表地址法
(4) 独立链表地址法是查找效率最好的解决冲突的方法,是解决冲突的首选方法。