分节目录
数据结构(完结)
数据结构Part1 绪论与线性表
数据结构Part2 栈和队列
数据结构Part3 串
数据结构Part4 树与二叉树
数据结构Part5 图
数据结构Part6 查找
数据结构Part7 排序
第七章 查找
1.查找的基本概念
1.1 基本概念
查找:在数据集合中寻找满足某种条件的数据元素的过程称为查找
查找表(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成
关键字:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。
1.2 基本操作
a.查找符合条件的数据元素;b.插入、删除某个数据元素
只需要进行a操作的是静态查找表,仅关注查找速度即可
需要进行a,b两周操作的是动态查找表,除了查找速度,也要关注插入、删操作是否方便实现。
1.3 查找算法的评价标准
查找长度:在查找运算中,需要对比关键字的次数称为查找长度
平均查找长度(ASL,Average Search Length):所有查找过程中进行关键字的比较次数的加权平均值,分为查找成功的ASL,与查找失败的ASL。
2.顺序查找与折半查找(二分)
2.1 顺序查找
顺序查找又称为线性查找,通常用于线性表(数组,链表),顺序遍历整个表。
2.1.1 算法实现
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
if(i==ST.TableLen) return -1; //查找失败
else return i; //查找成功
}
//顺序查找
int Search_Seq_flag(sSTable sT,ElemType key){
ST.elem[0]=key; //“哨兵",存放在零号位置,数据从1开始存储
int i;
for(i=ST.TableLen; ST.elem[i]!=key; --i); //从后往前找
return i; //查找成功,则返回元素下标;查找失败,则返回0
}
查找成功ASL = (n+1)/2;查找失败ASL = n+1;时间复杂度:O(n)
2.1.2 算法优化
查找判定树分析ASL:
—个成功结点的查找长度=自身所在层数;
—个失败结急的查找长度=其父节点所在层数;
默认情况下,各种失败情况或成功情况都等概率发生。
当序列为有序表时:
与当前元素进行比较,判断后面是否有可能出现查找目标。
查找失败ASL=(1+2+···+n+n)/(n+1)=n/2+n/(n+1),查找成功ASL不变,时间复杂度不变。
当序列内元素被查找概率不等时:
被查概率大的放在靠前位置。
查找成功ASL = Σ(i*pi),查找失败ASL不变,时间复杂度不变。
2.2 折半查找(二分)
折半查找,又称“二分查找”,仅适用于有序的顺序表。检查中间位置的元素与所查找元素的大小,判断在左侧还是右侧。
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//折半查找
int Binary_search( ssTable L,ElemType key){
int low=e, 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; //查找失败,返回-1
}
利用查找判定树求查找成功ASL 与查找失败ASL;
树高为⌈log2 (n+1)⌉时间复杂度为log2 n
如果当前low和high之间有奇数个元素,则 mid分隔后,左右两部分元素个数相等
如果当前low和high之间有偶数个元素,则mid分隔后,左半部分比右半部分少一个元素
即:右子树结点数-左子树结点数=0或1,是平衡的二叉排序树,且只有最下面一层是不满的
失败节点:n+1个(等于成功节点的空链域数量)
2.3 分块查询
块内无序,块间有序
//索引表
typedef struct{
ElemType maxValue;
int low, high;
}Index;
//顺序表存储实际元素
ElemType List[100];
对索引表二分查找,若索引表中不包含目标关键字,则折半查找索引表最终停在 low>high,要在low所指分块中查找。
若索引表采用顺序查找,ASL = ?
若索引表采用折半查找,ASL = ?
假设,长度为n妁查找表被均匀地分为b块,每块s个元素,n = s*b
设索引查找和块内查找的平均查找长度分别为Li、Ls,则分块查找的平均查找长度为ASL=Li+Ls
若索引表采用顺序查找,Li = (b+1)/2,Ls = (s+1)/2
则ASL = (b+s+2)/2=(s2+2*s+n)/2*s, 当s=√n时,ASL最小=√n + 1
若查找表是“动态查找表”,可以采用链式存储的方式(邻接表)
3.B树和B+树(重点/难点)
回顾:二叉查找树(BST)
3.1 m叉查找树
每个节点最多有m个分叉,m-1个关键字;最少有两个分叉,1个关键字.
//m=5
struct Node {
ElemType keys[4]; //最多4个关键字
struct Node * child[5]; //最多5个孩子
int num; //当前节点实际有多少个数据
};
如何提高查找效率(降低树的高度)
i.规定除了根节点外,任何结点至少有⌈m/2⌉个分叉,即至少含有[m/2]-1个关键字;
ii.规定对于任何一个结点,其所有子树的高度都要相同;
iii.若根节点不是终端节点,则至少有两棵子树。
满足以上三个条件的m阶查找树也称为B树
3.2 B树
3.2.1 B树的基本概念
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
1)树中每个结点至多有m棵子树,即至多含有m-1个关键字。
2)若根结点不是终端结点,则至少有两棵子树。
3)除根结点外的所有非叶结点至少有⌈m/2⌉棵子树,即至少含有⌈m/2⌉-1个关键字。
4)所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
5)所有非叶结点的结构如下:
n | P0 | K1 | P1 | K2 | P3 | …… | Kn | Pn |
---|
其中,Ki (i = 1, 2…, n)为结点的关键字,且满足K1<K2<…< Kn,即递增;
Pi (i = 0,1…, n)为指向子树根结点的指针,且指针Pi-1所指子树中所有结点的关键字均小于Ki,Pi所指子树中所有结点的关键字均大于Ki;
n(⌈m/2⌉-1≤n≤m -1)为结点中关键字的个数。
3.2.2 B树的查找效率
问:含n个关键字的m阶B树,最小高度、最大高度是多少?(不包括最后一层的叶子节点)
最小高度:让每个结点尽可能的满,有m-1个关键字,m个分叉,则有n ≤ (m-1)(1+m+m2+m3+···+mh-1) = mh-1,因此h≥logm(n+1);
最大高度:让各层的分叉尽可能的少,即根节点只有2个分叉,其他结点只有⌈m/2⌉个分叉各层结点至少有:第一层1、第二层2、第三层2⌈m/2⌉ …第h层2(⌈m/2⌉)h-2,
第h+1层共有叶子结点(失败结点)2(⌈m/2⌉)h-1个,
n个关键字的B树必有n+1个叶子结点,则n+1≥2(⌈m/2⌉)h-1,即h ≤ log⌈m/2⌉(n+1)/2 + 1
核心:
1)根节点的子树数∈[2, m],关键字数∈[1, m-1]。其他结点的子树数∈[⌈m/2⌉, m];关键字数∈[⌈m/2⌉-1, m-1];
2)对任一结点,其所有子树高度都相同;
3)关键字的值:子树0<关键字1<子树1<关键字2<子树2<…(类比二叉查找树左<中<右);
4)含n个关键字的m阶B树,logm(n+1) ≤ h ≤ log⌈m/2⌉(n+1)/2 + 1。
具体的查找方式类比二叉查找树即可
3.2.3 B树的插入(超难点)
核心要求:
1)除了根节点外,其他结点的关键字数n∈[⌈m/2⌉-1, m-1];
2)关键字的值:子树0<关键字1<子树1<关键字2<子树2<…(类比二叉查找树左<中<右)。
新元素一定是插入到最底层“终端节点”,用“查找”来确定插入位置;
在插入key后,若导致原结点关键字数超过上限,则从中间位置(⌈m/2⌉)将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置(⌈m/2⌉)的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度加1。
3.2.4 B树的删除(超难点)
i.若被删除关键字在终端节点。则直接删除该关键字〈要注意节点关键字个数是否低于下限「m/2⌉-1);
ii.若被删除关键字在非终端节点,则用直接前驱或直接后继来替代被删除的关键字,
直接前驱:当前关键字左侧指针所指子树中“最右下”的元素;
直接后继:当前关键字右侧指针所指子树中“最左下”的元素;
iii.若删除后低于下限,则有以下三种情况:
1)右兄弟够借,则用当前结点的后继、后继的后继依次顶替空缺
2)左兄弟够借,则用当前结点的前驱、前驱的前驱依次顶替空缺
3)左(右)兄弟都不够借,则需要与父结点内的关键字、左(右)兄弟进行合并。合并后导致父节点关键字数量-1,可能需要继续合并。
3.3 B+树
3.3.1 B+树的基本概念
—棵m阶的B+树需满足下列条件:
1)每个分支结点最多有m棵子树(孩子结点)。
2)非叶根结点至少有两棵子树,其他每个分支结点至少有「m/2⌉棵子树。
3)结点的子树个数与关键字个数相等。
4)所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。
5)所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。
3.3.2 B+树的操作
查找:从根节点往下朝找,直到找到最后一层叶子节点才能确定成功或失败。
从p指针开始查找,依次遍历顺序查找。
3.3.3 B+树与B树的比较
区别 | B树 | B+树 |
---|---|---|
n个关键字 | 对应n+1个子树 | 对应n个子树 |
根节点关键字数 | n∈[1, m-1] | n∈[1, m] |
其他节点关键字数 | n∈[⌈m/2⌉-1, m-1] | n∈[⌈m/2⌉, m] |
关键字位置 | 各结点中包含的 关键字是不重复的 | 叶结点包含全部关键字,非叶结点中 出现过的关键字也会出现在叶结点中 |
节点内容 | 结点中都包含了关键字 对应的记录的存储地址 | 叶结点包含信息,所有非叶结点仅起索引作用, 非叶结点中的每个索引项只含有对应子树的最大关键字 和指向该子树的指针,不含有该关键字对应记录的存储地址。 |
查找方式 | 不支持顺序查找。查找成功时,可能停在 任何一层结点,查找速度“不稳定” | 支持顺序查找。查找成功或失败都会到达 最下一层结点,查找速度“稳定” |
B+树的优点:在B+树中,非叶结点不含有该关键字对应记录的存储地址。可以使一个磁盘块可以包含更多个关键字,使得B+树的阶更大,树高更矮,读磁盘次数更少,查找更快。
关系型数据库的“索引”(如MySQL)一般适用B+树。
4.散列表(hash)
4.1 基本概念
散列表(Hash Table),又称哈希表。是一种数据结构,特点是:数据元素的关键字与其存储地址直接相关。
同义词:若不同的关键字通过散列函数映射到同一个值,则称它们为“同义词”;
冲突:通过散列函数确定的位置已经存放了其他元素,则称这种情况为“冲突”;
查找长度:在查找运算中,森要对比关键字的次数称为查找长度,可以为零;
装填因子 = 表中记录数 / 散列表长度,装填因子会直接影响散列表的查找效率
用拉链法(又称链接法、链地址法)处理“冲突”:把所有“同义词”存储在一个链表中。
例:数据元素关键字分别为{19,14,23,1,68,20,84,27,55,11,10,79},散列函数H(key)=key%13
ASL成功=(1*6+2*4+3+4)/12=1.75;12表示数据表中一共有12各元素
ASL失败=(0*7+4+2+2+1+2+1)/13=0.92;13表示散列表的长度,数值与装填因子相同
4.2 常见的散列函数
设计目标:让不同关键字的冲突尽可能地少,散列查找是典型的“用空间换时间”的算法,只要散列函数设计的合理,则散列表越长,冲突的概率越低。
1)除留余数法:H(key)=key%p,散列表表长为m,取一个不大于m但最接近或等于m的质数p
2)直接定址法:H(key) = key 或 H(key) = a*key + b,其中,a和b是常数。这种方法计算最简单,且不会产生冲突。
它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。
3)数字分析法:选取数码分布较为均匀的若干位作为散列地址
设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等﹔而在某些位上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
4)平方取中法:取关键字的平方值的中间几位作为散列地址。
具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
4.3 开放定址法(重点)
开放定址法:是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为︰
H
i
=
(
H
(
k
e
y
)
+
d
i
)
%
m
H_i = ( H(key) + d_i) \% m
Hi=(H(key)+di)%m
i = 0,1,2…, k (k≤m- 1) ,m表示散列表表长;di为增量序列;i可理解为“第i次发生冲突”
注意:采用“开放定址法"时,删除结点次能简单地将被删结点的空问置为空,否则将截断在它之后填入散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除;
4.3.1 线性探测法(常考)
di=0,1,2,3, …, m-1;即发生冲突时,每次往后探测相邻的下一个单元是否为空。
例:数据元素关键字分别为{19,14,23,1,68,20,84,27,55,11,10,79},散列函数H(key)=key%13
H(25) = 25 % 13 = 12,哈希函数值域[0,12]
H=(H(key)+1)%16 = 13,冲突处理函数值域[0,15]
ASL成功=(1+1+1+2+4+1+1+3+3+1+3+9)/12 = 2.5
ASL失败=(1+13+12+11+10+9+8+7+6+5+4+3+2)/13=7
线性探测法很容易造成同义词)非同义词的“聚集(堆积)"现象,严重影响查找效率。
4.3.2 平方探测法(常考)
当di= 02,12,-12,22,-22, …, k2, -k2时,称为平方探测法,又称二次探测法其中k≤m/2。
散列表长度m必须是一个可以表示成4*j+ 3的素数才能探测到所有位置。(完全剩余系相关)
比起线性探测法更不易产生“聚集(堆积)”问题,
4.3.3 伪随机序列法
di取一串随机数序列用于处理冲突。
4.4 再散列法
再散列法(再哈希法)︰除了原始的散列函数H(key)之外,多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个痴地址,直到不冲突为止
H
i
=
R
H
i
(
K
e
y
)
i
=
1
,
2
,
3
…
…
,
k
H_i = RH_i(Key)\qquad i=1,2,3……,k
Hi=RHi(Key)i=1,2,3……,k