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

news2024/12/24 1:11:40

文章目录

  • 一、查找的基本概念
  • 二、顺序查找和折半查找
    • 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删除


一、查找的基本概念

在这里插入图片描述

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

在这里插入图片描述

评价一个查找算法的优劣:主要看算法的平均查找长度ASL
在这里插入图片描述

举个例子,我们现在有如下二叉排序树
在这里插入图片描述
如果你要查的是50,那么从根节点出发只需要对比一次关键字就可以了,所以第一项是1 * 1

在这里插入图片描述
如果你要查的是第二层的26或者66,你找26总共需要进行两轮对比,找66又需要两轮对比,所以查第二层元素共需要是2 * 2次对比
在这里插入图片描述
第三层同理,长度为3,共4个数据,所以3 * 4

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

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

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

比如同样的二叉排序树,查找成功和查找失败的ASL是不同的:
在这里插入图片描述

在这里插入图片描述

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

二、顺序查找和折半查找

2.1顺序查找

顺序查找顾名思义,就是按照顺序一个个找,你可以从前往后,也可以从后往前。这个是最无脑的查找。
在这里插入图片描述
代码如下:

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,就说明查找失败了。
在这里插入图片描述

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

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

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

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

下面以此类推…

在这里插入图片描述

如果是查找失败,那就是把所有的n个元素对比完了,还要再和哨兵对比一下。
在这里插入图片描述
总之,不管是查找成功还是查找失败,时间复杂度都是O(n)

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

举个例子
你要在如下的顺序表中进行查找21的操作
在这里插入图片描述
其实不用遍历完所有元素,当你遍历到29发现还没找到21,就已经可以不用找了,因为后面全是比21大的元素。
在这里插入图片描述

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

2.3折半查找

2.3.1算法思想

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

先看一个查找成功的例子:
比如现在要在下面的顺序表中找33
我们会先用两个指针low和high分别指向我们目前要搜索的区间范围
在这里插入图片描述
第一轮要检查的元素是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
在这里插入图片描述

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

high=mid-1
mid=(low+high)/2
在这里插入图片描述
第三轮
对比mid指向的元素32和要查找的元素33
32<33
那么33肯定在mid右边
low=mid+1
mid=(low+high)/2
在这里插入图片描述

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

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

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

在这里插入图片描述
刚开始还是一样的,low和high指向顺序表开头和结尾元素
mid=(low+high)/2
在这里插入图片描述
第一轮对比,12<29,那么12只会出现在mid左边
在这里插入图片描述
在这里插入图片描述

第二轮对比12<13,如果存在只会在mid左边
在这里插入图片描述
在这里插入图片描述

第三轮对比,7<12,如果存在,12在mid右边
在这里插入图片描述
在这里插入图片描述
第四轮对比,10<12,如果存在,12在mid右边
在这里插入图片描述
此时出现了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查找效率分析

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3.4折半查找判定树的构造

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.3.5折半查找效率

在这里插入图片描述

2.3.6小结

在这里插入图片描述

在这里插入图片描述
ps:只能说大部分情况折半查找比顺序查找效率更高。
因为如果你要查的元素就是顺序表第一个元素,顺序查找是要更快的。

2.4分块查找

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

比如下面这个数组,看起来元素都是乱的
在这里插入图片描述
如果仔细观察,我们可以把这些元素分成一个个小区间
第一个区间:<=10
第二个区间:<=20
第三个区间:<=30

在这里插入图片描述
如果单独看元素是乱序的,但是如果我们把它们分成一个个小区间后,可以发现各个区间之间其实是有序的,我们可以给这个查找表建立上一级索引。
在这里插入图片描述
看上图,很好理解。索引表中保存的是每一个分块中最大的关键字,还有分块的存储区间。

比如说我们索引表中有个30,那就说明其中出现最大的关键字是30,然后这个分块的存储区间是6-8,也就是第三个分块下标起始到末尾是6到8
在这里插入图片描述

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

该索引表数据结构如下:

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

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

先来看一个查找成功的例子
比如现在查22
在这里插入图片描述
先查找索引表,从索引表第一个元素依次往后找,
第一个元素10<22,继续往下
第二个元素20<22,继续往下
第三个元素30>=22,如果22存在,一定在此区间
接下来就从分块的起始位置,也就是6号下标往8号下标里找
我们在7号下标找到22
在这里插入图片描述

再来看一个查找失败的例子
现在要查29,依次遍历索引表,确定如果29存在是在第三个分块内
在这里插入图片描述
然后我们从第三个分块开始查找,第一个27不满足
在这里插入图片描述
第二个22不满足
在这里插入图片描述
第三个30不满足
在这里插入图片描述
再往后,发现已经超出第三个分块范围了,就说明29不存在
在这里插入图片描述

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

