1
0
mirror of https://github.com/Didnelpsun/CS408.git synced 2026-02-11 22:55:57 +08:00
Files
CS408/Data-Structrue/9-search.md
Didnelpsun 1795de9caa 数据结构完结
对原有的文件添加上序号
2021-05-06 15:42:57 +08:00

19 KiB
Raw Blame History

查找

基本概念

  • 查找:在数据集合中寻找满足某种条件的数据元素的过程。
  • 查找表(查找结构):用于查找的数据集合,由同一类型的数据元素或记录组成。
  • 关键字:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该唯一。
  • 静态查找表:只查找符合条件的数据元素。
  • 动态查找表:出来要查找,还要进行删除或插入,除了查找速度还要考虑插入删除操作是否方便。
  • 查找长度:查找运算中,需要对比关键字的次数。
  • 平均查找长度ASL所有查找过程中进行关键字比较次数的平均值。$ASL=\sum_{i=1}^nP_iC_i$。其中$P_i$表示查找第i个元素的概率$C_i$表示查找第i个元素的查找长度。

线性表查找

顺序查找

又称为线性查找,常用于线性表,从头到尾逐个查找。

ASL查找成功为$\dfrac{1+2+3+\cdots+n}{n}=\dfrac{n+1}{2}$ASL查找失败为$n+1$,时间复杂度为$O(n)$。

顺序表

可以让数据集变为有序的,这样对比数据大小也可以知道是否还需要遍历从而减少查找时间。这样顺序结构从逻辑上就变成了一个二叉树结构,左子树代表小于,右子树代表大于,所有数据结点挂在右子树上。

查找情况还要比普通乱序查找加上一个大于最大值的情况。

ASL查找失败为$\dfrac{1+2+3+\cdots+n+n}{n+1}=\dfrac{n}{2}+\dfrac{n}{n+1}$。成功结点的查找长度等于自身所在层数,失败结点的查找长度等于其父结点所在层数。

查找概率不等

当数据元素查找概率不等时,可以将查找概率更大的元素放在靠前的位置,以减少大概率元素被遍历的时间。

但是此时数据是乱序的,所以查找失败的时间复杂度与没有优化的是一样的。

折半查找

也称为二分查找,只适用于有序的顺序表,链表无法适用,因为链表很难折半找到元素。

折半查找的结构

定义三个指针low指向查找范围的最小值high指向查找范围的最大值mid指向查找范围的中间值$mid=\lfloor(low+high)/2\rfloor$。(也可以向上取整,过程会有所不同)

折半查找的过程

查找时首先计算出mid判断是否相等若查找值小于mid的值则将high赋值为mid的值若查找值大于mid则将low赋值为mid的值重新计算mid。这样就可以不断二分区间来查找从而加快迭代。当查找最后low>high则查找失败。

ASL查找成功为$\dfrac{1+2+3+\cdots+n}{n}=\dfrac{n+1}{2}$ASL查找失败为$n+1$,时间复杂度为$O(n)$。

折半查找判定树

若当前low和high有奇数个元素则mid分割后左右两部分元素个数相等。

若当前low和high有奇数个元素则mid分割后左部分元素个数小于右部分一个。

折半查找判定树一定是一个平衡二叉树。只有最下面一层不满元素个数为n时树高与完全二叉树相等$h=\lceil\log_2(n+1)\rceil$。

且折半查找判定树也是一个二叉排序树,失败结点=n+1成功结点的空链域节点数

ASL查找成功查找失败都一定小于折半查找树的树高时间复杂度为$O(\log_2n)$。

分块查找

分块查找又称为索引顺序查找,需要对数据进行一定的排序,不一定全部是顺序的,但是要求在一个区间内是满足一定条件的,即块内无序,块间有序。其中分割的块数和每块里的数据个数都是不定的。

分块查找的结构

除了保存数据的顺序表外还需要定义一个索引表,保存每个分块的最大关键字和每块的存储空间。

很明显这种定义方式定义了两个顺序结构,并且如果插入删除时需要大量移动元素,所以可以采用链表的形式。

定义一种最大元素结点,包含数据、指向后继最大元素结点的指针、指向分块内元素的指针;定义一种块内元素结点,包含数据、指向后继分块内元素的指针。但是这时候就无法折半查找,只能顺序查找。

所以总的来说分块查找还是一种静态查找,动态插入删除的效率较低。

分块查找的过程

在查找时先根据关键字遍历索引表,然后找到索引表的分块(可以顺序也可以折半),再到存储数据的顺序表的索引区间中查找。

若适用折半查找查找索引表的分块索引表中若不存在目标关键字则折半查找索引表最终会停在low>high要在low所指向分块中查找。

分块查找的效率

