查找的概念
查找 —— 在数据集合中寻找满⾜某种条件的数据元素的过程称为查找
查找表(查找结构)—— ⽤于查找的数据集合称为查找表,它由同⼀类型的数据元素(或记录)组成
关键字 —— 数据元素中唯⼀标识该元素的某个数据项的值,使⽤基于关键字的查找,查找结果应该是唯⼀的。
查找的分类
1.静态查找表:仅查询,不改变内部结构
2动态查找表:往查找表中插入、删除元素等
学习的关键:查找的方法取决于查找表的结构,即数据元素是以何种关系组织,因此学习目的则是在于确定查找表怎么存、怎么加约束关系以及怎么提高查找效率
查找算法的评价指标
查找长度——在查找运算中,需要对⽐关键字的次数称为查找⻓度
平均查找长度(ASL, Average Search Length)—— 所有查找过程中进⾏关键字的⽐较次数的平均值,分为查找成功的ASL和查找失败的ASL
ASL的数量级反映了查找算法的时间复杂度
折半查找、顺序查找和分块查找
顺序查找
定义:顺序查找,⼜叫“线性查找”,通常⽤于线性表,算法思想:从头到 jio 挨个找(或者反过来也OK)
应用范围:顺序表和链表,且不要求元素间的有序性
一般线性表的顺序查找
一般思路
注:每执行一次循环需要进行两次判断——1.当前元素是否为所查元素2.是否越界
改进思路(引入哨兵)
引入哨兵的优点:无需判断越界,当循环到了哨兵处刚好可以判断查找失败,返回了哨兵所在位置(0位置)
查找效率分析
有序表的顺序查找
定义:如果在查找之前就已经知道表是有序的,则对于查找失败的情况则不用比对到表的最后一个元素就可以判断查找失败,所以说降低了顺序查找失败的ASL
有序表的查找过程
圆形节点为表中存在的元素,方形结点为失败结点,若有n个结点,则有n+1个失败结点,若查找到失败结点,则查找失败
再次注明:有序表的线性查找只是降低了一般线性表查找的查找失败的ASL,对于查找成功的ASL,二者是一样的
总结
折半查找
定义:又名二分查找,仅适用于有序线性表
算法实现
折半查找判定树(计算ASL)
(一)判定树构造方法
1.若low和high之间有偶数个元素,经mid分隔后,左半部分比右半部分元素个数少1
2.若low和high之间有奇数个元素,经mid分隔后,左右部分元素个数相等
结论
1.折半查找判定树中,若mid=下取整(low+high)/2,则对于任何一个节点必有:左子树结点数-右子树结点数=0或1,注:若mid=上取整(low+high)/2,则情况相反
2.每个成功结点的查找次数为其路径长度,每个失败结点的查找次数为其父结点的路径长度,原因是在while循环处就退出了,就不会进行下一次比较自身的操作
(二)判定树的一些性质
折半查找效率
总结
分块查找
定义:又叫索引顺序查找,吸取了折半查找和顺序查找的优点,既有动态结构,又适合于快速查找
基本思想:将线性表分为若干子块,块内元素可以无序,块间必须有序(也就是第一个块的最大关键字小于第二个块中所有记录的关键字),再建立一个索引表,索引表中每个元素含有各块的最大关键字和块中第一个元素的地址,索引表按关键字有序排列
查找过程:1.确定被查元素属于哪个块(可顺序可折半) 2.在块内进行顺序查找
索引表
两种特殊情况
1.被查元素通过索引表本身查询不到,则要最后在查一下low所在块
2.若折半查找过程中low来到了索引表界外则直接指明查找失败
查找效率分析
分块查找的平均查找长度=索引查找长度+块内平均查找长度,一般只讨论分块查找的查找成功的ASL,查找失败的ASL有点复杂,暂不做总结
注:Ci为查找索引表次数+块内查找次数
也就是说,把n个元素分成根号n块,每块根号n个元素这种情况下ASL最小
注:若线性表有序,则对索引表和块内查找都用折半查找显然是最快的
总结
二叉排序树(BST)
二叉排序树定义
二叉排序树的查找
递归实现与非递归实现
二叉排序树的插入
二叉排序树的构造
二叉排序树的删除(3种情况)
二叉排序树的效率分析
注:最好情况就是二叉排序树为平衡二叉树的情况
总结
1.二分查找的判定树唯一,二叉排序树的查找不唯一,相同关键字其插入顺序不同可能生成不同的二叉排序树
二叉平衡树(AVL树)
定义
为了避免树高度增长过快(因为树越高查找效率越低),降低二叉排序树性能,规定在在插入和删除二叉树结点时,要保证任意节点的左右子树高度差绝对值不超过1,空树也可以叫平衡二叉树
平衡因子:左子树与右子树的高度差为平衡因子,平衡二叉树结点平衡因子只可能是0、1、-1
插入操作
插入操作主要有以下四种情况
针对LL和RR的总结(A指的是最小不平衡子树)
LL:A左右上旋代替A,A右下旋成B右,B原右成A左
RR:A右左上旋代替A,A左下旋成B左,B原左成A右
注:在进行LR和RL旋转时,新结点究竟是插入C的左子树还是右子树不影响旋转过程,上图是以插入C的右子树为例的
查找效率分析
总结
B树(B-树)
回顾二叉排序树,我们可知二叉排序树的实质其实是通过每个结点将当前区间分为2部分,左子树是小于它的部分,右子树是大于它的部分,每个结点中只包含一个关键字和它所分的两个区间的指针,那么我们想一想能否增加结点的关键字的个数从而使它将每个区间分的更细些,由此就引出了B树的概念,二叉排序树我们可以看出它属于二路查找(就两个区间),而B树则就对于多路查找(因关键字的个数增加从而使区间变得更细化了,从而延伸出多条路供我们去查找)
图一
1.就如上图这个5叉查找树,之所以叫5叉是因为每个结点最多能有5个分支,那么每个结点的关键字有多少个呢?答案是分支数-1,因为是关键字将一个区间分成了若干份(也就是若干分支),而显然一个关键字可以使某个区间被一分为二(产生两个分支),所以n-1个关键字自然可以将某个区间分为n份(n个分支,这里n为5),所以上图中的每个结点关键字最多为4个,且结点内关键字有序排列,可升序可降序,其次图中的“失败结点其实也是对应于二叉排序树中的失败结点,只不过二者的区别在于我们可以发现在B树中失败结点(B树中将失败结点称叶子结点,叶子结点上一层的结点叫终端节点)永远位于最下面一层,而二叉排序树却没有这么“苛刻的条件”,那么为什么都在最后一层呢?原因是由于平衡因子的问题,请看下文介绍
如何保证一个树的查找效率
2.上图我们可以发现他每个节点中的关键字有多个,看起来比较“特殊”,那么我们现在如果假定回到我们之前二叉排序树那种规定,每个结点只有一个关键字,如下图
3.我们发现,才填了这么几个关键字就已经到了图一的高度了,如果再限制每个结点只有一个关键字且将所有关键字填入树中,我们不难想象它一定是一棵细高细高的树,而之前我们直到查找效率是由树的高度决定的,树高越高查找效率越低,所以我们就要想办法增加每个结点中的关键字的个数,让他看起来不至于那么高,所以引出了性质1:m叉查找树中,规定除了根节点外,任何结点⾄少有⌈m/2⌉个分叉,即⾄少含有⌈m/2⌉ − 1 个关键字,那么我们再看下面这种情况
4.它满足了性质1的要求,但为啥看起来还是辣么高?原因是它还不够平衡,只有满足了绝对平衡且满足性质1的前提下才可以构造出一棵比较矮的树,这里提到的绝对平衡指的是任何一个节点的所有子树高度均相同,这个条件是比较苛刻的了,这就意味着每个结点平衡因子都为0,而即使在二叉平衡树中平衡因子还可以为-1、0、1,这就意味着B树其实可以叫多路平衡查找树,由此我们正式引出B树的定义
5.由上图的这五条性质我们也就明白了1.中提出的为什么叶子结点都在最后一层的问题的答案了,因为它要保证绝对平衡,从而叶子结点就必须在最后一层,再看性质3),为什么要除根节点?答:假设你只有1个关键字,当然就不能保证性质3)了
核心特性
将上图的性质压缩可最终得到以下几条核心特性
B树高度问题
B树的插入
B树的插入总共有以下几种情况
1.你要插的关键字可以直接插入某个终端节点中,且插入后不会造成性质的不满足
2.你要插的关键字可以直接插入某个终端节点中,且插入后会造成性质的不满足
注:中间位置算出来的数字不是下标之类的,就是指的当前结点的第一个关键字而已,m为阶数
当出现这种情况时,将拿出来的这个节点放进指向它的那个指针的左边
3.你要插的关键字插入后造成连锁不满足
总之一句话
B树的删除
分为以下几种情况
1.目标删除关键字在终端节点中,直接删除即可,删除后检查下限
2.目标删除关键字在非终端节点中
结论:可以看出对每一个非终端结点关键字的删除操作都可以转化为对终端节点关键字的删除的操作
由此删除终端节点关键字后可能有以下几种情况
1.若删除终端节点的某个关键字后不满足B树特性,兄弟够借
2.若删除终端节点的某个关键字后不满足B树特性,兄弟不够借
结论
用不同的调整方式删除B树中的同一个关键字得到的B树不唯一
总结
B+树
B树与B+树的区别(m阶)
1.B树结点中的n个关键字对应n+1⼦树,B+结点中的n个关键字对应n棵⼦树
2.B树根节点的关键字数n∈[1, m-1],其他结点的关键字数n∈[⌈m/2⌉-1, m-1];
B+树根节点的关键字数n∈[1, m] ,其他结点的关键字数n∈[⌈m/2⌉, m];
3.B树中,各结点中包含的关键字是不重复的;B+树中,叶结点包含全部关键字,⾮叶结点中出现过的关键字也会出现在叶结点中;
4.B树的结点中都包含了关键字对应的记录的存储地址;在B+树中,叶结点包含信息,所有⾮叶结点仅起索引作⽤,⾮叶结点中的每个索引项只含有对应⼦
树的最⼤关键字和指向该⼦树的指针,不含有该关键字对应记录的存储地址。
散列表(哈希表)
概念部分
散列表:就是一种数据元素(关键字)与其存储地址有相对关系的数据结构
散列函数:就是根据这个函数从而确定某个数据元素对应散列表的地址的方法
同义词:若不同的关键字通过散列函数映射到同⼀个值,则称它们为“同义词”
冲突:通过散列函数确定的位置已经存放了其他元素,则称这种情况为“冲突”
装填因子:关键字个数/散列表长度,理论上装填因子值越小空间费的越多,但查找速度越快,典型的“以空间换时间”
注:散列表长度是你根据实际情况自己定的一个长度,并不是说散列表多长,其中就有多少个记录(关键字),很可能空出许多空间,但为了查找效率,我们认为这种牺牲空间的做法是可以接受的,所以说散列表是一种典型的“以空间换取时间的数据结构”
常见的散列函数
除留余数法
大致思想:给定一个表长m,取⼀个不⼤于m但最接近或等于m的质数p,让每一个关键字逐个取对p取余,并将关键字放入取余后的结果所对应的数组下标中,可以发现:假设就5个关键字,我选个表长100,p选为99,那么我就可以保证5个关键字被存储的地址上只存储了他自己,这样确实不会导致冲突,但是空间牺牲太大了
tips1:若关键字比较连续则用⼀个不⼤于m但最接近或等于m的合数p比较好;如果关键字偶数居多,则用取⼀个不⼤于m但最接近或等于m的质数p比较好。总言之,散列函数的选取依据关键字分布特点而定,不要教条化
直接定址法
数字分析法
平方取中法
冲突处理方法
拉链法(☆)
具体思想就是将同义词连接成一个链表
注::在拉链法中若你要查找某个关键字,过程是通过散列函数计算出地址,然后从该地址指向的那条链表依次顺序查找即可,但要注意再算查找成功的ASL时,查找次数不因将最开始的空指针的比较算入其中
开放定址法(☆)
其主要思想就是在存关键字时如果碰到了同义词,则根据不同的增量序列的要求去检查以此地址的偏移量所在的地址上是否为空,若为空则将关键字放进去即可,所以一般情况下有以下几种增量序列
- 线性探测法
需要注意的是:
这里的Hi = (H(key) + di) % m中m指的是散列表表长,而我们在通过除留余数法中H(key)=key%p中的p是根据散列表表长所选取的一个最接近表长m但小于m的质数,二者不要搞混
用开放定址法进行查找和删除操作
需要注意的是:
我们之前说当你通过关键字算出它的地址后从此地址开始一个个检查(依照所选增量序列),当检查到空时证明不存在,但实际这样做是不对的,如下这种情况,查找27会因2位置的空导致直接结束,但27确实存在,所以在删除时不能仅仅将其结点空间置为空,这样会阻断后面查找某个节点的路径,我们只能进行“逻辑删除,也就是给他一个标记而已,再回到查找这来,当我们从算出的地址一个个向后查找时当碰见“逻辑删除标志”不应该理会,应该继续往下查找,直到遇到真正的空地址,这里的空地址肯定是因为当前所给关键字的集合存不到的位置,这时在判断查找失败才是正确的
需注意:开放定址法查找时会将空位置上的比较次数算入总比较次数之内
基于这种“逻辑删除的方式,会导致散列表看上去很满,实则很空的特点”
查找效率分析
通过以上我们可以发现,线性探测法很容易造成同义词、⾮同义词的“聚集(堆积)”现象,严重影响查找效率,因为冲突后再探测⼀定是放在某个连续的位置,由此我们介绍下一种增量序列的选取方法——平方探测法
平方探测法
各个方法的不同仅体现在增量序列的选取规则,其余无区别
通过以上我们可以发现,平方探测法的好处就在于不易造成关键字堆积,查找效率有所提升,但须注意以下小坑‘’
伪随机序列法
就是自己指定一个增量序列
再散列法
再散列法(再哈希法):除了原始的散列函数 H(key) 之外,多准备⼏个散列函数,当散列函数冲突时,⽤下⼀个散列函数计算⼀个新地址,直到不冲突为⽌