但是由于索引表中保存的元素是有序的,并且我们索引表是用数组的方式,也就是顺序存储的方式来实现的。所以,针对索引表,我们可以采用折半查找而对每个分块内的元素(乱序),只能顺序查找。
在这里插入图片描述
ps:分块查找又称为索引顺序查找,先索引表,然后顺序查找分块内元素嘛

三、树形查找

3.1二叉排序树

3.1.1二叉排序树定义

在这里插入图片描述
二叉树又称二叉查找树BST
二叉排序树的左子树结点值<根节点值<右子树值,如果用中序遍历(左根右),就可以得到一个递增的序列。

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

3.1.2查找操作

在这里插入图片描述

先看一个查找成功的例子:
假设我们现在要在一棵二叉排序树上找30
在这里插入图片描述
从19出发,19<30,如果30存在,则在19的右子树
在这里插入图片描述
50>30,如果30存在,则在50的左子树
在这里插入图片描述
26<30,如果30存在,则在26的右子树
在这里插入图片描述
成功找到30

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

从19出发,如果12存在,则在19左子树
在这里插入图片描述
13>12,如果12存在,则在13左子树
在这里插入图片描述
11<12,如果12存在则在11右子树
在这里插入图片描述
但是我们发现,11没有右子树了,说明查找失败了
在这里插入图片描述
查找代码如下:

//二叉排序树结点
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);//在右子树查找
   }
}

如果采用非递归
在这里插入图片描述
显然,如果采用非递归算法,我们只需要常数级的辅助空间。非递归实现空间复杂度为O(1)

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

3.1.3插入操作

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

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

在这里插入图片描述
12<19,应该插到12左子树
在这里插入图片描述
12<13,应该插到13的左子树

在这里插入图片描述
12>11,应该插到11的右子树
在这里插入图片描述
在这里插入图片描述
需要注意,如果我们要插入的这个关键字本来就存在了,那么不该再插入值重复的结点了。另外,每次插入的新结点必定会成为一个叶子结点。

我们这里采用的是递归实现,所以这种算法的最坏空间复杂度为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++;
	}
}

在这里插入图片描述
在这里插入图片描述

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

3.1.5二叉排序树的删除

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

1.要删除的结点是叶子结点
在这里插入图片描述
删除叶子结点后依然可以保证二叉排序树的特性,你直接删就行了

2.要删除的结点只有左子树或者右子树
在这里插入图片描述

比如13这个结点,它只有左子树,那你把13删了之后,
把原先13的左孩子接到原先13的位置就行了。
在这里插入图片描述
在这里插入图片描述
3.要删除的结点有左子树,有右子树
在这里插入图片描述
比如50这个结点

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

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

所以,如果要删50,我们可以把50右子树中的最小结点60换上来,如下图:
在这里插入图片描述

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

所以,要删50,我们可以让50左子树中最大结点30换上来
在这里插入图片描述

3.1.6查找效率分析

在这里插入图片描述
在这里插入图片描述

3.1.7小结

在这里插入图片描述

3.2平衡二叉树的插入

3.2.1引子

在这里插入图片描述

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

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

如下图,我们插入67这个结点后,这个新插入结点的所有祖先的平衡因子都受到了影响:
在这里插入图片描述

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

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

所以,我们只要调整最小的不平衡子树就可以了。调整效果如下:
在这里插入图片描述

下面我们就是讨论如何调整最小的不平衡子树,让它恢复平衡
在这里插入图片描述

3.2.2LL

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

我们用一些方形的框来抽象的表示各个部分子树,H表示子树的高度
(当前各个子树是平衡的)
在这里插入图片描述
现在在B结点的左子树插入了一个新结点,导致了A结点不平衡。其实就是因为B结点的左子树长高了。
在这里插入图片描述
原先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嘛。然后你
在这里插入图片描述
你要是设一开始A右子树高度是H+1。那么你A左子树左孩子高度+1,A左子树高度其实就变成了H+2。A的平衡因子是(H+2)-(H+1)=1,其实这还是平衡的。

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

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

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

我们要求的是一开始平衡,加了个结点之后不平衡。所以这种情况也不可以。
在这里插入图片描述

有同学又要问了:“那我一开始设B的右子树高度为H-1可以不?”
在这里插入图片描述

如果是这样的话,那么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仍然是一棵二叉排序树

