查找是指根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。
若在查找的同时需要对表做修改操作,则称为动态查找表;仅做查询称为静态查找表。
查找算法的评价指标:关键字的平均比较次数,也称平均查找长度ASL。
应用范围:
*顺序表或线性链表表示的静态查找表
*表内元素之间无序
顺序查找表定义:
//数据元素定义
typedef struct{
KeyType key; //关键字域
...... //其他域
}ElemType;
//顺序查找表定义
typedef struct{
ElemType *R; //表基址
int length; //表长
}SSTable;
//定义顺序查找表ST
SStable ST;
【算法思想】
从表的一段开始,依次将记录的关键字与给定的值进行比较:
若某个记录的关键字和给定值相等,则查找成功;
若扫描整个表后仍未找到关键字和给定值相等则查找失败。
【算法实现1】从后往前查
int Search_Seq(SSTable ST,KeyType key){
for(i = ST.length;i>=1;--i){ // 从后往前查
if(ST.R[i].key == key) return i; //若有关键字相同,则查找成功
return 0;
}
}
【算法实现2】另一种形式
int Search_Seq(SSTable ST,KeyType key){
for(i=ST.length;ST.R[i].key != key; --i)
if(i<=0) break; //如果i为0,即查找失败
//上面for循环可以写成:for(i =ST.length;ST.R[i].key!=key&&i>0;--i);
//注意这里的for循环没有语句,需要加 ;
if(i>0) return i;//如果循环结束,i大于0,则查找成功
else return 0; //否则查找失败
}
【算法改进】
把待查关键字key存入表头(称之为“哨兵”或者“监视哨“),从后往前逐个比较,可免去查找过程中每一步都要检查是否查找完毕。
int Search_Seq(SSTable TS,keyType key){
ST.R[0].key=key;
for(i=ST.length;ST.R[i].key!=key;--i); //注意这里的for循环没有语句,需要加 ;
return i;
}
算法性能分析:
时间复杂度O(n):查找成功时的平均查找长度,设表中各记录查找概率相等,ASL(n)=(1+2+……+n)/n=(n+1)/2。
空间复杂度O(1):需要一个辅助空间,即表头的哨兵。
【拓展】
记录的查找概率不相等时,提高查找效率:按照查找概率高低存储,查找概率高的比较次数越少,查找概率低的比较次数越多。
【特点】
优点:算法简单,逻辑次序无要求,不同存储结构均适用。
缺点:AKL太长,时间效率低。
折半查找:每次将待查记录的所在区间缩小一半
应用范围:
*线性表必须采用顺序存储结构
*表中元素关键字有序排列
【算法思想】
①设表长n,设置指针low,high和mid分别指向待查元素所在区间的上界、下界和中点,key为要查找的值。
②初始,low=1,high=n,mid=(low+high)/2。(注:由于计算机存储原理,若mid有小数部分,则向下取整数)
③让key与mid所指记录比较:
—若key==R[mid].key,查找成功
—若key<=R[mid].key,high=mid-1
—若key>=R[mid].key,low=mid+1
④重复③,直到low>high时,查找失败。
【算法实现】非递归实现
int Search_Bin(SSTable ST,keyType key){
low = 1;
high=ST.length;
while(low<=high){
mid = (low + high) /2; //设置待查元素
if(ST.R[mid].key == key) returun mid;
else if(key<ST.R[mid].key) high = mid - 1; //在前半区查找
else low = mid +1; //在后半区查找
}
return 0;
}
【算法实现】递归实现
int Search_Bin(SSTable ST,keyType key,int low,int high){
if(low>high) return 0;
mid=(low + high) /2;
if(key == ST.R[mid].key) return 1;
else if(key < ST.R[mid].key)
Search_Bin(ST,key,low,mid-1);
else Search_Bin(ST,key,mid+1,high);
}
二分查找过程会形成一个判定树。如:1、2、3、4、5、6、7、8、9、10、11组成的一个顺序表。
其构成的判定树如下:
圆点表示查找成功,矩形表示查找失败。
这里一共有11个点,查找成功比较次数为判定树的深度。
则查找成功的平均查找长度ASL=1/11 ×(1×1 + 2×2 + 4×3 + 4 ×4) = 3
【特点】
优点:效率比顺序查找高
缺点:只适用于有序表,且限于顺序存储结构。
【算法思想】
①将表分成若干块,且表内数据有序,或者分块(索引表)有序。
即第n+1块中所有记录的关键字均大于第n快中最大关键字
②建立“索引表”
【例】
索引表查找过程:先确定待查记录所在块(顺序或者折半查找),再在块中查找(顺序查找)。
【查找效率】
AKL=对索引表查找的ASL+对块内查找的ASL。
【特点】
优点:插入删除比较容易,无需进行大量移动。
缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算
适用情况:若线性表既要快速查找又经常动态变化,可采用分开查找。
二叉排序树又称为二叉搜索树、二叉查找树。
定义:
①若其左子树非空,则左子树上所有结点值均小于根结点值。
②若其右子树非空,则右子树上所有结点值均大于等于根结点的值。
③其左右子树本身又各是一棵二叉树。
【例】
对此二叉排序树进行中序遍历得:3 12 24 37 45 53 61 78 90 99 。
结论:二叉排序树中序遍历非空的二叉排序树所得到的数据元素序列是一个按关键字排列的递增有序序列。
【定义】二叉排序树存储
typedef struct{
KeyType key; //关键字项
InfoType otherinfo; //其他数据域
}ElemType;
typedef struct BSTNode{
ElemType data; //数据域
struct BSTNode *lchild,*rchild; //左右孩子指针
}BSTNode,*BSTree;
BSTree T; //定义二叉排序树
【算法思想】二叉排序树递归查找
①若二叉排序树为空,则查找失败,返回空指针。
②若二叉排序树非空,将给定的值key与根结点的关键字T->data.key比较:
若key==T->data.key,则查找成功,返回根结点地址。
若key < T->data.key,则查找左子树
若key > T->data.key,则查找右子树
【算法实现】
BSTree SearchBST(BSTree T,KeyType key){
if((!T) || key==T->data.key) return T;
else if(key< T->data.key)
return SearchBST(T->lchild,key); //在左子树中继续查找
else
return SearchBST(T->rchild,key); //在右子树中继续查找
}
【二叉排序树的查找分析】
比较次数=此节点所在层数。
最多比较次数=树的深度。
二叉排序树的平均查找长度:
结论:含有n个结点的二叉排序树的平均查找长度和树的形态有关。
1.插入
【算法思想】
①若二叉排序树为空,则插入结点作为根结点插入到空树中。
②否则,继续在其左、右子树上查找:树中已有,则不再插入,树中没有,则查找至某个叶子结点的左子树或者右子树为空为止,则插入的结点应为叶子结点的左孩子或者右孩子。
【算法实现】
void InsertBST(BSTree &T,ElemType e){
if(!T){
S=new BSTNode; //生成新结点
S->data = e;
S->lchild=S->rchild=null;
T=S;
}
else if(e.key < T->data.key)
InsertBST(T->lchild,e);
else if(e.key > T->daata.key)
InsertBST(T->rchild,e);
}
2.生成
从空树出发,经过一系列的查找、插入操作之后,可生成一棵二叉排序树。
例:
3.删除
删除节点情况:
①当删除的结点是叶子结点:直接删除该结点。
②当删除的结点只有左子树或者右子树时,用这个左子树或者右子树替换它。
③当删除结点既有左子树也有右子树时:
一种情况:用这个结点中序遍历的前驱来代替它,即这个结点左子树上的最大结点,并删除相应的结点(删除操作与三种情况一致)。
一种情况:用这个结点中序遍历的后继来代替它,即这个结点右子树上的最小结点,并删除相应的结点(删除操作与三种情况一致)。
【例】
为了提高形态不均衡得到二叉排序树的查找效率,将二叉排序树做平衡化处理,变为“ 平衡二叉树 ”。
平衡二叉树又称AVL树,一棵平衡二叉树首先时一棵二叉排序树:
①左子树和右子树的高度之差绝对值小于等于1;
②左子树和右子树也是平衡二叉排序树。
平衡因子 = 结点左子树的高度 -结点右子树的高度
综上:平衡因子只能是-1,0,1。
对于一棵有n个结点的AVL树,其高度保持在O(log2 N)数量级,SAL也保持在O(log2 N)数量级。
1.插入
当在一个平衡二叉树上插入一个结点时,可能导致失衡,即可能出现平衡因子绝对值大于1的结点。如:2,-2。
如果在一颗AVL树中插入一个新结点后造成失衡,则必须重新调整树的结构,使其恢复平衡。
【平衡调整的四种类型】
找到失衡结点,如果不止一个失衡结点,则找到最小失衡子树的根结点。
如果插入结点在左子树的左孩子上,则称为LL型。如果是左子树上的右孩子上,则称为LR型。
如果插入结点在右子树的左孩子上,则成为RL型。如果是右子树上的左孩子上,则成为RR型。
调整过程如图:
(1)LL型
调整步骤:
①B结点带着左子树一起上升。
②A称为B结点的左孩子。
③剩下B结点的右孩子成为A左孩子。
【例子】
调整步骤:
①B结点带着右子树一起上升。
②A称为B结点的左孩子。
【例子】
(3)LR型
调整步骤:
①C结点穿过A、B结点上升
②B结点成为C结点的左孩子,A结点成为C结点的右孩子。
③C结点原来左子树成为B结点的右子树,C结点原来的右子树成为A结点的左子树。
【例子】
(3)RL型
调整步骤:
①C结点穿过A、B结点上升
②B结点成为C结点的右孩子,A结点成为C结点的左孩子。
③C结点原来左子树成为A结点的右子树,C结点原来的右子树成为B结点的左子树。
【例子】
输入关键字序列(16、 3、 7 、11 、9 、26 、18 、14 、15),构造AVL树步骤。
B树又称多路平衡查找树,可以理解为是多叉树。有的书籍会称为B-树。
1.B树的定义:
一棵m阶的B树中,所有结点孩子个数最大值称为B树的阶,例如上图的为5阶B树。
一棵m阶的B树需要满足一下特性:
①树中每个结点至多含有m-1个关键字,即每个结点至多有m棵子树。例如下图,这个结点为m阶B树的某个结点。若最多只有5棵子树,则最多只有4个关键字。
②若结点不是终端结点,至少有两棵子树。即最少有一个关键字。
③除了根结点外的所有非叶子结点,至少有 ⌈m/2⌉ 棵子树(⌈m/2⌉这里表示向上取整)。即至少含有 ⌈m/2⌉-1 个关键字。
④一般来说,把最底层不带查找信息的,称为m阶树的叶子结点。最后一层带有查找信息的称为终端结点。
⑤所有叶节点都出现在同个层次上,并且不带查找信息。也就是说,通常B树查找到叶子结点即为查找失败。
⑥所有非叶子结点上,每个结点上的关键字都是从小到大排序的,
⑦每个关键字的左子树上的关键字一定小于这个关键字,每个关键字的右子树上的关键字一定大于这个关键字。
特性总结:
①根结点的子树数量[2,m],关键字树[1,m-1]。
②其他非叶子结点子树数量[ ⌈m/2⌉ ,m ];关键字数[ ⌈m/2⌉-1 ,m-1 ]
③任意结点,所有子树高度都相同。即所有叶子结点在同一高度。
④每个结点关键字上是从小到大排序的。
⑤每个关键字左子树上的关键字都小于这个关键字,每个关键字右子树上的关键字都大于这个关键字。
【问题】
注:大部分算B树的高度不包括叶子结点(失败结点)
问题1:含n个关键字的m阶B树,最小高度,最大高度是多少。
最小高度:让每个几点尽可能满,即有m-1关键字,m个分叉。
最大高度:让各层分叉尽可能少,即根结点只有1个关键字。
2.B树的插入
【算法思想】
为了满足B树的要求,B树的插入操作如下:
①新元素一定是插入到最底层的终端结点。
②如果插入新元素后,导致原结点的关键字数超过上限,则从中间位置 ⌈m/2⌉ 中关键字分为两部分,这个关键字作为父结点,剩余两部分按照位置作为父节点这个关键字中的左子树和右子树。
【示例】从0开始构造一个5阶的B树
①当第一个结点插入到5个关键字时
将 ⌈5/2⌉位置,即第3个位置分为两部分,第3个位置结点作为父节点,剩余两部分按照位置作为父节点这个关键字的左子树和右子树。
②在这棵树上插入90这个关键字,由于新元素一定是插入到最底层的终端结点,所以要插入到80的右边。

