这章主要介绍了一些常用的查找方法
文章目录
|- 基本概念
|- 顺序查找
|- 线性结构------|- 折半查找
|- 分块查找
|- 二叉树排序
|- 树形结构------|- 二叉平衡树
|- B树,B+树
|- 性能分析
|- 散列结构 —— 散列表------- |- 冲突处理
|- 效率指标 —— 平均查找长度---|- 查找成功
|- 查找失败
一、查找的基本概念
静态查找表:顺序查表、折半查找、散列查找;
动态查找表:二叉排序树的查找、散列查找;
二、顺序查找和折半查找
1. 顺序查找
顺序查找又称为线性查找
(1)一般线性表的顺序查找
基本思想:从表的一端开始,逐个检查关键字是否满足给定的条件。
typedef struct{
ElemType *elem;
int TableLen;
}SSTable;
int Search_Seq(SSTable ST, ElemType key){
ST.elem[0] = key;
for(i = ST.TableLen; ST.elem[i] != key; --i);
return i;
}
ST.elem[0] 称为 “哨兵”。引入它的目的是使循环不用判断数组是否会越界,从而提高程序的效率。
- 查找成功时,平均查找长度为:
每个元素的查找概率相等时,平均查找长度为:
-
查找不成功时,平均查找长度为:
-
优点:对数据元素的存储没有要求,顺序存储或链式存储皆可,数据元素也不一定必须有序。
-
缺点:当n较大时,平均查找长度较大,效率低。
(2)有序表的顺序查找
- 当顺序表有序时,查找失败的时候就不用了再比较到表的另一端就能返回查找失败的信息,这样就能降低顺序查找失败的平均查找长度。
- 可以用如图的判定树来描述查找过程:
-
查找成功的平均查找长度:
-
查找失败的平均查找长度:
-
注意:有序表的顺序查找和后面的折半查找思想不同,而且有序表的顺序查找中的线性表是可以链式存储的。
2. 折半查找
折半查找,又称为二分查找。仅适用于有序的顺序表。
基本思想:key 值与中间元素比较,相等则成功;key 大则比较右半边;key 小则比较左半边。
package SearchPackage;
public class HalfSearch {
public static int halfSearch(int nums[], int num){
int index = -1;
int low = 0;
int high = nums.length;
int middle;
while (low <= high){
middle = (low+high) / 2;
if (num == nums[middle]){
index = middle;
}
else if (num > nums[middle]){
low = middle + 1;
}
else {
high = middle - 1;
}
}
return index;
}
public static void main(String[] args) {
int[] nums = {5,2,7,3,1,8,9,3};
int num = 7;
int index = halfSearch(nums, num);
System.out.println(index);
}
}
算法的时间复杂度为O(log2 n)
-
判定树(中序序列有序)如下图:
-
查找成功的平均查找长度为:
-
元素个数为n时,树高为
-
比较次数最多不会超过树高度。
-
注意:折半查找的存取结构必须具有随机存取的特性。仅适合于线性表的顺序存储结构,不适合链式存储结构,且要求元素有序。
-
折半查找和二叉排序树:
(1)折半查找:平均查找长度和最大查找长度都是 O(log2 n);
(2)二叉排序树:查找性能与数据的输入顺序有关,最好情况下平均查找长度为 O(log2 n),最坏情况下,形成单支树,查找长度为 O(n)。
3. 分块查找
-
分块查找,又称为索引顺序查找,吸取了顺序查找和折半查找各自的优点,既有动态结构,又适于快速查找。
-
基本思想:将查找表分为若干个子块,块内元素可以无序,块间元素有序。
-
分块查找的过程分为两步:
(1)在索引表中确定待查记录所在的块;(可顺序、可折半)
(2)在块内顺序查找。 -
分块查找的平均查找长度为索引查找(Ll)和块内查找(Ls)的平均长度之和。
将长度为 n 的表分为 b 块,每块有 s 个记录,
(1)若在块内和索引表都用顺序查找,则平均查找长度为:
当
平均查找长度最小,为:(2)若对索引表用折半查找,则平均查找长度为:
(3)若对索引表和内部都用折半查找,则平均查找长度为:
三、B树和B+树
(目前还没看懂,后续再添加)
1. B树及其基本操作
(1)B树的高度
(2)B树的查找
(3)B树的插入
(4)B树的删除
2. B+树基本概念
四、散列表
1. 散列表的基本概念
- 散列函数:一个把查找表中的关键字映射成该关键字对应地址的函数,记为Hash(key)=Addr;
- 散列函数可能会把两个或以上的不同关键字映射到同一地址,这种情况称为“冲突”。这些发生碰撞的不同关键字称为 同义词。
- 散列表:根据关键字而直接进行访问的数据结构。散列表建立了关键字和存储地址之间的一种直接映射关系。
2. 散列函数的构造方法
采用何种方法构造散列函数主要取决于关键字集合的情况,但目标是为了使产生冲突的可能性尽量降低。
常用的散列函数:
(1)直接定址法
适合关键字分布基本连续的情况。若分布不连续,将造成存储空间的浪费。
(2)除留余数法
假定表长为 m,取一个不大于 m 但接近或等于 m 的质数 p
(3)数字分析法
适合于已知的关键字集合,如果更换了关键字,就需要重新构建散列函数。
(4)平方取中法
适用于关键字的每一位取值都不够均匀或均小于散列地址所需的位数。
(5)折叠法
适用于关键字位数很多,而且关键字中每一位上数字分布大致均匀。
3. 处理冲突的方法
假设已经选定散列函数 H(key),下面用 Hi 表示发生冲突后第 i 次探测的散列地址。
(1)开放定址法
其中 m 为散列表表长,di 为增量序列。
当取定某一增量序列后,则对应的处理方法是确定的,通常由以下四种取法:
(1)线性探测法
缺点:大量元素在相邻的散列地址上聚集,降低了查找效率。
(2)平方探测法
线性表长度 m 必须是 3k+4 的素数,又称二次探测法
优点:避免出现堆积问题
缺点:不能探测到线性表上的所有单元,但至少能探测一半的单元
(3)再散列法
又称为双散列法,需要使用两个散列函数。具体的散列函数如下:
(4)伪随机序列法
di 为伪随机数序列
(2)拉链法
把所有的同义词存储在一个线性链表中,这个线性表由其散列地址唯一标识。拉链法适用于经常插入和删除的情况。
4. 散列查找及其性能分析
散列表的查找效率取决于三个因素:散列函数、处理冲突的方法、装填因子。
散列表的平均查找长度依赖于散列表的装填因子。装填因子越大,表示装填的记录越满,发生冲突的可能性就越大,反之发生冲突的可能性较小。
五. 字符串模式匹配
这一部分比较复杂,单独写了一个博客,欢迎大家一起讨论学习
(链接:字符串模式匹配算法)
1. 简单的模式匹配算法——BF模式匹配
串的模式匹配,是求模式串在主串中的位置。时间复杂度 O(n*m).
下面来看一下代码
int Index(SString S, SString T, int pos){
int i = pos;
int j = 1;
while(i <= S.Len && j <= T.Len){
if(S.ch[i] == T.ch[j]){
i++;
j++;
}
else{
i = i - j + 2;
j = 1;
}
}
if(j > T.Len){
return i - T.Len
}
else
return 0;
}
2. 改进的模式匹配算法——KMP算法
KMP的时间复杂度为 O(n+m)
KMP算法的改进在于:每当一趟匹配过程中出现比较的字符不相等时,不需要 i 回溯,二是利用已经得到的“部分匹配”的结果将模式串向右“滑动”尽可能远的一段距离后,继续进行比较。
这部分重点如下:单独写了一个博客,可以一起讨论学习(字符串模式匹配算法)
字符串的模式匹配——KMP算法
next 值的计算思想过程
next 值的手工计算过程
nextval 值是对 next 值的改进
下面来看一下代码
//求next函数值的算法
void get_next(char T[], int next[]){
int j = 1;
int k = 0;
next[1] = 0;
while(j < T.Len){
if(k == 0 || S.ch[j] == T.ch[k]){
++i;
++k;
next[j] = k;
}
else
k = next[k]
}
}
//KMP的主体代码如下
int KMP(SString S, SString T, int pos){
int i = pos;
int j = 1;
while( i <= S.Len && j <= T.Len){
if(j == 0 || S.ch[i] == T.ch[j]){
i++;
j++;
}
else
j = next[j];
}
if(j > T.Len){
return i - T.Len;
}
else
return 0;
}