在这里插入图片描述
显然,B左子树的值<B<B右子树的值<A<A右子树的值
在这里插入图片描述
那我们具体做法,就是让B结点右上旋转,代替A成为根结点,
然后A结点要成为B的右子树的根节点,也就是A要成为B的右孩子
在这里插入图片描述
然后BL<B,所以只能把BL放B左边
在这里插入图片描述
BR原先在B右边的,但是你B右边已经挂了A了,所以BL不能挂B右边了
BL原先是在A左边,也就是BL<A,我们把BL放A左边
在这里插入图片描述
AR>A,然后以前的AR继续挂A右边就行了
在这里插入图片描述

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

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

3.2.3RR

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

LL和RR的代码思路
在这里插入图片描述

3.2.4LR

LR就是在A结点的左孩子的右子树插入一个结点导致A结点变成最小不平衡子树的根节点
在这里插入图片描述
为了方便讨论,我们需要把BR进行展开。我们假设B的右孩子是叫C的结点,C的左子树称为CL,右子树称为CR。由于B右子树整个高度是H,所以CL和CR高度我们可以假设H-1

在这里插入图片描述
那么现在C这个子树长高了,我们可以假设把新结点插入在CR,导致CR高度为H
你假设插在CL也是一样的,处理方法都一样
在这里插入图片描述

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

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

首先是让C左旋替代B,B<C,那么B成为C的左孩子
在这里插入图片描述
然后由于BL<B,所以BL在B左边
在这里插入图片描述
CR>C,就放C右边
在这里插入图片描述
B<CL<C,就放B右边
在这里插入图片描述
所以,把C左旋后如下图,但是此时A仍然不平衡
在这里插入图片描述
接下来就是把C右旋替换A的位置
就是把C右孩子变成A左孩子,
A变成C右孩子
在这里插入图片描述
在这里插入图片描述
LR小结:在A左孩子的右子树插入一个新结点导致不平衡,我们采用A左孩子的右孩子左旋一次,再右旋一次

3.2.5RL

在这里插入图片描述
最后RL思路同理LR
RL小结:在A右孩子的左子树插入一个新结点导致不平衡,我们采用A右孩子的左孩子右旋一次,再左旋一次

3.2.5小结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3平衡二叉树的删除

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

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

在这里插入图片描述
在上一节介绍二叉排序树插入一个结点导致不平衡的四种情况如何解决。
该小节则是介绍二叉排序树删除一个结点导致不平衡的四种情况如何解决。
这四种情况和上节对应,依然是LL,RR,LR,RL

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

例1:现在删除下面树的9
在这里插入图片描述
单纯删除结点操作和二叉排序树是一样的,由于9这个阶段是叶子结点,我们直接删除就可以了。

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

删除9之后,对于13,平衡因子是-1,仍然平衡
在这里插入图片描述
对于25,平衡因子是1,仍然平衡

在这里插入图片描述
对于50,平衡因子是-1,仍然平衡

在这里插入图片描述
这就说明,刚才的删除操作并没有导致它的任何一个祖先出现不平衡现象,到此为止,删除操作结束。

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

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

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

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

在这里插入图片描述
由于产生了不平衡现象,我们就要进入第三步,找到最小不平衡子树下,个头最高的儿子、孙子

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

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

在这里插入图片描述
接下来进入第四步,根据孙子位置,调整平衡

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

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

下面就是让80左旋到75的位置
在这里插入图片描述

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

什么叫检查不平衡向上传导?在调整平衡前,这个不平衡的子树高度是4
在这里插入图片描述
调整后原先那棵不平衡子树高度从4变成了3
在这里插入图片描述
而这棵子树高度出现变化,就有可能导致它上面的树也产生不平衡。这就是所谓的不平衡向上传导。
显然这里是没有出现不平衡向上传导的。

例3:现在删除下面树的32
在这里插入图片描述

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

删除32之后,可以发现44是出现了不平衡现象
在这里插入图片描述
找到了最小不平衡子树,接下来就是找最高的儿子和孙子
可以确定78是最高的儿子,50是最高的孙子
在这里插入图片描述
然后50是44的RL,那我们就让50右旋再左旋
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

当然会有人好奇,出现不平衡向上传导的例子,就比如下面的树,要删除32
有时间,感兴趣自己研究
在这里插入图片描述

四、B树和B+树

4.1B树

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

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

先来回顾一下我们前面学的二叉查找树,也叫二叉排序树的特点。
在这里插入图片描述
如果我们要查的目标比当前结点值更小,我们会往左子树这边查找,如果比当前结点更小,就往右子树查找。

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

比如刚开始我们要搜索的范围是(-∞,∞),
那么根结点29就把(-∞,∞)划分为了(-∞,29)和(29,∞)
在这里插入图片描述
(-∞,29)就去左子树找,(29,∞)就去右子树找

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