ASL查找成功失败的情况都十分复杂所以一般不会考。

假设长度为$n$的查找表被均匀分为$b$块,每块$s$个元素假设索引查找和块内查找的平均查找长度ASL分别为$L_I$和$L_S$,则分块查找的平均查找长度为$ASL=L_I+L_S$。

使用顺序查找索引表,则$L_I=\dfrac{(1+2+\cdots+b)}{b}=\dfrac{b+1}{2}$$L_S=\dfrac{(1+2+\cdots+s)}{s}=\dfrac{s+1}{2}$。所以$ASL=\dfrac{b+1}{2}+\dfrac{s+1}{2}=\dfrac{s^2+2s+n}{2s}$,当$s=\sqrt{n}$时,$ASL_{min}=\sqrt{n}+1$。

使用折半查找索引表,则$L_I=\lceil\log_2(b+1)\rceil$$L_S=\dfrac{(1+2+\cdots+s)}{s}=\dfrac{s+1}{2}$。所以$ASL=\lceil\log_2(b+1)\rceil+\dfrac{s+1}{2}$。

数表查找

B树

B树的定义

为了保证m叉查找树中每个结点都能被有效利用避免大量结点浪费导致树高过大所以规定m叉查找树中除了根结点以外任何结点至少有$\lceil m/2\rceil$个分叉,即至少包含$\lceil m/2\rceil-1$个结点。

为了保证m叉查找树是一棵平衡树避免树偏重导致树高过大所以规定m叉查找树中任何一个结点其所有子树的高度都要相同。

而能保证这两点的查找树就是一棵B树多少叉就是一棵多少阶的B树。

B树即多路平衡查找树所有结点的孩子个数的最大值就是B树的阶一般用m表示。

B树的性质

  • 树的每个结点至多包含m棵子树至多包含m-1个关键字。
  • B树最底端的失败的不存在的结点就是常说的叶子结点而最底端的存在数据的结点就是终端结点。一般的树的叶子结点和终端结点都是指最底端的有数据的结点
  • 若根结点不是终端结点,则至少有两颗子树,任意结点的每棵子树都是绝对平衡的。
  • 除根结点以外的所有非叶结点至少有$\lceil m/2\rceil$棵子树,即至少包含$\lceil m/2\rceil-1$个结点。
  • 所有叶结点都出现在同一个层次上且不带信息。
  • 每个结点中的关键字是有序的。子树0 < 关键字1 < 子树2 < ...。

计算B树高度大部分不包括叶子结点。若含有n个关键字的m阶B树。

  • 最小高度:让每个结点尽可能满,有$m-1$个关键字,$m$个分叉,则一共有$(m-1)(m^0+m^1+m^2+\cdots+m^{h-1})$个结点,其中$n$小于等于这个值,从而求出$h\geqslant\log_m(n+1)$。
  • 最大高度:
    • 让各层分叉尽可能少,即根结点只有两个分叉,其他结点只有$\lceil m/2\rceil$个分叉所以第一层1个第二层2个第$h$层$2(\lceil m/2\rceil)^{h-2}$个结点,而$h+1$层的叶子结点有$2(\lceil m/2\rceil)^{h-1}$个,且$n$个关键字的B树必然有$n+1$个叶子结点,从而$n+1\geqslant2(\lceil m/2\rceil)^{h-1}$,即$h\leqslant\log_{\lceil m/2\rceil}\dfrac{n+1}{2}+1$。
    • 让各层关键字尽可能少,记$k=\lceil m/2\rceil$。第一层最少结点数和最少关键字为1第二层最少结点数为2最少关键字为$2(k-1)$,第三层最少结点数为$2k$,最少关键字为$2k(k-1)$,第$h$层最少结点数为$2k^{h-2}$,最少关键字为$2k^{h-2}(k-1)$,从而$h$层的m阶B数至少包含关键字总数$1+2(k-1)(k^0+k^1+\cdots+k^{h-2})=1+2(k^{h-1}-1)$,若关键字总数小于这个值,则高度一定小于$h$,所以$n\geqslant 1+2(k^{h-1}-1)$,则$h\leqslant\log_{\lceil m/2\rceil}\dfrac{n+1}{2}+1$。

B树的插入

新元素插入一定是插入到最底层的终端结点使用B树的查找来确定插入位置。

若导致原结点关键字数量超过上限溢出,就从中间位置$\lceil m/2\rceil$分开,将左部分包含的关键字放在原来结点,右部分包含的关键字放在一个新结点,并插入到原结点的父结点的后一个位置上,而在原结点的父结点连接后的结点后移一个连接让位给分割出来的右半部分结点,中间的一个结点$\lceil m/2\rceil$插入到原结点的父结点上,并考虑在父结点的顺序。

