数据结构——查找算法总结
一、线性结构
1、顺序查找(线性查找)
主要在线性表中进行查找。
1)一般线性表的顺序表
思路:从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置;若已经查找到表的另一端,还没有查找到符合给定条件的元素,则返回查找失败的信息。
算法分析:
查找成功时,顺序查找的平均长度:ASL成功=(n+1)/2
查找失败时,顺序查找的平均长度:ASL失败=n+1
缺点:
当n较大时,平均查找长度大,效率低;
优点:
对数据的存储没有要求,顺序存储或链式存储皆可;
对表中记录的有序性也没有要求,无论记录是否按关键码有序均可应用;
注:对线性链表只能进行顺序查找。
2)有序表的顺序查找
如果在查找之前就已经知道表是按关键字有序的,那么当查找失败时可以不用再比较到表的另一端就能返回查找失败的信息,这样能降低顺序查找失败的平均查找长度;
假设表L是按关键字从小到大排列时,查找的顺序是从前往后查找,待查找元素的关键字为key,当查找到第i个元素时,发现第i个元素对应的关键字小于key,但第i+1个元素对应的关键字大于key,这时就可以返回查找失败的信息了,因为第i个元素之后的元素的关键字均大于key,所以表中不存在关键字为key的元素。
算法分析:
在有序表的顺序查找中,查找成功的平均查找长度和一般线性表的顺序查找一样;
查找失败时,平均查找长度:ASL失败=n/2 + n/(n+1)
2、折半查找(二分查找)
仅适用于有序的顺序表。
思路:
首先将给定值key与表中中间位置元素的关键字比较,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素以外的前半部分或后半部分。然后在缩小的范围内继续进行同样的查找,如此重复直到找到为止,或者确定表中没有所需要查找的元素,则查找不成功,返回查找失败的信息。
算法分析:
时间复杂度:O(log2n)
平均情况下比顺序查找的效率高;
适用情况:
具有随机存取的特性。
该方法仅适用于线性表的顺序存储结构,不适合链式存储结构,且要求元素按关键字有序排列。
3、分块查找(索引顺序查找)
思路:
将查找表分为若干个子块。块内的元素可以无序,但块之间是有序的,即第一个块中的最大关键字小于第二个块中的所有记录的关键字,第二个块中的最大关键字小于第三个块中的所有记录的关键字,以此类推。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中第一个元素的地址,索引表按关键字有序排序。
查找过程:
1)在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表;
2)块内顺序查找;
算法分析:
平均查找长度:ASL=LI+LS
在长度为n的查找表均匀的分为b块,每块有s个记录,在等概率的情况下,若在块内和索引表中均采用顺序查找,则平均查找长度为:ASL=(s2+2s+n)/2s;
若对索引表采用折半查找,平均查找长度为:ASL=log2(b+1) + (s+1)/2;
二、树形结构
1、B树和B+树
1)B树(多路平衡查找树)
B树中所有节点的孩子节点数的最大值称为B树的阶,通常用m表示。
一颗m阶B树或为空树,或为满足以下特性的m叉树:
i. 树中每个节点至多有m棵子树(即至多含有m-1个关键字);
ii.若根节点不是终端节点,则至少有两棵子树;
iii.若除根节点外的所有非叶节点至少有m/2棵子树(即至少含有m/2 - 1个关键字)
vi. 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树 ;
v.所有的叶节点都在同一层次,并且不带信息;
B树的高度:
如果n>=1,则对于任意一棵包含n个关键字、高度为g、介数为m的B树:
i. h>=logm(n+1)
ii.h<=logm/2((n+1)/2)+1
B树的查找:
基本操作:①在B树中找结点;②在结点内找关键字;
由于B树常存储在磁盘上,则前一个查找操作是在磁盘上进行的,而后一个查找操作是在内存中进行的,即在找到目标结点后,先将结点中的信息读入内存,然后再采用顺序查找法或折半查找法查找等于K的关键字。
在B树上查找到某个结点后,先在有序表中进行查找,若找到则查找成功,否则按照对应的指针信息到所指的子树中去查找。当查找到叶节点时,则说明树中没有对应的关键字,查找失败。
B树的插入:
过程:
i.定位
利用B树查找算法,找出插入该关键字的最底层中某个非叶节点;
ii.插入
在B树中,每个非失败结点的关键字个数都在[(m/2)-1,m-1]之间。当插入后的结点关键字个数小于m,则可以直接插入;插入后检查被插入节点内关键字的个数,当插入后的节点关键字个数大于m-1时,则必须对节点进行分裂;
分裂:
取一个新节点,将插入key后的原节点从中间位置将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新的结点中,中间位置(m/2)的结点插入到原结点的父节点中。若此时导致其父节点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根节点为止,这样导致B树高度增1.
B树的删除:
当所删除的关键字k不在终端结点中时,有以下情况:
i. 如果小于k的子树中关键字个数>(m/2)-1,则找出k的前驱值k’,并且用k’来取代k,再递归地删除k’即可;
ii. 如果大于k的子树中关键字个数>(m/2)-1,则找出k的后继值k’,并且用k’来取代k,再递归地删除k’即可;
iii.如果前后两个子树中关键字个数均为(m/2)-1,则直接将两个子节点合并,直接删除k即可;
当被删除的关键字在终端结点中时,有以下情况:
i.直接删除关键字:若被删除关键字所在结点的关键字个数>(m/2)-1,表明删除该关键字后仍满足B树的定义,则直接删去该关键字;
ii.兄弟够借:若被删除关键字所在结点删除前的关键字个数=(m/2)-1,且与此结点相邻的右(左)兄弟节点的关键字个数>=m/2,需要调整该节点右(左)兄弟节点以及其双亲结点,以达到新的平衡;
iii. 兄弟不够借:若被删除关键字所在结点删除前的关键字个数=(m/2)-1,且与此结点相邻的右(左)兄弟节点的关键字个数>=m/2-1,则将关键字删除后与右(左)兄弟节点以及其双亲结点中的关键字进行合并;
在合并过程中,双亲结点中的关键字个数会减少。若其双亲结点是根节点并且关键字个数减少至0,则直接将根节点删除,合并后的新节点成为根;若双亲结点不是根节点,且关键字个数减少到m/2-1,又要与它的兄弟节点进行调整或合并操作,并重复上述步骤,直至符合B树的要求为止;
2)B+树
定义:
每个分支节点最多有m棵子树;
非叶根节点至少有两棵子树,其他每个分支节点至少有m/2棵子树;
节点的子树个数与关键字个数相等;
所有叶节点包含全部关键字及指向相应记录的指针,而且叶节点中将关键字按大小顺序排列,并且相邻叶节点按大小顺序相互链接起来;
所有分支节点中仅包含它的各个子节点中关键字的最大值及指向其子节点的指针;
m阶的B+ 树与B树的区别:
在B+树中,具有n个关键字的节点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的节点含有(n+1)棵子树;
在B+树中,每个节点关键字个数n的范围是(m/2)<=n<=m,在B树中,每个节点关键字个数n的范围是(m/2)-1<=n<=(m-1);
在B+树中,叶节点包含信息,所有非叶节点仅起到索引作用,非叶节点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址;
在B+树中,叶节点包含了全部关键字,即在非叶节点中出现的关键字也会出现在叶节点中;而在B树中,叶节点包含的关键字和其他节点包含的关键字是不重复的;
三、散列表
散列函数:
一个把查找表中的关键字映射成该关键字对应的地址的函数;
冲突:
散列函数可能会把两个或两个以上的不同关键字映射到同一地址;
同义词:
发生碰撞的不同关键字;
散列表:
根据关键字而直接进行访问的数据结构。
理想情况下:
对散列表进行查找的时间复杂度为O(1),与表中元素个数无关;
散列函数的构造方法
注:
1)散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围;
2)散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间,从而减少冲突的发生;
3)散列函数应尽量简单,能够在较短的时间内就计算出任一关键字对应的散列地址。
常用的散列函数
1、直接定址法
直接取关键字的某个线性函数值为散列地址:
H(key)=a*key+b
优点:
计算简单,不会产生冲突;
适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,将造成存储空间的浪费;
2、除留余数法
最简单、最常用的方法;
假定散列表表长为m,取一个不大于m但最接近或等于m的质数p,利用以下公式把关键字转换为散列地址。散列函数为:
H(key)=key%p
关键选好p,使得每一个关键字通过该函数转换后等概率地映射到散列空间上的任一地址,从而尽可能减少冲突的可能性。
3、数字分析法
关键字是r进制数,而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,则应选取数码分布较为均匀的若干位作为散列地址。
适合于已知的关键字集合,如果更换了关键字,就需要重新构造新的散列函数;
4、平方取中法
取关键字的平方值的中间几位作为散列地址。
适用于关键字的每一位取值都不够均匀或均小于散列地址所需的位数;
5、折叠法
将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为散列地址。
适用于关键字位数很多,而且关键字中每一位上数字分布大致均匀;
处理哈希冲突方法
1、开放定址法
可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。
Hi=(H(key)+di)%m
m表示散列表表长,di位增量序列。
1)线性探测法
di=0,1,2,3,…
特点:
冲突发生时,顺序查看表中下一个单元,直到找出一个空闲单元或查遍全表;
2)平方探测法
di=0²,1²,2²,…
散列表长度m必须是一个可以表示成4k+3的素数
优点:
避免出现“堆积”问题
缺点:
不能探测到散列表上的所有单元,但至少能探测到一半单元;
3)再散列法(双散列法)
使用两个散列函数,当通过第一个散列函数H(Key)得到的地址发生冲突时,则利用第二个散列函数Hash2(Key)计算该关键字的地址增量,形式:
Hi=(H(Key)+i*Hash(Key))%m
最多经过m-1次探测会遍历表中所有位置,回到H0位置;
4)伪随机序列法
di=伪随机数序列
2、拉链法
适用于经常进行插入和删除的情况;
步骤:
①检测查找表中地址为Addr的位置上是否有记录,若没有记录,返回查找失败;若有记录,比较它与key值,若相等,返回查找成功标志,否则执行步骤②;
②用给定的处理冲突方法计算“下一个散列地址”,并把Addr置为此地址,转入步骤①;
查找效率因素:散列函数、处理冲突的方法和装填因子;
装填因子:
定义为一个表的装满程度,即
α=表中记录数n/散列表长度m
散列表的平均查找长度依赖于散列表的装填因子α,而不直接依赖于n或m。