数据结构—查找(Part Ⅲ)—B树/B+树 & 散列查找


数据结构-查找(第九章)的整理笔记,若有错误,欢迎指正。

B树(B-树)及其基本操作

  • B树(B-树),又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
  1. 树中每个结点至多有m棵子树,即至多含有m-1个关键字。
  2. 若根结点不是终端结点,则至少有两棵子树。
  3. 除根结点外的所有非叶结点至少有 ⌈ m 2 ⌉ ⌈\frac m2⌉ 2m棵子树,即至少含有 ⌈ m 2 ⌉ − 1 ⌈\frac m2⌉-1 2m1个关键字。
  4. 所有非叶结点的结构如下:
n P 0 P_0 P0 K 1 K_1 K1 P 1 P_1 P1 K 2 K_2 K2 P 2 P_2 P2 K n K_n Kn P n P_n Pn

其中, K i ( i = 1 , 2 , … , m ) K_i(i=1,2,…,m) Ki(i=1,2,,m)为结点的关键字,且满足 K 1 < K 2 < … < K n ; P i ( i = 0 , 1 , … , n ) K_1<K_2<…<K_n;P_i(i=0,1,…,n) K1<K2<<KnPi(i=0,1,,n)为指向子树根结点的指针,且指针 P i − 1 P_{i-1} Pi1所指子树中所有结点的关键字均小于 K i K_i Ki P i P_i Pi所指子树中所有结点的关键字均大于 K i , n ( ⌈ m 2 ⌉ − 1 ≤ n ≤ m − 1 ) K_i,n(⌈\frac m2⌉-1≤n≤m-1) Kin(⌈2m1nm1)为结点中关键字的个数。

  1. 所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
    B树是所有结点的平衡因子均等于0的多路平衡查找树。

B树(B-树)的高度(磁盘存取次数)

  • 首先应该明确B树的高度不包括最后的不带任何信息的叶结点所处的那一层(有些书对B树的高度的定义中,包含最后的那一层)。
  • B树中的大部分操作所需的磁盘存取次数与B树的高度成正比。
  • 若n≥1,则对任意一棵包含n个关键字、高度为h、阶数为m的B树:
  • 最大高度为: h ≤ l o g ⌈ m 2 ⌉ n + 1 2 + 1 h≤log_{⌈\frac m2⌉}^{\frac{n+1}2}+1 hlog2m2n+1+1
  • 最小高度为: h ≥ l o g m n + 1 h≥log_m^{n+1} hlogmn+1

B树(B-树)的查找

  • 在B树上进行查找与二叉查找树很相似,只是每个结点都是多个关键字的有序表,在每个结点上所做的不是两路分支决定,而是根据该结点的子树所做的多路分支决定。
  • B树的查找包含两个基本操作:①在B树中找结点;②在结点内找关键字。由于B树常存储在磁盘上,因此前一个查找操作是在磁盘上进行的,而后一个查找操作是在内存中进行的,即在找到目标结点后,先将结点信息读入内存,然后在结点内采用顺序查找法或折半查找法。
  • 在B树上查找到某个结点后,先在有序表中进行查找,若找到则查找成功,否则按照对应的指针信息到所指的子树中去查找。查找到叶结点时(对应指针为空指针),则说明树中没有对应的关键字,查找失败。

