第七章 查找
文章目录
-
- [第七章 查找](#第七章 查找)
-
- [7.1 查找概念](#7.1 查找概念)
- [7.2 顺序查找](#7.2 顺序查找)
- [7.3 折半查找](#7.3 折半查找)
- [7.4 分块查找](#7.4 分块查找)
- [7.5 二叉排序树](#7.5 二叉排序树)
- [7.6 平衡二叉树](#7.6 平衡二叉树)
- [7.7 红黑树](#7.7 红黑树)
- [7.8 B树和B+树](#7.8 B树和B+树)
-
- [7.8.1 B树](#7.8.1 B树)
- [7.8.2 B树的基本操作](#7.8.2 B树的基本操作)
- [7.8.3 B+树](#7.8.3 B+树)
- [7.8.4 B树和B+树的比较](#7.8.4 B树和B+树的比较)
- [7.9 散列查找及其性能分析](#7.9 散列查找及其性能分析)
7.1 查找概念

-
**查找:**在数据集合中寻找满足某种条件的数据元素的过程称为查找。
-
**查找表(查找结构):**用于查找的数据集合称为查找表,它由同一类型的数据元素 (或记录)组成。
-
**关键字:**数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。
-
对查找表的常⻅操作:
- 查找符合条件的数据元素
- 插⼊、删除某个数据元素
- 只需要进行操作1的是静态查找表
- 1和2都需要进行的是动态查找表
-
**查找长度:**在查找运算中,需要对比关键字的次数称为查找长度。
-
平均查找长度(ASL,Average Search Length): 所有查找过程中进行关键字的比较次数的平均值。
-
-
ASL的数量级反应了查找算法时间复杂度
7.2 顺序查找

- **顺序查找,**又叫"线性查找",通常用于线性表算法。
- **思想:**从头到尾遍历
代码实现:
cpp
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;
}
哨兵方式代码实现:
思想:
顺序表从下表1开始存储,把key存储在下标为0的地方,从后往前遍历,只要找到key就退出循环。
查找失败的的话那么返回的i值为0,表示查找失败
查找成功则返回对应的下标值
优点是无需判断是否越界,因为遍历到下标为0的时候,它本身肯定和它本身相同,肯定就退出了

cpp
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST,ElemType key){
ST.elem[0]=key;
int i;
for(i=ST.TableLen;ST.elem[i]!=key;--i)
// 查找成功返回数组下标,否则返回0
return i;
}



圆形是成功,方格是失败

优化要根据具体情况具体分析
7.3 折半查找
如果是左闭右闭区间的话,查找失败循环结束时left=right+1
而如果是左闭右开区间的话,查找失败循环结束时是left=right
除了有序这个条件还必须是顺序存储

- 折半查找,又称"二分查找",仅适用于有序的顺序表

折半查找代码实现:
cpp
typedef struct{
ElemType *elem;
int TableLen;
}SSTable;
// 折半查找
int Binary_Search(SSTable L,ElemType key){
int low=0,high=L.TableLen,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;
}





如果mid是向上取整,那就是左子树比右子树多一个或者0个结点了

注:
折半查找一般都比顺序查找更优秀。但不是一定比顺序查找更优秀。
7.4 分块查找

分块查找所针对的情况:块内无序、块间有序。




这个太复杂了,会模拟就行,一般不会考,考也就是少量的数据

如果每个块中的元素数量都相同 的话就比较有规律,如上图所示(注:n=sb,b=n/s带入ASL算出最后的表达式求极值的得到最小值)
最后的结果是,每个块内如果是根号n个元素,那么一共有根号n个块,那么就会得到最小的ASL
如果n=10000,则ASL最小为根号n+1=100+1=101
也就是平均值需要对比101次关键字就可以查找到我们想要的关键字

如果使用折半查找查找块的话,asl如上图所示,有个印象就行不是很重要

这个例子说明了,要具体问题具体分析,删除插入频繁就不用数组而是用链表了
7.5 二叉排序树

**二又排序树,**又称二叉查找树(BST,Binary Search Tree)棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根结点的关键字;
- 右子树上所有结点的关键字均大于根结点的关键字;
- 左子树和右子树又各是一棵二叉排序树;
- 左子树结点值< 根结点值< 右子树结点值;
- 进行中序遍历,可以得到一个递增的有序序列。
【二叉排序树的查找】
- 若树非空,目标值与根结点的值比较;
- 若相等,则查找成功;
- 若小于根结点,则在左子树上查找;
- 否则在右子树上查找;
- 查找成功,返回结点指针;查找失败返回NULL 。

非递归实现最坏空间复杂度为O(1),递归实现最坏空间复杂度为O(h),为树的高度
cpp
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//在二叉排序树中查找值为key的结点(非递归)
//最坏空间复杂度:O(1)
BSTNode *BST_Search(BSTree T, int key){
while(T!=NULL && key!=T->key){ //若树空或等于跟结点值,则结束循环
if(key<T->key) //值小于根结点值,在左子树上查找
T = T->lchild;
else //值大于根结点值,在右子树上查找
T = T->rchild;
}
return T;
}
//在二叉排序树中查找值为key的结点(递归)
//最坏空间复杂度:O(h) h为树的高度
BSTNode *BSTSearch(BSTree T, int key){
if(T == NULL)
return NULL;
if(Kry == T->key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild, key);
else
return BSTSearch(T->rchild, key);
}
【二叉排序树的插入操作】
- 若原二叉排序树为空,则直接插入结点;否则;
- 若关键字k小于根结点值,则插入到左子树;
- 若关键字k大于根结点值,则插入到右子树。

cpp
//在二叉排序树中插入关键字为k的新结点(递归)
//最坏空间复杂度:O(h)
int BST_Insert(BSTree &T, int k){
if(T==NULL){ //原树为空,新插入的结点为根结点
T = (BSTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1; //插入成功
}
else if(K == T->key) //树中存在相同关键字的结点,插入失败
return 0;
else if(k < T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
【二叉排序树的构造】

cpp
//按照str[]中的关键字序列建立二叉排序树
void Crear_BST(BSTree &T, int str[], int n){
T = NULL; //初始时T为空树
int i=0;
while(i<n){
BST_Insert(T,str[i]); //依次将每个关键字插入到二叉排序树中
i++;
}
}
【二叉排序树的删除】
先搜索找到目标结点:
- 若被删除结点z是叶结点则直接删除,不会破坏二叉排序树的性质;
- 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置;
- 若结点z有左、右两棵子树,则令z的直接后继 (或直接前驱) 替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
- 直接前驱就是中序遍历二叉排序树的要删除结点的前一个结点,即该节点的左子树的最右下的节点
- 直接后继就是中序遍历二叉排序树的要删除结点的后一个结点,即该节点的右子树的最左下的节点
**查找长度:**在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度


7.6 平衡二叉树
平衡二叉树的插入

**平衡二叉树(Balanced Binary Tree),**简称平衡树(AVL树)--上任一结点的左子树和右子树的高度之差不超过1。
结点的平衡因子 = 左子树高 - 右子树高

objectivec
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;
平衡二叉树的插入
- 每次调整的对象都是"最小不平衡子树"
- 在插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡。


调整最小不平衡子树(LL):
调整最小不平衡子树(RR):

左旋右旋的的代码思路:
其实就还是链表的操作,这个指针指到这边,那个指针指到那边,唯一要注意的就是改完指针之后不要忘记还要连接原来的二叉树,即gf的操作
调整最小不平衡子树(LR):


调整最小不平衡子树(RL):




n3=4,n4=7,n5=12,n6=20
4层7个节点
5层12结点
做题所得结论(五颗星):当你用上面的递推公式推出来一个深度为h的最小节点数量nh,深度为h的二叉平衡树只有nh个节点的话,那么所有的非叶子节点的平衡因子都是1或-1,反过来也成立。
平衡二叉树的删除






对最小不平衡子树的旋转可能导致树变矮,从而导致上层祖先不平衡(不平衡向上传递)
7.7 红黑树
可能的考法

7.7.1 为什么要发明红黑树?
红黑树是适度平衡的二叉排序树
平衡二叉树是高度平衡的二叉排序树
所以一般相同结点的话,平衡二叉树的性能会更优秀

7.7.2 红黑树的定义和性质

定义:

叶节点,失败节点,null节点说的是一个东西。也就是说红黑树里面的叶子节点并不是最下面一层
左根右,根叶黑,不红红,黑路同
是二叉排序树,左子树小于根小于右子树,根节点和叶节点都是黑的,不可以有两个相连红色,从一个节点出发到达叶节点的路径上的黑色节点数量一定相同



性质:
红色节点数目最大可以是黑节点数目的两倍
若红黑树所有节点都是黑色的,那肯定是一颗满二叉树(因为根节点到叶节点的所有路径上的黑色节点数量必须相同)

查找:
和BST,AVL一样,从根出发,左小右大,若查找到一个空叶节点,则查找失败
7.7.3 红黑树的插入和删除
插入

1.如果插入的新结点不是根,为什么要染成红色呢?因为要保证黑路同,如果染成黑色,那必然导致新增加节点的;路径上的黑色节点数量比其他路径多
2.判断LL还是RR还是其他型都是通过爷节点来判断的而不是父节点
3.图中的染色的意思其实就是取反,只要涉及到x换y的,那么x和y的颜色都要取反,黑的变红的,红的变黑的
4.爷变为新结点是把爷结点看做新结点重新走一遍上述的流程,看爷节点作为新结点有没有破坏红黑树特性,比如:如果爷结点此时是红的,还是根节点,那必然要被染成黑色
5.其实每次插入的如果不是根节点,那么破坏的基本都是不红红这个特性
重点:所以每次插入的如果是非根节点就直接看有没有违背不红红就行,不用管其它特性有没有被违背,因为一定不会被违背
具体的例子
插入20,10,5

插入30

插入40

插入57

插入3

插入2

插入4

插入35,25,18都没有违反不红红,直接插入就好
其实当红黑树越来越大的时候插入很多时间都是直接插入的

插入22

插入23
第一步违反了不红红且叔叔是黑的,判断为LR型

第二步左旋

第三步右旋

第四步 儿子和爷染色

插入24
破坏不红红,且是红叔,则染色,爷变为新结点

爷变为新结点后发现违反了不红红,则继续进行之前的对应步骤

左旋

右旋

染色

插入19,18
19没有破坏不红红直接插入,而红黑树中已经有18这个关键字了,那么18插入到哪里就看自己的算法 是如何设计的了,可以在18左孩子,也可以去18右孩子

右旋

左旋

染色

到此为止插入完成
删除

23,24届考察概率不大,25不好说,还是看看吧
7.8 B树和B+树
7.8.1 B树

如果每个节点的的关键字太少,比如变成1个的话,那就退化为二叉查找树了,所以才要保证节点的最少分叉和最少关键字的数量

如果可以保证每个节点关键字不少,并且所有子树高度都相同,那这个其实就是一个b树




每个节点最多m-1个关键字,第一层1个节点第二层最多m个节点...最多就是1+...m的h-1次方个节点,就是上面这个公式
求最大高度的方法1:

b树的叶子节点代表失败节点,有n个关键字就肯定有n+1个失败的区间,那么就是n+1个叶子
求最大高度的方法2

k是一个节点内最少的分叉数量,减去1就是最少的关键字的数量

7.8.2 B树的基本操作

B树的查找:
- B树的查找操作与二叉查找树类似。
- B树的查找包含两个基本操作:① 在B树中找结点;② 在结点中找关键字。B树常存储在磁盘上,因此前一个查找操作在磁盘上进行,后一个查找操作在内存中进行。在B树中查找到某个结点后,先在有序表中进行查找,若找到则查找成功,否则按照对应指针信息到所指的子树中去查找。查找到叶子结点(对应指针为空指针),则说明树中没有对应的关键字,查找失败。
B树的插入:

具体的例子:
1.在一个节点已经满了的情况下再插入一个关键字

2.把中间的关键字提出来作为左右的父节点

3.继续插入新的关键字,而新的关键字一定是插入到最底层的终端节点,用查找来确定插入的位置
4.当插满之后继续分裂,分裂时需要把结点的中间的关键字提到父节点中,注意保持父节点有序性
5.重复上述过程,直到父节点也满了

6.分裂父节点,把中间节点作为新的父节点
B树的删除:
删除操作一定会导致叶节点的变化。如果删除的是叶节点,那么必然导致叶节点变化。如果不是叶节点的话,则要先将被删除的节点和它的前驱或者后继交换,最终转换为删除叶节点,还是导致叶节点发生了变化
也可以更简单的说,n个关键字有n+1个叶子,而n-1个关键字只有n个叶子,那么肯定会有叶子被删除的
综述:

具体的例子
情况1:如果删除的关键字在终端节点并且删除关键字之后,该节点还满足最低的关键字数量要求的话,那就直接删除该关键字就行
情况2:如果删除的额关键字在非终端节点的话,则使用当前关键字的直接前驱或者直接后继进行替代
比如要删除80,就可以用77和82来代替他,注意要看把77或者82移走之后原来的节点还够不够最低的标准


情况3:假如删除一个非终端节点的关键字以后,该节点不满足b树要求的话。但是兄弟结点借给该节点之后,自己的节点数量还可以满足b树要求
比如下图删除38,先把49拉下来,再把70给填到父节点


左兄弟同理,只是变成找前驱而已

情况4:删除49.即兄弟结点不够借
先把父节点的关键字拉下来一起和左右兄弟合并
再看父节点是否满足b树要求,如果不满足再去分析是情况三还是情况4,这个例子是情况4
也就是说还得再进行一次合并



7.8.3 B+树
定义:


这里说的非叶根节点的意思是,当根节点不是叶子节点的时候,那么当前的根节点最少要有两颗子树
查找
查找方式1:从上到下依次查找,和二叉排序树类似
在分支节点中找到的并不是最后结果,一定得下降到叶子结点才能找到对应的记录或者信息
而在叶子结点中找到才是真的找到,如果分支节点中没有该关键字,但是却可以确定是哪个叶子结点的话,那还要遍历该叶子节点的值才可以确定有没有找得到

也就是说在B+树中,无论查找成功与否,最终一定都要走到最下面一层结点
而b树却可以停在任何一层

查找方式2:顺序查找 利用p指针遍历叶子节点就好

7.8.4 B树和B+树的比较
1.关键字对应子树数量
m阶B树:结点中的n个关键字对应n+1棵子树
m阶B+树:结点中的n个关键字对应n棵子树。
2.结点关键字数量


3.关键字是否重复
m阶B树:在B树中,各结点中包含的关键字是不重复的
m阶B+树:在B+树中,叶结点包含全部关键字非叶结点中出现过的关键字也会出现在叶结点中。
4.关键字是否存储对应记录的地址
m阶B树:B树的结点中都包含了关键字对应的记录的存储地址
m阶B+树:在B+树中,叶结点包含信息,所有非叶结点仅起索引作用
,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址
5.查找方式
b树只支持随机查找,不支持顺序查找
而b+树两个都可以
6.b树和B+树都可以用于文件索引结构,但是b+树更合适,因为它的磁盘读写代价更低


7.9 散列查找及其性能分析

7.9.1 散列表的基本概念



如何减少冲突?
构造更合适的散列函数
7.9.2 散列函数的构造方法


1.除留余数法


2.直接定址法

3.数字分析法

4.平方取中法

7.9.3 处理冲突的方法
1.拉链法

插入操作:
题目如果没有说明的话,那么默认使用头插法


查找操作:
1.根据散列函数找到对应链表
2.遍历链表找到关键字
删除操作:
先查找,找到关键字,把它删除,没找到就是删除失败

2.开放地址法



每个探测序列的第一个d0都是0,是因为第0次发生冲突的散列地址就是它本身,其实就是第一次往一个位置插入的时候的理解吧算是
插入和查找操作
插入就按照对应的规则插入
查找也按照对应的规则查找,如果找到空,那么就是没找到答案




伪随机序列是人为设置的
删除操作





探测覆盖率:
了解就好,不考



说明:m为质数,表长m和小于它本身的数一定是互质的

7.9.4 散列查找及性能分析
装填因子,散列函数,冲突解决策略略都会影响ASL

以线性探测为例


查找失败的13,12,11,10怎么来的?因为第一个元素查找失败那就得看后面所有元素等不等于当前要找的元素,因为后面元素都有可能是因为冲突而被线性探测法弄到后面去的
还有经典的错误是,表长虽然为16,但是散列函数取值只有13
1是因为h(key)=0是空,查找一次就知道查找失败了
而第二个是13而不是12的原因是,要知道h(key)=1查找失败除了需要对比1-12以外,还要去看第13个位置,看了第13个位置发现是空的,这样才不会继续对比,那么就是13次而不是只对比1-12就可以了的

注意是逻辑删除而不是真的删除,计算查找次数的时候不要以为到了7就停了,如果线性探测的时候还往后走了,那么现在也要继续往后查找


如何减轻聚集现象呢?
使用平方探测法或者 设计合理的双散列法 或者 设计合理的伪随机序列法都可以减轻

ASL成功=(1+1/(1-装填因子))/2