第七章:查找
一、查找的基本概念
查找 —— 在数据集合中寻找满⾜某种条件的数据元素的过程称为查找
查找表(查找结构)—— ⽤于查找的数据集合称为查找表,它由同⼀类型的数据元素(或记录)组成
关键字 —— 数据元素中唯⼀标识该元素的某个数据项的值,使⽤基于关键字的查找,查找结果应该是唯⼀的。
对查找表的常见操作
①查找符合条件的数据元素
②插⼊、删除某个数据元素
查找算法的评价指标
①查找⻓度——在查找运算中,需要对⽐关键字的次数称为查找⻓度
②平均查找⻓度(ASL, Average Search Length)—— 所有查找过程中进⾏关键字的⽐较次数的平均值
二、顺序查找
1.算法思想
顺序查找,⼜叫“线性查找”,通常⽤于线性表。
算法思想:从头到 jio 挨个找(或者反过来也OK)
2.算法实现
typedef struct{ //查找表的数据结构(顺序表)
Elemtype *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST, ElemType key){
// 引入哨兵元素,在创建查找表时,0号单元留空,预留给带查找元素关键字
// 引入哨兵元素的目的是使得循环不必判断数组是否越界,可以避免很多不必要的判断语句,提高效率
ST.elem[0] = key; //“哨兵”
int i;
for(i = ST.TableLen; ST.elem[i] != key; --i); //从后往前找
return i; //查找成功,返回元素下标;查找失败,则返回0
}
时间复杂度:O(n)
三、折半查找
1.算法思想
折半查找,又称“二分查找”,仅适用于有序的顺序表
基本思想:
首先将给定值key于表中间位置的元素进行比较,若相等,则查找成功,返回元素位置;若不等,则目标元素只能在中间元素以外的前半部分或后半部分,然后在缩小的范围内继续同样的查找。
2.算法实现
typedef struct{ //查找表的数据结构(顺序表)
Elemtype *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//折半查找(以升序为例)
int binnarySearch(SSTable L, ElemType key){
int low = 0;
int high = L.TableLen - 1;
int mid;
while(low <= high){
mid = (low + high) / 2; //取中间位置(向下取整)
if(L.elem[mid] == key) //查找成功直接返回
return mid;
else if(L.elem[mid] < key)
low = mid + 1; //从后半部分开始查找
else
high = mid - 1; //从前半部分开始查找
}
return -1; // 查找失败,返回-1
}
时间复杂度O(log₂n)
3.折半查找判定树
构造:
由mid所指元素将原有元素分割到左右子树中
key:右子树结点数 - 左子树结点树 的绝对值小于等于1
特性:
折半查找判定树是平衡的二叉排序树
折半查找判定树,只有最下面一层是不满的
若查找表有n个关键字,则失败结点有 n+1 个
树高h=[log₂(n+1)] (不包含失败结点)
四、分块查找
1.算法思想
基本思想:
将查找表分为若干子块,块内元素可以无序,但块之间是有序的,即前一块中的最大关键字小于后一块中的最小关键字。再建立一个索引表,索引表中的每个元素含有各块中的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排序。
分块查找吸取了顺序查找和折半查找各自的优点,既有动态结构,又适合于快速查找。
查找分为两步,第一步是在索引表中确定待查记录所在的块,可以使用顺序查找或折半查找;第二部是在块内顺序查找。
五、B树
B树,又称多路平衡查找树,所有结点的最大孩子数称为阶。一般用m来指代
//5叉查找树的结点定义
struct Node{
Elemtype keys[4]; //最多四个关键字
struct Node* child[5]; //最多5个孩子
int num; //结点中有几个关键字
}
1.B树的性质
对于m阶的B树:
根节点的⼦树数∈[2, m],关键字数∈[1, m-1]。
其他结点的⼦树数∈[ , m];关键字数∈[ -1, m-1]
对任⼀结点,其所有⼦树⾼度都相同
关键字的值:⼦树0<关键字1<⼦树1<关键字2<⼦树2<…. (类⽐⼆叉查找树 左<中<右)
所有叶节点都位于同一层,并且是不带信息的
2.B树的查找
B树的查找可分为两步
1.在B树中找结点(磁盘上进行)
2.在结点内找关键字(内存上进行),可采用顺序查找法或折半查找法。
3.B树的插入
通过B树的查找算法确定查找失败的位置,这个位置就是应该插入的位置。
开始进行插入,如果要插入结点的关键字数量小于m,直接插入即可。如果大于等于m,那就不符合B树的特性了,那就要进行分裂。
分裂的方法是:位置为"向上取整m/2"的关键字,给到父节点,这个关键字的左部分保持在原来结点,右部分放到一个新节点。
如果给到父节点关键字后,父节点的关键字数量也大于等于m了,那么就要进行二次分裂。
4.B树的删除
删除可分为两种情况,被删的关键字在与不在终端结点中。
- 不在终端结点中
如果要删关键字k,找到k的直接前驱(左侧指针所指⼦树中“最右下”的元素 )或直接后继(右侧指针所指⼦树中“最左下”的元素 )来替代被删除的关键字。
- 在终端结点中,又分为三种情况
①直接删除。 当将要被删除关键字所在结点的关键字数量大于等于“向上取整m/2”,代表删掉之后就是“向上取整m/2”-1,依旧符合B树特性,可以直接删掉。
借兄弟一手。 顾名思义,为啥要借,就是自身不符合前面直接删除的条件(删掉之后不符合B树特性了),所以要借兄弟的,那兄弟够借嘛?所以又可以分为两种②兄弟够借。若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(⽗⼦换位法)
③兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结点的关键字个数均=⌈m/2⌉ - 1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进⾏合并
在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少⾄0(根结点关键字个数为1时,有2棵⼦树),则直接将根结点删除,合并后的新结点成为根;若双亲结点不是根结点,且关键字个数减少到 ,则⼜要与它⾃⼰的兄弟结点进⾏调整或合并操作,并重复上述步骤,直⾄符合B树的要求为⽌
六、B+树
B+树是专门为数据库设计的。
B+树的查找、插入、删除与B树类似,但是B+树查找到关键字后,并不终止,而是继续向下查找,直到叶节点上的关键字为止。
1.B+树的性质
⼀棵m阶的B+树需满⾜下列条件:
每个分⽀结点最多有m棵⼦树(孩⼦结点)。
⾮叶根结点⾄少有两棵⼦树,其他每个分⽀结点⾄少有 棵⼦树。
结点的⼦树个数与关键字个数相等。
所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按⼤⼩顺序排列,并且相邻叶结点按⼤⼩顺序相互链接起来。
所有分⽀结点中仅包含它的各个⼦结点中关键字的最⼤值及指向其⼦结点的指针。
2.B+树 vs B树
相同点:
-
除根节点外,最少 [m/2] 个分叉(确保结点不要太“空”)
-
任何⼀个结点的⼦树都要⼀样⾼(确保“绝对平衡”)
不同点:
m阶B树 | m阶B+树 | |
---|---|---|
类比 | ⼆叉查找树的进化——>m叉查找树 | 分块查找的进化——>多级分块查找 |
关键字与分叉 | n个关键字对应n+1个分叉(⼦树) | n个关键字对应n个分叉 |
节点包含的信息 | 所有结点中都包含记录的信息 | 只有最下层叶⼦结点才包含记录的信息 (可使树更矮) |
查找方式 | 不⽀持顺序查找。查找成功时,可能停在 任何⼀层结点,查找速度“不稳定” | ⽀持顺序查找。查找成功或失败都会到达 最下⼀层结点,查找速度“稳定 |
七、散列查找
1.概念
散列表(Hash Table),⼜称哈希表。是⼀种数据结构,特点是:数据元素的关键字与其存储地址直接相关 。
散列函数:把查找表中的关键字映射成该关键字对应地址的函数,记为Addr=Hash(key)。这里的地址可以是数组下标、索引或内存地址等。
若不同的关键字通过散列函数映射到同⼀个值,则称它们为**“同义词”**
通过散列函数确定的位置已经存放了其他元素,则称这种情况为**“冲突”**
理想情况下,散列表的查找时间复杂度为O(1),即与表中元素的个数无关。
2.构造散列函数
①除留余数法
H(key) = key % p
假定散列表表长为m,取一个不大于m但最接近m的质数p
(取质数的话大多数情况下能让冲突发生得更少,分布得更均匀,参见《数论》)
②直接定址法
H(key) = key 或 H(key) = a*key + b
其中,a和b是常数。这种⽅法计算最简单,且不会产⽣冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。
③数字分析法
选取数码分布较为均匀的若⼲位作为散列地址
设关键字是r进制数(如⼗进制数),⽽r个数码在各位上出现的频率不⼀定相同,可能在某些位上分布均匀⼀些,每种数码出现的机会均等;⽽在某些位上分布不均匀,只有某⼏种数码经常出现。
此时可选取数码分布较为均匀的若⼲位作为散列地址。这种⽅法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
④平⽅取中法
取关键字的平⽅值的中间⼏位作为散列地址。
具体取多少位要视实际情况⽽定。
这种⽅法得到的散列地址与关键字的每位都有关系。
因此使得散列地址分布⽐较均匀,适⽤于关键字的每位取值都不够均匀或均⼩于散列地址所需的位数。
例:存储学生的身份证号
3.处理冲突的办法
3.1 拉链法
⽤拉链法(⼜称链接法、链地址法)处理“冲突”: 把所有“同义词”存储在⼀个链表中
散列表的查找过程于构造散列表的过程基本一致。根
据散列函数和关键字可以计算出记录的散列地址。
若散列地址上:
①无记录,说明查找失败;
②若有记录且关键字相同,则查找成功;
③若有记录但关键字不同,使用给定的处理冲突方法计算下一个散列地址,再次进行比较。
装填因子:定义一个表的装满程度,即α = 表中记录数/散列表长度
3.2 开放定址法
开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,⼜向它的⾮同义词表项开放。
数学递推公式:
Hi = (H(key) + di) % m
i = 0, 1, 2,…, k(k≤m - 1), m表示散列表表⻓; di为增量序列; i 可理解为“第i次发⽣冲突”
取定某一增量序列di后,对应的处理方法就是确定的。通常有以下4种取法
①线性探测法(线性探测再散列法)
di = 0,1,2,···,m-1
冲突发生时,顺序查看表中下一个单元,直到找出一个空单元或查遍全表(此时表已满)。
线性探查法可能使第 i 个散列地址的同义词存入第 i+1 个散列地址,这样本应存入第 i+1 个散列地址的元素就要争夺 i+2 个散列地址。造成大量元素在相邻的散列地址上聚集(堆积)起来,大大降低了查找效率。
采⽤“开放定址法”时,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填⼊散列表的同义词结点的查找路径,可以做⼀个“删除标记”,进⾏逻辑删除
②平方探测法(二次探测再散列法)
di = 0²,1²,-1²,2²,-2²,···,k²,-k²(k≤m/2)
- 散列表长度m必须是一个可以表示为4k+3的素数。
- 是一种处理冲突的较好方法,可以避免出现堆积问题。
- ⼩坑:散列表⻓度m必须是⼀个可以表示成4j + 3的素数,才能探测到所有位置,参加《数论》
③再散列法(双散列法)
Hi = RHi(Key) i=1,2,3….,k
- 除了原始的散列函数 H(key) 之外,多准备⼏个散列函数,当散列函数冲突时,⽤下⼀个散列函数计算⼀个新地址,直到不冲突为⽌
④伪随机序列法:
di为伪随机数序列
探测法**(二次探测再散列法)
di = 0²,1²,-1²,2²,-2²,···,k²,-k²(k≤m/2)
- 散列表长度m必须是一个可以表示为4k+3的素数。
- 是一种处理冲突的较好方法,可以避免出现堆积问题。
- ⼩坑:散列表⻓度m必须是⼀个可以表示成4j + 3的素数,才能探测到所有位置,参加《数论》
③再散列法(双散列法)
Hi = RHi(Key) i=1,2,3….,k
- 除了原始的散列函数 H(key) 之外,多准备⼏个散列函数,当散列函数冲突时,⽤下⼀个散列函数计算⼀个新地址,直到不冲突为⽌
④伪随机序列法:
di为伪随机数序列