查找的基本概念
- 查找:在数据集合中寻找满足某种条件的数据元素的过程。
- 查找表(查找结构):用于查找的数据集合,称为查找表,由同一类数据元素组成,可以是一个数组或链表等数据类型,对查找表的常用操作有:
(1)查找某元素是否在查找表中
(2)检索某数据元素是否在查找表中
(3)插入元素
(4) 删除元素 - 静态查找表:如果一个查找表的操作只涉及(1)和(2)的操作,则无需动态修改查找表,此类查找表称为静态查找表。【顺序查找、折半查找、散列查找】
- 动态查找表:需要动态的插入删除元素的查找表。【二叉排序树的查找、散列查找、二叉排序树、B树】
- 关键字:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找的结果应该是唯一的。
- 平均查找长度:查找长度是查找一个元素需要比较的关键字次数,而平均查找次数就是所有查找过程中对关键字比较次数的平均值。平均查找长度是衡量查找算法效率的最主要的指标。
ASL=sum(Pi*Ci);
//Pi是查找的概率
//Ci是找到第n个元素所需进行的比较次数
顺序查找法(线性查找)
分类:一般的无序的线性表的查找、对按关键字有序的顺序表的顺序查找
1. 一般的无序的线性表的查找
思路:从线性表的一端开始,逐个检查关键字是否满足给定条件,若找到某个元素的关键字满足给定条件,则查找成功,返回该元素所在线性表中的位置;若查找到线性表的另一端仍然没有找到符合条件的元素,则返回查找失败信息。
typedef struct{
Elemtype *elem;//元素存储空间基址,建表时按实际长度分配,0号单元留空
int tablelen;//表的长度
}SSTable;
int search_Seq(SSTable ST,Elemtype key){
ST.elem[0] = key;
for(int i=ST.tablelen;ST.elem[i]!=key;--i)
return i;
}
可借鉴思路:使用“哨兵结点"使得在查找表中查找的时候循环不需要判断数组是否越界,因为当i==1的时候数组一定会跳出循环,通过引入哨兵结点可以避免很多不必要的判断。
通常查找表中的元素的查找概率不相等,若能预先得知每个记录的查找概率,则应先对元素的查找概率进行排序,使表中元素按照查找概率从大到小的顺序排列。
缺点:当n很大时,查找效率低,需要比较次数太多
优点:元素不需要有序;对元素的存储也没有要求
注意:对线性的链表只能用顺序查找
2. 对按关键字有序的顺序表的顺序查找
在查找之前就知道表是按照关键字有序的顺序排列的,当查找失败时不需要比较到表的另一端就可以判断出,处于两个结点中间就失败直接返回。
平均查找长度和无序线性表查找相同。
有序表的顺序查找和后面的折半查找是不一样的,而且有序表的顺序查找中的线性表可以是链式存储的。
折半查找法
int binary_search(SeqList L,int key){
int low = 0,high = L.tablelen-1,mid;
while(low<=high){
mid = (low + high)/2;
if(L.elem[mid]==key)
return mid;
else if(L.elem[mid]>key)
high = mid-1;
else
low = mid+1;
}
return -1;
}
折半查找的过程可以使用二叉树来表示,称为判定树。判定树的递归定义:
pNode create(int low,int high,int* T){
if(low<high) {
int mid = (high - low)/2;
Bitree bt = new Node;
bt->data = T[mid];
bt->lchild = create(low,mid-1,T);
bt->rchild = create(mid+1,high,T);
}
return bt;
}
使用折半查找法查到给定值的比较次数最多不超过树的高度。
优点:折半查找需要方便地定位查找区域,所以适合折半查找的存储结构必须具有随机存取的特点
缺点:仅适合线性表的顺序存储结构,不适合链式存储结构,且要求元素按关键字有序排列
分块查找法
基本思想:将查找表分成若干个子块,块内元素可以无序,但块间元素有序。再建立一个索引表,索引表中的元素还有各块的最大关键字
和各块中第一个元素的地址
,索引表按关键字有序排列。
查找步骤:首先使用折半查找或者顺序查找找到待查记录所在的块(利用关键字找到所属块,利用第一个元素的地址定位找到的块),然后在块内进行顺序查找。
B树及其基本操作、B+树的基本概念
B树
1. B树(多路平衡查找树)
B树中所有结点的孩子结点数的最大值称为B树的阶。一般用m表示,一棵m阶B树或为空树,或为满足以下条件的m叉树:
(1) 树中每个结点至多有m棵子树
(2) 若根节点不是终端结点,则至少有两棵子树
(3) 除根节点外的所有非叶节点至少有(m/2+1)棵子树
(4) 所有非叶节点的结构:n|P0|K1|P1|K2…Kn|Pn.Ki是关键字信息;Pi是指向子树根结点的指针,指针Pi-1所指子树中所有结点的关键字均小于Ki.Pi所指子树的关键字都大于Ki.n为节点中关键字的个数。
(5) 所有叶节点都出现在同一层次上,并且不带信息(可以看作是外部节点或者类似于折半查找判定树的查找失败结点,实际上这些结点都不存在,指向这些结点的指针为空)
B树是所有结点的平衡因子都是0的树
2. B树的高度(磁盘的存储次数)
B树中的大部分操作所需的磁盘存取次数与B树的高度成正比。
🐷这里不考虑最后一层叶子结点(有的题考虑,注意一下这里)
如果n>=1,则对任意一棵包含n个关键字、高度为h、阶数为m的B树:
注:在总的结点数目固定的条件下
3. B树的查找
在B树上的查找与二叉查找树相似,但是对于每个结点来说是多路查找,思想和顺序查找中的分块查找类似,包括两个步骤:
- 在B树中查找结点
- 在结点内查找关键字
由于B树常存储在磁盘上,则前一个查找操作是在磁盘上进行的,而后一个查找操作是在内存中进行的,即在找到目标结点后,先将结点中的信息读入内存,然后再采用顺序查找或折半查找查找等于K的关键字.
在B树上查找到某个结点之后,先在有序表中进行查找,若找到则成功,否则按照对应的指针信息到所指的子树中去查找,当查找到叶结点时(对应的指针为空指针),说明树中没有关键字,查找失败。
4. B树的插入
查找过程:
- 定位: 利用B树查找算法,找出插入关键字的最底层中的某个非叶结点(注意:插入B树中插入关键字一定是插入到最底层中的某个非叶结点内的。
- 插入:在B树中每个非失败结点的关键字的个数都在
[m/2,m-1]
之间。当插入后的结点的关键字个数小于m时,可以直接插入;插入检查被插入结点内关键字的个数,当插入后的关键字结点个数大于m-1时,则必须对结点进行分裂。
分裂方法
5. B树的删除
删除后结点中的关键字个数>=m/2,涉及结点合并问题。
-
当所删除的关键字不在终端结点(最底层非叶结点)中时,有下列几种情况:
(1)如果小于k的子树中关键字个数>m/2
,则找出k的前驱值k‘,并且用k’代替k,再递归的删除k;
(2)如果大于k的子树中关键字个数>m/2
,则找出k的后继值k‘,并且用k’代替k,再递归的删除k;
(3)如果前后两个子树的关键字个数都是m/2
,则直接将两个子结点合并,删除k即可。
-
当所删除的结点在终端结点(最底层非叶结点)中时,有下列几种情况:
(1)直接删除关键字:若被删除关键字所在的结点关键字个数>m/2-1
,表明删除该关键字后仍然满足B树的定义
(2)兄弟够借:若被删除关键字所在的结点关键字个数=m/2-1
,且与此结点相连的左/右结点的关键字个数>=m/2+1
.需要调整该结点的左右兄弟结点及其双亲结点的(父子换位法),以达到新的平衡。
(3)兄弟不够借:
在合并的过程中,双亲结点中的关键字个数会减少,若其双亲结点是根结点并且关键字的个数减少至0(根结点关键字个数为1时,有两棵子树),则直接将根结点删除,合并后的新结点成为根,若双亲结点不是根结点,且关键字个数减少到m/2-1,又要与它自己的兄弟结点进行调整或合并工作,并重复上述步骤,直至符合B树的要求为止。
B+树
-
B+树的定义:
一棵m阶的B+树需满足以下条件:
(1)每个分支结点最多有m棵子树
(2)非叶根结点至少有两棵子树,其它分支结点至少有m/2+1
棵子树
(3)结点的子树个数与关键字个数相等
(4)所有叶结点包含全部关键字及指向相应记录的指针,而且叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序互相连接起来
(5)所有分支结点(可以看成是索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针 -
B+树和B树的区别:
(1)B+树中每个关键字对应1棵子树,B树中具有n个关键字的结点含有n+1棵子树
(2)关键字个数n的范围不同:B——m/2<=n<=m-1
;B+——m/2+1<=n<=m
(3)在B+树中,叶结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键和指向该子树的指针,不含有该关键字对应记录的存储地址
(4)叶结点包含信息不同:B树中叶结点包含的关键字和其它结点的关键字是不重复的;但是B+中叶结点包含了全部关键字,即非叶结点中出现的关键字也会出现在叶结点中
(5)查找过程不同:在B+树中的查找如果非叶结点上的关键字值等于给定值时不停止查找,还是要查找到叶结点才能找到关于这个关键字值的信息,所以在B+树中进行查找无论找到还是没找到都是一条从根结点到叶结点的路径。
线性表和树表的查找中,记录在表中的位置和记录的关键字之间不存在确定关系,在这些表中查找记录时需要进行一系列的关键字的比较,这类查找方法是建立在比较的基础上的,一般比较的次数决定了查找的效率。
散列表
1. 散列表的构造方法
- 散列函数
一个把查找表中的关键字映射成该关键字对应的地址的函数Hash(key)=Address
(这里的地址可以是数组下标、索引、内存地址等)
换言之就是建立关键字和存储它的数据结构的位置之间的关系 - 散列表
根据关键字直接进行访问的数据结构,即散列表建立了关键字和存储地址之间的一种联系 - 冲突
两个或两个以上的关键字映射同一个地址 - 同义词
映射到同一个地址的不同关键字
【想办法避免冲突,但是毕竟无法彻底消除就要考虑如果发生了应该怎么进行错误处理】
2. 散列函数的构造方法
构造散列函数需要注意的:
- 定义域必须包括所有的关键字,而值域的范围依赖于散列表的大小或地址范围
- 散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间,从而减少冲突的发生
- 散列函数应尽量简单,能够在较短的时间内计算出任一关键字对应的散列地址
散列函数 | 使用说明 | 优点 | 缺点 |
---|---|---|---|
直接定址法 | 使用一个线性函数H(key)=axkey+b | 简单且不会发生冲突 | 若关键字分布不连续,空位较多,将造成存储浪费 |
除留余数法 | 假设表长为m,取一个不大于m但最接近或等于m的质数pH(key)=key%p ,关键在于p的选取 | 最简单最常用 | 选好p,使得每一个关键字通过该函数转换后等概率地映射到散列空间的任一地址,尽可能减少冲突 |
数字分析法 | r进制数有r个数码,每种数码出现的机会均等,但是在某些位上分布不均匀,只有某几种经常出现,则应选取数码分布较为均匀的若干位作为散列地址 | 适合已知的关键字集合 | 更换关键字需要更换散列表 |
平方取中法 | 取关键字的平方值的中间几位作为散列地址 | 得到的散列地址和关键字的每一位都有关系,使得散列地址分布的比较均匀 | 适用于关键字的每一位都不够均匀或均小于散列地址所需的位数 |
折叠法 | 将关键字分割成位数的几部分(最后一部分的位数可能短些),然后取这几部分的叠加和作为散列地址 | 适用于关键字位数很多,而且关键字中每一位上的数字分布大致均匀时,可以采用折叠法得到散列地址 | 和优点相反自己想 |
3. 处理冲突的方法
任何设计出来的散列函数都可能绝对地避免冲突,为此,需要考虑在发生冲突时应该如何处理,即为产生冲突关键字寻找下一个空的hash地址:实际上就是增量序列di的选择方法
Hi=(H(key)+di)%m
//m是散列表表长
//key是关键字
//Hi是再定位的下一个可能为空的存储空间的地址
//di是增量序列
处理方法 | 基本内容 | 优点 | 缺点/适用情况 |
---|---|---|---|
线性探测法 | 冲突发生时顺序查看表中下一个单元(当探测到表尾地址m-1时,下一个探测地址是表首地址0,直到找到一个空闲单元或着查遍全表 | 简单 | 可能使第i个散列地址的同义词存入第i+1个地址单元,那么本该存到第i+1个位置上的元素就会争夺第i+2个地址单元,造成大量元素在相邻的散列地址上“堆积”起来,大大降低查找效率 |
平方探测法(二次探测法) | 增量序列按照di=02,12,-12…k2,-k2的顺序进行(k<=m),散列表的长度m必须是一个可以表示成4k+3 的素数 | 较好的处理冲突,可以避免出现“堆积”问题 | 不能探测到散列表上的所有地址单元,但是至少能探测到一半单元 |
再散列法 | 使用两个hash函数,当通过第一个散裂函数得到的地址发生冲突时,则利用第二个散列函数hash2计算该关键字的地址增量:Hi=(H(key)+i*hash2(key))%m | 初始探测位置是H0=H(key)%m ,i是冲突的次数,最开始为0 | 在散列法中,最多经过m-1次探测会遍历表中的所有位置,回到h0 |
伪随机序列法 | 增量di为一个伪随机序列 | 实现简单 | 没有固定的规律,但是吧伪随机就哈哈哈了 |
拉链法 | 由其散列地址唯一标识的线性链表,如果出现同义词,则连接在同一个地址位置的链表上,继续增长链表 | 处理冲突简单,绝对不会发生冲突,平均查找长度较短 | 适用于经常插入和删除的情况,适合造表前无法确定表长的情况 |
-
平方探测法细节
(1)平方探测法在探测过程中hash value是不变的,直到找到合适位置后,重新给hash value赋值。
(2)基本流程上和线性探测的区别是向右增长的过程中如果+i2之后的位置被占用,则继续向右探测-i2,如果两个都不合适,则i++之后继续向右进行。
(3)关键处理部分:
a. 出现hash value+i2>hashtablesize
%hashtablesize:如果得到的新的位置没被占用,hashvalue更新;否则hashvalue不变,继续进行平方探测
b. 出现hash value-i2<hashtablesize
%hashtablesize+hashtablesize:同上 -
⚠️在开放地址的情况下,不能随便物理删除表中已有的元素,因为若删除元素将会截断其它具有相同散列地址的元素的查找地址,所以若想删除一个元素时,给他做一个删除标记,进行逻辑删除。但是这样做的副作用就是:在执行多次删除后,表面上看起来散列表很满,实际上有很多位置没有利用,因此需要定期维护散列表,要把删除标记的元素物理删除。
4. 散列查找及其性能分析
(1)散列查找过程
- 初始化
Addr=Hash(key)
- 检测查找表中地址为
Addr
的位置上是否有记录,若没有记录,返回查找失败;若有记录,比较它与key的值,若相等则返回查找成功标志 - 用给定的处理冲突的方法计算“下一个散列地址”
(2)散列表的查找效率
散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子
装填因子:装填因子一般记为alpha,定义为一个表的装满程度,
alpha = 表中记录数n/散列表长度m
散列表的平均查找长度依赖于散列表的装填因子alpha,而不直接依赖与n或m,alpha越大,装填记录越满,发生冲突的可能性越大,反之冲突的可能性越小。
字符串的模式匹配
主要是KMP算法,这个算法看八百遍了就是记不住,每次都得重看一遍,我服了,这个算法和我不对付,不知道这辈子能不能记住这个算法。
⚠️注意事项
-
不需要回溯主串当前指向位置的指针,利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的距离后,继续进行比较
-
next数组实际上是对模式串的每一个位置查找最长公共前缀
-
next数组的求解只与模式串有关
# include<iostream>
#include <string.h>
# define Maxsize 100000
using namespace std;
//计算next数组
void Next(char* p,int *next,int len2){
next[0]=-1;
for(int j = 1;j<len2;j++)
{
int i = next[j-1];
while (p[j]!=p[i+1]&&i>=0)
{
i = next[i];
}
if(p[j]==p[i+1]){
next[j] = i+1;
}
else{
next[j]=-1;
}
}
}
//KMP匹配
int KMP(char * s,char * p,int* next,int len1,int len2){
int i = 0;
int j = 0;
while(i<len2&&j<len1)
{
if(s[j]==p[i])
{
i++;
j++;
}
else{
if(i==0){//第一个字符匹配失败直接从主串的下一个位置开始
j++;
}
else{//否则由失败函数确定下一个要比较的位置
i = next[i-1]+1;
}
}
}
int pos;
if(i<len2){
pos = -1;
}else{
pos = j-len2;
}
return pos;
int main() {
char s[Maxsize];
char p[Maxsize];
cin>>s>>p;
int next[Maxsize];
int len1 = int(strlen(s));//主串长度
int len2 = int(strlen(p));//模式串长度
Next(p,next,len2);
for(int i=0;i<len2;i++)
cout<<next[i]<<" ";
cout<<endl;
int pos = KMP(s, p, next, len1,len2);
cout<<pos<<endl;
return 0;
}
}
查找算法的分析及应用
依靠平均查找长度ASL来度量各种查找算法的性能。查找算法本身依托于查找结构,查找结构又是由相同数据类型的记录或结点构成,故最终落脚于数据结构类型的区别
查找成功的平均查找长度:
ASL(success)=sum(Pi*Ci);
查找失败的平均查找长度:
ASL(failure)=sum(Qj*Cj);
若一个查找集合中有n个数据元素,每个元素的查找概率为Pi,查找成功的数据比较次数是Ci,而不在此集合中的数据元素分布在由该n个元素的间隔构成的n+1个子集合中,每个子集合的元素查找概率为Qi,如果所有元素的查找概率相等:
(1)综合考虑,即查找成功的概率和查找失败的概率和为1,Pi=Qi=1/(2n+1)
(2)分开考虑,查找成功的概率和查找失败的概率分别为1,Pi=1/n,Qi=1/(n+1)
虽然综合考虑更理想,但是在实际应用中,多数是分开考虑的,因为对于查找不成功的情况很多场合下没有明确指出。