查找:在数据集合上寻找满足某种条件的数据元素的过程称之为查找
查找表(查找结果):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成
关键字:数据结构中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。
也就是说你查到哪个数据结构,那么这个数据结构记忆是你的查找表。关键字是唯一的,需要可区分性,唯一性;
如看下图:
对查找表的常见操作
1.查找符合条件的数据元素
2.插入 删除某个数据元素
只需要进行操作1的是静态查找表。两者都要进行操作的是动态查找表
如下图:
评价指标
查找长度–在查找运算中,需要对比的关键词的次数称之为查找长度
平均查找长度(ASL)–所有查找过程中进行关键字的比较次数的平均值
公式如下:
举个例子:二叉排序树的成功/失败的ASL
ASL的数量级反应了查找算法的时间复杂度.
顺序查找,又叫线性查找,通常用于线性表
算法思想:
从头到脚挨个查找(或者反过来欸个查找)
如下图:查找43,逆从头开始到尾部查找就行或者反过来
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);
return i==ST.TableLen?-1:i;
}
类似算法 :哨兵算法
查找成功:
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);
return i;
}
查找效率分析
基于这个算法的效率,这个算法是否可以优化呢?
对表进行有序排列,再进行查找,如下图:
由此引申了查找判定树,上图的查找判定树如下
另外一种优化方法:当你的每一个结点的查找概率不一样的时候,按照概率由大到小排序。
这样成功的几率会增大。
折半查找,又称之为二分查找,仅仅适用于有序的顺序表。
设置两个指针,分别指向数组的头部与数组的尾部,在设置一个指针指向前两者除以二取整的地方。
然后三个指针分别指向了最大值,最小值,中间值。然后拿现在查找的数字和中间值比较,如果大于只能在右边,设置low=mid+1;如果小于,只能在左边,设置high=mid-1;
重复上面过程直到查找成功low=high=mid=该查找的数值。
如果low=high还没有查找成功,那么下面就会变成low>high,查找失败.
具体过程如下图。
,
上面的例子是查找成功的例子,那咱们看一看一个查找失败的例子;
以上就是算法的基本思想。
代码实现:
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;
}
如果当前low和high之间有奇数个元素,则mid分隔后,左右两部分元素个数相等
如果当前Low和high之间有偶数个元素,则mid分隔后,左半部分比右半部分少一个元素
这样的话就有这么一个规律:如下
做个练习按照上面的规律:画出对应的折半查找判定树:
由以上发现:折半查找判定树一定是个平衡二叉树
直接举个例子:如下
对于查找成功的例子,对于上图一共要进行四轮查找。对于失败则最多要五轮
“索引表”中保存每个分块的最大关键字和分块的存储空间
特点:块内无序,块间有序
分块查找,又称之为索引顺序查找,算法过程如下:
1.在索引表中确定待查记录所属的分块(可顺序,可折半)
2.在块内顺序查找(块内是乱序的)
typedef struct{
elemtype maxvalum;
int low,high;
}index;
elemtype list[100];// 实际存储元素的储存结构;
用折半查找索引:
看这样一个索引表,怎么利用折半查找呢?
首先第一类就是查找的数是在索引表有的,比如上面索引表中有的有10 20 30 40 50;待查找的数就是30在索引表中有。利用之前的折半查找是一定能返回对应的表格的。然后再到对应的表中顺序查找。
第二类就是查找的数不在这个索引表中,但是在表内一定是有的。这样他在索引表中一定会不断查找到失败为止,但是这个失败左后的指针都指向索引表内,也就是说最后失败的点就是所查找的数值在对应的表里。
第三类是查找失败了,在索引表中也是不断查找,但是最后指针的指向超出了索引表,那就是说明要查找的数值不在表内;
有什么弊端,当你插入一个删除一个元素的时候,你的索引表相应的也会做出相应的调整,这样的话,就意味着要花费掉较大的开销;这是我们不能后接受的
但是我们可以通过改变储存结构来解决这一问题:
回顾:二叉查找树
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
struct Node{
elemtype keys[4];
struct node *child[5];
int num;
};
图如下:
我们来看一个查找成功的例子;
从第一层开始查找,9<22,进入第二层第一个;进行比较,9>5继续查找,9<11;进入第三层第二个;分别和6,8,9比较;最后的定位在9。查找成功;
注意:如果每一层的模块里面的个数比较多,那么也是可以进行折半查找的。
我们在看一个查找失败的例子;
查找41;首先和22比较,大于进入第二层第二个;进入第二层开始比较,和第一个首先和36比较大于,再和45比较小于,进入第三层,如下图;开始和40比较,再和42比较。如果比较不成功;则进入退回进入失败节点;
如何保证查找效率?
若每个结点内的关键字太少,导致树变高,要查更多层结点,效率低;
咱们来看一看有什么解决办法;我们规定一个策略:
也就是说,对于5×排序树,规定除了根节点外,任何结点都至少有三个分叉,2个关键字;
那么我们看下面这棵树:你觉得合理么;显然不合理,但他满足了上面的要求,那么接下来看看他那里不合理;
那就再加一个要求:
如果我满足了这两点策略,那么我就可以称之为这颗树为B树;来来看看他的定义:
咱们压缩一下特性:
新元素一定是插入到最底层“终端节点”,用“查找”来确定位置
在插入key后,若导致原结点关键字超过上限,则从中间位置将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放在新节点中,中间位置的结点插入到原结点的父节点。若此时此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直到这个过程到根结点为止,进而导致B树高度增1;
若删除的关键字是在终端节点,则直接删除该关键字(要注意结点的关键字的个数是否低于下限)
若删除的关键字是在非终端结点,则用直接前驱或直接后继代替被删除的关键字:
直接前驱:当前关键字左侧指针所指子树中最右下的元素
直接后继:当前关键字右侧指针所指子树中最左下的元素
说白了对非终端结点关键字的删除,其实最后必然转化为终端结点的删除操作
兄弟够借。若被删除的关键字所在的结点删除前的关键字个数低于下限,且此节点右(或左)兄弟节点的关键字数还很宽裕,则需要调整该结点,右(或左)兄弟结点(父子换位法)说白了,就是当右兄弟很宽裕的时候,用当前结点的后继,后继的后继来填补空缺。
当左兄弟很宽裕的时候,用当前节点的前驱,前驱的前驱来填补空缺。
兄弟不够借用。若被删除的关键字所在结点删除前的关键字低于下限,且此时与该节点相邻的左右兄弟结点的关键字个数均复合最低要求,则将关键字删除后与左(或右)兄弟节点以及双亲结点中的关键字进行合并
我们来看一看这个图m阶B+树的性质:
1.每个分支结点最多有m棵子树(孩子结点)
3.结点的子树个数与关键字个数相等(对于B树来说,子树个个数会比关键字多一个)
4.所有叶子结点包含全部的关键字以及指向相应的记录的指针,叶子节点中将关键字按照大小顺序进行排序,并且相邻叶子结点按大小顺序连接起来,也支持顺序查找。
5.所有分支结点中仅仅包含他的各个子节点中的关键字的最大值以及指向其子节点的指针
查找成功:从上到下一一比较,直到比较到最后一层的时候。才算彻底找到,在其他层找到不算,因为他和B树的差别还是有的。
查找失败:
依然是一层一层到最后一层,直到在最后一层没找到他的结点就算失败。应为结点是顺序排列,所以说到最后来说如果找到比他大的之前找到该节点,如果没找到就说明该节点查找失败。
无论是查找成功还是查找失败,最终都要走到最后一层;
顺序查找,你看到那个指针没的,可以通过指针进行链表的顺序查找
1.在前者n个关键字对应n个子树,在后者n个关键字对应n+1个子树
2.
3.在B树中,各个结点中包含的关键字是不重复的。在B+树中,叶结点包含全部关键字,非叶结点中出现过的关键字也会出现在叶结点中。
4.在B+数中,叶结点包含信息,所有的非叶结点仅仅起到索引作用,非叶节点中的每个索引项只含有对应子树的最大关键字和指向子树的指针,不含有该关键字对应记录的存储地址;而在B树中,B树的结点都包含了关键字对应的记录的存储结构
操作系统文件管理学完之后,看一下这一章节;能了解B活着B+树的本质区别;
散列表,又称为哈希表。是一种数据结构。
为什么用这种数据结构呢 ,他的作用是什么呢?
原因是数据元素,即数据元素的关键字往往要储存地址相联系。类似于一种映射关系
而用哈希表来储存形成两者对应
就比如咱们看一个例子:
从这个例子咱们看到数据元素的关键字,应该储存在哪儿呢,具体每一个储存地址应该在那儿呢?咱们用哈希函数就把这个关系找出来了,看上图右边,咱们看他的计算得到对应的存储地址;一一计算你会发现,下面两种现象:
1.若不同的关键字通过散列函数映射到同一个值,则称之为同义词
2.通过散列函数确定的位置已经存放了其他元素,则称之为这种请况为冲突
用拉链法/链接法/链地址法处理冲突:把所有同义词存储在一个链表中
如下图
所谓开放定址法,是指可存放新表项的空闲地址既向他的同义词开放,又向他的非同义词开放。其数学递推公式为:
其di 有以下三种方法:
如果第0次确实冲突了:则
具体存放过程看王道视频(查找09)
对应结构哈希表的查找:
同义词,非同义词都要被检查,也算作查找长度
空位置的判断也算作一次比较 ,即长度(拉链法没有这样做)
越早遇到空位置,就可以越早确定查找失败
怎么查找一个特定字,以及他的查找长度:
首先咱们利用哈希函数球的对应的地址空间,然后再对应地址空间中的链表里面进行一一对比,如果对出相等就称之为这个这个查找到了,对于查找长度而言,空指针不算做查找一次,所以说查找长度就是对比次数的总和咯
平均查找长度呢?
看:
最理想情况:
查找失败:
13代表表长度为13,每次查找失败的概率是相同的。
装填因子,公式如下,其等价查找失败的平均长度。装填因子回直接印象散列表的查找效率。比如:对于查找失败,他是公式相等的,就相当于是同比影响的,再者说对于查找成功来说,装填因子越大,说明装填越多,就会导致冲突越多,就意味着表长越长,就意味着访问的时候开销越大。
你仔细看这个,直接定地址 或者线性的平移 ;其中a,b是常数。这种方法简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布基本不均匀,空位较多,则会造成储存空间上的浪费。
看一个例子:
质数:又称之为素数。指除了1和此整数自身外,不能被其他自然数整除的数;反之称为合数;
比如如下取散列表:
为什么用这个取一个不大于m但最接近或等于m的质数呢
为了让不同关键字的冲突能够最少;咱们才这么做;但是呢实际情况并不是绝对的,只是综合来说咱们遇到的数更符合使用最接近或等于m的质数这一选择。
比如如下:
当关键字是连续自然数时候:
当关键字是偶数是时候:
那到底为什么:《数论》中说,用质数取模,分布更均匀,冲突更少;
建议:虽然咱们考试是如此取值,三十具体散列函数的设计要结合实际关键字的分布特点来考虑,不要教条化;
选取数码分布较为平均的若干位作为散列地址
设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率也不一定相同,可能在某些位上分布更均匀一些,每种数码出现的机会均等;而在某些位置上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址。这种方法适用于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
平方取中法:取关键字的平方值的中间几位作为散列地址。
具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布较为均匀,适用于关键字的每位取值都不够均匀或小于散列地址所需要的位数;
例如:
但是这样任然是有冲突的,那么咱们有什么办法绝避免冲突。有是有,就是给存储空间么。存储空间够多就能够了。如下