举个例子:现在有图示的5叉查找树
在这里插入图片描述
其实我们虽然把它拓展成了5叉查找树,但是原理和2叉查找树一样的。

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

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

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

在这里插入图片描述
像上图这种5叉查找树,在一个结点中,我们最少可以允许有1个关键字,2个分叉;最多允许4个关键字,5个分叉
一个关键字会把一个查找区间分割成两个部分

图中给出的紫色结点,其是就查找失败的情况,比如下面圈出的失败结点,就是对应的(15,22)这个范围
在这里插入图片描述

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

假设我们现在要查找9
根节点中第一个被保存的关键字是22
在这里插入图片描述
显然9要比22更小,所以下面要选22左边的路
在这里插入图片描述

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

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

比如我们要找41,现在从根节点出发
在这里插入图片描述

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

在这里插入图片描述
来到下一层结点,第一个元素36<41,指针往右

在这里插入图片描述
第二个元素45>41,如果41存在,则在45左边的指针所指结点中
在这里插入图片描述
到新的结点,第一个元素40<41,指针右移
在这里插入图片描述
第二个元素42要比41更大,如果41存在,则在42左边指针指向的结点
在这里插入图片描述
但是你发现,如果沿着42左边指针往下走,居然是一个失败结点,那么就说明查找失败
在这里插入图片描述
ps:虽说是失败结点,其实就是一个NULL

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

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

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

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

比如5叉查找树,m/2向上取整应该是⌈5/2⌉=3
所以,对于5叉查找树,我们可以规定每个阶段至少3个分叉,也就是2个关键字
在这里插入图片描述

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

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

想法不错,但是实际是做不到的。
比如现在5叉查找树,刚开始只有1个元素,1个关键字。
在这里插入图片描述
那么这种情况下,就只有一个根节点(一个关键字)
就一个关键字,你那里来能放2个关键字呢?

再来看另一个问题:下面这棵二叉树,你觉得它优秀不?
在这里插入图片描述
上面这个5叉查找树已经满足了我们刚才提出的特性:根节点除外,每个结点有3个或3个以上的分叉。

但是这棵树有一个问题:它不平衡
在这里插入图片描述
这种不平衡的特性,显然也会导致我们的5叉查找树长的很高。

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

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

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

如果能满足上面提到的两个策略,那么就是B树
在这里插入图片描述
我们通常会把失败结点称为叶子结点,而最下面一层含有实际数据的结点,我们称为终端结点。

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

B树中所有结点孩子个数的最大值称为B数的阶,也就是这个B树你最多看到多少个分叉
如下图,最多是有5个分叉。
所以,所谓的5阶B树其实就是一个5叉查找树
在这里插入图片描述

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

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

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

在这里插入图片描述
pi-1的所指结点的所有关键字<ki<pi的所指结点的所有关键字
在这里插入图片描述

下面是对B树特性的总结:
在这里插入图片描述

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

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

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

那么对于m阶B树,每个结点最多有m-1个关键字,有m个分叉。
最上层的根节点只有一个(注意,我这里说的是结点数量是1,不是关键字数量是1),根结点有m个分叉。
在这里插入图片描述

由于第一层下来m个分叉,所以第二层有m个结点
在这里插入图片描述
第三层,又会由第二层m个结点下来m2个结点(每个结点m个分叉)
在这里插入图片描述
如果有h层,那么就应该是mh-1
在这里插入图片描述
也就是有h层,有(1+m+…+mh-1)个结点

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

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

在这里插入图片描述
下面来看一下最大高度是多少
要让一棵树长的尽可能的高,就要让这颗数的分叉尽可能的少。
对于根节点最少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
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
小贴士:关于为什么B树叫B树,B树这个数据结构的发明者也没有说明,个人觉得是balance,平衡的意思。

4.2B树的插入删除

4.2.1插入

下面我们从零开始建立一棵B树,
并且规定我们这个B树是5阶B树,也就是关键字个数最少不能少于2个,最多不能多于4个
在这里插入图片描述

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

在这里插入图片描述
我们会以中间位置,也就是⌈m/2⌉ 位置的关键字,把原有的结点拆分为左右两部分,然后⌈m/2⌉ 位置的关键字提到父节点中。

该例子中也就是第三个元素49提到父节点位置
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

从根节点出发,90大于49,而49右边已经没有元素了,所以顺着49右边指针往下找
在这里插入图片描述

接下来检查新结点关键字,60<90,80<90,所以90应该插到80右边
在这里插入图片描述
在这里插入图片描述
所以,我们其实是通过一次查找来确定我们新元素应该插到什么位置。
再次强调:每次插入的新元素一定是插到最底层的终端结点中!

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

