考研数据结构:第七章 查找

文章目录

  • 一、查找的基本概念
  • 二、顺序查找和折半查找
    • 2.1顺序查找
    • 2.3折半查找
      • 2.3.1算法思想
      • 2.3.2代码实现
      • 2.3.3查找效率分析
      • 2.3.4折半查找判定树的构造
      • 2.3.5折半查找效率
      • 2.3.6小结
    • 2.4分块查找
  • 三、树形查找
    • 3.1二叉排序树
      • 3.1.1二叉排序树定义
      • 3.1.2查找操作
      • 3.1.3插入操作
      • 3.1.4二叉排序树的构造
      • 3.1.5二叉排序树的删除
      • 3.1.6查找效率分析
      • 3.1.7小结
    • 3.2平衡二叉树的插入
      • 3.2.1引子
      • 3.2.2LL
      • 3.2.3RR
      • 3.2.4LR
      • 3.2.5RL
      • 3.2.5小结
    • 3.3平衡二叉树的删除
  • 四、B树和B+树
    • 4.1B树
    • 4.2B树的插入删除
      • 4.2.1插入
      • 4.2.2删除
      • 4.2.3小结
    • 4.3B+树
      • 4.3.1定义
      • 4.3.2B+树的查找
      • 4.3.3B+树和B树的对比
      • 4.3.4小结
  • 五、散列表
    • 5.1散列表的基本概念
      • 5.1.1散列表、散列函数
      • 5.1.2冲突、同义词
      • 5.1.3关于散列表,有待解决的问题
    • 5.2散列函数的构造
      • 5.2.1除留余数法
      • 5.2.2直接定址法
      • 5.2.3数字分析法
      • 5.2.4平方取中法
      • 5.2.5小结
    • 5.3处理冲突的方法——拉链法
      • 5.3.1插入
      • 5.3.2查找
      • 5.3.3删除
      • 5.3.4小结
    • 5.4处理冲突的方法——开放定址法
      • 5.4.1基本原理
      • 5.4.2线性探测法
      • 5.4.3平方探测法
      • 5.4.3双散列法
      • 5.4.4伪随机序列法
      • 5.4.5删除


一、查找的基本概念

考研数据结构:第七章 查找_第1张图片

ps:查找表可以是线性结构、树状结构、图状结构等等

考研数据结构:第七章 查找_第2张图片

评价一个查找算法的优劣:主要看算法的平均查找长度ASL
考研数据结构:第七章 查找_第3张图片

举个例子,我们现在有如下二叉排序树
考研数据结构:第七章 查找_第4张图片
如果你要查的是50,那么从根节点出发只需要对比一次关键字就可以了,所以第一项是1 * 1

考研数据结构:第七章 查找_第5张图片
如果你要查的是第二层的26或者66,你找26总共需要进行两轮对比,找66又需要两轮对比,所以查第二层元素共需要是2 * 2次对比
考研数据结构:第七章 查找_第6张图片
第三层同理,长度为3,共4个数据,所以3 * 4

第四层同理,长度为4,共1个数据,所以4 * 1

然后累和除8,这里8是指一共8个数据,平均到每个数据身上是1/8

我们之前还讨论过二叉排序树查找失败的情况,所以在评价一个查找算法的查找效率时,我们通常会分开考虑查找成功和查找失败两种情况下的平均查找长度

比如同样的二叉排序树,查找成功和查找失败的ASL是不同的:
考研数据结构:第七章 查找_第7张图片

考研数据结构:第七章 查找_第8张图片

ASL的数量级可以直接反映出你的查找算法的时间复杂度
ASL的计算也是考研中的重点,请务必掌握

二、顺序查找和折半查找

2.1顺序查找

顺序查找顾名思义,就是按照顺序一个个找,你可以从前往后,也可以从后往前。这个是最无脑的查找。
考研数据结构:第七章 查找_第9张图片
代码如下:

typedef struct{//查找表的数据结构(顺序表)
    ElemType *elem;//动态数组基址
    int TableLen;//表的长度
}SSTable;

//顺序查找
int Search_Seq(SSTable ST,ElemType key){
    int i;
    for(i=0;i<ST.TableLen&&ST.elem[i]!=key;i++);
    //查找成功,则返回元素下标,查找失败则返回-1
    return i==ST.TableLen?-1:i;
}

还有一种顺序查找就是带哨兵位的,也就是0号位置空出来,放你要找的那个东西
在这里插入图片描述
这种哨兵就是从后往前找,如果找到哨兵的下标,也就是这里的0,就说明查找失败了。
考研数据结构:第七章 查找_第10张图片

我们前面说过,当我们在评价一个查找算法时,我们通常是算它的平均查找长度ASL。而平均查找长度又分为查找成功和查找失败两种情况。

查找成功情况:
如果是带哨兵的实现方式,我们是从最后的位置开始往前扫描。

如果是找最后一个关键字,那你所需要对比关键字的次数就是1。
如果假设我们找任何一个关键字概率都是相同的,那么找最后一个关键字的概率就是1/n
查找长度为1 * (1/n)

如果是找倒数第二个关键字,那要对比关键字次数就是2
查找长度为2 * (1/n)

下面以此类推…

考研数据结构:第七章 查找_第11张图片

如果是查找失败,那就是把所有的n个元素对比完了,还要再和哨兵对比一下。
考研数据结构:第七章 查找_第12张图片
总之,不管是查找成功还是查找失败,时间复杂度都是O(n)

顺序查找的优化(对有序表)
如果要查的表本来就是有序的,那我们查找可以进一步优化

举个例子
你要在如下的顺序表中进行查找21的操作
考研数据结构:第七章 查找_第13张图片
其实不用遍历完所有元素,当你遍历到29发现还没找到21,就已经可以不用找了,因为后面全是比21大的元素。
考研数据结构:第七章 查找_第14张图片

顺序查找的优化(被查概率不相等情况)
如果是被查概率不同的,我们可以把被查找概率更大的元素放在查找相对靠前的位置
考研数据结构:第七章 查找_第15张图片
如果采取这种策略,可以减少查找成功的平均查找长度
但是对于查找失败的情况,我们只能从头扫到尾,来确定是不是查找失败。
所以,如果你实际运用中如果查找成功更多,用这种方法会更好一些。

2.3折半查找

2.3.1算法思想

折半查找又称为二分查找,它只适用于有序的顺序表

先看一个查找成功的例子:
比如现在要在下面的顺序表中找33
我们会先用两个指针low和high分别指向我们目前要搜索的区间范围
考研数据结构:第七章 查找_第16张图片
第一轮要检查的元素是low和high中间的一个元素,我们用指针mid来指向它
mid的计算方式也很简单,就是(low+high)/2
比如这里high=10,low=0,mid=(low+high)/2=5

所以我们第一轮要检查的元素就是29,对比mid指向的元素29和我们要查的33
发现29<33,如果33存在,那么肯定是在我们mid的右边

low=mid+1,
mid=(low+high)/2
考研数据结构:第七章 查找_第17张图片

第二轮
对比mid指向的元素37和要查找的元素33
33<37,如果33存在,那应该在mid左边

high=mid-1
mid=(low+high)/2
考研数据结构:第七章 查找_第18张图片
第三轮
对比mid指向的元素32和要查找的元素33
32<33
那么33肯定在mid右边
low=mid+1
mid=(low+high)/2
考研数据结构:第七章 查找_第19张图片

下一轮检查发现刚好mid指向值等于33,那么查找成功。

再看一个查找失败的例子:

还是刚才那个表,我们现在要查找一个不存在的元素12

考研数据结构:第七章 查找_第20张图片
刚开始还是一样的,low和high指向顺序表开头和结尾元素
mid=(low+high)/2
考研数据结构:第七章 查找_第21张图片
第一轮对比,12<29,那么12只会出现在mid左边
考研数据结构:第七章 查找_第22张图片
考研数据结构:第七章 查找_第23张图片

