145.DS--第六章
查找

前面的数据结构基本介绍完了 后续就是关于方法查找和排序
查找方法就是插入和删除的前提
查找顾名思义就是找元素
看几个概念后续用到的:
- 关键字:唯一标识元素的某个数据项的值
- 平均查找长度(ASL):查找关键字比较次数的加权平均值 (衡量查找算法效率的核心指标)
- 查找表:只涉及查找无须动态修改即静态查找表 否则即动态查找表
一.线性结构(顺序 折半 分块查找)
先看顺序查找(线性查找)
- 适应于顺序表/链表
- 无序/有序
假设线性表是无序的 顺序查找
就是从一端开始挨个找 直到找到 到另一端若没找到就失败
我们看算法实现
int Search_Seq(SSTable ST,ElemType key){
ST.elem[0]=key; //哨兵
for(int i=ST.TableLen;ST.elem[i]!=key;--i);
return i;
}
上述是从后往前找 引入哨兵就是为了防止重复判断是否越界
其实就是省去了边界判断提高效率
看其ASL(查找每个元素的概率相等)
之前概念提过 ASL主要就是比较次数
可以看出当数据量大的时候 效率就低 只用于顺序表
再看有序的线性表顺序查找
比如这个例子 有序的线性表可以看作一棵二叉排序树
关于其ASL
适用于顺序表和链式存储
看一道题目
折半查找(二分查找)
仅适用于关键字有序的顺序表
原理很多跟有序线性表的顺序查找相似
比如举个例子就能搞懂
比如找元素8
上下界 然后通过mid找中间 就可以直接折半找
每次查找采用相同的取整方式
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;
if(L.elem[mid]>key) high=mid-1;
else low=mid+1;
}
return -1;
}
这个很好理解不过多阐述过程了
我们通过此例题来分析一下ASL
- ASL成功:根结点到目的结点路径上的结点数
- ASL失败:根结点到对应失败结点父结点路径上的结点数

两者所除的分母不同前者是成功结点就是存在的 后者是失败的不存在树中的结点
核心就是折半查找判定树是一棵平衡二叉树
后续讲解平衡二叉树
时间复杂度
效率高于顺序查找
再次强调只适用于有序的顺序表
再看关于比较次数
看判定树 通过判定树来分析
分块查找(索引顺序查找)
块内元素可以无序 块间元素必须有序
很好理解先找到对应的块 然后再在块内找
它的ASL
当块大小取根号n的时候最优
虽然索引占一定存储空间 但分块结构限制了块内查找的范围所以效率仍然高于普通顺序查找
找出块数和每块的记录数 套公式
二.树形查找(二叉排序树 平衡二叉树 红黑树 B(+)树)
这节比较重量级
一个一个来看
1.二叉排序树 BST
也叫做二叉查找树 动态查找插入删除
前面介绍过特点就是:关键字
- 左子树<根
- 根<右子树
依次类推
中序遍历可以得到有序关键字序列
然后来探讨构造 查找 插入删除
通过一道题来看构造
ABD选项一样 我们就以C来分析
void Creat_BST(BiTree &T,KeyType str[],int n){
T=NULL;
int i=0;
while(i<0){
BST_Insert(T,str[i]);
i++;
}
}
再来看插入操作
比如一棵空二叉排序树 依次插入80 60 90
依次类推继续这样插入 最终满足BST的要求即可
int BST_Insert(BiTree &T,KeyType k){
if(T=NULL){
T=(BiTree)malloc(sizeof(BSTNode));
T->data=k;
T->lchild=T->rchild=NULL;
return 1;
}
else if(k==T->data) return 0;
else if(k<T->data) return BST_Insert(T->lchild,k);
else return BST_Insert(T->rchild,k);
}
插入的元素除了空的时候 一定是叶结点
二叉排序树删除操作
有限制:
- 如果是叶子结点 直接删
- 如果删除的只有一棵子树 删掉它后用那棵子树上移
- 如果删除的有两棵子树 则通过中序序列找到后驱(前驱)替代 然后后面采用第二种方式调整
比如还以那道构建题为例