在这里插入图片描述
ps:这么做可以保证二叉树的特性,也就是一个阶段的左子树结点值<当前结点值<右子树结点值

再接下来插入83,插入87,没啥好说的,直接插终端结点
在这里插入图片描述
再接下来插入70,肉眼扫一下应该插80和83中间,那么又会溢出,该结点又需要分裂

在这里插入图片描述
把中间元素80提到父节点,两边元素放左右子树
而这里中间元素80>49,80<88,所以我们把80放49和88之间
在这里插入图片描述
如果一个关键字,它因为需要分裂而提到父节点中,我们需要把这个关键字放到它所属结点这条指针的右边

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

在这里插入图片描述

这种情况,我们就需要把这个父节点继续向上分裂,由于根节点上面已经没有父节点了,所以我们创一个新结点
在这里插入图片描述
这里根节点是可以只有一个关键字的,其他结点关键字个数n必须满足2<=n<=4

下面是B树插入的总结
在这里插入图片描述

4.2.2删除

比如我们现在要删B树的60
在这里插入图片描述
由于60是在终端结点中,我们直接删了,然后把60所在结点的其他关键字左移一下
需要注意的是,你删了一个结点之后,要保证这个结点关键字个数还是>=⌈m/2⌉-1

这里删完60还剩3个关键字,没有低于一个结点最少关键字个数,是合法的
在这里插入图片描述

再比如,我们现在要删80,80删了之后根节点就空了。
在这里插入图片描述

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

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

如果用直接前驱来顶替
现在要删根节点的77,那么我们可以找77右子树中最小的来顶替77,也就是找右子树中最左边的元素82
在这里插入图片描述
在这里插入图片描述

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

比如删38
在这里插入图片描述
你删了38之后,下图所指结点的关键字个数小于B树规定的最小值2了
在这里插入图片描述
那么我们需要分为多种情况来考虑

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

可能会有同学会想直接把右兄弟的一个关键字放到当前结点,但是这样是有问题的
在这里插入图片描述
你直接把70放过去,会导致70>49,而我们B树是要求左子树值<根值<右子树值
这样就矛盾了。

解决办法是先将49拉下来,然后70去顶替49的位置
在这里插入图片描述
在这里插入图片描述
上面的解决方案可以做一个小总结:如果右兄弟的手头宽裕,我们可以找到当前结点的后继、后继的后继来填补空缺

举个例子,当前这个结点,也就是25这个元素的后继是49
在这里插入图片描述
而25后继的后继,也就是49的后继应该是70,即49右边指针所连结点的第一个元素
在这里插入图片描述
所以,这就是用当前结点后继、后继的后继去顶替它们所需要顶替的位置

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

比如现在要删90这个关键字
在这里插入图片描述
而删完90之后,发现右兄弟已经不宽裕了,但是左兄弟还宽裕
在这里插入图片描述
具体做法:如果左兄弟的手头宽裕,我们可以找到当前结点的前驱、前驱的前驱来填补空缺

92的直接前驱是88,也就是顺着指向当前结点的指针左边的元素
在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述

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

比如现在要删49
在这里插入图片描述
删完之后当前结点关键字已经不够了,但是右兄弟也不宽裕了

在这里插入图片描述
我们的策略是让当前结点和它的右兄弟合并
在这里插入图片描述
合并时,还需要把这两个结点中间的关键字也一起合并,这样才能满足B树特性

合并过程如下动图:
在这里插入图片描述
但是这里又出现问题了,由于我们从父节点拿走一个元素,父节点的关键字又不够了
所以我们接下来继续要合并
在这里插入图片描述
合并过程如下动图
在这里插入图片描述
而此时,根结点已经没有任何关键字了,删掉它
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

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

4.2.3小结

在这里插入图片描述

4.3B+树

对于B+树,考研不会考很深,基本都是概念的东西
在这里插入图片描述
细心的同学可能会发现,B+树和我们的分块查找比较类似
在这里插入图片描述
在分块查中,我们会把这些元素分成一个个块,在索引表中,我们会保存每个块中最大的关键字。

再回头看B+树,其实也是保存了每个块的最大关键字
在这里插入图片描述
然后每层索引往上,都是一样的规律

4.3.1定义

下面是B+树的定义
在这里插入图片描述
1.每个分支结点最多m棵子树
比如这里的4阶B+树,它最多就是有4个子树

2.非叶根节点至少有两棵子树,其他分支结点至少有⌈m/2⌉棵子树
什么叫非叶根节点?
首先要明确的是,我们在B+树中,把最下面一层称为叶子结点
在这里插入图片描述
下面三个例子中,第一个例子,只有一个根节点,它同时也是叶子结点。
而第三个例子,根节点并不是最下面一层的结点,也就是非叶子结点,它至少有2棵子树才能算是B+树