若父结点插入时也溢出了,则同理在父结点的中间进行分割,左半部分在原来父结点;右半部分新建一个父结点,并把中间结点右边开始的所有连接移动到新父结点上;中间的结点上移到祖父结点,如果没有就新建,然后建立两个指针分别指向原父结点和新父结点。

B树的删除

  • 若被删除关键字在终端结点,且结点关键字个数不低于下限,则直接删除该关键字,并移动后面的关键字。
  • 若被删除关键字在非终端结点,则用直接前驱或直接后继来替代被删除关键字,然后后面的元素直接前移。
    • 直接前驱:当前关键字左侧指针所指子树最右下的元素。
    • 直接后继:当前关键字右侧指针所指子树最左下的元素。
  • 若被删除关键字在终端结点,但是结点关键字个数删除后低于下限:
    • 右兄弟够借:若原结点右兄弟结点里的关键字在删除一个后高于下限,则可以用结点的后继以及后继的后继来顶替:
      1. 将原结点在父结点的连接的前一个关键字下移到原结点并放在最后面。
      2. 原结点父结点里的关键字全部前移一位。
      3. 将原结点右兄弟结点的第一个关键字上移插入到原结点父结点的关键字的最后面。
      4. 原结点右兄弟结点里的关键字全部前移一位。
    • 左兄弟够借:若原结点里右兄弟的关键字在删除一个后低于下限,但是左兄弟的结点足够,则可以用结点的前驱以及前驱的前驱来顶替:
      1. 将原结点在父结点的连接的前一个关键字下移到原结点并放在最前面。
      2. 原结点父结点里的关键字全部前移一位。
      3. 将原结点左兄弟结点的最后一个关键字上移插入到原结点父结点的关键字的最前面。
      4. 原结点兄左弟结点里的关键字全部前移一位。
    • 左右兄弟都不够借:若 左右兄弟结点的关键字个数均等于下限值,则将关键字删除后与左或右兄弟结点以及父结点中的关键字进行合并:
      1. 将原结点的父结点关键字插入到原结点关键字后面。
      2. 将原结点的左或右兄弟结点的关键字合并到原结点(前插或后插),并将连接也转移到原结点上。
      3. 若父结点的关键字个数又不满于下限,则父结点同样要于与它的兄弟父结点进行合并,并不断重复这个过程。

B+树

B+树考的并不是很深。

与分块查找类似是对B树的一种变型。

B+树的定义

一个m阶的B+树需要满足以下条件:

  1. 每个分支结点最多有m棵子树或孩子结点。
  2. 为了保持绝对平衡,非叶根结点至少有两棵子树,其他每个分支结点至少有$\lceil m/2\rceil$棵子树。不同于B树B+树又重新将最下面的保存的数据定义为叶子结点)
  3. 结点的子树个数与关键字个数相等。B树结点子为树个数与关键字个数加1
  4. 所有叶结点包含所有关键字以及指向记录的指针叶结点中将关键字按大小排序并且相邻叶子结点按大小顺序相互连接起来。所以B+树支持顺序查找。
  5. 所有分支结点中仅包含其各子结点中关键字的最大值以及指向其子结点的指针(即分支结点只是索引)。

B+树的查找

无论查找成功与否B+树的查找一定会走到最下面一层结点否则无法确认。而B树查找可以停留在任何一层。

B+树可以遍历查找,即从根结点出发,对比每个结点的关键字值,若目标值小于当前关键字值且大于前一个关键字值,则从当前关键字的指针向下查找。

B+树可以顺序查找,在叶子结点的块之间定义指向后面叶子结点块的指针,从而能顺序查找。

B+树与B树的区别

对于m阶B+树与B树

  B+树 B树
结点的n个关键字对应的子树个数 n n+1
根结点的关键字数 [1,m] [1,m-1]
其他结点的关键字数 [\lceil m/2\rceil,m] [\lceil m/2\rceil-1,m-1]
关键字分布 叶子结点包含所有关键字,非叶结点包含部分重复关键字 所有结点的关键字不重复
结点作用 叶子结点包含信息,非叶子结点是索引作用 所有结点都包含信息
结点存储内容 叶子结点包含关键字与对应记录的存储地址,非叶子结点包含对应子树的最大关键字和指向该子树的指针 所有结点都包含关键字与对应记录存储地址
查找位置 需要查找到叶子结点的最底层才能判断是否查找成功或失败 查找到数的任何地方都能判断
查找速度 非叶子结点不包含关键字对应记录的存储地址,可以使一个磁盘块含有多格关键字,从而让树的阶数更大,树更矮,读磁盘次数更是,查找更快 所有结点都包含存储地址,保存的关键字数量更少,树高更高,所以读写磁盘次数更多,查找更慢

