数据结构——查找
查找的概念
查找(Searching) :就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素( 或记录)。
查找表(Search Table) :是由同一类型的数据元素(或记录)构成的集合。
关键字(Key) :数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。例如,在由一个学生元素构成的数据集合中,学生元素中“学号”这一数据项的值唯一地标识一名学生。
静态查找表(Static Search Table) :只作查找操作的查找表。 动态查找表(Dynamic Search Table) : 在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。
平均查找长度 :在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度,则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为$\ ASL=\sum_{i=1}^{n}P_iC_i$
式中,n是查找表的长度;P是查找第i个数据元素的概率,一般认为每个数据元素的查找概率相等,即P,= 1/n;C;是找到第i个数据元素所需进行的比较次数。平均查找长度是衡量查找算法效率的最主要的指标。
顺序表查找
顺序查找
1 | /*有哨兵顺序查找*/ |
这种在查找方向的尽头放置“哨兵”免去了在查找过程中每一次比较后都要判断查找位置是否越界的小技巧,看似与原先差别不大,但在总数据较多时,效率提高很大,是非常好的编码技巧。 上述顺序表查找时间复杂度是O (n) 。
折半查找
当查找表是有序表时,可采用折半查找的方法。
算法思路:
1 | int Binary_Search(SeqList L, ElemType key){ |
折半查找的过程可用二叉树来描述,称为判定树。
节点的树高代表该节点的查询次数
因此,长度为13的有序表进行折半查找的平均查找长度ASL=(1×1+2×2+3×4+4×6)/13 =41/13。
折半查找的时间复杂度为 O(log2n),平均情况下比顺序查找的效率高。
分块查找
为了减少索引项的个数,我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。
分块有序,是把数据集的记录分成了若千块,并且这些块需要满足两个条件:
块内无序:即每一块内的记录不要求有序。
块间有序:例如,要求第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块的所有记录的关键字均要大于第二块的所有记录关键字…因为只有块间有序,才有可能在查找时带来效率。
对于分块有序的数据集,将每块对应一个索引项, 这种索引方法叫做分块索引。如下图所示,我们定义的分块索引的索引项结构分三个数据项:
最大关键码:它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中的最小关键字也能比这一块最大的关键字要大;
块长:存储了块中的记录个数,以便于循环时使用;
块首指针:用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历。
在分块索引表中查找,就是分两步进行: 1.在分块索引表中查找要查关键字所在的块。由于分块索引表是块间有序的,因此很容易利用折半、插值等算法得到结果。例如在上图的数据集中查找62,我们可以很快可以从左上角的索引表中由57<62<96得到62在第三个块中。 2.根据块首指针找到相应的块,并在块中顺序查找关键码。
树表的查找
二叉排序树
二叉排序树(也称二叉查找树)或者是一棵空树,或者是具有下列特性的二叉树:
- 若左子树非空,则左子树上所有结点的值均小于根结点的值。
- 若右子树非空,则右子树上所有结点的值均大于根结点的值。
- 左、右子树也分别是一棵二叉排序树。
根据二叉排序树的定义,左子树结点值<根结点值<右子树结点值,所以对二叉排序树进行中序遍历,可以得到一个递增的有序序列。例如,下图所示二叉排序树的中序遍历序列为123468。
二叉排序树的插入和建立
在一棵二叉排序树中插入值为系的结点的步骤如下:
①若二叉排序树为空,则生成值为k的新结点s,同时将新结点s作为根结点插入。
②若k小于根结点的值,则在根的左子树中插入值为k的结点。
③若k大于根结点的值,则在根的右子树中插入值为k的结点。
④若k等于根结点的值,表明二叉排序树中已有此关键字,则无需插入。
二叉排序树插入算法
1 | //构造函数 |
二叉排序树的建立
1 | // 构造函数:利用数组 a[] 和大小 n 建立二叉排序树 |
二叉排序树的查找过程
根据二叉排序树的定义,在二叉排序树中查找给定值k的过程如下:
①若二叉排序树为空,则表明查找失败,返回空指针;否则,若给定值k等于根结点的值,则表明查找成功,返回根结点。
②若给定值k小于根结点的值,则继续在根的左子树中查找。
③若给定值k大于根结点的值,则继续在根的右子树中查找。
1 | // 非递归查找函数 |
若二叉排序树是平衡的(即形态均匀),则进行查找的时间复杂度为 O(log2n);若退化为一棵单支树(最极端和最差的情况),则其时间复杂度为 O(n)。对于一般情况,其时间复杂度可以认为是 O(log2n)。
二叉排序树的删除
二叉排序树的查找和插入都很简单,但是删除操作就要复杂一些,此时要删除的结点有三种情况:
- 叶子结点;
- 仅有左或右子树的结点;
- 左右子树都有的结点;
前两种情况都很简单,第一种只需删除该结点不需要做其他操作;第二种删除后需让被删除结点的直接后继接替它的位置;复杂就复杂在第三种,此时我们需要遍历得到被删除结点的直接前驱或者直接后继来接替它的位置,然后再删除。
第三种情况如下图所示:
代码如下:
1 | /* |
下面是Delete()方法:
1 | /*从二叉排序树中删除结点p,并重接它的左或右子树。*/ |
二叉排序树的查找性能取决于二叉排序树的形状。
例如{ 62 , 88 , 58 , 47 , 35 , 73 , 51 , 99 , 37 , 93 }
{62,88,58,47,35,73,51,99,37,93}{62,88,58,47,35,73,51,99,37,93}这样的数组,我们可以构建如下左图的二叉排序树。但如果数组元素的次序是从小到大有序,如{35,37,47,51,58,62,73,88,93,99},则二叉排序树就成了极端的右斜树,如下面右图的二叉排序树:

也就是说,我们希望二叉排序树是比较平衡的,即其深度与完全二叉树相同,那么查找的时间复杂也就为 O(log2n),近似于折半查找。 不平衡的最坏情况就是像上面右图的斜树,查找时间复杂度为O(n),这等同于顺序查找。 因此,如果我们希望对一个集合按二叉排序树查找,最好是把它构建成一棵平衡的二叉排序树。
平衡二叉树
平衡二叉树:是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。
我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF
那么平衡二叉树上所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
平衡二叉树的插入
新结点插入后,若造成查找路径上的某个结点不再平衡,则需要做出相应的调整。可将调整的规律归纳为下列4种情况:
- LL平衡旋转(右单旋转):由于在结点A的左孩子(L)的左子树(L)上插入了新结点
- RR平衡旋转(左单旋转):由于在结点A的右孩子(R)的右子树(R)上插入了 新结点
- LR平衡旋转(先左后右双旋转):由于在A的左孩子(L)的右子树(R)上插入新结点
- RL平衡旋转(先右后左双旋转):由于在A的右孩子(R)的左子树(L)上插入新结点
举例
B树
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。 B树是所有结点的平衡因子均等于0的多路平衡查找树。
下图所示的B树中所有结点的最大孩子数m = 5,因此它是一棵5阶B树,在m
mm阶B树中结点最多可以有m个孩子。可以借助该实例来分析上述性质: 