而第二个例子,根节点只有左子树没有右子树,不符合B+树的定义,它不是B+树
在这里插入图片描述
因为我们希望B+树高度尽可能低,所以我们会追求绝对平衡——所有子树高度相同
所以,我们会要求非叶根节点至少两棵子树

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

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

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

比如下面这个结点,对应3个关键字,每个关键字也对应1个分支
在这里插入图片描述
所以B+树当中一个结点里面,它含有多少个关键字,对应的它就有多少个分支,有多少个子树。

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

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

在B+树中,我们可以通过指针p,从第一个叶子结点开始,一个个往后遍历,把所有叶子结点都遍历完。
在这里插入图片描述
也就是说,B+树是支持顺序查找的

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

4.3.2B+树的查找

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

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

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

在这里插入图片描述
从左往右依次检查,就可以在这个叶子结点中找到9这个关键字
在这里插入图片描述
那么通过9,就可以找到9号学生对应的相关信息了

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

那么从根结点出发,7<15,应该往15所指结点找
在这里插入图片描述
3<7,7肯定不在3所指结点内,指针右移
在这里插入图片描述
7<9,如果7存在,肯定在9所指结点内
在这里插入图片描述
检查下一个结点,6<7,指针右移
在这里插入图片描述
下一个元素是8,8>7
在这里插入图片描述
注意!这里已经是最下面一层的叶子结点了,到这里没找到7就说明7不存在了。

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

除了根节点往下找,还可以通过叶子结点那里保存的指针p进行顺序查找
在这里插入图片描述

4.3.3B+树和B树的对比

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3.4小结

在这里插入图片描述

五、散列表

5.1散列表的基本概念

5.1.1散列表、散列函数

在这里插入图片描述
我们根据散列函数可以计算出一个数据元素在散列表中的存储地址。
比如这里散列函数是H(key)=key%13
那么如果你给我19,H(19)=19%13=6,我就可以立马知道如果19存在,则存放在6号位置

再比如你给我16,H(16)=16%13=3,我就可以立马知道如果16存在,则存放在3号位置
在这里插入图片描述
可以看到,在理想情况下,我们散列表查找一个元素时间复杂度只需要O(1)

5.1.2冲突、同义词

在这里插入图片描述
在这里插入图片描述
我们并不希望冲突频繁发生,冲突越少,散列表性能越高

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.2散列函数的构造

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

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

该小节将介绍4种散列函数构造方式:
在这里插入图片描述
考试中最常考、现实中最常用的都是除留余数法
在这里插入图片描述

5.2.1除留余数法

在这里插入图片描述
在这里插入图片描述

5.2.2直接定址法

这种方法适用于关键字分布基本连续
在这里插入图片描述

5.2.3数字分析法

在这里插入图片描述

5.2.4平方取中法

在这里插入图片描述

5.2.5小结

在这里插入图片描述

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

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

散列表中冲突这种现象是无法避免的,可能会有多个元素映射到同一个散列地址。
这些元素在插入时就会发生冲突
在这里插入图片描述

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

我们把这些同义词用一个链表连起来
在这里插入图片描述

5.3.1插入

现有如下的空散列表,长度13,散列函数H(key)=key%13,我们用拉链法解决冲突
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

5.3.2查找

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3.3删除

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3.4小结

在这里插入图片描述
在这里插入图片描述

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

5.4.1基本原理

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

在这里插入图片描述
举个例子,我们有散列函数H(key)=key%13,现在14已结占了1的位置,如果现在1再进来,想占据1的位置怎么办?

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

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

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

假设我们现在原始地址发生冲突了,我们从原始地址往右偏移1位
在这里插入图片描述
如果还发生冲突,就去探索一下原始位置左边是否有冲突
在这里插入图片描述
如果还发生冲突,去原始地址右边两位看看有没有空位
在这里插入图片描述
如果还冲突,就探索原位置左边两位有没有空位。。。

也就是说,当发生冲突时,我们可以设计这样一个序列:0,1,-1,2,-2,3,-3…
来规定发生冲突时我们探测每个位置的顺序。
在这里插入图片描述
在这里插入图片描述

5.4.2线性探测法

在这里插入图片描述
对于散列表的插入,就是你先根据散列函数算出一个原始地址,然后根据那个探测法去探测原始地址周围的位置。

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

5.4.3平方探测法

在这里插入图片描述

5.4.3双散列法

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

不同关键字的探测序列也是不同的
在这里插入图片描述

5.4.4伪随机序列法

在这里插入图片描述

5.4.5删除

在这里插入图片描述

