介绍:
继续上一次内容,更新了红黑树、B树、B+树和散列表的内容。
上一篇
6、红黑树
平衡二叉树:适用于以查找为主,很少有增删操作的情形。
红黑树: 适用于频繁插入、删除的场景,实用性很强。
一颗红黑树是满足如下红黑树性质的二叉排序树:
1、每个结点或是红色,或是黑色的。
2、根节点是黑色的。
3、叶节点(虚拟的外部节点、NULL结点)都是黑色的。
4、不存在两个相邻的红节点(即红结点的父结点和孩子结点都是黑色的)。
5、对每个结点,从该结点到任意叶结点的简单路径上,所含黑结点的数量相同。
黑高bh: 从某结点出发(不含该节点)到达一个叶结点的任一简单路径上黑结点总数。
根结点的黑高称为红黑树的黑高。
- 结论1:从根结点到叶结点的最长路径不大于最短路径的2倍。
- 结论2:有n个内部结点的红黑树的高度 h < = 2 l o g 2 ( n + 1 ) h<=2log_2(n+1) h<=2log2(n+1)。
- 结论3:新插入红黑树的结点初始着为红色。
结论的证明----p277
可见,红黑树的“适度平衡”,由AVL树的“高度平衡”,降低到“任一结点左右子树的高度差,相差不超过2倍”,也降低了动态操作时调整的频率。c++的map和set都是红黑树来实现的。
1、红黑树的插入
红黑树的插入过程和二叉查找树的插入过程基本类似,不同之处在于, 红黑树中插入新结点后需要进行调整(主要通过重新着色或旋转操作进行),以满足红黑树的性质。
插入过程描述:
1、用二叉查找树插入法插入,并将结点z着为红色。若结点z的父结点是黑色的,无序做任何调整,此时就是一棵标准的红黑树。
2、如果结点z是根结点,将z着为黑色(树的黑高增1),结束。
3、如果结点z不是根结点,并且z的父结点z.p是红色的,则分为以下三种情况,区别在于z的叔结点y的颜色不同,因z.p是红色的,插入前的树是合法的,根据性质2和性质4,爷爷的结点z.p.p必然存在且为黑色。性质4只在z和z.p之间被破坏了。
情况1:z的叔结点y是黑色的,且z是一个右孩子。
情况2:z的叔结点y是黑色的,且z是一个左孩子。
情况3:如果x的叔结点y是红色。(z是左孩子或右孩子无影响)
(情况1、2:其实和平衡二叉树类似,只是树的颜色需要改变)
(情况3:先局部保证黑高一致,将z.p和z的叔搞成黑色,z.p.p搞成红色,之后z.p.p作为新的z结点重复循环,保证整棵树的红黑树性质。)
只要满足情况3,就会一直循环,直到整棵树满足红黑树性质。
2、红黑树的删除
红黑树的插入操作容易导致连续的两个红结点,破坏性质4。而删除操作容易造成子树黑高的变化(删除黑结点会导致根结点到叶结点间的黑结点数量减少),破坏性质5。
删除操作描述:
删除过程也是先执行二叉树的删除方法。若待删除结点由两个孩子,不能直接删除,而要找到该结点的中序后继(或前驱)填补,即右子树中最小的结点,然后转换为删除该后继结点。由于后继结点至多只有一个孩子,这样就转换为待删结点时叶结点或仅有一个孩子的情况。
-待删结点没有孩子。
-待删结点只有右子树或左子树。
1、如果待删结点只有右子树或左子树,则只有两种情况。(所删除的结点不可能是红色,且所删黑结点的子树必然是红色的)
2、如果待删结点没有孩子,若该结点是红色的,则直接删除,无须做任何调整。
3、如果待删结点没有孩子,并且该结点是黑色的。(双黑结点,破坏性质1,双黑结点-->普通红黑树结点)
情况1:x的兄弟结点w是红色的。(兄弟结点w旋到x.p-->情况变为2、3、4)
情况2:x的兄弟结点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的。(先右旋再左旋,红色结点)
情况3:x的兄弟结点w是黑色的,且w的右孩子是红色的。(左单旋)
情况4:x的兄弟结点w是黑色的,且w的两个孩子结点都是黑色的。(如果x.p是红色,可以终止,否则就继续循环)
7、B树
B树,又称多路平衡查找树,B树种所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一颗m阶B树或为空树,或为满足如下特性的m叉树。
- 树中每个结点至多有m棵子树,即至多含有m-1个关键字
- 若根节点不是终端结点,则至少有两棵子树
- 除根节点外的所有非叶结点至少有 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉棵子树,即至少含有 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil-1 ⌈m/2⌉−1个关键字
- 所有非叶结点的结构如下
- 所有的叶节点都出现同一层次上,并且不带信息(可以视为查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)
B树是所有结点的平衡因子等于0的多路平衡查找树。
8、B+树
B+树是应数据库所需而出现的一种B树的变形树。
一棵m阶的B+树需满足下列条件:
- 每个分支结点最多有m棵子树(孩子结点)。
- 非叶根结点至少有两棵子树,其他每个分支结点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉棵子树。
- 结点的子树个数与关键字个数相等。
- 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。
- 所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针。
m阶的B+树与m阶的B树的主要差异如下:
- 在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的结点含有n+1棵子树。
- 在B+树中,每个结点(非根内部结点)的关键字个数n的范围是 ⌈ m / 2 ⌉ < = n < = m \lceil m/2 \rceil<=n<=m ⌈m/2⌉<=n<=m(根结点: 2 < = n < = m 2<=n<=m 2<=n<=m);在B树中,每个结点(非根内部结点)的关键字个数n的范围是 ⌈ m / 2 ⌉ − 1 < = n < = m − 1 \lceil m/2 \rceil-1<=n<=m-1 ⌈m/2⌉−1<=n<=m−1(根结点:1<=n<=m-1)。
- 在B+树中,叶结点包含信息,所有非叶节点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
- 在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在B树中,叶结点(最外层内部结点)包含的关键字和其他结点包含的关键字是不重复的。
可以知道,分支结点的某个关键字是其子树中最大关键字的副本。
通常在B+树中有两个指针:一个指向根结点,另一个指向关键字最小的叶结点。
因此可以对B+树进行两种查找运算:一种是从最小关键字开始的顺序查找,另一种是从根结点开始的多路查找。
在B+树上中查找时,无论查找成功与否,每次查找都是一条从根结点到叶结点的路径。
9、散列表
一、散列函数的构造
典型的空间换时间的算法。
散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数。记为 H a s h ( k e y ) = A d d r Hash(key)=Addr Hash(key)=Addr
冲突:散列函数可能会把两个或两个以上的不同关键字映射到同一个地址。
这些发生碰撞的成为同义词。
散列表:根据关键字而直接进行访问的数据结构。也就是说散列表建立了关键字和存储地址之间的一种直接映射关系。
1、直接定址法
散列函数: 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)=key或H(key)=a∗key+b。
计算方法最简单,且不会产生冲突。适合关键字的分布基本连续的情况,若关键字分布不连续,且空位较多,则会造成存储空间的浪费。
2、保留余数法
最简单最常用的一种方法。取一个不大于m但最接近或等于m的质数p。(质数的原因:让不同的关键字冲突尽可能少)
散列函数: H ( k e y ) = k e y % p H(key)=key \% p H(key)=key%p
3、数字分析法
设关键字是r进制数,①而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均一些,每种数码出现的机会均等;②若在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较为均匀的若干位作为散列地址。
这种只适合已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。
适合数字序列作为关键字的数据集,比如身份证号,使用普通的散列函数会造成空间的极大浪费。
4、平方取中法(是数字分析法的改进)
取关键字的平方值的中间几位作为散列地址。具体几位视具体情况而定。
这种方法得到的散列地址和关键字的每一位都有关系,因此使得到的散列地址分布较为平均。
适于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
二、处理冲突的方法
用 H i H_i Hi表示处理冲突种第i次探测得到的散列地址,假设得到的另一个散列地址 H 1 H_1 H1仍然发生冲突,只得继续求下一个冲突地址 H 2 H_2 H2,以此类推,直到 H k H_k Hk不发生冲突为止, H k H_k Hk为关键字在表中的位置。
-
开放定址法,也称再散列法。
基本思想:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,··· ,直到找出一个不冲突的哈希地址pi,将相应元素存入其中。 H_i=(H(key)+d_i)\%m,i=0,1,2,··· ,n
指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。数学递推公式: H i = ( H ( k e y ) + d i ) % m H_i=(H(key)+d_i)\%m Hi=(H(key)+di)%m。H(Key)为散列函数;i=0,1,2···k(k<=m-1);m表示散列表表长; d i d_i di为增量序列。取定某个增量序列后,对应的处理方法就是固定的,通常有以下3种取法。
-
线性探测法💜。 d i d_i di=0,1,2,···,k(k<=m-1)。直到找出下一个空闲单元,或查遍全表。
-
平方探测法💜。 d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , ⋅ ⋅ ⋅ , k 2 , − k 2 d_i=0^2,1^2,-1^2,2^2,-2^2,···,k^2,-k^2 di=02,12,−12,22,−22,⋅⋅⋅,k2,−k2,其中k<=m/2。散列表长度m,必须是一个可以表示成4j+3的素数(k可以是任何整数,否则不能探测全部位置),又称二次探测法。
缺点: 优点:比起线性探测法,更不易产生堆积。(可避免堆积问题)
-
双散列法
-
伪随机序列法。 d i d_i di是伪随机数序列。
注意:开放地址法中,不能随便物理删除表中的已有元素,因为若删除元素,则会截断其他具有相同散列地址的元素的查找地。因此要删除一个元素时,可给他做一个删除标记,进行逻辑删除。但这样做的副作用是:执行多次删除后,表面看起来散列表很满,实际上有许多位置未利用,因此需要定期维护散列表,要把删除标记的元素的物理删除。
-
-
再哈希法。
严蔚敏//除了原始的散列函数H(key)之外,多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止。
H i = R H i ( K e y ) i = 1 , 2 , 3 , ⋅ ⋅ ⋅ , k H_i=RH_i(Key) i=1,2,3,···,k Hi=RHi(Key)i=1,2,3,⋅⋅⋅,k
//王道——双散列法// d i = H a s h 2 ( k e y ) d_i=Hash_2(key) di=Hash2(key)。即需要两个散列函数。具体散列函数形式如下:
H i = ( H ( k e y ) + i ∗ H a s h 2 ( k e y ) ) % m H_i=(H(key)+i*Hash_2(key))\%m Hi=(H(key)+i∗Hash2(key))%m。
初始探测位置 H 0 = H ( k e y ) H_0=H(key)%m H0=H(key)。 i i i是冲突的次数,初始为0。在双散列法中,最多经过m-1次探测就会遍历表中所有位置,回到 H 0 H_0 H0位置。
-
链地址法
为了避免非同义词发生冲突,把所有的同义词存储到一个线性链表中,这个线性链表由其散列地址唯一标识。优化:进行一个排序。
拉链法用于经常进行插入和删除的情况。
三、性能分析
1、由于**“冲突”的产生**,使得散列表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需要以平均查找长度作为衡量散列表的查找效率的度量。
2、散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子
装填因子:散列表的装填因子一般记为α,定义一个表的装满程度。
α
=
表中记录数
n
散列表长度
m
α=\frac{表中记录数n}{散列表长度m}
α=散列表长度m表中记录数n
散列表的平均查找长度依赖于散列表的装填因子(装填因子),而不直接依赖于n或m。
冲突:散列函数将不同关键字的数据元素映射到相同位置
聚集:在处理同义词冲突或非同义词冲突的过程中,需要向后计算新的地址存储数据元素产生的的堆积。
*参考:*王道考研