(1)查找
在数据集合中寻找满足条件的数据元素的过程。
(2)查找表(查找结构)
用于查找的数据集合称为查找表。
(3)静态查找表
无法动态的插入或者删除数据,就是静态查找表;于此对应的就是动态查找表
静态:顺序查找,折半查找,散列查找
动态:二叉排序树的查找,散列查找
(4)关键字
数据元素中唯一标识该元素的某个数据项的值,基于关键字的查找,结果都是唯一的
(5)平均查找长度
所有查找过程中进行关键字的比较次数的平均值,即ASL= ∑ i = 1 n P i C i \sum\limits_{i=1}^n{P~i~C~i~}\quad i=1∑nP i C i
n是表长度,Pi是查找第i个数据元素的概率(一般相等),即Pi=1/n;Ci是找到第i个数据元素所需进行的比较次数
(1)一般线性表的顺序查找
typedef struct
{
Elemtype *elem;
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;
}
}
引入哨兵可以使得循环不必判断数组是否会越界,在多个算法中都适用
查找概率都为1/n,定位第i个元素时需要进行n-i+1次对比,故
ASL成功=(n+1)/2
ASL不成功=n+1
当n较大,平均查找长度也大,效率低,优点是对元素的存储没有要求,顺序存储和链式存储都可以
(2)有序表的顺序查找
查找之前就知道表是有序的,则查找失败就不用再比较表的另一端的数据,直接返回失败信息,降低了ASL
可以用判定树来描述有序线性表的查找过程
有序表的查找成功ASL和一般表的一样
但是查找失败的 ASL不成功= n 2 \dfrac{n}{2} 2n+ n n + 1 \dfrac{n}{n+1} n+1n
又称二分查找,仅适用于有序的顺序表
给定key值与表的中间元素对比,相等就成功;如果不成功,如果key大于中间值,则左指针右移到刚刚的中间位置,如果key小于中间值,则右指针左移到刚刚的中间位置
如此重复,直到找到或者查找失败,即左指针在右指针的右边
int Binarysearch(Sstable L,Elemtype 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;
}
折半查找的过程可以用一棵判定树来描述
折半查找的比较次数最火不会超过树的高度
ASL成功=log2(n+1)-1
时间复杂度:O(log2n)
折半查找仅适合顺序存储区结构,不适合链式存储结构,同时还要有序排列
又称索引顺序查找,吸取了顺序查找和折半查找各自的优点,既有动态结构,又适于快速查找
基本思想:把查找表分为若干子块,块内元素可以无序,但是块间元素时有序的;再建立一个索引表,索引表的每个元素含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列
查找过程:在索引表中确定key所在的块,可以顺序也可以折半,然后在块内顺序查找
如下图:
设索引查找和块内查找的平均长度为Li和Ls
则ASL=Li+Ls
将长度为n的表分为b块,每块s个记录,等概率的情况下,均采用顺序查找,则ASL= b + 1 2 \dfrac{b+1}{2} 2b+1+ s + 1 2 \dfrac{s+1}{2} 2s+1= s 2 + 2 s + n 2 s \dfrac{s^2+2s+n}{2s} 2ss2+2s+n
若左子树非空,则左子树上所有结点的值均小于根结点的值;若右子树非空,则右子树上所有结点的值均大于根结点的值;左右子树也是一棵二叉排序树==(左小右大)==
对二叉排序树进行中序遍历就得到一个递增的有序序列
树非空,给定值key先于根结点比较,如果大于根结点,就进入右子树查找,小于就进入左子树查找,不断重复,直到找到或失败,是递归的过程,递归算法的执行效率低
非递归算法:
BSTnode* BSTsearch(Bitree T,Elemtype key)
{
while(T!=NULL && key!=T->data)
{
if(key<T->data)
{
T=T->lchild;
}
else
{
T=T->rchild;
}
}
return T;
}
二叉排序树是一种动态树表,树的结构不是一次性生成,是在查找的过程不存在关键字时进行插入
树为空直接插入,key大于根结点插入右子树,否则插入左子树,一定是个叶结点
int BSTinsert(Bitree &T, keytype k)
{
if(T==NULL)
{
T=(Bitree)malloc(sizeof(BSTnode));
T->data=k;
T->lchild=T->rchild=NULL;
return 1;
}
else if(k==T->data)
{
return 0;
}
else if(k<T->data)
{
return BSTinsert(T->lchild,k);
}
else
{
return BSTinsert(T->rchild,k);
}
}
从空树出发,依次输入元素,再插入
void CreatBST(Bitree &T, Keytype str[],int n)
{
T=NULL;
itn i =0;
while(i<n)
{
BSTinsert(T,str[i]);
i++;
}
}
删除时不能把这个结点的子树的结点都删除,把删除结点从链表上摘下来,再把断开的二叉链表重新链接起来,同时确保左小右大
删除情况:
(1)若删除结点z是叶结点,直接删除
(2)若z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,代替z的位置
(3)若结点z有左右子树,则z的直接后继或直接前驱代替z,然后从二叉排序树中删去这个直接后继或直接前驱,然后变为1,2中情况
若左右子树高度之差不超过1,ASL=O(log2n)
若只有左/右孩子的单支树,ASL=O(n)
为避免树的高度增长过快,减低二叉排序树的性能,规定在插入和删除结点时,保证任意结点的左右子树高度差的绝对值不超过1,这样的二叉树称为平衡二叉树或AVL树
平衡因子:结点左子树与右子树的高度差,则平衡二叉树的平衡因子只可能是-1,0,1
保证平衡的基本思想:在插入或删除一个结点时,首先检查插入路径上的结点是否因为此次操作导致不平衡。若不平衡则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡
(1)LL平衡旋转(右单旋转)
在结点A的左孩子的左子树上插入了新结点,需要一次向右的旋转操作,将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则成为A结点的左子树
(2)RR平衡旋转(左单旋转)
在结点A的右孩子的右子树上插入新结点,需要一次向左的旋转操作;将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树
(3)LR平衡旋转(先左后右双旋转)
由于在A的左孩子的右子树插入结点,需要进行两次旋转操作,先左旋转后右旋转。
将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后把该C结点向右上旋转提升到A结点的位置
(4)RL平衡旋转(先右后左双旋转)
A的右孩子的左子树上插入新结点,需要进行两次旋转操作,先右旋转后左旋转
将A的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后把C结点向左上旋转提升到A结点的位置
LR和RL旋转时,新结点究竟是插入C的左子树还是插入C的右子树不影响旋转过程
步骤:(1)用二叉排序树的方法对结点w进行删除
(2)若导致不平衡,则从结点w开始向上回溯,找到第一个不平衡的结点z;y为结点z的高度最高的孩子结点;x是结点y的高度最高的孩子结点
(3)然后对以z为根的子树进行平衡调整,其中想x,y和z可能的位置有4种情况:
y是z的左孩子,x是y的左孩子(LL)
y是z的左孩子,x是y的右孩子(LR)
y是z的右孩子,x是y的右孩子(RR)
y是z的右孩子,x是y的左孩子(RL)
插入操作仅需要对以z为根的子树进行平衡调整,删除对以z为根的子树进行平衡调整,如果调整后子树的高度减1,则可能需要对z的祖先结点进行平衡调整,甚至到根结点
与给定值进行比较的关键字个数不超过树的深度,ASL=O(log2n)
为了保持AVL树的平衡性,插入和删除后,非常频繁的调整全树整体拓扑结构,代价大,所以在AVL树上引入红黑树
性质:(1)每个结点或是红色,或是黑色
(2)根结点是黑色
(3)NULL结点都是黑色
(4)不存在两个相邻的红结点
(5)对每个结点,从该结点到任意一个叶结点的简单路径长度上,所含黑结点的数量相同
从某节点出发到达一个叶结点的任意一个简单路径上的黑结点总数称为该结点的黑高(bh),根结点的黑高就是红黑树的黑高
结论1:从根到NULL结点的最长路径不大于最短路径的2倍,最短即全是黑结点
结论2:有n个内部结点的红黑树的高度h$\leqslant$2log2(n+1)
插入新结点需要进行着色调整以满足红黑树的性质
结论3:新插入红黑树中的结点初始着为红色
插入过程:
(1)用二叉查找树插入法插入,并将结点z着为红色,若父结点是黑色,无须做任何调整
(2)如果结点z是根结点,将z着为黑色,结束
(3)如果z不是根结点,并且z的父结点p是红色,则分三种情况
情况1:z的叔结点y是黑色的,且z是一个右孩子
LR,先左旋再右旋,z是爷结点的左孩子的右孩子
情况2:z的叔结点y是黑色的,z是一个左孩子
LL,右单旋,z是爷结点的左孩子的左孩子
若父结点是爷结点的右孩子,则还有两种对称的情况:RL和RR
情况3:如果z的叔结点y是红色
z是左孩子还是右孩子无影响,z的父结点和叔结点都是红色的,因为爷结点是黑色的,把父结点和叔结点着为黑色,爷结点着为红色,然后把爷结点作为新的z来重复循环,指针z在树中上移两层
若父结点是爷结点的右孩子,也还有两种对称情况
只要满足情况3的条件,就会不断循环,每次循环指针z都会上移两层,直到满足第二步,或情况1或情况2的条件
举例说明:
以上可总结为如果插入结点违反了不红红,即两个红结点不能相邻,则:
(1)如果是黑叔:旋转+染色
LL:右单旋,父换爷+染色
RR:左单旋,父换爷+染色
LR:左右双旋,儿换爷+染色
RL:右左双旋,儿换爷+染色
(2)如果是红叔:染色+变新
叔父爷染色,爷变为新结点
删除操作容易造成黑高的变化,删除黑结点会导致根结点到叶结点间的黑结点数量减少
删除过程也是先执行二叉查找树的删除方法;若待删结点有两个孩子,不能直接删除,而要找到该结点的中序后继或前驱填补,即右子树中最小的结点,然后转换为删除该后继结点,由于后继结点至多只有一个孩子,这样就转换为待删结点是终端结点或仅有一个孩子的情况。有以下两种情况:
(1)如果只有右子树或左子树,则有两种情况如图
子树只有一个结点且必然是红色
(2)如果待删结点没有孩子,若该结点是红色的,直接删除,无须做任何调整
(3)如果待删结点没有孩子,且该结点是黑色;删除该结点会破坏性质5,即黑高应该是一样的,简单的修正方法就是将替换待删结点y的结点x视为还有额外一重黑色,定义为双黑结点,但是这又破坏了性质1,于是删除操作的任务就转化为将双黑结点恢复为普通结点
这又分为四种情况:
情况1:x的兄弟结点w是红色的
w必须有黑色左右孩子和父结点。交换w和父结点x.p的颜色,然后对x.p做一次左旋,现在x的兄弟是旋转之前w的某个孩子结点,其颜色是黑色,情况1就转换成情况2,3,或4处理
情况2:x的兄弟结点w是黑色的,且w的右孩子是红色的
RR左单旋,即这个红结点是其爷结点的右孩子的右孩子,交换w和父结点x.p的颜色,把w的右孩子着为黑色,并对x的父结点x.p做一次左旋,将x变为单重黑色,此时不成破坏任何性质
情况3:x的兄弟结点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的
RL先右旋,再左旋,即这个红结点是其爷结点的右孩子的左孩子,交换w和其左孩子的颜色,然后对w做一次右旋,而不破坏红黑树的任何性质,现在x的新兄弟结点w的右孩子是红色,这样就变为情况2
情况4:x的兄弟结点w是黑色的,w的两个孩子结点都是黑色的
可以从x和w上去掉一重黑色,使得x只有一重黑色而w变为红色,同时把x的父结点x.p额外着一层黑色,以保持局部的黑高不变;通过将x.p作为新结点x来循环,x上升一层。如果通过情况1进入情况4,原来的x.p就是红色,把新结点x变为黑色,终止循环
若x是父结点x.p的右孩子,则还有四种对称的情况,处理方式类似
总结:情况4中x的兄弟结点w及左右孩子都是黑色,可以从x和w中各提取一重黑色,让x变为普通黑结点,并把调整任务向上推给他们的父结点;
情况1,2,3中兄弟结点w或w的左右孩子有红结点,所以只能在x.p子树内调整和着色,且不能改变x原根结点的颜色;
情况1虽然会转变为情况4,但因为新x的父结点x.p是红色,所以执行一次情况4就会结束;
情况1,2,3在各执行常数次的颜色改变和至多三次旋转后便终止,情况4可能是重复执行的唯一情况,每执行一次指针x上升一层,至多O(log2n)次
举例:
m阶B树是所有结点的平衡因子均等于0的m路平衡查找树,可以为空树
性质:
(1)树中么个结点至多有m棵子树,即至多含有m-1个关键字
(2)若根结点不是叶结点,则至少有两棵子树
(3)除根结点外的所有非叶结点至少有 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉,至少含有 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉-1个关键字
(4)所有的非叶结点的结构如下:
(5)所有叶结点出现在同一层,并且不带信息(可以视为外部结点或失败结点,指向的指针为空)
以上图为例
(1)结点的孩子个数等于该结点中关键字个数加1
(2)根结点没有关键字就没有子树,为空;根结点有关键字,则其子树个数必然大于或等于2,因为子树个数等于关键字个数加1
(3)除根结点外的所有非叶结点至少有 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉=5/2=3棵子树,至少含有 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉-1=2个关键字,至多5棵子树,至多4个关键字
(4)结点中的关键字从左到由递增有序,关键字两侧均有指向子树的指针,左侧指针所指子树的所有关键字均小于该关键字,右侧指针所指子树的所有关键字均大于该关键字
(5)所有叶结点均在第4层,代表查找失败的位置
B树操作所需的磁盘存取次数与B树的高度成正比;高度不包含空结点那层
n$\geqslant$1,对任意一棵包含n个关键字,高度为h,阶数为m的B树
(1)关键字个数n ⩽ \leqslant ⩽mh-1,因此 h ⩾ \geqslant ⩾logm(n+1)
(2)若每个结点的关键字个数最少,则容纳同样多关键字的B树敢赌达到最大
h ⩽ \leqslant ⩽logm/2(向上取整)((n+1)/2)+1
与二叉查找树相似,两个基本操作:在B树中找结点;在结点内找关键字
B树一般存储在磁盘上,因此前一个查找操作是在磁盘上进行,后一个在内存中进行,内存中采用顺序查找或折半
(1)定位:利用查找算法找出插入该关键字的最底层中某个非叶结点,会找到查找失败的叶结点,其上一层就是插入位置
(2)插入:每个非失败结点的关键字个数都在区间[ ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉-1,m-1]内,插入后个数小于m可以直接插入,大于m-1需要进行分裂
分裂:取一个新结点,在插入key后的原结点,从中间位置 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉将其中的关键字分为两部分,左部分包含的关键字放在原结点,右部分包含的关键字放到新结点中,中间位置的结点插入原结点的父结点,若此时父节点的个数也超过上限,继续分裂,直到传到根结点为止,树的高度增1
要使得删除后的结点的关键字个数 ⩾ \geqslant ⩾ ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉-1,设计合并问题
被删关键字不在终端结点,可以用k的前驱或后继来替代,然后删除k
当被删关键字在终端结点中,有三种情况:
(1)直接删除关键字:个数仍满足条件,可以直接删
(2)兄弟够借:兄弟的结点够多,可调整结点和结点左右兄弟及其双亲结点(父子换位法)达到新的平衡
(3)兄弟不够借:将关键字删除后与左右兄弟及双亲结点的关键字进行合并
合并过程双亲结点的关键字个数会减1,若又发生了以上情况,重复合并,直到满足B树的性质
性质:
(1)每个分支结点最多有m棵子树(孩子结点)
(2)非叶根结点至少有两棵子树,其他每个分支结点至少有 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉棵子树
(3)结点的子树个数与关键字个数相等
(4)所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来
(5)所有分支结点中仅包含它的各个子节点中关键字的最大值及指向其子节点指针
B树和B+树的差异
(1)B+:n个关键字的结点——n棵子树
B:n个关键字的结点——n+1棵子树
(2)B+:结点的关键字个数 ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉ ⩽ \leqslant ⩽n ⩽ \leqslant ⩽m
B: ⌈ \lceil ⌈m/2 ⌉ \rceil ⌉-1 ⩽ \leqslant ⩽n ⩽ \leqslant ⩽m-1
(3)B+:叶结点包含了全部关键字,非叶结点的关键字也会出现在叶结点
B:终端结点和其他结点的关键字不重复
(4)B+:叶结点包含所有信息,所有非叶结点只有索引作用
之前的查找都是建立在比较的基础上,查找的效率取决于比较的次数。
散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为Hash(key)=Addr(可以是数组下标,索引或内存地址)
散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为冲突,发生碰撞的不同关键字称为同义词
散列表:根据关键字而直接进行访问的数据结构,散列表建立了关键字和存储地址直接的一种直接映射关系
理想情况的时间复杂度:O(1)
注意事项:(1)散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围
(2)散列函数计算出来的地址应该能等概率,均匀的分布在整个地址空间中,从而减少冲突的发生
(3)散列函数尽可能简单
常用的几种:
(1)直接定址法
直接取关键字的某个线性函数值为散列地址
H(key)=key或H(key)=a*key+b
计算简单,不会发生冲突,适合关键字分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费
(2)除留余数法
最简单常用,假设表长为m,取一个不大于m但最接近或等于m的质数p,利用公式把关键字转换成散列地址
H(key)=key%p
(3)数字分析法
假设关键字是r进制数,r个数码在各位上出现的频率不一定相同,可能分布均匀也可能分布不均匀,此时选取数码分布较为均匀的若干位作为散列地址;但是更换了关键字就需要重新构造新的散列函数
(4)平方取中法
取关键字的平方值的中间几位作为散列地址,使得散列地址分为较为均匀
指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。数学递推公式为:
Hi=(H(key)+di)%m
i=0,1,2,….k(k ≤ \leq ≤m-1);m表示表长,di为增量序列
取定某一增量序列后,对应的处理方法就是确定的,通常有四种取法:
(1)线性探测法
即di=0,1,2……m-1
冲突发生时,顺序查看表中下一个单元(探测到表尾时,下一个就是表头),直到找出一个空闲单元或查遍全表
造成了大量元素在相邻的散列地址上聚集起来,大大降低了查找效率
(2)平方探测法
di=02,12,-12,22,-22……k2,-k2,k ≤ \leq ≤m/2;m必须是一个可以表示成4k+3的素数,又称二次探测法
可以避免堆积,但是不能探测到散列表的所有单元,至少能探测一半单元
(3)双散列法
当di=Hash2(key)时,称为双散列法
需要使用两个散列函数,当通过第一个散列函数H(key)得到的地址发生冲突时,则利用第二个散列函数Hash2(key)计算该关键字的地址增量
Hi=(H(key)+i*Hash2(key))%m
初始探测位置H0=H(key)%m,i是冲突的次数,初始为0,最多经过m-1次探测就会遍历表中所有位置,回到H0
开放地址不能随便物理删除表中已有元素,若删除会截断其他具有相同散列地址的元素的查找地址,因此要删除时做一个删除标记,进行逻辑删除,但是又会导致散列表表面上看起来很满,实际上有许多位置未利用,因此需要定期维护,把标记的元素物理删除
可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识
假定散列地址为i的同义词链表的头指针存放在散列表的第i个单元中,因而查找,插入和删除操作主要在同义词链中进行
例如关键字{19,14,23,01,68,20,84,27,55,11,10,79},散列函数H(key)=key%13
初始化:Addr=Hash(key)
(1)检测查找表中地址为Addr的位置上是否有记录,若无,返回查找失败;若有记录,比较它与key的值,相等返回成功,否则执行步骤2
(2)用给定的处理冲突办法计算下一个散列地址,并把Addr置为此地址,转入步骤1
举例:
散列函数:H(key)=key%13
查找84:H(84)=6,但是L[6]$\neq 84 ,则找第一次冲突处理的地址 H 1 = ( 6 + 1 ) 84,则找第一次冲突处理的地址H~1~=(6+1)%16=7,L[7] 84,则找第一次冲突处理的地址H 1 =(6+1)\neq$84,则找第二次冲突处理的地址H2=(6+2)%16=8,L[8]=84,成功并返回8
同一组关键字,设定相同的散列函数,不同的处理方法得到的散列表不同,ASL也不同
(1)冲突的产生导致散列表的查找过程仍然是一个给定值和关键字进行比较的过程,就需要以ASL来衡量散列表的查找效率
(2)查找效率取决于:散列函数,处理冲突的方法和装填因子
装填因子 α \alpha α= 表中记录数 n 散列表长度 m \dfrac{表中记录数n}{散列表长度m} 散列表长度m表中记录数n,定义为一个表的装满程度,ASL就依赖于 α \alpha α,而不直接依赖n或m
α \alpha α越大装填的记录越满,冲突的可能性越大