文章目录
查找
查找表
用于查找的数据结构。一般为数组或者链表。定义为结构体,用来存储相同的数据。
//定义查找表结构体
typedef struct
{
int *data;
int TableLen;
}SSTable;
key是唯一标识值
查找的对象是唯一的,这意味着,在查找的时候不考虑重复值的情况。
ASL
平均查找长度:所有查找过程中进⾏关键字的⽐较次数的平均值。
1,默认概率是相等的,实际就是比较总次数/元素数。
2,也有一些题会给不同的概率,按公式计算即可。
3,ASL的计算还分为查找成功和查找失败两种。
顺序查找
思路
暴力搜索,从头到尾遍历数组,找到该元素就返回元素下标,否则返回-1。
代码
1,不带哨兵,待排数据存在data[0]-data[n-1],从前向后遍历,需要多一步比较防止数组越界。
//定义查找表结构体
typedef struct
{
int *data;
int TableLen;
}SSTable;
//查找
int Search_Seq(SSTable* L, int key) {
int i;
for (i = 0; i < L->TableLen && L->data[i] != key; ++i);//需要比较i < L->TableLen ,防止数组越界
return i == L->TableLen ? -1 : i;
}
2,带哨兵,data[0]存哨兵数据,待排数据存在data[1]-data[n],从后向前遍历,如果没有找到最后会回到哨兵处,返回0;少一步比较
//查找
int Search_Seq(SSTable* L, int key) {
L->data[0]=key;
int i;
for (i = L->TableLen; L->data[i] != key; --i);//需要比较i < L->TableLen ,防止数组越界
return i;
}
效率
查找成功:如果是从前向后遍历,第一个元素遍历1次即可找到,第二个遍历2次,依次类推ASL成功=(n+1)/2
查找失败:从头向后遍历完序列,直到遇到哨兵,对比n+1次。
则顺序查找的时间复杂度为0(n)
链表实现
//定义查找表结构体
typedef struct SSTable//必须先加SSTable,否则next指针无法使用
{
int data;
SSTable* next;
}SSTable;
int Search_Seq(SSTable* L, int key) {
SSTable* p = L->next;
if (p != NULL) {
while (p != NULL && p->data != key) {
p = p->next;
}
}
return p == NULL ? -1 : p->data;//p必须为全局变量,返回的是值不是下标
}
优化:有序序列
思路:从前向后对有序序列查找,如果遇到一个大于key的元素,说明后面所有的元素都大于key,则无需继续查找。
int Search_Seq(SSTable* L, int key) {
int i;
for (i = 0; i < L->TableLen && L->data[i] != key; ++i) {
if (L->data[i] > key)
return -1;
}
return i == L->TableLen ? -1 : i;
}
效率(查找判定树)
可见优化主要是在查找失败的时候体现。
优化:概率不相等序列
1,ASL的计算,对比次数乘以对应的概率
2,查找概率大的放在序列靠前位置,能提升查找成功的效率,但是同时查找失败的效率会降低。
折半查找
思路
仅适合有序的顺序表(数组);key与序列中间位置元素对比,等于返回key;小于查找左子表,大于查找右子表。直到找到key或者low>high,找不到返回-1。
代码
//定义查找表结构体
typedef struct
{
int* data;
int TableLen;
}SSTable;
//折半查找
int Binary_Search(SSTable* L, int key) {
int low = 0, high = L->TableLen - 1, mid;
while (low <= high) {
mid = (low + high) / 2;
if (L->data[mid] == key)
return mid;
else if (L->data[mid] > key)
high = mid - 1;
else
low = mid + 1;
}
return -1;
}
效率
查找判定树
构建查找判定树(非叶节点为查找成功情况,叶节点为查找失败情况)
快速构建方法——根据序列元素的奇偶数。
右子树节点数>左子树节点数=0 or 1
性质
1,一定是一棵平衡二叉树(折半查找的性质,每次都分为两半来找)
2,判定树,只有最后一层不满,类似于完全二叉树(只是节点优先在右边)
树高为h,则最多
2
h
−
1
2^{h}-1
2h−1个节点,最少
2
h
−
1
2^{h-1}
2h−1个节点(上一层最多
2
h
−
1
2^{h-1}
2h−1-1个,加一个就是本层最少的节点数)最终可以计算出判定树的树高。
3,是一棵二叉排序树(左<根<右),且关键字节点数为n,失败节点数为n+1。
4,ASL<=h,则查找的时间复杂度为O(
l
o
g
n
log^{n}
logn)
5,如果换成向上取整,则判定树变为左子树节点数>右子树节点数=0 or 1
二叉排序树
代码实现
查找
插入
构造
删除
1,没有子树,直接删除
2,只有左或者右子树,让左右子树根节点代替该节点。
3,有左右子树,按中序遍历,直接前驱(或后继)节点替代该节点,然后删除前驱或后继(代替之前的)
效率
与二分查找(折半)
相同点:二叉排序树与二分查找的查找判定树的查找方式类似。
不同:
1,对于同一个序列,二分查找判定树固定(查找方式始终一样);二叉排序树则可以构造为不同形态
2,逻辑结构不同,二分查找是采用顺序表进行,插入删除要移动大量元素(O(n)),适合静态查找;二叉排序树则是采用指针链表进行,移动删除元素较少,适合动态查找。
平衡二叉树
插入节点
插入一个新节点后,整个书上所有节点都可能受到影响。从插入点开始向根节点找到第一个不平衡的节点,也就是最小不平衡子树,对其进行调整。
效率
只需要记得平衡后平均查找长度是O(
l
o
g
n
log^{n}
logn)。
查找长度与树的深度挂钩。对于二叉树每层节点肯定是大于等于1;则h<=n。小于n这个级别的也只有
l
o
g
n
log^{n}
logn和常数级。则查找长度最坏是O(n),平均是
l
o
g
n
log^{n}
logn这个级别的。
分块查找(索引顺序查找)
思路
存储结构
实际顺序存储在数组中;按块存储,块内无需,块与块之间有序(后面的块中元素均小于前面的块)
索引表结构体中存储每个块的最大元素,以及块的范围(数组下标)。
//索引表
typedef struct{
ElemType maxvalue;
int low,high;
};
//实际数据
ElemType List[100];
查找思路
1,对模板元素key,先在索引表中查找,找到第一个大于等于key的元素(顺序查找或者折半查找)。
2,找到了就在该元素对应的块内顺序查找(块中无序)。
对于折半查找而言:
若索引表中有该元素,则在mid指向处找到。
若索引表中没有该元素,则在low>high后,在low指向的块中查找(指向大于该元素的最小元素)
若low最后超出了索引范围,则说明查找失败,索引表所有元素都小于key。
效率(一般不考)
1,给一个序列,手动求ASL
2,给n长度序列,分为b块,每块s个元素,求ASL
B树
基本概念
推导
B树是在m叉查找树基础上发展而来。对于m叉查找树,每个节点内至少1个关键字2个分叉,最多m-1个关键字m个分叉。且在节点内关键字有序排序。
但是查找树存在两个问题,如果每个节点内的关键字太少,会导致树变高,查找更多层效率低。因此要保证每个节点内的关键字最低数量为⌈m/2⌉-1。
同时左右子树高度不同(不平衡)也会增加查找的时间。因此要求任意节点左右子树高度相同。
基本性质
满足以上两个条件的m叉查找树就是B树,也就是m阶B树。
高度
1,下限
使每个结点装满,m-1个关键字,m个分叉。
第一层有1个节点,m个分叉
第二层有m个节点,
m
2
m^{2}
m2个分叉
则第h层有
m
h
−
1
m^{h-1}
mh−1个节点
节点求和并乘以m-1(每个节点的关键字个数)得到了关键字数的上限。由不等式求高度上限。
2,上限
从叶子节点出发求解
从总的最少关键字个数出发求解
插入与删除
插入
1,插入后满了,要执行分裂
2,新元素插入要沿着指针插在最下层。
3,插入后满了,分裂时,中间位置节点往上层插;插入的位置就是从左向右遍历,插在第一个大于该元素的元素前面即可。
4,插入后父节点满了,则对父节点分裂,直到根节点为止。
5,总结
删除
1,在终端节点内删除,直接删即可。
2,对非终端节点删除后,用直接前驱或后继代替(实际就是对终端节点删除)
3,删除后节点内关键字低于下限,从兄弟节点中借调
右兄弟:先把直接后继(父关键字)下移插入到该节点中;然后将右兄弟节点中最小关键字(最左边)上移代替父关键字位置。
左兄弟:先把直接后继(父关键字)下移插入到该节点中;然后将左兄弟节点中最大关键字(最右边)上移代替父关键字位置。
4,删除后,兄弟不够借;合并兄弟节点以及双亲节点关键字。
双亲关键词下沉合并后,若导致上层节点(非根节点时)低于下限,则继续合并上层,直到满足要求。
B+树
特性
应数据库而出现的一种B树的变形树。
分块查找和B+树都有索引这个概念,分块查找是在索引表中存放每个分块的区间和最大关键字。B+树索引中只存储下一索引的最大关键字和地址,通过树结构连接到叶子节点,最终通过叶节点指向数据。
特征
B+树追求绝对平衡
B树与B+树
1,主要差异点
2,查找不同
B+树的查找,最终都会到叶子节点。非叶子节点不会指向数据。因为非叶节点是在外存(磁盘)中存储,每次查找需要存入内存中执行。如果不存储数据的地址,使得每次传入内存空间的节点更多,减少磁盘存取次数(B+树阶更大,更矮,提高查找效率)。
B树则可能在任何一层查找成功,因为每个节点都存储了数据的地址。
3,子树不同
B树,每个节点的n个关键字,被分割成n+1段。
4,关键字数目不同
B+树有m个分支,对应m个关键字。
B树有m个分支,对应m-1个关键字。
6,关键字是否重复
6,数据存储结构
最终由叶节点执行数据地址。
关键字地址分布于各节点
7,应用
散列查找(哈希查找)
概念
1,散列表(哈希表):根据关键字直接进行访问的数据结构,建立关键字与地址的直接关系。
2,散列函数:关键字输入函数,输出地址。将关键字映射到地址上。
存储:关键字输入函数,获得地址,将关键字存入地址中。
查找:输入要查找的关键字,获取地址,在改地址中找关键字,理想情况下查找速度为O(1)。
查找效率
散列函数
不同的散列函数映射的地址分布状况也不相同,好的散列函数可以均匀分布地址,减少冲突。并尽可能简单,快速计算结果。由此散列函数影响查找效率
冲突处理方法
理想情况下(没有冲突)查找长度为O(1);如果遇到冲突,就会回到对比元素大小的情况,如果冲突越多,ASL就会变大。ASL=每个元素查找次数/序列长度
装填因子
装填因子:衡量散列表的利用率;值越大说明装的约满,再插入新元素,或者查找的话冲突可能性越大。计算每一个格子内的元素数量,然后除以格子数。
衡量效率的因素:散列函数、处理冲突的方法和装填因子。
散列函数
直接定址
除留余数
质数的公因子最少,除的时候不会集中分布。比如关键字多为偶数,用偶数除,导致地址都分布在偶数上。用质数则会尽可能的均匀。
数字分析法
比如手机号,身份证号,可以用后四位进行计算。结果分布均匀一些。如果用前四位则会造成大量重复。
平方取中
平方后,中间几位是每一个元素都参与了相乘(影响了结果);由此取中间结果比较均匀。
空间换时间
散列函数合理尽可能的均匀,则列表越长,冲突概率越低。
处理冲突的方法
不同的关键字被映射到同一个地址,由此引发了冲突;必须对冲突进行处理。
拉链法
原始数列存储指针,初始都指向NULL;存入数据就通过指针构建链表,如果遇到同义词就依次插入链表中。
效率
1,查找长度:
如果链表为空,不算元素之间的比较,查找长度为0;如查找21,8中为空直接结束,查找长度为0。
链表不为空从链表第一个元素的对比计算,如查找27,在链表中和14 1 27对比后找到,查找长度为3。查找66,对14 1 27 79对比后查找失败,长度为4。
2,平均查找长度
ASL=每个元素查找次数/序列长度
小优化
开放定址法
开放表中空闲地址给同义词使用;可能造成同义词和非同义词的集中。
主要有三种方法。
线性探测法
每次冲突都像下一个单元探测,1与14冲突,向后探测,插在14后面。
可能会造成不断向后查找,直到遇到第一个NULL,如79要从1一直找到9才能插入。
哈希函数和线性探测的范围可能不同,哈希函数确定的初始范围,线性探测是在初始冲突后调整的范围。
查找
查找的时候,没有被挤占,就很快
如果被挤占了,则同义词、非同义词也可能都要查找。
查找失败,要一直比较到NULL处,且空位置的比较也算一次。
删除
直接删除,可能造成后面查找出现错误(直接判定为失败)
删除后在原位置加一个标记。
也可能造成空间的浪费
效率
逐个分析[0,12]的查找失败的对比次数。
关键字映射到0处,对比1次失败
关键字映射到1处,要遍历到13才失败
。。。。
求和,除以关键字个数即可。
平方探测法
关键字遇到冲突后d按平方加正负取值。如下,本应都存在6处。冲突后按平方探测处理H=(H(key)+d)%m;注意这里是对表长取模,不是对序列数取模。
19:(6+1)%27=7
32:6和7都冲突,再计算H=(6-1)%27=5
查找的时候也是按H的计算方法,如71依次查找6 7 5 10 2 最后再15找到。
表长应该为4J+3的素数
伪随机数
按伪随机数随机放置;如19和6冲突,取随机数5;往后移5格(6+5=11)到11处。
删除都要标记
开放定址法删除都要标记。
再散列法
用多个散列函数进行处理,当函数计算地址冲突的时候,用下一个函数计算新地址。