同理分裂,将88作为父节点进行分裂。
④在中间孩子上,插入到第5个关键字时
同理分裂,将80作为父节点进行分裂,而80必88小,要注意80和分裂的右边部分存放位置。也就是80放在指向这个结点的边的右边。
⑥继续在某个非根节点上插入到第5个关键字。
⑦由于分裂完,根结点有5个关键字。则需要对根结点继续进行分裂操作。
3.B树的删除
【算法思想】
为了满足B树的要求,B树的插入操作如下:
①若被删除的关键字在终端结点,则直接删除该关键字。且要注意这个结点关键字个数是否低于定义的下限 ⌈m/2⌉-1。
如果低于下限:
若右兄弟结点可借,则将父结点的关键字转到此结点,将右兄弟的最小关键字作为父结点的关键字。
若左兄弟结点可借,则将父结点的关键字转到此结点,将左兄弟最大的关键字作为父结点的关键字。
若右兄弟不够借,则将父结点的关键字与有兄弟结点合并到此结点。并且要注意上面的父结点够不够关键字,依次操作。
②若被删除的关键字不在终端结点:
则需要用直接前驱顶替,即这个关键字“左子树最右下”的元素。
也可以用直接后继顶替,即这个关键字“右子树最左下”的元素。
B+树是一种B-树的变形,也是追求绝对平衡的树,即所有查找失败结点都在同一高度。
B+树定义:
一棵m阶的B树需要满足一下特性:
①每个分支节点最多有m棵子树,也有m个关键字。例如下图,这个结点为m阶B+树的某个结点。这是B+树与B树结点最大的区别。
②非叶根结点至少有两棵子树,其他每个分支结点至少有 ⌈m/2⌉ 棵子树。
③所有叶结点包含了全部的关键字以及相应的记录指针。叶子结点中将关键字按大小顺序排列,并且相邻叶子结点按照大小顺序链接起来。如图为某个m阶B+树的所有叶子结点。
所以B+树支持顺序查找。
④所有非叶子结点的分支结点中仅包含它各个子节点中关键字的最大值,以及指针。
⑤B+树的查找,无论成功失败,都要走到最下面一层结点。
对于平衡二叉树而言,插入或者删除很容易破坏“平衡”的特性,需要频繁调整树的形态。而红黑树在插入或者删除很多时候不会破坏“红黑“特性,无需频繁调整树的形态,即便需要调整也可以在常数级时间内完成。
红黑树满足二叉排序树且具有:
①每个结点是红色或黑色。
②根结点一定是黑色。
③叶节点都是黑色(失败结点)。
④不存在相邻的红结点
⑤对每个结点,从该结点到任一叶节点的简单路径上,所含黑结点的数目相同。
1.插入
【算法思想】
父节点为黑色:满足红黑树定义,插入结束。
父节点为红色:不满足红黑树的定义,按照以下重新调整:
①叔结点是黑色或者缺失:旋转+染色(染色就是换颜色)
*LL型:父节点爷结点染色,右单旋。
*RR型:父节点爷结点染色,左单旋。
*LR型:新结点和爷结点染色。先左旋,将父结点放到左孩子,然后右旋。将爷结点放到右孩子。
*RL型:新结点和爷结点染色。先右旋,将父结点放到右孩子,然后左旋。将爷结点放到左孩子。
②叔结点是红色:叔、父、爷结点染色,爷结点变为新结点。
【例子】
从一棵红黑树开始,插入:20,10,5,30,40,57,3,2,4,35,25,18,22,23,24,19,18
①插入20,10,没问题,插入到5开始有问题。
这里父节点是红色,叔结点是黑色,LL型:父、爷结点染色,右单旋。
②插入30
这里父节点是红色,叔结点是红色:叔、父、爷结点染色,爷结点变为新结点。
③插入40,这里父节点是红色,叔结点是黑色:父、爷结点染色,左单旋。
④插入57,父节点是红色,叔结点是红色:叔、父、爷结点染色,爷结点变为新结点。
爷变为新结点后,30的父结点是黑色,不影响
⑤插入3,父节点是黑色,不影响 。插入2,父结点是红色,叔结点是黑色。LL型:父、爷结点染色,右单旋
⑥插入4,父结点是红色的,叔结点是红色的:叔、父、爷结点染色,爷结点变为新结点。
⑦插入35,25,18,父节点是黑色,不影响。插入22,父结点是红色,叔结点是红色:叔、父、爷结点染色,爷结点变为新结点。
爷结点变为新结点后,父结点是红色,叔结点是红色:叔、父、爷结点染色,爷结点变为新结点。
⑧插入23,父结点是红色,叔结点是黑色,LR型:新结点和爷结点染色。先左旋,将父结点放到左孩子,然后右旋。将爷结点放到右孩子。
⑨插入24,父节点是红色,叔结点是红色:叔、父、爷结点染色,爷结点变为新结点。
爷变为新结点后,父节点红色,叔结点黑色,LR型:新结点和爷结点染色。先左旋,将父结点放到左孩子,然后右旋。将爷结点放到右孩子。
⑩插入19。不影响。插入18,已经有了18,看自己感觉。这里为了全面换算,放到了19的左孩子。
这里父结点红色,叔结点黑色,RL型:新结点和爷结点染色。先右旋,将父结点放到右孩子,然后左旋。将爷结点放到左孩子。
【基本思想】
记录的存储位置与关键字之间存在对于关系。这个对应关系为哈希(hash)函数。这种存储方式即散列存储方式,也称哈希存储方式。
【例】
有6个元素的关键码分别为:(25, 21, 39, 9, 23, 11)。
而选取关键码与元素位置间函数为H(k) = k mod 7(mod为模运算,即整除后取其余数),地址编号从0-6。
同义词:具有相同函数值的多个关键字。即上图中 23、9和39、25、11两组同义词。
构造哈希函数需要考虑到的因素:执行速度、关键字长度、散列表大小、关键字分布情况、查找频率。
根据元素集合特性构造哈希函数时:地址空间尽量小,均匀存放元素避免冲突。
由于需要考虑的因素以及构造函数的两个要求,构造哈希函数方法:直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法等。
1.直接定址法
哈希函数:Hash(key) = a*key + b;
【例】有一组关键码为:(100, 300, 500, 700, 800, 900)
散列函数Hash(key) = key/100(a=1/100,b=0)
【特点】
优点:以关键码key的某个线性函数值为散列地址,不会产生冲突。
缺点:要占用连续地址空间,空间效率低。
2.除留余数法(常用)
哈希函数:Hash(key) = key mod p;
关键:如何选取合适的p值。
技巧:设表长为m,取p<=m且为指数。即选取小于等于m的最大质数。
【例】有一组关键码为:(15, 23, 27, 38, 53, 61, 70)
散列函数Hash(key) = key mod 7(p取7)
不同关键码映射到同一个散列地址会产生冲突,则需要对其进行处理。
处理冲突方法有:开放定址法(开地址法)、链地址法(拉链法)、再散列法(双散列函数法)、建立一个公共溢出区等。
1.开放定址法
【基本思想】
在发生冲突时,就寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将元素存入。
【例如】构造散列函数方法采用除留余数法。Hi=(Hash(key) + di) mod m,这里di为增量序列。
简单来说,就是哈希函数中添加某个动态值为di,加到哈希函数当中,使得下一个到相同的地址的元素能够通过增量存入到下一个空的地址当中。
【线性探测法】di为1,2,3,……,m-1 线性序列
例:关键码集为(47,7,29,11,16,92,22,8,3),散列表的表长为m=11,散列函数为Hash(key) = key mod 11,
地址Hi=(Hash(key) + di) mod m。
用线性探测法构造散列表如下:
平均查找长度ASL=(1+2+1+1+1+4+1+2+2)/9 = 1.67
【二次探测法】di为1²,-1²,2²,-2²,……q² 二次序列(即在右边存入后存左边,并且是次方增量)
例:关键码集为(47,7,29,11,16,92,22,8,3),散列表的表长为m=11,散列函数为Hash(key) = key mod 11,
地址Hi=(Hash(key) + di) mod m。
这里的m即表长要取4k+3的某个质数,k为整数。
用二次探测法构造散列表如下:
2.链地址法(拉链法)
【基本思想】
相同散列地址的记录链成一个单链表。m个散列地址就设m个链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构。
【例】
有一组关键字为(19,14,23,1,68,20,84,27,55,11,10,79),哈希函数为Hash(key) = key mod 13;
用链地址法构造哈希表为:
【构造步骤】
①取数据元素关键字key,计算其散列函数值(地址),若该地址对应的链表为空,则将该元素插入此链表,若不为空执行②。
②若该地址对应链表不为空,则利用链表前插法或者后插法将该元素插入次链表。
优点:非同义词不会冲突,无聚集现象。且链表上结点空间动态申请,更适合于表长不确定的情况。
【例题】已知一组关键字(19,14,23,1,68,20,84,27,55,11,10,79),设散列函数为H(key) = key MOD 13,散列表长为m=16,设每个记录的查找概率相等。
(1)用线性探测法处理冲突,即Hi=(H(key) + di) mod m
平均查找长度ASL=(1×6+2+3×3+4+9)/12=2.5。
(2)用链地址法处理冲突
【构造步骤】
①取数据元素关键字key,计算其散列函数值(地址),若该地址对应的链表为空,则将该元素插入此链表,若不为空执行②。
②若该地址对应链表不为空,则利用链表前插法或者后插法将该元素插入次链表。
优点:非同义词不会冲突,无聚集现象。且链表上结点空间动态申请,更适合于表长不确定的情况。
【例题】已知一组关键字(19,14,23,1,68,20,84,27,55,11,10,79),设散列函数为H(key) = key MOD 13,散列表长为m=16,设每个记录的查找概率相等。
(1)用线性探测法处理冲突,即Hi=(H(key) + di) mod m
平均查找长度ASL=(1×6+2+3×3+4+9)/12=2.5。
(2)用链地址法处理冲突
学习视频:数据结构——王卓;
参考文献:数据机构C语言版第2班——严蔚敏