散列表查找

散列表定义

散列表又称哈希表,是一种数据结构,数据元素的关键字与其存储地址直接相关。一个散列结构是一块地址连续的存储空间。

散列函数

关键字与地址通过散列函数(哈希函数)来实现映射。即记录位置=散列函数(记录关键字)。

  • 直接定址法:可表示为散列函数(关键字)=a*关键字+b其中a、b均为常数。这种方法计算特别简单并且不会发生冲突但当关键字分布不连续时会出现很多空闲单元会将造成大量存贮单元的浪费。
  • 数字分析法:分析关键字的各个位的构成,截取其中若干位作为散列函数值,尽可能使关键字具有大的敏感度,即最能进行区分的关键字位,这些位数都是连续的。
  • 平方取中法:先求关键字的平方值,然后在平方值中取中间几位为散列函数的值。因为一个数平方后的中间几位和原数的每一位都相关,因此,使用随机分布的关键字得到的记录的存储位置也是随机的。适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
  • 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列函数的值称为折叠法。例如假设关键字为某人身份证号码430104681015355则可以用4位为一组进行叠加即有53558101104643014932舍去高位则有H(430104681015355)4932。
  • 随机数法:对于存储位置给定随机数安排,查找起来会很麻烦。
  • 除留余数法:散列函数(关键字)=关键字%pp一般是不大于表长的最大质数。这种方法使用较多关键是选取较理想的p值使得每一个关键字通过该函数转换后映射到散列空间上任一地址的概率都相等从而尽可能减少发生冲突的可能性。一般情形下取p为一个素数较理想如果是合数则因为可以被多个数整除从而多个关键字余数相同造成冲突。

映射冲突

一般情况下,设计出的散列函数很难是单射的,即不同的关键字对应到同一个存储位置,这样就造成了冲突(碰撞)。此时,发生冲突的关键字互为同义词。

开放地址法

可存放新表项的空闲地址既向同义词开放叶向非同义词开放。从发生冲突的那个单元开始,按照一定的次序,从哈希表中找出一个空闲的存储单元,把发生冲突的待插入关键字存储到该单元中,从而解决冲突。既指如果当前冲突,则将元素移动到其他空闲的地方。

$H_i=(H(key)+d_i)\mod m$。

  • $i$表示发生第$i$次冲突,$i=1,2,\cdots,m-1)$。
  • $m$为散列表长度,类似于循环队列,超出表长以后就循环到最左边。
  • $d_i$为增量序列是指发生第i次冲突的时候H(key)往后偏移了多少位。

增量序列选择:

  • 线性探测再散列:$d_i=1,2,3,\cdots,m-1$。线性探测再散列法充分利用了哈希表的空间,但在解决一个冲突时,可能造成新的冲突(聚集:同义和非同义关键字都堆积到一起)。另外,也不能随便对结点进行删除。
  • 二次探测再散列:$di=1,-1,2^2,-2^2\cdots,(\dfrac{m}{2})^2,-(\dfrac{m}{2})^2$。对比线性探测法更不容易产生聚集问题。注意:散列表长度$m$必须是一个可以表示为$4j+3$的素数才能探测到所有位置。
  • 伪随机探测再散列,定义$d_i$是一个伪随机数。

链地址法

又称为拉链法或链接法,是把相互发生冲突的同义词用一个单链表链接起来,若干组同义词可以组成若干个单链表。思想类似于邻接表的基本思想,且这种方法适合于冲突比较严重的情况。

指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

再散列法

再散列法或再哈希法其实很简单,就是再使用散列函数去散列一个输入的时候,输出是同一个位置就再次使用另一个散列函数计算,直至不发生冲突:

$H_i=RH_i(Key),i=1,2,\cdots,k$。

每次冲突都要重新哈希,计算时间增加。

公共溢出区法

为所有冲突的关键字记录建立一个公共的溢出区来存放。在查找时,对给定关键字通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表进行顺序查找。如果相对于基本表而言,在有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

散列查找

先通过散列函数计算目标元素存储地址,然后根据解决冲突的方法进行下一步的查询。

如果使用拉链法通过散列函数计算得到存储地址为空,则可以直接代表查找失败,这时候一般定义查找长度这里不算。

而如果使用开放地址法计算得到空位置的时候,代表查找失败,但是这时候需要定义查找长度要算这个地址。

若散列函数设计得足够好,散列查找时间复杂度可以达到$O(1)$,即不存在冲突。

装填因子=表中记录数/散列表长度。装填因子代表一个散列表中的满余情况,越大则查找效率越低。

若只给出了装填因子$\alpha$,则此时平均查找长度为:$ASL=\dfrac{1}{2}(1+\dfrac{1}{1-\alpha})$。