第二轮对比12<13,如果存在只会在mid左边
考研数据结构:第七章 查找_第24张图片
考研数据结构:第七章 查找_第25张图片

第三轮对比,7<12,如果存在,12在mid右边
考研数据结构:第七章 查找_第26张图片
考研数据结构:第七章 查找_第27张图片
第四轮对比,10<12,如果存在,12在mid右边
考研数据结构:第七章 查找_第28张图片
此时出现了low>high的情况,那么查找失败

2.3.2代码实现

typedef struct{//查找表的数据结构
    ElemType *elem;//动态数组的基址
    int TableLen;//表的长度
}SSTable;

//折半查找-升序(降序需要对下面的判断条件进行相应更改)
int Binary_Search(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;
}

2.3.3查找效率分析

考研数据结构:第七章 查找_第29张图片

考研数据结构:第七章 查找_第30张图片
考研数据结构:第七章 查找_第31张图片
考研数据结构:第七章 查找_第32张图片
考研数据结构:第七章 查找_第33张图片
考研数据结构:第七章 查找_第34张图片
考研数据结构:第七章 查找_第35张图片

2.3.4折半查找判定树的构造

考研中还是比较喜欢喜欢考察折半查找判断树的构造,下面我们来进行介绍

考研数据结构:第七章 查找_第36张图片
考研数据结构:第七章 查找_第37张图片
考研数据结构:第七章 查找_第38张图片
考研数据结构:第七章 查找_第39张图片
考研数据结构:第七章 查找_第40张图片
考研数据结构:第七章 查找_第41张图片
考研数据结构:第七章 查找_第42张图片
考研数据结构:第七章 查找_第43张图片
在这里插入图片描述

考研数据结构:第七章 查找_第44张图片
考研数据结构:第七章 查找_第45张图片

2.3.5折半查找效率

考研数据结构:第七章 查找_第46张图片

2.3.6小结

考研数据结构:第七章 查找_第47张图片

考研数据结构:第七章 查找_第48张图片
ps:只能说大部分情况折半查找比顺序查找效率更高。
因为如果你要查的元素就是顺序表第一个元素,顺序查找是要更快的。

2.4分块查找

分块查找我们主要是介绍算法思想,考试中很少考代码题,大家会用手算模拟即可。

比如下面这个数组,看起来元素都是乱的
考研数据结构:第七章 查找_第49张图片
如果仔细观察,我们可以把这些元素分成一个个小区间
第一个区间:<=10
第二个区间:<=20
第三个区间:<=30

考研数据结构:第七章 查找_第50张图片
如果单独看元素是乱序的,但是如果我们把它们分成一个个小区间后,可以发现各个区间之间其实是有序的,我们可以给这个查找表建立上一级索引。
考研数据结构:第七章 查找_第51张图片
看上图,很好理解。索引表中保存的是每一个分块中最大的关键字,还有分块的存储区间。

比如说我们索引表中有个30,那就说明其中出现最大的关键字是30,然后这个分块的存储区间是6-8,也就是第三个分块下标起始到末尾是6到8
考研数据结构:第七章 查找_第52张图片

所以分块查找的特点:快内无序,快间有序

该索引表数据结构如下:

//索引表
typedef struct{
    ElemType maxValue;//每个表项最大关键字
    int low,high;//分块的区间范围
}Index;

//顺序表存储实际元素
ElemType List[100];

先来看一个查找成功的例子
比如现在查22
考研数据结构:第七章 查找_第53张图片
先查找索引表,从索引表第一个元素依次往后找,
第一个元素10<22,继续往下
第二个元素20<22,继续往下
第三个元素30>=22,如果22存在,一定在此区间
接下来就从分块的起始位置,也就是6号下标往8号下标里找
我们在7号下标找到22
考研数据结构:第七章 查找_第54张图片

再来看一个查找失败的例子
现在要查29,依次遍历索引表,确定如果29存在是在第三个分块内
考研数据结构:第七章 查找_第55张图片
然后我们从第三个分块开始查找,第一个27不满足
考研数据结构:第七章 查找_第56张图片
第二个22不满足
考研数据结构:第七章 查找_第57张图片
第三个30不满足
考研数据结构:第七章 查找_第58张图片
再往后,发现已经超出第三个分块范围了,就说明29不存在
考研数据结构:第七章 查找_第59张图片

刚才在查找索引表时,我们是按顺序查找的方式,也就是从索引表的第一个元素一个个往后找

但是由于索引表中保存的元素是有序的,并且我们索引表是用数组的方式,也就是顺序存储的方式来实现的。所以,针对索引表,我们可以采用折半查找而对每个分块内的元素(乱序),只能顺序查找。
考研数据结构:第七章 查找_第60张图片
ps:分块查找又称为索引顺序查找,先索引表,然后顺序查找分块内元素嘛

三、树形查找

3.1二叉排序树

3.1.1二叉排序树定义

考研数据结构:第七章 查找_第61张图片
二叉树又称二叉查找树BST
二叉排序树的左子树结点值<根节点值<右子树值,如果用中序遍历(左根右),就可以得到一个递增的序列。

二叉排序树的左子树和右子树也是一棵二叉排序树

3.1.2查找操作

考研数据结构:第七章 查找_第62张图片

先看一个查找成功的例子:
假设我们现在要在一棵二叉排序树上找30
考研数据结构:第七章 查找_第63张图片
从19出发,19<30,如果30存在,则在19的右子树
考研数据结构:第七章 查找_第64张图片
50>30,如果30存在,则在50的左子树
考研数据结构:第七章 查找_第65张图片
26<30,如果30存在,则在26的右子树
考研数据结构:第七章 查找_第66张图片
成功找到30

再看一个查找失败的例子:
比如现在要查12

从19出发,如果12存在,则在19左子树
考研数据结构:第七章 查找_第67张图片
13>12,如果12存在,则在13左子树
考研数据结构:第七章 查找_第68张图片
11<12,如果12存在则在11右子树
在这里插入图片描述
但是我们发现,11没有右子树了,说明查找失败了
考研数据结构:第七章 查找_第69张图片
查找代码如下:

//二叉排序树结点
typedef struct BSTNode{
    int key;
    struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;

//在二叉排序树中查找值为key的结点
BSTNode *BST_Search(BSTree T,int key){
    while(T!=NULL&&key!=T->key){//若树空或等于根节点值,则结束循环
        if(key < T->key){
           T=T->lchild;//key小于当前值,如果key存在,在左子树上
		}
		else{
		   T=T->rchild;//key大于当前值,如果key存在,在右子树上
		}
    }
    return T;
}

上面的算法是用一个while循环,来找到目标结点(非递归)

下面的算法则是由于二叉排序树的递归特性,我们用递归来实现(递归)

//在二叉排序树中查找值为key的结点(递归实现)
BSTNode *BSTSearch(BSTree T,int key){
   if(T==NULL){
	 return NULL;//查找失败
   }
   if(key==T->key){
	 return T;//查找成功
   }
   else if(key < T->key){
	 return BSTSearch(T->lchild,key);//在左子树查找
   }
   else{
     return BSTSearch(T->rchild,key);//在右子树查找
   }
}

如果采用非递归
考研数据结构:第七章 查找_第70张图片
显然,如果采用非递归算法,我们只需要常数级的辅助空间。非递归实现空间复杂度为O(1)

但是如果用递归实现的话,一棵树有多高,它就有可能要往下递归几次。而每次递归都需要在函数调用栈中分配一片空间。递归实现空间复杂度为O(h)

3.1.3插入操作

对于插入操作,插入新结点后,我们也需要保证二叉排序树的一个特性,所以我们首先也需要用到查找操作的那种逻辑找到我们应该插入的具体位置。

比如我们现在要插入12这个数据

考研数据结构:第七章 查找_第71张图片
12<19,应该插到12左子树
考研数据结构:第七章 查找_第72张图片
12<13,应该插到13的左子树

考研数据结构:第七章 查找_第73张图片
12>11,应该插到11的右子树
考研数据结构:第七章 查找_第74张图片
考研数据结构:第七章 查找_第75张图片
需要注意,如果我们要插入的这个关键字本来就存在了,那么不该再插入值重复的结点了。另外,每次插入的新结点必定会成为一个叶子结点。

我们这里采用的是递归实现,所以这种算法的最坏空间复杂度为O(h),h为树的高度

显然,插入操作也可以用循环的方式来实现,也就是用非递归的方式实现,非递归算法空间复杂度会更低一些。这个大家自己去练习

3.1.4二叉排序树的构造

其实就是不断插入结点的过程,插入结点你调用刚才介绍的插入函数就行了

//按照str[]中的关键字序列建立二叉排序树
void Create_BST(BSTree &T,int str[],int n){
    T=NULL;//初始时T为空树
    int i=0;
    while(i<n){//依次将每个关键字插入到二叉排序树中
	    BST_Insert(T,str[i]);
	    i++;
	}
}

在这里插入图片描述

同样的数据,如果我们把插入顺序换一下,也会得到不同的二叉排序树
在这里插入图片描述
考研数据结构:第七章 查找_第76张图片
这也是一个常考的内容,就是给你一个二叉排序树序列,让你构造一个二叉排序树。

3.1.5二叉排序树的删除

也就是要删除某个指定值的结点,那第一步肯定是要找到那个要删除的结点。
接下来要分下面几种情况:

1.要删除的结点是叶子结点
考研数据结构:第七章 查找_第77张图片
删除叶子结点后依然可以保证二叉排序树的特性,你直接删就行了

2.要删除的结点只有左子树或者右子树
考研数据结构:第七章 查找_第78张图片

比如13这个结点,它只有左子树,那你把13删了之后,
把原先13的左孩子接到原先13的位置就行了。
考研数据结构:第七章 查找_第79张图片
考研数据结构:第七章 查找_第80张图片
3.要删除的结点有左子树,有右子树
考研数据结构:第七章 查找_第81张图片
比如50这个结点

你要删50,还得保证删除50后的二叉排序树还是一棵二叉排序树

法1:从要删除的结点的右子树中,找出一个最小的结点——右子树中最左边的结点

所以,如果要删50,我们可以把50右子树中的最小结点60换上来,如下图:
考研数据结构:第七章 查找_第82张图片

法2:从要删除的结点的左子树中,找出一个最大的结点——左子树中最右边的结点

所以,要删50,我们可以让50左子树中最大结点30换上来
考研数据结构:第七章 查找_第83张图片

3.1.6查找效率分析

考研数据结构:第七章 查找_第84张图片
考研数据结构:第七章 查找_第85张图片

3.1.7小结

考研数据结构:第七章 查找_第86张图片

3.2平衡二叉树的插入

3.2.1引子

考研数据结构:第七章 查找_第87张图片

我们在上小节说过,如果要让二叉排序树保持平衡,就可以保证这棵二叉排序树的查找效率可以达到O(Log2n)这个数量级

那我们下面要研究的就是,如何在插入一个新结点后保持二叉树的平衡?

如下图,我们插入67这个结点后,这个新插入结点的所有祖先的平衡因子都受到了影响:
考研数据结构:第七章 查找_第88张图片

要让新插入结点后的二叉树恢复平衡的方法:我们从新插入的结点往上找,找到第一个不平衡结点。在上图中就是70这个结点。

我们把以70为根的这棵子树称为最小的不平衡子树
为什么称为最小?因为你如果从70继续往上找,66为根的子树也是不平衡的,但是70为根的树是60为根的树的子树,所以70为根的子树是最小的不平衡子树。

所以,我们只要调整最小的不平衡子树就可以了。调整效果如下:
考研数据结构:第七章 查找_第89张图片

下面我们就是讨论如何调整最小的不平衡子树,让它恢复平衡
考研数据结构:第七章 查找_第90张图片

3.2.2LL

在A结点的左孩子的左子树中插入了一个新结点导致不平衡

我们用一些方形的框来抽象的表示各个部分子树,H表示子树的高度
(当前各个子树是平衡的)
考研数据结构:第七章 查找_第91张图片
现在在B结点的左子树插入了一个新结点,导致了A结点不平衡。其实就是因为B结点的左子树长高了。
考研数据结构:第七章 查找_第92张图片
原先A左子树高度为H+1,现在左子树长高1,那么左子树高度变为H+2

但是A右子树高度还是H,则A平衡因子变成了(H+2)-(H)=2,也就是A不平衡了

可能有同学会问:“你凭啥就认为A右子树高度是H,我认为A右子树高度为H+1,一开始A这个树不也是平衡的吗?”

解释如下:
如果A右子树高度为H+1,那么A原本平衡因子应该是0,你左边是H+1,右边也是H+1嘛。然后你
考研数据结构:第七章 查找_第93张图片
你要是设一开始A右子树高度是H+1。那么你A左子树左孩子高度+1,A左子树高度其实就变成了H+2。A的平衡因子是(H+2)-(H+1)=1,其实这还是平衡的。

但是我们讨论的是加了结点后不平衡的情况,所以这种情况不考虑。

有同学又要问了:“A右子树高度是H+1不可以,A右子树高度为H-1可以不”

这其实也是不可以的,因为你左子树高度是H+1,你右子树高度是H-1,那么一开始就是不平衡的。

我们要求的是一开始平衡,加了个结点之后不平衡。所以这种情况也不可以。
考研数据结构:第七章 查找_第94张图片

有同学又要问了:“那我一开始设B的右子树高度为H-1可以不?”
考研数据结构:第七章 查找_第95张图片

如果是这样的话,那么B一开始的平衡因子是(H)-(H-1)=1,

你在B左子树上高度+1,那么B左子树高度就变成了H+1,B右子树高度还是H-1,B的平衡因子就变成了(H+1)-(H-1)=2,那么B就成了最小不平衡子树了。

我们要探讨的是A是最小不平衡子树,所以B的右子树高度也只能是H

下面回到主线剧情,如何调整这个最小的不平衡子树
那调整后我们要得到的树需要满足:1.恢复平衡,2仍然是一棵二叉排序树

考研数据结构:第七章 查找_第96张图片
显然,B左子树的值 考研数据结构:第七章 查找_第97张图片
那我们具体做法,就是让B结点右上旋转,代替A成为根结点,
然后A结点要成为B的右子树的根节点,也就是A要成为B的右孩子
考研数据结构:第七章 查找_第98张图片
然后BL 考研数据结构:第七章 查找_第99张图片
BR原先在B右边的,但是你B右边已经挂了A了,所以BL不能挂B右边了
BL原先是在A左边,也就是BL 考研数据结构:第七章 查找_第100张图片
AR>A,然后以前的AR继续挂A右边就行了
考研数据结构:第七章 查找_第101张图片

这样我们就可以让加入结点后不平衡的的子树继续平衡,且保证二叉排序树的特性。

LL小结:在A左孩子的左子树插入一个新结点导致不平衡,我们采用A左孩子右旋一次

3.2.3RR

如果在A的右孩子的右子树加入一个结点导致不平衡
(我们这里也是假设了在进行加结点操作后,A是最小的不平衡子树,所以AL=BL=BR=H)
考研数据结构:第七章 查找_第102张图片
对于这种情况,我们采用的是左旋一次
考研数据结构:第七章 查找_第103张图片
也就是让B左上旋转,让A左下旋转
B变成根,A变成B左孩子
考研数据结构:第七章 查找_第104张图片
然后就是把剩余的AL,BL,BR挂上去
考研数据结构:第七章 查找_第105张图片
BR是大于B的,我们放B右边
考研数据结构:第七章 查找_第106张图片
AL是小于A的,我们放A左边即可
考研数据结构:第七章 查找_第107张图片
还剩一个BL,BL小于B但是大于A,我们就放A右边就行
考研数据结构:第七章 查找_第108张图片
RR小结:在A右孩子的右子树插入一个新结点导致不平衡,我们采用A右孩子左旋一次

LL和RR的代码思路
考研数据结构:第七章 查找_第109张图片

3.2.4LR

LR就是在A结点的左孩子的右子树插入一个结点导致A结点变成最小不平衡子树的根节点
考研数据结构:第七章 查找_第110张图片
为了方便讨论,我们需要把BR进行展开。我们假设B的右孩子是叫C的结点,C的左子树称为CL,右子树称为CR。由于B右子树整个高度是H,所以CL和CR高度我们可以假设H-1

考研数据结构:第七章 查找_第111张图片
那么现在C这个子树长高了,我们可以假设把新结点插入在CR,导致CR高度为H
你假设插在CL也是一样的,处理方法都一样
考研数据结构:第七章 查找_第112张图片

对于这种情况,我们要先让C左旋顶替B的位置;接下来再让C右旋顶替A的位置

如何把结点左旋/右旋,方法和前面讲LL和RR是一样的

首先是让C左旋替代B,B 考研数据结构:第七章 查找_第113张图片
然后由于BL 在这里插入图片描述
CR>C,就放C右边
考研数据结构:第七章 查找_第114张图片
B 考研数据结构:第七章 查找_第115张图片
所以,把C左旋后如下图,但是此时A仍然不平衡
考研数据结构:第七章 查找_第116张图片
接下来就是把C右旋替换A的位置
就是把C右孩子变成A左孩子,
A变成C右孩子
考研数据结构:第七章 查找_第117张图片
考研数据结构:第七章 查找_第118张图片
LR小结:在A左孩子的右子树插入一个新结点导致不平衡,我们采用A左孩子的右孩子左旋一次,再右旋一次

3.2.5RL

考研数据结构:第七章 查找_第119张图片
最后RL思路同理LR
RL小结:在A右孩子的左子树插入一个新结点导致不平衡,我们采用A右孩子的左孩子右旋一次,再左旋一次

3.2.5小结

考研数据结构:第七章 查找_第120张图片
考研数据结构:第七章 查找_第121张图片
考研数据结构:第七章 查找_第122张图片

3.3平衡二叉树的删除

平衡二叉树的第一个特性就是具有排序树的特性,也就是左<中<右

第二个特性就是:在二叉排序的基础上,要保证每个结点都是平衡的,也就是左右子树高度之差不超过1

考研数据结构:第七章 查找_第123张图片
在上一节介绍二叉排序树插入一个结点导致不平衡的四种情况如何解决。
该小节则是介绍二叉排序树删除一个结点导致不平衡的四种情况如何解决。
这四种情况和上节对应,依然是LL,RR,LR,RL

先来看一下删除的规则,具体怎么用我们下面会举例给大家详细讲解:
考研数据结构:第七章 查找_第124张图片
对于5.不平衡向上传导,如果出现这种情况那么题目给的树就一定非常复杂,并且会出现多种解决方案,解题过于繁琐,所以考试中百分之99是不会考的,我们这里只介绍一些较为简单的例子

例1:现在删除下面树的9
考研数据结构:第七章 查找_第125张图片
单纯删除结点操作和二叉排序树是一样的,由于9这个阶段是叶子结点,我们直接删除就可以了。

接下来要判断,删除这个元素之后,它上面的祖先结点有没有出现不平衡的现象。
所以下面要做的就是“一路向北”,一路向上看是否有不平衡子树出现,如果没有不平衡子树,那么就可以结束算法了。

删除9之后,对于13,平衡因子是-1,仍然平衡
考研数据结构:第七章 查找_第126张图片
对于25,平衡因子是1,仍然平衡

考研数据结构:第七章 查找_第127张图片
对于50,平衡因子是-1,仍然平衡

考研数据结构:第七章 查找_第128张图片
这就说明,刚才的删除操作并没有导致它的任何一个祖先出现不平衡现象,到此为止,删除操作结束。

例2:现在删除下面树的55

55这个结点是二叉排序树的叶子,我们可以直接删掉

接下来就是要看55的祖先有没有出现不平衡的现象

那么我们可以发现,55删了之后,75是出现了不平衡现象,平衡因子-2

考研数据结构:第七章 查找_第129张图片
由于产生了不平衡现象,我们就要进入第三步,找到最小不平衡子树下,个头最高的儿子、孙子

75有两个儿子,60高度为1,80高度为3,所以最高的儿子是80

再从最高的儿子往下找最高的孙子,77高度为1,90高度为2,所以最高的孙子是90

考研数据结构:第七章 查找_第130张图片
接下来进入第四步,根据孙子位置,调整平衡

显然,孙子是在爷爷RR的位置,也就是90在75RR的位置

在这里插入图片描述
当孙子在RR的位置,我们就是要让儿子左旋一次
左旋右旋在上节平衡二叉树讲过了,忘记了自己去回顾一下

下面就是让80左旋到75的位置
考研数据结构:第七章 查找_第131张图片

接下来第五步需要检查不平衡是否向上传导
考试基本不可能出现向上传导的情况,我们这里是为了介绍,防止大家有疑问。

什么叫检查不平衡向上传导?在调整平衡前,这个不平衡的子树高度是4
考研数据结构:第七章 查找_第132张图片
调整后原先那棵不平衡子树高度从4变成了3
考研数据结构:第七章 查找_第133张图片
而这棵子树高度出现变化,就有可能导致它上面的树也产生不平衡。这就是所谓的不平衡向上传导。
显然这里是没有出现不平衡向上传导的。

例3:现在删除下面树的32
考研数据结构:第七章 查找_第134张图片

首先根据二叉排序树的删除方法进行操作,32是叶子结点我们可以直接删除

删除32之后,可以发现44是出现了不平衡现象
考研数据结构:第七章 查找_第135张图片
找到了最小不平衡子树,接下来就是找最高的儿子和孙子
可以确定78是最高的儿子,50是最高的孙子
考研数据结构:第七章 查找_第136张图片
然后50是44的RL,那我们就让50右旋再左旋
考研数据结构:第七章 查找_第137张图片
考研数据结构:第七章 查找_第138张图片
考研数据结构:第七章 查找_第139张图片

发现没有出现不平衡向上传导,删除操作结束

(基本不可能考不平衡向上传导,如果你感兴趣可以自己去研究,会发现如果出现不平衡向上传导,那个树将非常非常复杂,并且答案有很多种。我这里就介绍考试会考的几个例子)

当然会有人好奇,出现不平衡向上传导的例子,就比如下面的树,要删除32
有时间,感兴趣自己研究
考研数据结构:第七章 查找_第140张图片

四、B树和B+树

4.1B树

考试中对B树的考察一般侧重考性质、插入、删除、查找操作,代码基本不会出现。

所以我们下面将重点介绍B树的性质还有手算方法。

先来回顾一下我们前面学的二叉查找树,也叫二叉排序树的特点。
考研数据结构:第七章 查找_第141张图片
如果我们要查的目标比当前结点值更小,我们会往左子树这边查找,如果比当前结点更大,就往右子树查找。

所以二叉查找树无非就是用一个关键字,把一个我们有可能搜索到的目标关键字范围分割成两份。

比如刚开始我们要搜索的范围是(-∞,∞),
那么根结点29就把(-∞,∞)划分为了(-∞,29)和(29,∞)
考研数据结构:第七章 查找_第142张图片
(-∞,29)就去左子树找,(29,∞)就去右子树找

问题来了:能否把二叉查找树拓展成m叉查找树?

举个例子:现在有图示的5叉查找树
考研数据结构:第七章 查找_第143张图片
其实我们虽然把它拓展成了5叉查找树,但是原理和2叉查找树一样的。

第一个根节点是22
如果当前要查找的元素比22小,就往左走
如果当前要查找的元素比22大,就往右走

假设我们要找的那个值比22更大,接下来就应该在22的右子树去找,即(29,∞)里面找
考研数据结构:第七章 查找_第144张图片
往下找发现当前结点又出现了两个关键字:36和45,
就相当于我们在这个区间内又插入了两个隔板。又把(29,∞)划分为3个区间
(29,36)、(36,45)、(45,∞)

综上,5叉排序树里面这些关键字还有关键字之间的指针,信息其实和二叉查找树是非常类似的。数据结构定义如下:
考研数据结构:第七章 查找_第145张图片
每个结点最多可以包含4个关键字,最多5个孩子,也就是5棵子树。然后用一个num变量记录当前结点一共多少个关键字。

考研数据结构:第七章 查找_第146张图片
像上图这种5叉查找树,在一个结点中,我们最少可以允许有1个关键字,2个分叉;最多允许4个关键字,5个分叉
一个关键字会把一个查找区间分割成两个部分

图中给出的紫色结点,其是就查找失败的情况,比如下面圈出的失败结点,就是对应的(15,22)这个范围
考研数据结构:第七章 查找_第147张图片

下面我们进行5叉查找树的查找成功的举例:

假设我们现在要查找9
根节点中第一个被保存的关键字是22
考研数据结构:第七章 查找_第148张图片
显然9要比22更小,所以下面要选22左边的路
考研数据结构:第七章 查找_第149张图片

到新的结点,我们会顺序扫描一个结点中的各个关键字
第一个关键字5要比9更小,往右检查下一个关键字
考研数据结构:第七章 查找_第150张图片
第二个关键字11又要比9大,所以9在11左边的路线上
考研数据结构:第七章 查找_第151张图片
到了新的结点,我们还是一样的从左往右依次扫描,然后扫描3次找到9
考研数据结构:第七章 查找_第152张图片
ps:刚才在查找每个结点中的数据时,是用的顺序查找。
但是由于每个结点中数据是有序排放的,所以在查找每个阶段数据时,你也可以用折半查找

下面我们进行5叉查找树的查找失败的举例:

比如我们要找41,现在从根节点出发
考研数据结构:第七章 查找_第153张图片

22要比41更小,我们指针右移,发现当前指针所指位置已经超出这个结点关键字个数了,所以如果41存在,我们应该往回找这个指针,然后指针往下走

考研数据结构:第七章 查找_第154张图片
来到下一层结点,第一个元素36<41,指针往右

考研数据结构:第七章 查找_第155张图片
第二个元素45>41,如果41存在,则在45左边的指针所指结点中
考研数据结构:第七章 查找_第156张图片
到新的结点,第一个元素40<41,指针右移
考研数据结构:第七章 查找_第157张图片
第二个元素42要比41更大,如果41存在,则在42左边指针指向的结点
考研数据结构:第七章 查找_第158张图片
但是你发现,如果沿着42左边指针往下走,居然是一个失败结点,那么就说明查找失败
考研数据结构:第七章 查找_第159张图片
ps:虽说是失败结点,其实就是一个NULL

刚才我们提出的5叉排序树,我们只是规定了每一个结点中最多有5个分叉。

那么如果我们每个结点只保留一个关键字,也就是每个结点只有2个分叉的情况。

考研数据结构:第七章 查找_第160张图片
如上图所示,这个5叉查找树就退化成了2叉查找树。
在这种情况下,由于每个结点的关键字数量变少,
所以,如果关键字总数相同,这个树会变成“细狗”,也就是又细又高的树。
而查找树越高,我们在进行查找时就需要更多层的结点,效率也会更低

那么如何保证5叉查找树的效率?
我们可以规定:在m叉查找树中,除了根节点,其他任何一个结点都至少有⌈m/2⌉个分叉

比如5叉查找树,m/2向上取整应该是⌈5/2⌉=3
所以,对于5叉查找树,我们可以规定每个阶段至少3个分叉,也就是2个关键字
考研数据结构:第七章 查找_第161张图片

这样就可以保证每个阶段中关键字个数不会太少,就可以保证这棵树不会变的很高,层数不会太多,相应的查找效率也可以得到保证

这里可能大家会有疑惑,为什么除了根结点之外呢?我们让根结点也有3个分叉不行?

想法不错,但是实际是做不到的。
比如现在5叉查找树,刚开始只有1个元素,1个关键字。
考研数据结构:第七章 查找_第162张图片
那么这种情况下,就只有一个根节点(一个关键字)
就一个关键字,你那里来能放2个关键字呢?

再来看另一个问题:下面这棵二叉树,你觉得它优秀不?
考研数据结构:第七章 查找_第163张图片
上面这个5叉查找树已经满足了我们刚才提出的特性:根节点除外,每个结点有3个或3个以上的分叉。

但是这棵树有一个问题:它不平衡
考研数据结构:第七章 查找_第164张图片
这种不平衡的特性,显然也会导致我们的5叉查找树长的很高。

和刚才一样,如果树长得太高,就会导致我们查找的过程中要查很多层的结点,从而导致效率降低。

我们可以规定:对于每个结点,它的左右子树高度都相同

如果能做到各个结点的子树都没有高度差,就可以保证我们的多叉查找树是平衡的,也就保证了它不会有太多层,从而保证查找效率。

如果能满足上面提到的两个策略,那么就是B树
考研数据结构:第七章 查找_第165张图片
我们通常会把失败结点称为叶子结点,而最下面一层含有实际数据的结点,我们称为终端结点。

注:由于平衡要求,我们是让树中所有结点子树高度都相同。那么就会导致,失败结点(叶子结点)一定会出现在最下层(同一层)

B树中所有结点孩子个数的最大值称为B数的阶,也就是这个B树你最多看到多少个分叉
如下图,最多是有5个分叉。
所以,所谓的5阶B树其实就是一个5叉查找树
考研数据结构:第七章 查找_第166张图片

然后,根节点如果不是终端结点,则至少两个子树。
这个特性就是保证每个结点都平衡,所以对于根节点来说,如果这个根节点不是终端结点,那么它必须是要有两棵子树,不然它自己不平衡啊。

还有一个特性,就是所有非叶子结点的结构如下:其实就是我们刚才给出的那个数据结构的定义。

如下图:
p0,p1,p2…pn指的是结点当中的一个个指针,总共有n+1个指针
而k1,k2,…kn指的是结点的关键字,总共是n个关键字
n记录实际关键字是几个

考研数据结构:第七章 查找_第167张图片
pi-1的所指结点的所有关键字 考研数据结构:第七章 查找_第168张图片

下面是对B树特性的总结:
考研数据结构:第七章 查找_第169张图片

下面我们根据B树的特性来看一下B树有多高
注:大多数学校在计算B树高度时,都是不包括最下面的失败结点的,因为失败结点本质就是一个NULL

先来看一下最小高度是多少

如果要让树高最小,那么在关键字数量不变的情况下,我们应该尽可能让每一个结点都填满关键字。

那么对于m阶B树,每个结点最多有m-1个关键字,有m个分叉。
最上层的根节点只有一个(注意,我这里说的是结点数量是1,不是关键字数量是1),根结点有m个分叉。
考研数据结构:第七章 查找_第170张图片

由于第一层下来m个分叉,所以第二层有m个结点
考研数据结构:第七章 查找_第171张图片
第三层,又会由第二层m个结点下来m2个结点(每个结点m个分叉)
考研数据结构:第七章 查找_第172张图片
如果有h层,那么就应该是mh-1
考研数据结构:第七章 查找_第173张图片
也就是有h层,有(1+m+…+mh-1)个结点

而每个结点有m-1个关键字,所以共(m-1)(1+m+…+mh-1)个关键字

那么n个关键字的B树高度为h,那么n的范围肯定是小于等于我们刚才给出的数值

考研数据结构:第七章 查找_第174张图片
下面来看一下最大高度是多少
要让一棵树长的尽可能的高,就要让这颗数的分叉尽可能的少。
对于根节点最少2个分叉,对于其他结点则是最少⌈m/2⌉个分叉

对于m叉树来说:
在分叉最少的情况下,第一层只有1个根节点,2个分叉
第二层有2个结点,每个结点⌈m/2⌉个分叉,也就是2[m/2]个分叉
第三层有2[m/2]个结点,每个结点⌈m/2⌉个分叉,也就是2[m/2]2个分叉

第h层2[m/2]h-2个结点,每个结点⌈m/2⌉个分叉,也就是2[m/2]h-1个分叉

再往下推,叶子结点(失败结点)也就是h+1层,至少有2[m/2]h-1个结点

补充重要特性:对于n个关键字的B树,必然有n+1个叶子结点
解释:B树中n个关键字,相当于是在(-∞,+∞)这个区间内插入n个关键字,这n个关键字会把整个数值区间切分为n+1个部分。

这n+1个部分就对应了n+1种失败的情况。

所以,n个关键字的B树必有n+1个叶子结点,而m阶B树的叶子结点下限应该是2[m/2]h-1
n+1>=2[m/2]h-1
考研数据结构:第七章 查找_第175张图片
考研数据结构:第七章 查找_第176张图片
考研数据结构:第七章 查找_第177张图片
考研数据结构:第七章 查找_第178张图片
小贴士:关于为什么B树叫B树,B树这个数据结构的发明者也没有说明,个人觉得是balance,平衡的意思。

4.2B树的插入删除

4.2.1插入

下面我们从零开始建立一棵B树,
并且规定我们这个B树是5阶B树,也就是关键字个数最少不能少于2个,最多不能多于4个
考研数据结构:第七章 查找_第179张图片

我们插入第一个元素25,直接放入根节点中
考研数据结构:第七章 查找_第180张图片
插38,38>25,放25后面即可
考研数据结构:第七章 查找_第181张图片
插49,49>38,放38后面
考研数据结构:第七章 查找_第182张图片
插60,60>49,放49后面
考研数据结构:第七章 查找_第183张图片
到目前为止,根节点可以存放的结点个数达到上限
接下来如果继续往里面插入一个元素,比如80,此时就导致根节点中关键字的个数超过4
考研数据结构:第七章 查找_第184张图片
对于这种情况,需要把当前这个结点分裂成两个结点。

考研数据结构:第七章 查找_第185张图片
我们会以中间位置,也就是⌈m/2⌉ 位置的关键字,把原有的结点拆分为左右两部分,然后⌈m/2⌉ 位置的关键字提到父节点中。

该例子中也就是第三个元素49提到父节点位置
考研数据结构:第七章 查找_第186张图片
考研数据结构:第七章 查找_第187张图片
在这里插入图片描述

接下来插入90这个元素
注意:我们每次新插入的元素一定是插入到最底层的终端结点。
可以用上小节介绍的查找规则来确定我们应该插入到什么位置。

从根节点出发,90大于49,而49右边已经没有元素了,所以顺着49右边指针往下找
考研数据结构:第七章 查找_第188张图片

接下来检查新结点关键字,60<90,80<90,所以90应该插到80右边
考研数据结构:第七章 查找_第189张图片
考研数据结构:第七章 查找_第190张图片
所以,我们其实是通过一次查找来确定我们新元素应该插到什么位置。
再次强调:每次插入的新元素一定是插到最底层的终端结点中!

接下来再插99
考研数据结构:第七章 查找_第191张图片
接下来插88,我们用肉眼扫一下可以发现,88应该插在80和90之间
考研数据结构:第七章 查找_第192张图片
到这里又会导致当前这个结点关键字个数又超出上限了
考研数据结构:第七章 查找_第193张图片
处理方法和刚才一样,用⌈m/2⌉ 位置的关键字,这里是⌈5/2⌉ =3位置的关键字提到父节点,
然后把3位置的关键字左右两边元素分别放到其左右子树上。具体做法如下:

考研数据结构:第七章 查找_第194张图片
ps:这么做可以保证二叉树的特性,也就是一个阶段的左子树结点值<当前结点值<右子树结点值

再接下来插入83,插入87,没啥好说的,直接插终端结点
考研数据结构:第七章 查找_第195张图片
再接下来插入70,肉眼扫一下应该插80和83中间,那么又会溢出,该结点又需要分裂

考研数据结构:第七章 查找_第196张图片
把中间元素80提到父节点,两边元素放左右子树
而这里中间元素80>49,80<88,所以我们把80放49和88之间
考研数据结构:第七章 查找_第197张图片
如果一个关键字,它因为需要分裂而提到父节点中,我们需要把这个关键字放到它所属结点这条指针的右边

考研数据结构:第七章 查找_第198张图片
这样做可以保证我们的树仍然保持B树的特性
考研数据结构:第七章 查找_第199张图片
接下来再插入4个新元素92,93,94,99
那么又有结点需要分裂了
考研数据结构:第七章 查找_第200张图片
还是一样的办法,把中间的93提到父节点中88的右边
(把93放到指向当前结点这个指针对应点的右边,也就是88的右边)
考研数据结构:第七章 查找_第201张图片
然后93左右成为左子树和右子树
考研数据结构:第七章 查找_第202张图片
如果再往树里面插入73,74,75
那么结点又需要分裂了,我们需要把73提到指向该结点指针右边的位置
考研数据结构:第七章 查找_第203张图片
但是这里我们会发现,73提上去之后,根节点又不够用了

考研数据结构:第七章 查找_第204张图片

这种情况,我们就需要把这个父节点继续向上分裂,由于根节点上面已经没有父节点了,所以我们创一个新结点
考研数据结构:第七章 查找_第205张图片
这里根节点是可以只有一个关键字的,其他结点关键字个数n必须满足2<=n<=4

下面是B树插入的总结
考研数据结构:第七章 查找_第206张图片

4.2.2删除

比如我们现在要删B树的60
考研数据结构:第七章 查找_第207张图片
由于60是在终端结点中,我们直接删了,然后把60所在结点的其他关键字左移一下
需要注意的是,你删了一个结点之后,要保证这个结点关键字个数还是>=⌈m/2⌉-1

这里删完60还剩3个关键字,没有低于一个结点最少关键字个数,是合法的
考研数据结构:第七章 查找_第208张图片

再比如,我们现在要删80,80删了之后根节点就空了。
考研数据结构:第七章 查找_第209张图片

那我们可以找出80这个元素的直接前驱或者直接后继来顶替80的位置

如果用直接前驱来顶替
在这里插入图片描述
80的直接前驱就是80左子树中最大的,你就找左子树中最右边的元素,也就是77
考研数据结构:第七章 查找_第210张图片
上面的操作,就相当于我们把非终端结点的删除转换成了终端结点的删除。

如果用直接前驱来顶替
现在要删根节点的77,那么我们可以找77右子树中最小的来顶替77,也就是找右子树中最左边的元素82
考研数据结构:第七章 查找_第211张图片
考研数据结构:第七章 查找_第212张图片

前面我们探讨的情况都很简单,也就是我们删除一个终端结点关键字时,这个终端结点关键字数量还没有低于它的下限,那么我们下面来介绍一些低于下限的情况:

比如删38
考研数据结构:第七章 查找_第213张图片
你删了38之后,下图所指结点的关键字个数小于B树规定的最小值2了
考研数据结构:第七章 查找_第214张图片
那么我们需要分为多种情况来考虑

情况1:被删结点它的兄弟可以借
什么意思?当前结点关键字数量不够了,但是它的右兄弟关键字数量还够。

可能会有同学会想直接把右兄弟的一个关键字放到当前结点,但是这样是有问题的
考研数据结构:第七章 查找_第215张图片
你直接把70放过去,会导致70>49,而我们B树是要求左子树值<根值<右子树值
这样就矛盾了。

解决办法是先将49拉下来,然后70去顶替49的位置
考研数据结构:第七章 查找_第216张图片
考研数据结构:第七章 查找_第217张图片
上面的解决方案可以做一个小总结:如果右兄弟的手头宽裕,我们可以找到当前结点的后继、后继的后继来填补空缺

举个例子,当前这个结点,也就是25这个元素的后继是49
考研数据结构:第七章 查找_第218张图片
而25后继的后继,也就是49的后继应该是70,即49右边指针所连结点的第一个元素
考研数据结构:第七章 查找_第219张图片
所以,这就是用当前结点后继、后继的后继去顶替它们所需要顶替的位置

上面例子是借右兄弟的例子,下面我们介绍借左兄弟的例子

比如现在要删90这个关键字
考研数据结构:第七章 查找_第220张图片
而删完90之后,发现右兄弟已经不宽裕了,但是左兄弟还宽裕
考研数据结构:第七章 查找_第221张图片
具体做法:如果左兄弟的手头宽裕,我们可以找到当前结点的前驱、前驱的前驱来填补空缺

92的直接前驱是88,也就是顺着指向当前结点的指针左边的元素
考研数据结构:第七章 查找_第222张图片

而92前驱的前驱是87,也就是88的前驱,应该是88左子树最右边的元素

考研数据结构:第七章 查找_第223张图片

我们用88和87填补所需位置

考研数据结构:第七章 查找_第224张图片
考研数据结构:第七章 查找_第225张图片

上面都是介绍了左兄弟或者右兄弟宽裕的情况,下面我们介绍左兄弟和右兄弟都不宽裕的情况
情况2:被删结点它的兄弟不可以借

比如现在要删49
考研数据结构:第七章 查找_第226张图片
删完之后当前结点关键字已经不够了,但是右兄弟也不宽裕了

考研数据结构:第七章 查找_第227张图片
我们的策略是让当前结点和它的右兄弟合并
考研数据结构:第七章 查找_第228张图片
合并时,还需要把这两个结点中间的关键字也一起合并,这样才能满足B树特性

合并过程如下动图:

但是这里又出现问题了,由于我们从父节点拿走一个元素,父节点的关键字又不够了
所以我们接下来继续要合并
考研数据结构:第七章 查找_第229张图片
合并过程如下动图

而此时,根结点已经没有任何关键字了,删掉它
考研数据结构:第七章 查找_第230张图片
考研数据结构:第七章 查找_第231张图片

考研数据结构:第七章 查找_第232张图片

考试不会考5阶以上的B树插入删除,上面介绍的已经是比较难的例子,掌握好应对考试足矣

4.2.3小结

考研数据结构:第七章 查找_第233张图片

4.3B+树

对于B+树,考研不会考很深,基本都是概念的东西
考研数据结构:第七章 查找_第234张图片
细心的同学可能会发现,B+树和我们的分块查找比较类似
考研数据结构:第七章 查找_第235张图片
在分块查中,我们会把这些元素分成一个个块,在索引表中,我们会保存每个块中最大的关键字。

再回头看B+树,其实也是保存了每个块的最大关键字
考研数据结构:第七章 查找_第236张图片
然后每层索引往上,都是一样的规律

4.3.1定义

下面是B+树的定义
考研数据结构:第七章 查找_第237张图片
1.每个分支结点最多m棵子树
比如这里的4阶B+树,它最多就是有4个子树

2.非叶根节点至少有两棵子树,其他分支结点至少有⌈m/2⌉棵子树
什么叫非叶根节点?
首先要明确的是,我们在B+树中,把最下面一层称为叶子结点
考研数据结构:第七章 查找_第238张图片
下面三个例子中,第一个例子,只有一个根节点,它同时也是叶子结点。
而第三个例子,根节点并不是最下面一层的结点,也就是非叶子结点,它至少有2棵子树才能算是B+树

而第二个例子,根节点只有左子树没有右子树,不符合B+树的定义,它不是B+树
考研数据结构:第七章 查找_第239张图片
因为我们希望B+树高度尽可能低,所以我们会追求绝对平衡——所有子树高度相同
所以,我们会要求非叶根节点至少两棵子树

另外,我们还要求其他每个分支结点至少有⌈m/2⌉棵子树,这里m=4,也就是至少要有2棵子树

至于为啥这样,这个原因和B树是一样的,我们要保证每个结点关键字个数(结点个数)不要过少,因为如果每个结点子树数量太少,就会导致这棵B+树长得很高,这样查找效率就低了。

3.结点子树个数和关键字个数相等(重要)
这一点比较容易在选择题中进行考察,因为这也是B+树和B树比较大的一点区别

比如下面这个结点,对应3个关键字,每个关键字也对应1个分支
考研数据结构:第七章 查找_第240张图片
所以B+树当中一个结点里面,它含有多少个关键字,对应的它就有多少个分支,有多少个子树。

ps:B树中,如果一个结点有2个关键字,那它是对应3个分支

4.所有叶结点包含全部关键字及指向对应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。

在B+树中,我们可以通过指针p,从第一个叶子结点开始,一个个往后遍历,把所有叶子结点都遍历完。
考研数据结构:第七章 查找_第241张图片
也就是说,B+树是支持顺序查找的

5.所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子节点的指针。
这个和分块查找原理很类似,这里不再赘述

4.3.2B+树的查找

先看一个查找成功的例子:
比如现在要查编号9的学生对应信息

从根节点出发,9<15,15表示的是它所指向的一整个分块中最大的关键字是15

所以如果9存在,一定是在15的分块中
考研数据结构:第七章 查找_第242张图片
考研数据结构:第七章 查找_第243张图片
3<9,指针后移
在这里插入图片描述
第二个正好是9,那么9是在当前关键字所指结点中
ps:如果在B+树查找中,我们在分支结点中找到我们所需关键字,其实整个查找并没有结束。
你得找到最下面一层叶子结点才能知道相关记录信息。
考研数据结构:第七章 查找_第244张图片
指针继续往下移,到叶子结点了

考研数据结构:第七章 查找_第245张图片
从左往右依次检查,就可以在这个叶子结点中找到9这个关键字
考研数据结构:第七章 查找_第246张图片
那么通过9,就可以找到9号学生对应的相关信息了

再看一个查找失败的例子:
比如我们要查找的关键字是7,

那么从根结点出发,7<15,应该往15所指结点找
考研数据结构:第七章 查找_第247张图片
3<7,7肯定不在3所指结点内,指针右移
考研数据结构:第七章 查找_第248张图片
7<9,如果7存在,肯定在9所指结点内
考研数据结构:第七章 查找_第249张图片
检查下一个结点,6<7,指针右移
考研数据结构:第七章 查找_第250张图片
下一个元素是8,8>7
考研数据结构:第七章 查找_第251张图片
注意!这里已经是最下面一层的叶子结点了,到这里没找到7就说明7不存在了。

B+树中无论是查找成功还是查找失败,都要找到最下面一层的叶子结点。

除了根节点往下找,还可以通过叶子结点那里保存的指针p进行顺序查找
考研数据结构:第七章 查找_第252张图片

4.3.3B+树和B树的对比

考研数据结构:第七章 查找_第253张图片
考研数据结构:第七章 查找_第254张图片

考研数据结构:第七章 查找_第255张图片
考研数据结构:第七章 查找_第256张图片
考研数据结构:第七章 查找_第257张图片
考研数据结构:第七章 查找_第258张图片
考研数据结构:第七章 查找_第259张图片
考研数据结构:第七章 查找_第260张图片
考研数据结构:第七章 查找_第261张图片

4.3.4小结

考研数据结构:第七章 查找_第262张图片

五、散列表

5.1散列表的基本概念

5.1.1散列表、散列函数

考研数据结构:第七章 查找_第263张图片
我们根据散列函数可以计算出一个数据元素在散列表中的存储地址。
比如这里散列函数是H(key)=key%13
那么如果你给我19,H(19)=19%13=6,我就可以立马知道如果19存在,则存放在6号位置

再比如你给我16,H(16)=16%13=3,我就可以立马知道如果16存在,则存放在3号位置
考研数据结构:第七章 查找_第264张图片
可以看到,在理想情况下,我们散列表查找一个元素时间复杂度只需要O(1)

5.1.2冲突、同义词

考研数据结构:第七章 查找_第265张图片
考研数据结构:第七章 查找_第266张图片
我们并不希望冲突频繁发生,冲突越少,散列表性能越高

5.1.3关于散列表,有待解决的问题

考研数据结构:第七章 查找_第267张图片
考研数据结构:第七章 查找_第268张图片
考研数据结构:第七章 查找_第269张图片
考研数据结构:第七章 查找_第270张图片

5.2散列函数的构造

散列函数的作用是把一个关键字映射到与之对应的存储地址上。

当我们设计一个散列函数时,应尽可能减少不同关键字之间发生冲突的情况。

该小节将介绍4种散列函数构造方式:
考研数据结构:第七章 查找_第271张图片
考试中最常考、现实中最常用的都是除留余数法
考研数据结构:第七章 查找_第272张图片

5.2.1除留余数法

考研数据结构:第七章 查找_第273张图片
考研数据结构:第七章 查找_第274张图片

5.2.2直接定址法

这种方法适用于关键字分布基本连续
考研数据结构:第七章 查找_第275张图片

5.2.3数字分析法

考研数据结构:第七章 查找_第276张图片

5.2.4平方取中法

考研数据结构:第七章 查找_第277张图片

5.2.5小结

考研数据结构:第七章 查找_第278张图片

5.3处理冲突的方法——拉链法

散列表通常不考代码,着重掌握手算分析方法

散列表中冲突这种现象是无法避免的,可能会有多个元素映射到同一个散列地址。
这些元素在插入时就会发生冲突
考研数据结构:第七章 查找_第279张图片

我们第一种解决冲突的方式就是拉链法

我们把这些同义词用一个链表连起来
考研数据结构:第七章 查找_第280张图片

5.3.1插入

现有如下的空散列表,长度13,散列函数H(key)=key%13,我们用拉链法解决冲突
考研数据结构:第七章 查找_第281张图片
考研数据结构:第七章 查找_第282张图片

考研数据结构:第七章 查找_第283张图片

考研数据结构:第七章 查找_第284张图片
考研数据结构:第七章 查找_第285张图片

5.3.2查找

考研数据结构:第七章 查找_第286张图片
考研数据结构:第七章 查找_第287张图片
考研数据结构:第七章 查找_第288张图片
考研数据结构:第七章 查找_第289张图片

5.3.3删除

考研数据结构:第七章 查找_第290张图片
考研数据结构:第七章 查找_第291张图片
考研数据结构:第七章 查找_第292张图片
考研数据结构:第七章 查找_第293张图片
考研数据结构:第七章 查找_第294张图片

5.3.4小结

考研数据结构:第七章 查找_第295张图片
考研数据结构:第七章 查找_第296张图片

5.4处理冲突的方法——开放定址法

5.4.1基本原理

散列表中冲突是不可避免的,我们可以用拉链法,也可以用开放定址法

考研数据结构:第七章 查找_第297张图片
举个例子,我们有散列函数H(key)=key%13,现在14已结占了1的位置,如果现在1再进来,想占据1的位置怎么办?

我们可以让给1找另一个空闲位置。

所谓开放定址法,就是一个散列地址,既对同义词开放,也对非同义词开放。

那么问题也随之而来,发生冲突时,我们要给新元素找另一个位置,那这个位置怎么确定?

假设我们现在原始地址发生冲突了,我们从原始地址往右偏移1位
考研数据结构:第七章 查找_第298张图片
如果还发生冲突,就去探索一下原始位置左边是否有冲突
考研数据结构:第七章 查找_第299张图片
如果还发生冲突,去原始地址右边两位看看有没有空位
考研数据结构:第七章 查找_第300张图片
如果还冲突,就探索原位置左边两位有没有空位。。。

也就是说,当发生冲突时,我们可以设计这样一个序列:0,1,-1,2,-2,3,-3…
来规定发生冲突时我们探测每个位置的顺序。
考研数据结构:第七章 查找_第301张图片
考研数据结构:第七章 查找_第302张图片

5.4.2线性探测法

考研数据结构:第七章 查找_第303张图片
对于散列表的插入,就是你先根据散列函数算出一个原始地址,然后根据那个探测法去探测原始地址周围的位置。

而对于散列表的查找,和插入基本一样,也就先算原始地址,然后根据对应探测法去探测。
但是对于查找有个需要注意的地方,就是如果你探测下来,探测到空位置还没探测到,说明原先就没有这个关键字。那么查找失败。

5.4.3平方探测法

考研数据结构:第七章 查找_第304张图片

5.4.3双散列法

对于双散列法,需要设计第二个散列函数,发生冲突时,需要根据关键字的值去确定第二个散列函数的计算结果。

不同关键字的探测序列也是不同的
考研数据结构:第七章 查找_第305张图片

5.4.4伪随机序列法

考研数据结构:第七章 查找_第306张图片

5.4.5删除

考研数据结构:第七章 查找_第307张图片

需要注意,删除元素时会有一个坑!以下是删除的错误示范!!!
比如我们现在用线性探测法,要删15
考研数据结构:第七章 查找_第308张图片
首先应该查找15
第一次定位15%13=2,但是2号位已经被占了,往右1位,找到15
考研数据结构:第七章 查找_第309张图片
然后删15
考研数据结构:第七章 查找_第310张图片

你这样做好像没啥问题,但是如果我们现在要你查找1怎么办?
由于你之前把3号位置清空了,那么你查1的时候经过3,系统会误认为没有1,这样就错了!
考研数据结构:第七章 查找_第311张图片

那该如何进行正确的删除呢?比如现在要删15
我们先找到15
考研数据结构:第七章 查找_第312张图片
找到15之后要删,但是这里的删,不是物理上的删,是逻辑上的删!

我们可以给15所在位置做一个标记,比如你设一个flag,把flag置为1,则表示已经删除

考研数据结构:第七章 查找_第313张图片
我们这里没有物理删除,而是逻辑删除,这样就可以保证线性探测法中间不会出现空档,也就避免了有数但是没找到的情况

考研数据结构:第七章 查找_第314张图片

需要注意的是,如果用的是开放定址法,你删了很多元素之后,会导致整个散列表看起来很满,但是很空(逻辑删除,物理上没删除)

这样其实是会导致查找效率低下的
比如我们要找的元素初始地址是0,但是你一个个往右发现都没有找到,最后才确定没有这个元素,这就很浪费时间了。
考研数据结构:第七章 查找_第315张图片

所以,如果散列表中有很多这种逻辑删除之后的元素,我们可以不定期的去整理散列表的数据

比如我们把元素1放到1号位置,其他位置逻辑删除的位置全部物理删除掉
考研数据结构:第七章 查找_第316张图片

ps:你插新元素的时候,也是可以插到逻辑删除的位置上的,就相当于是一个数据覆盖。
考研数据结构:第七章 查找_第317张图片
考研数据结构:第七章 查找_第318张图片
考研数据结构:第七章 查找_第319张图片
考研数据结构:第七章 查找_第320张图片
考研数据结构:第七章 查找_第321张图片
考研数据结构:第七章 查找_第322张图片

你可能感兴趣的:(数据结构专栏,顺序查找,折半查找,分块查找,二叉排序树,平衡二叉树,B树,散列表)