数据结构笔记七:查找
查找
查找的基本概念
查找——在数据集合中寻找满足某种条件的数据元素的过程称为查找
查找表(查找结构)——用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成
关键字——数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该时唯一的。
查找长度——在查找运算中,需要对比关键字的次数称为查找长度
平均查找长度(ASL)——所有查找过程中进行关键字的比较次数的平均值
顺序查找
算法思想
顺序查找,又叫“线性查找”,通常用于线性表
算法思想:从头到jio挨个找
顺序查找的实现
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
return i==ST.TableLen?-1:i;
}
//顺序查找的实现(哨兵)
//优点:无需判断是否越界
int Search_Seq(SSTable ST,ELemtype key)
{
ST.elem[0]=key; //“哨兵”
int i;
for(i=ST.TableLen;ST.elem[i]!=key;--i);//从后往前
//查找成功,则返回元素下标,查找失败,则返回0
return i;
}
查找效率分析
顺序查找的优化(对有序表)
顺序查找的优化(被查概论不相等)
折半查找
算法思想
折半查找,又称“二分查找”,仅适用于有序的顺序表
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; //查找失败,返回-1
}
查找效率分析
折半查找判定树的构造
折半查找判断树种,若\(mid=\lfloor(low+high)/2\rfloor\),则对于任何一个结点,必有:\(右子树结点数-左子树结点数=0或1\)
折半查找的判定树一定是平衡二叉排序树
折半查找的判定树中,只有最下面一层时不满的,因此,元素个数为n时树高\(h=\lceil log_2(n+1) \rceil\)
查找效率
分块查找
算法思想
//索引表
typedef struct{
ElemType maxValue;
int low,high;
}Index;
//顺序表存储实际元素
ElemType List[100];
分块查找,又称索引顺序查找,算法过程如下:
- 在索引表中确定待查记录所属的分块(可顺序,可折半)
- 在块内顺序查找
查找效率分析(ASL)
设索引查找和块内擦杭州的平均查找长度分别为\(L_I,L_s,\)则分块查找的平均查找长度为\(ASL=L_I+L_S\)
用顺序查找索引表,则\(L_I=\frac{(1+2+3+...+n)}{b}=\frac{b+1}{2}\),\(L_S=\frac{(1+2+3+...+n)}{s}=\frac{s+1}{2}\)
则\(ASL=\frac{b+1}{2}=\frac{S+1}{2}=\frac{s^2+2s+n}{2s}\),当\(S=\sqrt n\)时,\(ASL_{最小}=\sqrt n+1\)
用折半查找索引表,则\(L_I=\lceil log_2(b+1) \rceil,L_S=\frac{(1+2+3+...+n)}{s}=\frac{s+1}{2}\)
则\(ASL=\lceil log_2(b+1) \rceil+\frac{s+1}{2}\)
拓展
B树
B树,又称多路平衡查找树,B树种所有结点的孩子个数的最大值称为B树的3,通常用m表示。一颗m阶B树或为空树,或为满足如下特性的m叉树:
-
树中每个结点至多有m课子树,即至多含有m-1个关键字
-
若根节点不是终点结点,则至少有两颗子树
-
除根结点外的所有非叶结点至少有\(\lceil m/2 \rceil\)子树,即至少含有\(\lceil m/2 \rceil-1\)个关键字
-
所有叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
-
所有非叶结点的结构如下:
其中,\(k_i(i=1,2,...,n)\)为结点的关键字i,且满足\(K_,<k_2<K_n\);\(P_i(i=0,1,...,n)\)为指向子树根结点的指针,且指针\(P_{i-1}\)所指子树中所有结点的关键字均小于\(k_i\),\(P_{i-}\)所指树中所有结点的关键字均大于\(k_i\),\(n(\lceil m/2 \rceil-1\le n\le m-1)\)为结点中关键字的个数
B树的高度
最小高度——让每个结点尽可能的满,有\(m-1\)个关键字,\(m\)个分叉,则有\(n\le(m-1)(1+m+m^2+m^3+...+m^{h-1})=m^h-1\),因此\(h\ge log_m(n+1)\)
最大高度——让各层的分叉尽可能的少,即根节点只有2个分叉,其他节点只有\(\lceil m/2 \rceil\)个分叉
各层结点至少有:第一层 1,第二层 2,第三层 2\(\lceil m/2 \rceil\)...第h层\(2(\lceil m/2 \rceil)^{h-2}\)
第\(h+1\)层共有叶子结点(失败点):\(2(\lceil m/2 \rceil)^{h-1}\)个
n个关键字的B树必有n+1个叶子结点,则\(n+1>2(\lceil m/2 \rceil)^{h-1}\),即\(h\le log_{\lceil m/2 \rceil}\frac{n+1}{2}+1\)
B树的核心特性
B的插入
核心要求:
- 对m阶B树——除根节点外,结点关键字个数\(\lceil m/2 \rceil \le n\le m-1\)
- 子树0<关键字<子树1<关键字2<子树2<...
新元素一定时插入到最底层“终端节点”,用“查找”来确定插入位置
在插入key后,若导致原结点关键字超过上次,则从中间位置(\(\lceil m/2 \rceil\))将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关机按放到新结点中,中间位置(\(\lceil m/2 \rceil\))的结点插入原结点的父结点。若此时导致其父结点的关键字个数叶超过了上限,则继续进行这种分裂操作,直到这个过程传到根结点位置,进而导致B树高度增1。
B树删除
终端节点:则直接删除该关键字(要注意节点关键字个数是否低于下限\(\lceil m/2 \rceil-1\))
非终端节点:
- 兄弟够借:若被删除关键字所在节点删除前的关键字个数低于下限,且此结点右(左)兄弟结点的关键字个数还很宽裕,则需要调整该结点,右(左)兄弟结点及其双亲结点(父子换位法)
- 右兄弟宽裕:用当前结点的后继,后继的后继来填补空缺
- 左兄弟宽裕:用当前结点的前驱,后继的前驱来填补空缺
- 兄弟不够借:若被删除关键字所在节点删除前的关键字个数低于下限,且此结点右(左)兄弟结点的关键字个数均为\(\lceil m/2 \rceil-1\),则将关键字删除后与左(右)兄弟结点及双亲结点中的关键字进行合并。
- 在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根节点且关键字个数减少至0,则直接将根接待你删除,合并后的新结点成为根;若双亲结点不是根结点,且关键字个数减少到\(\lceil m/2 \rceil-2\),则又要和它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符号B树的要求为止。
B+树
一颗m阶的B+树需满足下列条件:
- 每个分支结点最多有m课子树(孩子结点)
- 非叶根结点至少有两颗子树,其他每个分支结点至少有\(\lceil m/2 \rceil\)棵子树
- 结点的子树个数与关键字个数相等
- 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。(支持顺序查找)
- 所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。
B+树的查找
B树 VS B+树
散列查找
散列表
散列表(Hash Table),又称哈希表。是一种数据结构,特点:数据元素的关键字与其存储地址直接相关。
处理冲突
拉链法
用拉链法(又称链接法,链地址法)处理“冲突”:把所有“同义词”存储在一个链表中
装填因子\(\alpha=表中记录数/散列表长度\)
常见散列函数:
除留余数法——\(H(key)=key%p\),散列表表厂为m,取一个不大于m但最接近或等于m的质数p
直接定址法——\(H(key)=key或H(Key)=a*key+b\)
其中,a和b是常熟。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。
数字分析法——选取数码分布较为均匀的若干位作为散列地址
若关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时课选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
平方取中法——取关键字的平方值的中间几位作为散列地址。
具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适合于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
开放地址法
指可存放新表现得空闲地址既向他的同义词表项开放,又向它得非同义词表项开放。
其数学递推公式为:\(H_i=(H(key)+d_i)\%m-1,(i=0,1,2,...,k(k\le m-1))\),m表示散列表表长,\(d_i\)为增量序列;\(i\)可理解为"第i次发生冲突"
注意:采用“开放定址法”时,删除结点不能简单地将被删结点得空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除。
- 线性探测法:——\(d_i=0,1,2,3,...,m-1\);即发生冲突时,每次往后探测相邻得下一个单元是否为空。
线性探测法很容易造成同义词,非同义词的“聚集(堆积)”现象,严重影响查找效率。
- 平方探测法——当\(d_i=0^2,1^2,-1^2,2^2,...,k^2-k^2\)成为平方探测法,又称二次探测法其中\(k\le m/2\)
平方探测法:比起线性探测法更不易产生“聚集(堆积)”问题
非重点小坑:散列表长度m必须是一个可以表示成\(4j+3\)的素数,才能探测到所有的位置
- 伪随机序列法——\(d_i\)是一个伪随机序列,如\(d_i=0,5,24,11\)
再散列法
再散列法(再哈希法):除了原始的散列函数\(H(key)\)之外,多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止: