查找
基本概念
- 查找表,存储元素的数组或链表等
- 静态查找表,在查找过程中不需要对查找表进行修改,如顺序查找、折半查找、散列查找等
- 动态查找表,在查找过程中需要对查找表进行修改,如二叉排序树的查找、散列查找等
- 关键字,唯一标识元素的值
- 平均查找长度,查找过程中关键字比较次数的平均值
顺序查找
又称线性查找。
对于链表和顺序表都适用。
当每个元素查找概率相同时:
- 平均成功查找长度 = (n+1)/2
- 平均失败查找长度 = n+1
通常,查找表中的元素的查找概率并不相等。若能预先得知每个记录的查找概率,则应先对记录的查找概率进行排序,使表中元素按查找概率由大到小排列
优:对元素存储没有要求,顺序或链式皆可。对有序性也没有要求。
缺:当n较大时,平均查找长度较大,效率低。
对线性的链表只能顺序查找
顺序查找有序表
若在查找前知道查找表是有序的,则在查找失败时,不需要再比较到表的另一端就能返回查找失败的信息,从而降低顺序查找失败的平均查找长度。查找成功的平均查找长度还是和一般线性表一样。
查找失败的平均查找长度降为n/2 + n/(n+1),(查找概率相同)
折半查找
又称二分查找。
只适用于有序的顺序表。
思想:
- 将给定值与中间位置比较,若相等则比较成功,不相等则在该位置之前或之后查找。
- 重复1.直到查找成功或失败。
折半查找的过程可以用二叉树来描述,称为判定树。判定树是一棵平衡二叉树。
折半查找的比较次数不会超过树的高度。
由判定树可知,查找成功的ASL = (11 + 22 + 34 + 44) / 11 = 3,查找失败的ASL = (34 + 48)/12 = 11/3。
在等概率查找时:
- 查找成功的平均查找次数:log2(n+1) - 1
- 查找失败的平均查找次数:log2(n+1) - 1
优:平均情况下查找效率比顺序查找高
缺:需要线性表必须能随机存取(不能是链表),且要求关键字有序排列。
分块查找
又称索引顺序查找。
吸取了顺序查找和折半查找的优点,既有动态结构,又适用于快速查找。
思想:
- 将查找表分成若干块,块内可以无序,但块之间有序(第一个块内最大关键字大于小于第二个块内所有关键字)。
- 建立索引表,每个表项含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列。
- 顺序查找或折半查找索引表得到块索引
- 块内顺序查找
设索引查找和块内查找的平均查找长度为Li,Ls
查找成功的平均查找长度为:Li+Ls
将长度为n的查找表均分为b块,每块s个记录,等概率查找时,均采用顺序查找,则平均查找长度为:(b+1)/2+(s+1)/2 = (s2+2s+n)/2s
若s = (n)1/2,取平均查找长度为(n)1/2+1,对索引表采用折半查找时,则平均查找长度为:log2(b+1)+(s+1)/2
树型查找
二叉排序树(BST)
Binary search tree
定义:
- 左子树上所有结点的值均小于根结点的值
- 右子树上所有结点的值均大于根结点的值
- 左右子树分别又是一棵BST
对二叉排序树做中序遍历,可以得到一个递增的有序序列。
二叉排序树的插入
作为一种动态树表,其结构是在查找过程中,当树不存在关键字值等于给定结点时插入的。
- 若二叉树为空,则插入到根结点
- 若给定结点的关键字小于根结点,插入到左子树
- 若给定结点的关键字大于根结点,插入到右子树
例:插入28和58,虚线为比较路径
二叉排序树的删除
在二叉排序树中删除一个结点时,需要将其子树重新链接。
有三种情况:
- 若被删结点是叶节点,直接删。
- 若被删结点只有左子树或右子树,将子树连接到父结点上。
- 若被删结点既有左子树又有右子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删除这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
注:已知二叉排序树经过中序遍历可以得到一个递增的有序序列,这里的直接后继(或直接前驱)应该是指被删除结点在这个中序遍历序列中的直接后继(或直接前驱)。
体现在二叉排序树的图中:
某个结点的直接后继为以该结点为根的右子树中最左下位置的结点,即右子树的最小值;
某个结点的直接前驱为以该结点为根的左子树中最右下位置的结点,即左子树的最大值。
二叉排序树的查找效率
取决于树的高度,若二叉排序树的左右高度之差不超过1,则这样的二叉排序树称为平衡二叉树,它的平均查找长度为O(log2n),若二叉排序树是一个只有左(右)孩子的单支树(成了链表),则平均查找长度退化为O(n)。
最坏情况下,构造二叉排序树的输入是有序的,就会造成退化成链表。
从查找过程来看,二叉排序树与二分查找相似。
从平均查找时间来看,二叉排序树与二分查找相似。
二叉排序的判定树唯一,但是二叉排序树的查找不唯一,相同关键字的插入顺序不同可能导致二叉排序树不同。
从维护时间来看,二叉排序树无需移动结点(链式),插入和删除只需要O(log2n);二分查找的对象是有序顺序表,移动元素需要O(n)。
当有序表是静态查找表时,应用顺序表作为存储结构,采用二分查找;
当有序表是动态查找表时,应用二叉排序树作为逻辑结构。
平衡二叉树(BBT)
Balanced binary tree
为了避免二叉排序树高度增长过快,降低排序性能,规定在插入和删除结点时,保证任意结点的左右子树高度差不超过1。
思想:
- 插入结点时检查是否因为此次操作导致了不平衡
- 若导致了不平衡,找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点,对其子树进行调整
每次调整的对象都是最小不平衡子树。
结点的左子树与右子树的高度差为该结点的平衡因子。
平衡二叉树的插入
插入后造成的不平衡有以下四种情况
- LL旋转(右单旋转)
在结点A的左子树(L)的左子树(L)上插入了新结点,A的平衡因子由1->2。
将A的左孩子B向右旋转替代A成为根结点,A右旋成为右子树,B的原右子树作为A的左子树。
- RR旋转(左单旋转)
在结点A的右子树(R)的右子树(R)上插入了新结点,A的平衡因子由-1->-2。
将A的右孩子向左旋转替代A成为根结点,A左旋成为左子树,B的原左子树作为A的右子树。
- LR旋转(先左旋后右双旋)
在结点A的左子树(L)的右子树(R)上插入了新结点,A的平衡因子由1->2。
将A的左孩子B的右子树的根结点C左旋替代B,再右旋替代A
- RL旋转(先右旋后左双旋)
在结点A的右子树(R)的左子树(L)上插入了新结点
A的平衡因子由-1->-2。将A的右孩子B的左子树的根结点C右旋替代B,再左旋替代A
平衡二叉树的删除
用二叉排序树的方式删除结点w后:
- 从结点w向上回溯,找到第一个不平衡的结点z(最小不平衡子树);y为结点z的高度最高的孩子结点;x是结点y的高度最高的孩子结点。
- 根据z,y,x的关系
- y是z的左孩子,x是y的左孩子(LL,右单旋转)
- y是z的右孩子,x是y的右孩子(LL,右单旋转)
- y是z的左孩子,x是y的右孩子(LR,先左旋后右旋)
- y是z的右孩子,x是y的左孩子(RL,先右旋后左旋)
插入时仅需要对以z结点为根的子树进行平衡调整,而删除时,如果调整完后子树的高度-1,则可能需要对z的祖先结点进行平衡调整,甚至回溯到根结点。
红黑树
为了保持AVL树的平衡性,插入和删除操作后,会非常频繁地调整全树整体的拓扑结构,代价较大。
为此在AVL树的平衡标准上进一步放宽条件,引入了红黑树的结构。
满足:
- 每个结点是红色或黑色的。
- 根结点是黑色的。
- 叶节点(虚构的外部结点,NULL结点)都是黑色的。
- 不存在两个相邻的红结点(即红结点的父结点和孩子结点都是黑色的)。
- 对每个结点,从该结点到任一叶节点的简单路径上,所含黑结点的数量相同。
从某结点出发(不包含该结点)到达一个叶节点的任一简单路径上的黑结点总数称为该结点的黑高(记为bh)。根结点的黑高称为红黑树的黑高。
结论:
- 从根到叶节点的最长路径不大于最短路径
由性质5,当从根结点到任一叶节点的简单路径最短时,这条路径必然全由黑结点构成。
由性质4,当某条路径最长时,这条路径必然是由黑结点和红结点相间构成的,此时黑结点与红结点的数量相同。
- 有n个内部结点的红黑树的高度h<=2log2(n+1)
由结论1,从根结点到叶节点(不含叶节点)的任一条简单路径上都至少有一半是黑结点,因此根的黑高至少为h/2,于是有h>=2h/2-1
红黑树的平衡降低为了“任一结点左右子树的高度相差不超过2倍”,从而降低了动态操作的频率。
红黑树的插入
假设新插入结点初始为黑色,那么该结点所在路径比其他路径多出一个黑结点(每次插入都会破坏性质5),调整起来也会比较麻烦。
新插入结点设为红色,则此时所有路径上的黑结点数量不变,仅在出现连续两个红结点时(破坏性质4)才需要调整,调整也简单。
- 用二叉查找树的方式插入新结点z,并设置为红结点。若z的父结点是黑色的,无需做任何调整,结束。
- 若z为根结点,将z设置为黑结点(树的黑高+1),结束。
- 若z不是根结点,并且z的父结点p是红色的,且是爷结点的左孩子:
- z的叔结点y是黑结点,并且z是一个右孩子
(LR,z是爷结点的左孩子的右孩子)先左旋变成情况b,再右旋
- z的叔结点y是黑结点,并且z是一个左孩子
(LL,z是爷结点的左孩子的左孩子)右旋
还有两种对称情况,即父结点p是爷结点的右孩子:(RL)先右旋再左旋,(RR)右旋
- z的叔结点y是红结点
z是左孩子还是右孩子都没有影响。
z的父结点和叔结点都是红结点,而爷结点是黑结点。
将父结点和叔结点都设为黑结点,将爷结点设为红结点。然后将爷结点设为结点z,重复此过程直至情况a或情况b。
还有两种对称情况,即父结点p是爷结点的右孩子,与上述处理方法相同。
红黑树的删除
红黑树的删除容易导致子树黑高的变化(破坏性质5)。
删除过程也是先执行二叉查找树的删除方法,若待删结点有两个孩子,则不能直接删除,而要找到该结点的中序后继(或前驱)填补(也就是右子树中的最小结点),然后转化为删除该后继结点。由于后继节点至多只有1个孩子,这样就转换为了待删结点是叶结点或仅有一个孩子的情况。
最终,删除一个结点有以下两种情况:待删结点没有孩子,待删结点只有右子树或左子树。
- 若待删结点只有右子树没有左子树,则只有两种情况:
子树只有一个结点,且必然是红色,否则会破坏性质5。
- 若待删结点没有孩子,且为红结点,直接删除,结束。
- 若待删结点没有孩子,且为黑结点。
设待删结点为y,替换结点为x(当y是终端结点时,x为黑色的NULL结点)
删除y会导致先前包含y的所有路径黑结点数量-1,从而y的任何祖先都破坏了性质5。
解决方法:将y视为还有额外一重黑色(双黑结点)
分以下四种情况(区别在于x的兄弟结点w以及w的孩子结点颜色):
1. x的兄弟结点w是红结点
w必须有黑色左右孩子和父结点。交换w和父结点颜色,对父结点做一次左旋。
2. x的兄弟节点w是黑结点,w的左孩子是红结点
RL,即红结点是其爷结点右孩子的左孩子。
先右旋再左旋。交换w和其左孩子的颜色,然后对w做一次右旋,得到情况iii。
3. 的兄弟结点w是黑结点,w的右孩子是红结点
RR,红结点是其爷结点的右孩子的右孩子。
左旋。交换w和父结点的颜色,将w的右孩子设为黑结点,对x的父结点做一次左旋,将x变为单重黑色。
4. x的兄弟结点w是黑结点,w的两个孩子都是黑结点
从x和w上去掉一重黑色,使得x只有一重黑色而w变为红结点。
为了补偿从w和w上去掉的黑色,把x的父结点额外加一重黑色,以保持局部的黑高不变。
然后将x的父结点作为新结点x来循环,直至得到情况i,将新结点x设为黑结点,结束。
在情况iv中,因为x的兄弟结点w以及其孩子结点都是黑色,所以可以从x和w中各提取一重黑色(让x变为普通黑结点)不会破坏性质4,并把调整任务上推到它们的父结点。
在情况i,ii,iii中,因为x的兄弟结点w或其孩子结点中有红结点,所以只能在该子树内用调整和重新着色的方式,且不能改变x的颜色(x为原根结点)(否则向上可能会破坏性质4)
情况i虽然可能会转为情况iv,但因为新x的父结点为红色的,所以执行一次情况4的处理就会结束。
情况iv是可能多次执行的情况,至多O(log2n)次。
B树
多路平衡查找树。
B树中所有节点的孩子个数的最大值称为B树的阶,常用m表示。
一棵m阶B树的定义:
- 树中每个结点至多有m棵子树,即至多有m-1个关键字
- 若根结点不是终端结点,则至少有两棵子树
- 除根结点外的所有非叶结点至少有m/2棵子树,即至少含有m/2-1个关键字
- 所有非叶结点的结构如下
- 所有叶结点都出现在同一层次上,并且不带信息(实际上并不存在)
B树是所有结点的平衡因子均=0的多路平衡查找树。
结合上图对性质进行理解
- 结点的孩子个数 = 该结点中关键字个数+1
- 如果根结点没有关键字就没有子树;如果根结点有关键字,则其子树必>=2
- 除根节点外的所有非终端结点至少有m/2=5/2=3棵子树,至多有5棵子树(至多有4个关键字)
- 结点中关键字从左到右递增,关键字两侧均有指向子树的指针,左边指针指向子树的所有关键字均小于该关键字,右边指针指向子树的所有关键字均大于该关键字
- 所有叶结点均在第4层,代表查找失败的位置
B树的高度
B树大部分操作所需的磁盘存取次数与B树的高度成正比。
对任意一棵包含n个关键字、高度为h、阶数为m的B树:(这里高度不算最后叶结点的一层)
- 因为B树每个结点最多有m棵子树,m-1个关键字
所以 n <= (m-1)(1+m+m2+...+mh-1) = mh - 1
因此有 h >= logm(n+1)
- 若让每个结点中的关键字最少,则B树的高度达到最大。
由B树的定义:第1层至少有1个结点,第2层至少有2个结点;除根结点外的每个非终端结点至少有m/2棵子树,则第3层至少有2(m/2)个结点.....第h + 1层至少有2(m/2)h-1个结点。即对关键字个数为n的B树,查找不成功的结点为n+1个。由此有n+1>=2(m/2)h-1,即h<=logm/2((n+1)/2) +1
例如,假设一棵3阶B树共有8个关键字,则其高度范围为2<= h <= 3.17。
B树的查找
与二叉排序树的查找很相似,只是每个结点都是多个关键字的有序表,在每个结点上所做的不是两路分支决定,而是根据该结点的子树所做的多路分支决定。
主要包括两步:1.在B树内找结点 2.在结点内找关键字
由于B树常存在磁盘上,因此1.是在磁盘上进行的,2.是在内存中进行且通常采用折半查找法
一路向下查找,查找到叶结点时,说明树中没有对应的关键字,查找失败。
B树的插入
- 定位。找出插入该关键字的最底层中的某个非叶结点(插入位置)
- 插入。每个非叶结点的关键字个数都在区间[m/2-1, m-1]内,如果插入后超出区间,需要对结点进行分裂
分裂:取一个新结点,插入原结点后,将原结点从中间位置(m/2)将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放入新结点中。中间位置的结点(m/2)插入原结点的父节点,此时若导致父节点的关键字个数超出范围,则对父节点进行分裂,一直循环,甚至最后导致B树高度+1。
B树的删除
要使得删除后结点的关键字个数 >= m/2 - 1,涉及到结点的合并。
- 当被删关键字k不在终端结点中(最低层非叶结点),可以用k的前驱(或后继)k'来代替k,然后删除k',关键字k'必定落在某个终端结点中,就变成了删除终端结点中的关键字。
- 当被删关键字k在终端结点中
- 直接删除关键字
k所在结点的关键字个数 >= m/2,表明删除该关键字后仍满足B树的定义
- 兄弟够借
k所在结点删除前的关键字个数 = m/2 - 1,且与此结点相邻的右(或左)兄弟结点的关键字个数 >= m/2。
则可以调整该结点、兄弟结点、双亲结点,以达到新的平衡。
- 兄弟不够借
k所在结点删除前的关键字个数 = m/2 - 1,且与此结点相邻的右(或左)兄弟结点的关键字个数均 = m/2 - 1。
将k删除后,与左或右兄弟结点以及双亲结点中的关键字合并。
合并后,双亲结点的关键字个数会-1。若双亲结点是根结点且关键字个数减少为0,则直接将根结点删除,合并后的新结点称为根结点;若双亲结点不是根结点,且关键字个数减少为m/2 - 2,则又要与它的兄弟结点和双亲节点进行合并,直至满足平衡位置。
B+树
B树的变形,常用于数据库。
满足:
- 每个分支结点最多有m棵子树(孩子结点)
- 非叶根结点至少有两棵子树,其他每个分支结点至少需要m/2棵子树
- 结点的子树个数与关键字个数相等
- 所有叶结点包含全部的关键字及指向相应记录的指针,叶结点中将关键字按大小排列,并且相邻叶结点按大小顺序相互连接起来
- 所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针
与B树的差异
- 在B+树中,具有n个关键字的结点只含有n棵子树(B树中n个关键字对应n+1棵子树)
- 在B+树中,每个结点(非根内部结点)的关键字个数n的范围是m/2 <= n <= m(根结点:2 <= n <= m)(在B树中,每个结点(非根内部结点)的关键字个数n的范围是m/2-1 <= n <= m-1(根结点:1 <= n <= m-1))
- 在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
- 在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中(在B树中,叶结点包含的关键字和其他结点的关键字是不重复的)
B+树的查找、插入与删除操作都与B树类似,只是在查找过程中,非叶结点上的关键字等于给定值时并不终止,而是继续向下查找,直到叶结点上的该关键字为止。
所以,在B+树中,无论查找成功与否,每次查找都是一条从根结点到叶结点的路径。
散列查找
散列表
前面介绍的线性表和树的查找中,记录在数据结构中的位置与其关键字不存在确定关系。
散列函数:一个把查找表中的关键字映射成不同地址的函数,记为Hash(Key) = Addr(这里的地址可以是数组下标、索引或内存地址等)
散列函数可能会把两个或两个以上不同的关键字映射到同一个地址,这种情况叫做冲突(碰撞)。
发生冲突(碰撞)的不同关键字称为同义词。
散列表:根据关键字直接进行访问的数据结构。建立了关键字和存储地址的直接映射关系。
理想情况下,对散列表进行查找的时间复杂度为O(1)。
散列表的查找效率取决于三个要素:散列函数、处理冲突的方法和装填因子。
装填因子:定义一个表的装满程度,α = 表中记录数n/散列表长度m,散列表的平均查找长度ASL依赖于α,而不直接依赖于n或m。直观地看,α越大,表示装填的记录越“满”,发生冲突的可能性越大。
散列函数的构造方法
构造过程中,需要注意以下几点:
- 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。
- 散列函数计算出来的地址应等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。
- 散列函数应尽可能简单,能够在较短时间内计算出任一关键字对应的散列地址。
直接定址法
直接取关键字的某个线性函数值为散列地址,散列函数为 H(Key) = Key 或 H(Key) = a × key + b
这种计算方法最简单,且不会产生冲突。适合于关键字的分布基本连续的情况,若关键字不连续,空位较多,会造成空间的浪费。
除留余数法
假定散列表长度为m,取一个不大于m但最接近或等于m的质数p,散列函数为 H(Key) = key % p
这种计算方法也最简单,且最常用。关键是选择好p,使得每个关键字通过散列后等概率地映射到整个地址空间上,从而减少冲突的可能性。
数字分析法
设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上出现的概率均匀一些,而在某些位上分布不均匀,此时应选取数码较均匀的若干位作为散列地址。
这种方法适用于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
平方取中法
取关键字的平方值的中间几位作为散列地址。
这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
处理冲突的方法
任何散列函数都不可避免地会出现冲突的情况。
在发生冲突时,则必须为产生冲突的关键字寻找下一个“空”的Hash地址。
用Hi表示处理冲突中第i次探测得到的散列地址,假设得到的另一个散列地址H1仍然发生冲突,只得继续求下一个散列地址H2,直至不发生冲突为止。
开放定址法
是指空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。即Hi = (H(key) % di) % m,di为增量序列(di = 0,1,2,....,m-1),m为散列表表长。
确定某一增量序列后,对应的处理方法就是确定的。通常有以下四种取法:
- 线性探测法:当di = 0,1,2,...,k时,就成为线性探测法。这种方法的特点是:冲突发生时,顺序查看表中下一单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),知道找到下一个空闲单元或查遍全表。
线性探测法能够使第i个散列地址的同义词存入第i+1个散列地址,这样本应在i+1个散列地址存入的同义词就争夺第i+2个散列地址....从而造成大量元素在相邻的散列地址上“聚集”(或堆积)起来,大大降低了查找效率。
- 平方探测法(二次探测法):当di = 02,12,22,....,k2,-k****2时,称为平方探测法,其中k <= m/2,散列表长度m必须是一个可以表示为4k+3的素数。
平方探测法是一种处理冲突的较好方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。
- 双散列法:当di = Hash2(key)时,称为双散列法。需要使用两个散列函数,当通过第一个散列函数得到的地址发生冲突时,则利用第二个散列函数计算该关键字的_地址增量_。即Hi=(H(key)+i×H2(key)) % m,i是冲突的次数,初始值为0。最多经过m-1次探测,就会遍历散列表中的全部位置,回到H0;
- 伪随机序列法。当di = 伪随机数序列时,称为伪随机序列法。
在开放定址法中,不能随便物理地删除表中的已有元素,若删除元素,则会截断其他同义词的查找过程。因此要删除一个元素时,可以做一个删除标记,进行逻辑删除。但多次删除后,表面上看起来散列表很满,实际上有许多位置未利用,因此要定期维护散列表,把逻辑删除的元素物理删除。
拉链法(链接法,chaining)
发生同义词冲突时,把同义词存储在一个线性链表(二叉树等链式结构也可)中,这个线性链表由其散列地址唯一标识。适用于经常进行插入和删除的情况。