B树(B-树)的插入

  • 与二叉查找树的插入操作相比,B树的插入操作要复杂得多。在二叉查找树中,仅需查找到需插入的终端结点的位置。但是,在B树中找到插入的位置后,并不能简单地将其添加到终端结点中,因为此时可能会导致整棵树不再满足B树定义中的要求。将关键字key插入B树的过程如下:
  1. 定位。利用前述的B树查找算法,找出插入该关键字的最低层中的某个非叶结点(在B树中查找key时,会找到表示查找失败的叶结点,这样就确定了最底层非叶结点的插入位置。!注意:插入位置一定是最低层中的某个非叶结点)。
  2. 插入。在B树中,每个非失败结点的关键字个数都在区间 [ ⌈ m 2 ⌉ [⌈\frac m2⌉ [⌈2m-1,m-1]内。插入后的结点关键字个数小于m,可以直接插入;插入后检查被插入结点内关键字的个数,当插入后的结点关键字个数大于m-1时,必须对结点进行分裂。
  • 分裂的方法是:取一个新结点,在插入key后的原结点,从中间位置( ⌈ m 2 ⌉ ⌈\frac m2⌉ 2m)将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置( ⌈ m 2 ⌉ ⌈\frac m2⌉ 2m)的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增1。
    在这里插入图片描述

B树(B-树)的删除

  • B树中的删除操作与插入操作类似,但要稍微复杂一些,即要使得删除后的结点中的关键字个数≥ ⌈ m 2 ⌉ − 1 ⌈\frac m2⌉-1 2m1,因此将涉及结点的“合并”问题。
  • 当被删关键字k不在终端结点(最低层非叶结点)中时,可以用k的前驱(或后继)k来替代k’,然后在相应的结点中删除k’,关键字k必定落在某个终端结点中,则转换成了被删关键字在终端结点中的情形。
    在这里插入图片描述
  • 当被删关键字在终端结点(最低层非叶结点)中时,有下列三种情况:
  1. 直接删除关键字。若被删除关键字所在结点的关键字个数≥ ⌈ m 2 ⌉ ⌈\frac m2⌉ 2m,表明删除该关键字后仍满足B树的定义,则直接删去该关键字。
    在这里插入图片描述
  2. 兄弟够借。若被删除关键字所在结点删除前的关键字个数= ⌈ m 2 ⌉ − 1 ⌈\frac m2⌉-1 2m1,且与此结点相邻的右(或左)兄弟结点的关键字个数≥ ⌈ m 2 ⌉ ⌈\frac m2⌉ 2m,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法),以达到新的平衡。
  • 找右兄弟借:
    在这里插入图片描述
  • 找左兄弟借:
    在这里插入图片描述
  1. 兄弟不够借。若被删除关键字所在结点删除前的关键字个数= ⌈ m 2 ⌉ − 1 ⌈\frac m2⌉-1 2m1,且此时与该结点相邻的左、右兄弟结点的关键字个数均= ⌈ m 2 ⌉ − 1 ⌈\frac m2⌉-1 2m1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。
  • 在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少至0(根结点关键字个数为1时,有2棵子树),则直接将根结点删除,合并后的新结点成为根若双亲结点不是根结点,且关键字个数减少到 ⌈ m 2 ⌉ − 2 ⌈\frac m2⌉-2 2m2,则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合B树的要求为止。
    在这里插入图片描述

B+树的基本概念

  • B+树是应数据库所需而出现的一种B树的变形树。
  • 一棵m阶的B+树需满足下列条件:
  1. 每个分支结点最多有m棵子树(孩子结点)。
  2. 非叶根结点至少有两棵子树,其他每个分支结点至少有 ⌈ m 2 ⌉ ⌈\frac m2⌉ 2m棵子树。
  3. 结点的子树个数与关键字个数相等。
  4. 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。
  5. 所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针。

B树(B-树) VS B+树

  • m阶的B+树与m阶的B树(B-树)的主要差异如下:
  1. 在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树(B-树)中,具有n个关键字的结点含有n+1棵子树。
  2. 在B+树中,每个结点(非根内部结点)的关键字个数n的范围是 ⌈ m 2 ⌉ ≤ n ≤ m ⌈\frac m2⌉≤n≤m 2mnm(根结点: 1 ≤ n ≤ m 1≤n≤m 1nm);而在B树(B-树)中,每个结点(非根内部结点)的关键字个数n的范围是 ⌈ m 2 ⌉ − 1 ≤ n ≤ m − 1 ⌈\frac m2⌉-1≤n≤m-1 2m1nm1(根结点: 1 ≤ n ≤ m − 1 1≤n≤m-1 1nm1)。
  3. 在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址;而B树(B-树)中,每个关键字对应一个记录的存储地址。
  4. 在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在B树(B-树)中,叶结点(最外层内部结点)包含的关键字和其他结点包含的关键字是不重复的。
m阶B树m阶B+树
类比⼆叉查找树的进化 → \rightarrow m叉查找树分块查找的进化 → \rightarrow 多级分块查找
关键字与分叉n个关键字对应n+1个分叉(子树)n个关键字对应n个分叉
结点包含的信息所有结点中都包含记录的信息只有最下层叶子结点才包含记录的信息(可使树更矮)
查找方式不⽀持顺序查找。查找成功时,可能停在任何⼀层结点,查找速度“不稳定”⽀持顺序查找。查找成功或失败都会到达最下⼀层结点,查找速度“稳定”
  • 相同点:①除根节点外,最少 ⌈ m 2 ⌉ ⌈\frac m2⌉ 2m个分叉(确保结点不要太“空”),任何⼀个结点的子树都要⼀样高(确保“绝对平衡”) 。 ②B树和B+树都可以用于文件索引结构。


散列查找(hash)

  • 线性表和树表的查找中,记录在表中的位置与记录的关键字之间不存在确定关系,因此,在这些表中查找记录时需进行一系列的关键字比较。这类查找方法建立在“比较”的基础上,查找的效率取决于比较的次数。

散列表的基本概念

  • 散列函数(hash function):一个把查找表中的关键字映射成该关键字对应的地址的函数,记为Hash(key)=Addr(这里的地址可以是数组下标、索引或内存地址等)。
  • 散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为哈希冲突(hash collision),这些发生碰撞的不同关键字称为同义词(synonym)。一方面,设计得好的散列函数应尽量减少这样的冲突;另一方面,由于这样的冲突总是不可避免的,所以还要设计好处理冲突的方法。
  • 散列表(hash table):根据关键字而直接进行访问的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系。
  • 理想情况下,对散列表进行查找的时间复杂度为O(1),即与表中元素的个数无关。

哈希函数的构造方法

  • 构造哈希函数的目标是使所有元素的哈希地址尽可能均匀地分布在m个连续内存单元上,同时使计算过程尽可能简单以达到尽可能高的时间效率。根据关键字的结构和分布的不同可构造出许多不同的哈希函数。这里主要讨论几种常用的整数类型关键字的哈希函数构造方法。
  • 在构造散列函数时,必须注意以下几点:
  1. 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围
  2. 散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。
  3. 散列函数应尽量简单,能够在较短的时间内计算出任一关键字对应的散列地址。

直接定址法

  • 直接取关键字的某个线性函数值为散列地址,散列函数为
    H ( k e y ) = k e y 或 H ( k e y ) = a × k e y + b H(key)=key或H(key)=a×key+b H(key)=keyH(key)=a×key+b
    式中,a和b是常数。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字不连续,空位较多,则会造成存储空间的浪费。

除留余数法

  • 这是一种最简单、适用范围广、最常用的方法。除留余数法是用关键字k除以某个不大于哈希表长度m的整数p所得的余数作为哈希地址。
  • 除留余数法的哈希函数h(k)通常为 h ( k ) = k    m o d    p h(k)= k\;mod\;p h(k)=kmodp(mod为求余运算,p≤m)
  • 这种方法的关键是选好p,使得元素集合中的每一个关键字通过该函数转换后映射到哈希表范围内的任意地址上的概率相等,从而尽可能减少发生冲突的可能性。理论研究表明,取不大于m的素数时效果最好。

数字分析法

  • 设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。

平方取中法

  • 顾名思义,这种方法取关键字的平方值的中间几位作为散列地址,具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。

    在不同的情况下,不同的散列函数具有不同的性能,因此不能笼统地说哪种散列函数最好。在实际选择中,采用何种构造散列函数的方法取决于关键字集合的情况,但目标是尽量降低产生冲突的可能性。

哈希冲突的解决方法

  • 应该注意到,任何设计出来的散列函数都不可能绝对地避免冲突。为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个“空”的Hash地址。用 H i H_i Hi表示处理冲突中第i次探测得到的散列地址,假设得到的另一个散列地址 H 1 H_1 H1仍然发生冲突,只得继续求下一个地址 H 2 H_2 H2,以此类推,直到不发生冲突为止,则 H k H_k Hk为关键字在表中的地址。
  • 解决哈希冲突方法有许多,主要有开放定址法拉链法两大类。

开放定址法(open addressing)

  • 所谓开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为
    H i = ( H ( k e y ) + d i ) H_i=(H(key)+d_i)%m Hi=(H(key)+di)
    式中, H ( k e y ) H(key) H(key)为散列函数; i = 0 , 1 , 2 , … , k ( k ≤ m − 1 ) i=0,1,2,…,k(k≤m-1) i=0,1,2,,k(km1) m m m表示散列表表长; d i d_i di为增量序列。
  • 取定某一增量序列后,对应的处理方法就是确定的。通常有以下4种取法:
  1. 线性探测法(linear probing)。当 d i = 0 , 1 , 2 , . . . , m − 1 d_i=0,1,2,...,m-1 di=0,1,2,...,m1时,称为线性探测法。这种方法的特点是:冲突发生时,顺序查看表中下一个单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。
  • 线性探测法可能使第i个散列地址的同义词存入第i+1个散列地址,这样本应存入第i+1个散列地址的元素就争夺第i+2个散列地址的元素的地址……从而造成大量元素在相邻的散列地址上“聚集”(或堆积)起来,大大降低了查找效率。
  • 在开放定址法中,散列到同一个地址而产生的"堆积"问题,是同义词冲突的探查序列和非同义词之间不同的探查序列交织在一起,导致关键字查询需要经过较长的探测距离,降低了散列的效率。
  1. 平方探测法(square probing)。当 d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , … k 2 , − k 2 d_i=0²,1²,-1²,2²,-2²,…k²,-k² di=0212122222k2k2时,称为平方探测法,其中 k ≤ m 2 k≤\frac m2 k2m,散列表长度m必须是一个可以表示成4k+3的素数,又称二次探测法。
  • 平方探测法是一种处理冲突的较好方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。
  1. 再散列法。当 d i = H a s h 2 ( k e y ) d_i= Hash_2(key) di=Hash2(key)时,称为再散列法,又称双散列法。需要使用两个散列函数,当通过第一个散列函数 H ( k e y ) H(key) H(key)得到的地址发生冲突时,则利用第二个散列函数 H a s h 2 ( k e y ) Hash_2(key) Hash2(key)计算该关键字的地址增量。它的具体散列函数形式如下 H i = ( H ( k e y ) % m + i × H a s h 2 ( k e y ) ) % m H_i=(H(key)\%m+i×Hash_2(key))\%m Hi=(H(key)%m+i×Hash2(key))%m
    初始探测位置 H 0 = H ( k e y ) % m H_0=H(key)\%m H0=H(key)%m。i是冲突的次数,初始为0。在再散列法中,最多经过m-1次探测就会遍历表中所有位置,回到 H 0 H_0 H0位置。
  • 采用再散列法处理冲突时不易产生聚集。
  1. 伪随机序列法。当 d i = d_i= di=伪随机数序列时,称为伪随机序列法。
    !注意:在开放定址的情形下,不能随便物理删除表中的已有元素,因为若删除元素,则会載断其他具有相同散列地址的元素的查找地址。因此,要删除一个元素时,可给它做一个删除标记,进行逻辑删除。但这样做的副作用是:执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用,因此需要定期维护散列表,要把删除标记的元素物理删除。
    点击!具体代码实现

拉链法(chaining)

  • 显然,对于不同的关键字可能会通过散列函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。假设散列地址为i的同义词链表的头指针存放在散列表的第i个单元中,因而查找、插入和删除操作主要在同义词链中进行。拉链法适用于经常进行插入和删除的情况

  • 与开放定址法相比,拉链法有以下几个优点:

  1. 拉链法处理冲突简单,且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短;
  2. 由于拉链法中各单链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  3. 开放定址法为减少冲突要求装填因子a较小,故当数据规模较大时会浪费很多空间,而拉链法中可取a≥1,且元素较大时拉链法中增加的指针域可忽略不计,因此节省空间;
  4. 在用拉链法构造的哈希表中,删除结点的操作更加易于实现。
  • 拉链法也有缺点:指针需要额外的空间,故当元素规模较小时开放定址法较为节省空间,若将节省的指针空间用来扩大哈希表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高了平均查找速度。
    点击!具体代码实现

散列查找及性能分析

  • 散列表的查找过程与构造散列表的过程基本一致。对于一个给定的关键字key,根据散列函数可以计算出其散列地址,执行步骤如下:
    初始化:addr=Hash(key);
    ①检测查找表中地址为Addr的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与key的值,若相等,则返回查找成功标志,否则执行步骤②
    ②用给定的处理冲突方法计算“下一个散列地址”,并把Addr置为此地址,转入步骤①

在哈希表中,虽然冲突很难避免,但发生冲突的可能性却有大有小,这会影响哈希查找的性能。哈希查找性能主要与3个因素有关:

  1. 与装填因子a有关。所谓装填因子(load factor)是指哈希表中已存入的元素数n与哈希地址空间大小m的比值,即 a = n m a=\frac nm a=mn。a越小,冲突的可能性就越小;a越大(最大可取1),冲突的可能性就越大。这很容易理解,因为a越小,哈希表中空闲单元的比例就越大,所以待插入元素和已插入的元素发生冲突的可能性就越小;反之,a越大,哈希表中空闲单元的比例就越小,所以待插入元素和已插入的元素冲突的可能性就越大。另一方面,a越小,存储空间的利用率就越低;反之,存储空间的利用率也就越高。为了既兼顾减少冲突的发生,又兼顾提高存储空间的利用率这两个方面,通常使最终的a控制在0.6~0.9的范围内。
  2. 与所采用的哈希函数有关。若哈希函数选择得当,就可以使哈希地址尽可能均匀地分布在哈希地址空间上,从而减少冲突的发生;否则,若哈希函数选择不当,就可能使哈希地址集中于某些区域,从而加大冲突的发生。
  3. 当出现哈希冲突时需要采取解决哈希冲突的方法,所以哈希查找性能也与解决冲突的方法有关。
  • 对于预先知道且规模不大的关键字集合,通常可以找到不发生冲突的哈希函数,从而避免出现冲突,使查找时间复杂度为O(1),提高了查找效率,因此对频繁进行查找的关键字集应尽力设计一个完美的哈希函数。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值