需要注意,删除元素时会有一个坑!以下是删除的错误示范!!!
比如我们现在用线性探测法,要删15
在这里插入图片描述
首先应该查找15
第一次定位15%13=2,但是2号位已经被占了,往右1位,找到15
在这里插入图片描述
然后删15
在这里插入图片描述

你这样做好像没啥问题,但是如果我们现在要你查找1怎么办?
由于你之前把3号位置清空了,那么你查1的时候经过3,系统会误认为没有1,这样就错了!
在这里插入图片描述

那该如何进行正确的删除呢?比如现在要删15
我们先找到15
在这里插入图片描述
找到15之后要删,但是这里的删,不是物理上的删,是逻辑上的删!

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

在这里插入图片描述
我们这里没有物理删除,而是逻辑删除,这样就可以保证线性探测法中间不会出现空档,也就避免了有数但是没找到的情况

在这里插入图片描述

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

这样其实是会导致查找效率低下的
比如我们要找的元素初始地址是0,但是你一个个往右发现都没有找到,最后才确定没有这个元素,这就很浪费时间了。
在这里插入图片描述

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

比如我们把元素1放到1号位置,其他位置逻辑删除的位置全部物理删除掉
在这里插入图片描述

ps:你插新元素的时候,也是可以插到逻辑删除的位置上的,就相当于是一个数据覆盖。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/877121.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

arm:day2

.text 文本段 .global _start 声明一个_start全局函数的入口 _start: _start标签&#xff0c;就是c语言的函数/*mov r0,#9 r09mov r1,#15 r115cmp r0,r1 比较r0和r1beq stop 如果r0等于r1&#xff0c;跳转stopsubhi r0,r0,r1 如果r0大于r1&#xff0c;r0r0-r1su…

leetcode剑指 Offer 05. 替换空格(两种方法)

题目&#xff1a;leetcode剑指 Offer 05. 替换空格 描述&#xff1a; 请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20"。 示例 1&#xff1a; 输入&#xff1a;s “We are happy.” 输出&#xff1a;“We%20are%20happy.” 思路&#xff1a; 第一…

Python爬虫:单线程、多线程、多进程

前言 在使用爬虫爬取数据的时候&#xff0c;当需要爬取的数据量比较大&#xff0c;且急需很快获取到数据的时候&#xff0c;可以考虑将单线程的爬虫写成多线程的爬虫。下面来学习一些它的基础知识和代码编写方法。 一、进程和线程 进程可以理解为是正在运行的程序的实例。进…

MySQL索引总结

MySQL索引总结 1.索引的概念、作用与使用场景 本质上就是减少读写磁盘的次数。 索引是一种特殊的文件&#xff0c;包含这对数据表中所有记录的引用指针&#xff0c;可以对表中的一列或多列创建索引&#xff0c;并指定索引的类型&#xff0c;每种类型都有对应数据结构实现。 …

大数据必回之LSM树

LSM树&#xff08;Log-Structured-Merge-Tree&#xff09;并不像B、红黑树一样是一颗严格的树状数据结构&#xff0c;它其实是一种存储结构&#xff0c;像HBase、RocksDB这些NoSQL存储都是采用LSM树。它是一种分层、有序、面向磁盘的数据结构&#xff0c;核心思想是顺序写性能远…

Leetcode链表篇 Day3

.24. 两两交换链表中的节点 - 力扣&#xff08;LeetCode&#xff09; 1.构建虚拟结点 2.两两一组&#xff0c;前继结点一定在两两的前面 3.保存结点1和结点3 19. 删除链表的倒数第 N 个结点 - 力扣&#xff08;LeetCode&#xff09; 1.双指针&#xff1a;快慢指针 两个指针的差…

初步认识OSI/TCP/IP一(第三十八课)

1 初始OSI模型 OSI参考模型&#xff08;Open Systems Interconnection Reference Model&#xff09;是一个由国际标准化组织&#xff08;ISO&#xff09;和国际电报电话咨询委员会&#xff08;CCITT&#xff09;联合制定的网络通信协议规范&#xff0c;它将网络通信分为七个不…

React antd tree树组件 - 父子节点没有自动关联情况下 - 显示半选、全选状态以及实现父子节点互动

实现的效果图如下&#xff1a; 如Ant Design Vue 中所示&#xff0c;并没有提供获取半选节点的方法&#xff0c;当设置checked和checkStrictly时&#xff0c;父子节点也不再自动关联了 前提&#xff1a;从后端可以获取的数据分别是完整的树型数据、所有选中的节点数据&#…

在pycharm中对使用脚本文件运行的程序进行调试

在github中下载的许多项目都可以使用给出的脚本文件运行&#xff0c;本文介绍如果在pycharm中对使用脚本文件运行的程序进行调试的方法。 1.点击 edit configurations 2.选择要debug的py文件&#xff0c;并且填写参数 3.点击运行旁边的debug按钮

odoo16 tree视图没有导出按钮

odoo16 tree视图没有导出按钮 做了一个odoo应用,很奇怪,同样的角色,不同的用户,有些有导出按钮,有些没有,一直没搞清楚为啥, 今天刚好有时间,好好研究一下. 1 先按浏览器F12,导出按钮对应的html: <button type"button" class"btn btn-secondary fa fa-dow…

理解持续测试,才算理解DevOps

软件产品的成功与否&#xff0c;在很大程度上取决于对市场需求的及时把控&#xff0c;采用DevOps可以加快产品交付速度&#xff0c;改善用户体验&#xff0c;从而有助于保持领先于竞争对手的优势。 作为敏捷开发方法论的一种扩展&#xff0c;DevOps强调开发、测试和运维不同团队…

使用BP插件captcha-killer识别图片验证码绕过系统验证码机制

使用BP插件captcha-killer绕过验证码 前置条件 1、下载安装插件 burp2020前使用&#xff1a;https://github.com/c0ny1/captcha-killer/tree/0.1.2 burp2020后使用&#xff1a;https://github.com/Ta0ing/captcha-killer-java8 2、导入插件 分为三个部分&#xff1a;上面为验…

【Sklearn】基于朴素贝叶斯算法的数据分类预测(Excel可直接替换数据)

【Sklearn】基于朴素贝叶斯算法的数据分类预测&#xff08;Excel可直接替换数据&#xff09; 1.模型原理2.模型参数3.文件结构4.Excel数据5.下载地址6.完整代码7.运行结果 1.模型原理 模型原理&#xff1a; 朴素贝叶斯分类是基于贝叶斯定理的一种分类方法。它假设特征之间相互…

【一定要会】为ChatGPT插上翅膀的5个常用插件!

自从ChatGPT接入插件功能后&#xff0c;便不再受限于预训练数据中的知识&#xff0c;第三方开发者也可以基于ChatGPT增强自家应用程序的功能。自此 ChatGPT更像是一个开放的平台甚至是生态了。 截止的今年的7月20日&#xff0c;ChatGPT已经接入了700多个插件&#xff08;当然&…

智能驾驶系列报告之一:智能驾驶 ChatGPT时刻有望来临

原创 | 文 BFT机器人 L3 功能加速落地&#xff0c;政策标准有望明确 L2 发展日益成熟&#xff0c;L3 功能加速落地。根据市场监管总局发布的《汽车驾驶自动化分级》与 SAE发布的自动驾驶分级标准&#xff0c;自动驾驶主要分为 6 个级别&#xff08;0 级到 5 级&#xff0c;L0 …

五、linux系统下创建新目录的mkdir命令

mkdir命令 在Linux系统中&#xff0c;如果想要创建一个新目录&#xff0c;即文件夹&#xff0c;可以使用mkdir命令&#xff1a; 1、语法&#xff1a;mkdir [-p](可以不存在) Linux路径(必须存在) 2、基础操作&#xff1a;mkdir 目录名 &#xff08;1&#xff09;方法一&…

pytest数据驱动(最简单)

目录 第一种&#xff1a;通过yaml文件获取数据&#xff08;一维列表&#xff09; 第二种&#xff1a;通过yaml文件获取数据&#xff08;二维列表&#xff09; 第三种&#xff1a;通过yaml文件获取数据&#xff08;pytest.fixture&#xff09; 资料获取方法 第一种&#xff…

【Vue-Router】别名

后台返回来的路径名不合理&#xff0c;但多个项目在使用中了&#xff0c;不方便改时可以使用别名。可以有多个或一个。 First.vue <template><h1>First Seciton</h1> </template>Second.vue&#xff0c;Third.vue代码同理 UserSettings.vue <tem…

【需求输出】流程图输出

文章目录 1、什么是流程图2、绘制流程图的工具和基本要素3、流程图的分类和应用场景4、如何根据具体场景输出流程图 1、什么是流程图 2、绘制流程图的工具和基本要素 3、流程图的分类和应用场景 4、如何根据具体场景输出流程图

ARTS 挑战打卡的第9天 --- 如何知道一个数是否为2的若干次幂(Algorithm)

前言 &#xff08;1&#xff09;今天看到一个有意思的问题&#xff0c;如何判断一个数字是否为2的若干次幂。这个问题并不难&#xff0c;但是对于我们的C语言功底还是有一点点的考验的。 &#xff08;2&#xff09;希望各位可以先自行思考&#xff0c;实在想不出来再看后面的讲…