查找过程很简单:从根开始找 比根大就往右子树找 比根小就往左子树找 依次类推直到找到或没有
BSTNode *BST_Search(BiTree T,ElemType key){
while(T!=NULL&&key!=T->data){
if(key<T->data) T=T->lchild;
else T=T->rchild;
}
return T;
}
也可以采用递归实现 但递归栈需要消耗存储空间效率低
二叉排序树查找效率分析
静态就采用顺序表 频繁插入删除动态就采用二叉排序树
2.平衡二叉树 AVL
前面所有的疑问在这儿都将解开
关于定义就一句话:任意结点的左右子树高度差绝对值不超过1
这里引入一个概念:平衡因子=左右子树高度差 值为1 0 -1
怎么说来看
平衡二叉树可为空
通过上图可以看出关于平衡二叉树的查找很简单
就取决于树深度
平均查找时间复杂度
结点数最多情况就是满二叉树
难点在于插入删除 因为插入或删除后 依旧需要维持它是一棵平衡二叉树
先看插入规则:
- LL 左左 1->2 右单旋转
- RR 右右 -1->-2 左单旋转
- LR 左右 1->2 先左旋 再右旋
- RL 右左 -1->-2 先右旋 再左旋
来通过题理解
先右旋
然后再左旋
注意几点:一般这个二叉树本身是二叉排序树 然后再构造平衡二叉树
左右子树高度差 而不是左右子树结点数差
构造过程本质就是插入的过程
还不懂?再来一道从头构建的题目理解
再看平衡二叉树的删除
操作本质就是插入的操作
删除后看如何调整二叉树继续维持成平衡二叉树
我们通过一道题来理解
其他不再做例子 其实删除本质也跟插入的操作一样 采取插入的规则进行调整 使其维持平衡二叉树
3.红黑树
这是一个难点 后续用一篇文章结合代码来专门研究这个
参考:https://www.cnblogs.com/gaodiyuanjin/p/20072531
4.B(+)树
重点了解B树
定义:自平衡的m路查找树 所有叶结点位于同一层次
同样可为空树 m阶B树
- 每个结点至多m棵子树 至多包含m-1个关键字
- 任意结点子树个数=其关键字个数+1
- 除根结点和叶结点外至少有 向上取整m/2棵子树 至少关键字向上取整m/2-1
- 所有叶结点位于同一层次
- 结点中的关键字从左到右递增
- 根结点不是叶结点的话 至少2棵子树 至少包含1个关键字
具体构造的样子我们在后面插入和删除来看
先来看看算B树结点数
通过上述公式可得最小/大高度 或求出最多/少结点
注意公式中的n是关键字个数 不是结点数
还有就是求出来的高度不包括叶结点所在的那一层
看题
任意结点子树个数=其关键字个数+1
这道题目就可以套公式了
得出高后 还有关键字数限制
磁盘存取次数和B树的高度密切相关 注意一点B树高度不包括最底层外部叶结点所处那一层 所以-1
通过这道题也可以看出B树的一些性质
最多包含3-1=2个关键字 任意结点子树个数=其关键字个数+1
根结点至少2棵子树至少1个关键字
除根叶结点外至少向上取整3/2=2棵子树 至少含关键字向上取整3/2-1=1个关键字
前面已经含沙射影的介绍了B树的查找
两个操作:
- 1.在磁盘上找目标结点
- 2.在内存中的结点内查找关键字
磁盘查找次数即B树高度就是决定查找效率的关键因素
其实关于插入删除就一个核心-关键字数的变化:

B树的插入:

插入后关键字数没超过m-1那就好说 超了就需要分裂
分裂
上述操作都是在关键字插入后进行的
这个过程清楚弄懂插入然后关键字数超了分裂的情况
B树的删除:
这个操作比较复杂
核心就是关键字个数不少于下限

- 换位:删除后 父子换位 兄弟父节点调整
- 合并:删除后 一个兄弟结点及父节点分隔它们的关键字合并
搞懂下面这道题就行
注意这里合并的就是 左/右兄弟结点+父结点中分隔它们的+剩下的结点
换位容易理解
这里的操作都是关键字插入前
插入删除构造的过程本质类似还是二叉排序树
至于B+树简单阐述一下:
- n个关键字结点有n棵子树
- 关键字数量范围不同
- 关键字存储位置不同
- 结点功能不同
- 支持顺序查找
还有就是查找无论成功与否 每次查找都对应一条从根结点到某个叶结点的路径
因为涉及不多不过多阐述
三.散列表(哈希表)
将关键字映射到其对应存储地址的函数
我们还需要弄懂几个概念:
- 冲突:两个或两个以上的不同的关键字映射到同一地址
- 同义词:发生冲突的不同关键字
散列表查找的时间复杂度为O(1) 与表中元素的个数无关
先看散列函数的构造:
- 直接定址法:H(key)=a*key+b 线性 适合关键字分布基本连续 简单不会产生冲突
- 除留余数法:H(key)=key%p 常用 选择一个不超过散列表长m且尽可能接近m的指数p
- 数字分析法:使用进制数码 适用于已知且固定的关键字集合
- 平方取中法:取关键字平方值中间几位作为散列地址 用于关键字取值分布不均或位数少
任何散列函数都无法绝对避免冲突
处理冲突的方法:
- 开放定址法
- 拉链法
我们先看开放定址法 其实就是应用散列表中所有空闲地址
又可以分为:
- 线性探测法(线性探测再散列法):就一个一个往后找空闲 容易发生聚集(堆积)现象
- 平方探测法(二次探测法):增量序列 1² -1² 2² -2²... 无法探测所有位置
- 双散列法:使用两个散列函数 第一个若冲突就用第二个函数计算增量
- 伪随机序列法:增量序列是一个伪随机数生成器产生
注意:采用开放定址法 不能直接物理删除表中已有的元素 而是逻辑删除
拉链法 就是将同义词组织成一个链表
适合频繁插入删除场景
几道题目看一下应用:
拉链法的应用
注意按照概念是错误 应该是H(key)%17
再看一下关于ASL的查找成功失败分析
三步走:
- 1.表长 画出来
- 2.找对应函数位置
- 3.解决冲突
顺带配合之前综合总结
注意看p 就算范围那些的比较次数
再配合看一个删除 注意是逻辑删除
还有一个补充:装填因子

一道综合题:
这道题目关键是看清楚解决冲突的时候始终是从H0开始加的
还有就是查找一个关键字失败就是定位到空位置说明失败了
先看第一题整理出来
然后第二题就是带进去找
(14*3)%11=9 3
(9+1)%11=10 18
(9+2²)%11=2 14 所以序列为{3,18,14}
第三题
综上有个小疑问关于解决冲突的方式是

注意是让得到的地址去加 而不是关键字去加 这是概念紧扣 即使答案正确 所以上述有一些题目解法并非是正确的 但可以做出来
按照大纲其实还应该有:字符串模式匹配 后续专门出一篇补充参考https://www.cnblogs.com/gaodiyuanjin/p/20094810























































浙公网安备 33010602011771号