数据结构系列6——查找
查找
概念
- 查找表:用于查找的数据集合,由同一类数据元素或记录组成
- 静态查找表;顺序查找、折半查找、散列查找等
- 动态查找表:二叉排序树查找、散列查找等
- 关键字:数据元素中唯一标识该元素的某个数据项的值
- 平均查找长度:所有查找过程中进行关键字比较次数的平均住,\(n\)是查找表的长度,\(P_i\)是查找第\(i\)个元素的概率,一般认为每个元素的查找概率相等,即\(P_i = 1 / n\),\(C_i\)是找到第\(i\)个元素所进行的比较次数
注意,除非特别说明,否则最后一次与空结点的比较也要计算在\(ASL_{失败}\)中
\[ASL = \sum_{i = 1}^nP_iC_i
\]

一般线性表的顺序查找
- ASL
\[C_i = n - i + 1\\
ASL_{成功} = \sum_{i = 1}^nP_i(n - i + 1)\\
当P_i = 1 / n时,有(i = 1,C_i = n; i = n; C_i = 1, sum = \frac{(n + 1)n}{2})\\
ASL_{成功} = \sum_{i = 1}^nP_i(n - i + 1) = \frac{n + 1}{2}\\
ASL_{不成功} = n + 1
\]
- 优化思路
- 若预先得知每个记录的查找概率,先对记录的查找概率排序,使得从概率大向概率小查找
- 实现(哨兵版本)
// 数组从1开始存,下标范围[1, n]
struct SSTable {
int* elem;
int len;
};
int seqSearch(SSTable a, int target) {
a.elem[0] = target; // 设置哨兵,可以避免不必要的判断语句
int i;
for (i = a.len; a.elem[i] != target; i--);
// 返回的下标从1开始,若为0,则表示查找到了哨兵,此时查找失败
return i;
}
有序表的顺序查找
- 若已知表有序,则不用比较到表的另一端即可返回查找失败的信息,从而降低查找失败的ASL
- ASL
\[ASL_{成功} = \frac{n + 1}{2}\\
ASL_{不成功} = \sum_{j = 1}^nq_j(l_j - 1) = \frac{1 + 2 + \cdots + n + n}{n + 1} = \frac{n}{2} + \frac{n}{n + 1}
\]

折半查找(二分查找)
- 仅适用于有序的顺序表(链表不适用)
大多数情况下二分查找快,但是某些特殊情况顺序查找快,如在线性表有序的时候
- 时间复杂度\(O(log_2n)\)
- 判定树
- 根据mid计算方法计算出mid(在实现中\(mid = \lfloor (left + right) / 2 \rfloor\)),然后划分出左右子树,分别下沉递归计算

\[在图中,ASL_{成功} = (1 * 1 + 2 * 2 + 3 * 4 + 4 * 4) / 11 = 3\\
ASL_{失败} = (3 * 4 + 4 * 8) \ 12 = 11 \ 3
\]
- ASL
\[ASL_{成功} = \frac{1}{n}\sum_{i = 1}^nl_i = \frac {1}{n}(1 \times 1 + 2 \times 2 \times 2 + \cdots + h \times 2 ^{h - 1})\\
= \frac{n + 1}{n}log_2(n + 1) - 1 \approx log_2(n + 1) - 1
\]
散列表
- 概念
- 散列函数:把查找表中的关键字映射成该关键字对应的地址的函数,记为\(Hash(key) = Addr\)
- 冲突:散列函数可能会把两个或两个以上的不同关键字映射到同一地址,冲突不可避免
- 散列表:根据关键字二直接进行访问的数据结构,散列表建立了关键字和存储地址的一种直接映射关系
理想情况下,对散列表进行查找的时间复杂度为\(O(1)\)
- 散列函数
- 定义域必须包含全部存储的关键字,值域的范围依赖散列表的大小或地址范围
- 散列函数计算出来的地址应该能够等概率均匀地分布在整个地址空间中,散列函数应尽量简单,
- 常用的散列函数
- 直接定址法:直接取关键字的某个线性函数作为散列地址,不会冲突,适合关键字分布基本连续的情况,但是空位较多,会造成空间浪费
\[H(key) = key或H(key) = a \times key + b \]- 除留余数法,假定散列表表长为m,取一个不大于m但是最接近或等于m的质数p
\[H(key) = key \% p \]- 数字分析法,设关键字是r进制数,而r个数码在各位上出现的频率不同,选取数码分布较为均匀的若干位作为散列地址,这种方法适合已知的关键字集合,若集合更换,要重新构造散列函数
- 平方取中法,取平方数的中间几位作为散列地址,具体多少位视情况而定,这种方法与关键字的每位,因此散列函数分布较均匀,适合关键字的每位取值都不够均匀或均小于散列值地址所需的位数
- 解决冲突的方法
- 开放定址法,\(H_i\)表示处理冲突中第\(i\)次探测得到的散列地址(可能会多次冲突),开放定址法只有\(d_i\)的计算方法不同
\[H_i = (H(key) + d_i) \% m \]注意,最有有取模,如果是线性探测法,查找到了表尾再加一,要进行取模决定下一个位置(一般会到表头),计算ASL时要注意,如果是链地址法,空链或链为为
nullptr,因此该次没有比较,不计算在内,比较次数为0- 线性探测法:\(d_i = 0, 1, 2, \cdots , m - 1\),冲突发生时,查看下一单元直到找出空闲块,这样可能导致大量元素在散列地址上聚集,大大降低查找效率
- 平方探测法:\(d_i = 0^2, 1^2, -1^2, \cdots , k^2, -k^2\),其中\(k \leq m / 2\),散列表长度必须是一个可以表示成\(4k + 3\)的素数,缺点是不能探测到所有单元,但至少可以探测到一半单元
- 再散列法(双散列法),需要使用两个散列函数,\(i\)表示冲突的次数,初始为\(0\),最多经过\(m - 1\)次探测就会遍历表中所有位置,回到\(H_0\)的位置
\[H_i = (H(key) + i \times Hash_2(key)) \% m \]- 伪随机数法,\(d_i = 伪随机数\)
- 拉链法(链接法、chaining),可以把所有的同义词存储在一个线性链表中,适合经常插入和删除的情况

- 散列表的性能分析
- 查找,计算$$Addr = Hahs(key)$$,若无记录,返回查找失败,若有记录,若相等返回查找成功,否则计算下一个地址,重复之前步骤
- 计算ASL时先求出散列表,再模拟查找过程,统计查找次数然后除以关键字个数
- 不同的处理冲突的方式得到的散列表不同,它们的平均查找长度也不同
- 装填因子,一般记为\(\alpha\),\(\alpha\)越大,装填的越满,发生冲突的可能性越大
\[\alpha = \frac{表中的记录数n}{散列表长度m}\\ \]\[m = \lceil \frac{n}{\alpha} \rceil \]

浙公网安备 33010602011771号