1.基础知识
术语
-
节点的度:一个节点含有的子树的个数称为该节点的度;
-
树的度:一棵树中,最大的节点度称为树的度;
-
叶节点或终端节点:度为零的节点;
-
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
-
深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0(等价于p到根节点有多少条边)
-
高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
-
平衡二叉树(AVL树):是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
-
满二叉树:所有叶节点都在最底层的完全二叉树;
-
完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;
完全二叉树
2. 平衡二叉树
在二叉搜索树的基础上,是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树可以避免二叉搜索树退化成近似链或链。
平衡因子(平衡度)
:节点的左子树的高度减去右子树的高度(反之同理). 由此可知平衡二叉树
是每个结点的平衡因子都为 1、-1、0 的树。
旋转类型的判定:、
离插入点最近的,平衡因子的绝对值大于1的点称为失衡点。
从失衡点开始,沿树“寻找”插入点。且只记录“寻找”的前两步的路径方向。由此判定旋转类型。
插入点为3,失衡点为9。从9开始,“寻找”3。发现3在9的左子树的左子树上。 将路径简记为“左左”。所以需要LL旋转。
“寻找”路径简记为“右左”。所以需要RL旋转。
具体旋转恢复平衡方法同红黑树
删除操作:
同插入操作一样,删除结点时也有可能破坏平衡性,这就要求我们删除的时候要进行平衡性调整。
删除分为以下几种情况:
首先在整个二叉树中搜索要删除的结点,如果没搜索到直接返回不作处理,否则执行以下操作:
1.要删除的节点是当前根节点T。
如果左右子树都非空。在高度较大的子树中实施删除操作。
分两种情况:
(1)、左子树高度大于右子树高度,将左子树中最大的那个元素赋给当前根节点,然后删除左子树中元素值最大的那个节点。
(1)、左子树高度小于右子树高度,将右子树中最小的那个元素赋给当前根节点,然后删除右子树中元素值最小的那个节点。
如果左右子树中有一个为空(非空的也只有一个节点),那么直接用那个非空子树或者是NULL替换当前根节点即可。
2、要删除的节点元素值小于当前根节点T值,在左子树中进行删除。
递归调用,在左子树中实施删除。
这个是需要判断当前根节点是否仍然满足平衡条件,
如果满足平衡条件,只需要更新当前根节点T的高度信息。
否则,需要进行旋转调整:
如果T的左子节点的左子树的高度大于T的左子节点的右子树的高度,进行相应的单旋转。否则进行双旋转。
3、要删除的节点元素值大于当前根节点T值,在右子树中进行删除。
双旋转
对于左右和右左两种情况,单旋转不能解决问题,要经过两次旋转。
3.红黑树
红黑树是平衡二叉查找树的一种,是一种高效的查找树。
红黑树的定义如下:
- 任何一个节点都有颜色,黑色或者红色
- 根节点是黑色的
- 所有叶子都是黑色(叶子是NIL节点)。"nil 叶子" 它不包含数据而只充当树在此结束的指示。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
从性质5又可以推出:
性质5.1:如果一个结点存在黑子结点,那么该结点肯定有两个子结点(如果只有一个黑节点,那么为空的一侧到叶子节点会少一个黑节点,与性质5冲突)
有了上面的几个性质作为限制,即可避免二叉查找树退化成单链表的情况。
同时性质4和性质5作为约束,即可保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍。
即:
当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。
此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。
红黑树操作
红黑树是一种自平衡的二叉查找树,红黑树总是通过旋转和变色达到自平衡。
旋转操作
左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
将右子节点(v)提到支点位置,支点做右子节点(v)的左节点。此时将右子节点(v)的原左节点(R)作为支点的右节点。其余不变。
右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
将支点的左节点(F)提到支点的位置,支点做其右节点,左节点(F)原右节点(k)做支点的左节点。
左旋只影响旋转结点和其右子树的结构,把右子树的结点往左子树挪了。
右旋只影响旋转结点和其左子树的结构,把左子树的结点往右子树挪了。
变色:结点的颜色由红变黑或由黑变红。
红黑树查找
因为红黑树是一颗二叉平衡树,并且查找不会破坏树的平衡,所以查找跟二叉平衡树的查找无异:
- 从根结点开始查找,把根结点设置为当前结点;
- 若当前结点为空,返回null;
- 若当前结点不为空,用当前结点的key跟查找key作比较;
- 若当前结点key等于查找key,那么该key就是查找目标,返回当前结点;
- 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤2;
- 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤2;
插入
插入操作包括两部分工作:一查找插入的位置;二插入后自平衡。查找插入的父结点很简单,跟查找操作区别不大:
- 从根结点开始查找;
- 若根结点为空,那么插入结点作为根结点,结束。
- 若根结点不为空,那么把根结点作为当前结点;
- 若当前结点为null,返回当前结点的父结点,结束。
- 若当前结点key等于查找key,那么该key所在结点就是插入结点,更新结点的值,结束。
- 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤4;
- 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤4;
插入节点的颜色是红色。如果插入的节点是黑色,那么这个节点所在路径比其他路径多出一个黑色节点。插入的节点是红色,此时所有路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的情况。这种情况下,通过变色和旋转进行调整即可,
插入情景
-
红黑树为空树
直接把插入结点作为根结点就行,但根据红黑树性质2:根节点是黑色。还需要把插入结点设为黑色。
-
插入结点的Key已存在
插入结点的Key已存在,既然红黑树总保持平衡,在插入前红黑树已经是平衡的,那么把插入结点设置为将要替代结点的颜色,再把结点的值更新就完成插入。
-
插入结点的父结点为黑结点
由于插入的结点是红色的,并不会影响红黑树的平衡,直接插入即可,无需做自平衡。
-
插入结点的父结点为红结点
4.1 叔叔结点存在并且为红结点
从红黑树性质4可以,祖父结点肯定为黑结点,因为不可以同时存在两个相连的红结点。那么此时该插入子树的红黑层数的情况是:黑红红。显然最简单的处理方式是把其改为:红黑红。
可以看到,我们把PP结点设为红色了,如果PP的父结点是黑色,那么无需再做任何处理;但如果PP的父结点是红色,根据性质4,此时红黑树已不平衡了,所以还需要把PP当作新的插入结点,继续做插入操作自平衡处理,直到平衡为止。
若PP刚好为根结点时,那么根据性质2,我们必须把PP重新设为黑色,那么树的红黑结构变为:黑黑红。换句话说,从根结点到叶子结点的路径中,黑色结点增加了。这也是唯一一种会增加红黑树黑色结点层数的插入情景。(由于pp为根结点设置黑色,不影响性质5,红黑树的生长是自底向上的)
4.2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子结点
叔叔节点实际无法为黑色节点,因为这里考虑的为父节点为红色。叔叔结点非红即为叶子结点(Nil)。
这种情况G,J相当于插入节点,但其为黑色,不影响上述结论
4.2.1:插入结点是其父结点的左子结点
4.2.2:插入结点是其父结点的右子结点
4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点
4.3.1:插入结点是其父结点的右子结点
4.3.2:插入结点是其父结点的左子结点
删除情景
- 情景1:若删除结点无子结点,直接删除
- 情景2:若删除结点只有一个子结点,用子结点替换删除结点
删除节点 a,并且把节点 b 替换到节点 a 的位置
节点 a 只能是黑色,节点 b 也只能是红色,其他情况均不符合红黑树的定义。这种情况下,我们把节点 b 改为黑色;调整结束,不需要进行二次调整
- 情景3:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点
情景3的后继结点是大于删除结点的最小结点,也是删除结点的右子树种最左结点。也可以拿前继结点(删除结点的左子树最右结点)替代。但习惯上大多都是拿后继结点来替代。
找前继和后继结点的直观的方法:把二叉树所有结点投射在X轴上,所有结点都是从左到右排好序的,所有目标结点的前后结点就是对应前继和后继结点。
对于P来说,轴上前后紧挨的M,R就是前后继结点
3.1 如果要删除的节点 a 有两个非空子节点,并且它的后继节点就是节点 a 的右子节点 c。
如果节点 a 的后继节点就是右子节点 c,那右子节点 c 肯定没有左子树。我们把节点 a 删除,并且将节点 c 替换到节点 a 的位置。然后把节点 c 的颜色设置为跟节点 a 相同的颜色;
这里因为c的左子树为空,则d为红色,且为末尾。那么c为黑色,c移上去后,d改为黑色。
3.2 如果要删除的是节点 a,它有两个非空子节点,并且节点 a 的后继节点不是右子节点,
找到后继节点 d,并将它删除,删除后继节点 d 的过程参照 情景一;
将节点 a 替换成后继节点 d;
把节点 d 的颜色设置为跟节点 a 相同的颜色;
4. b树
有这么几个数字:1,2,3,4,5,6,7,8,9,0,分别生成AVL树,B树
B树是一种平衡的多分树,通常我们说m阶的B树,它必须满足如下条件:
- 每个节点最多只有m个子节点。
- 每个非叶子节点(除了根)具有至少⌈ m/2⌉(上限)子节点。(分裂机制)
- 如果根不是叶节点,则根至少有两个子节点。
- 具有k个子节点的非叶节点包含k -1个key。且key按顺序升序排序
- 所有叶子都出现在同一水平(高度一致)。
什么是B树的阶 ?
B树中一个节点的子节点数目的最大值,用m表示,假如最大值为10,则为10阶,如图
所有节点中,节点【13,16,19】拥有的子节点数目最多,四个子节点(灰色节点),所以可以定义上面的图片为4阶B树
插入
针对m阶高度h的B树,插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素。
- 若该节点元素个数小于m-1,直接插入;
- 若该节点元素个数等于m-1,引起节点分裂;以该节点中间元素为分界,取中间元素(偶数个数,中间两个随机选取)插入到父节点中;
- 重复上面动作,直到所有节点符合B树的规则;最坏的情况一直分裂到根节点,生成新的根节点,高度增加1;
上面三段话为插入动作的核心,接下来以5阶B树为例,详细讲解插入的动作;
5阶B树关键点:
- 2<=根节点子节点个数<=5(根节点至少两个)
- 3<=内节点子节点个数<=5(至少⌈ m/2⌉(上限)子节点)
- 1<=根节点元素个数<=4(至多元素个数阶数减一,至少为子节点个数减一)
- 2<=非根节点元素个数<=4
插入8
图(1)插入元素【8】后变为图(2),此时根节点元素个数为5,不符合 1<=根节点元素个数<=4,进行分裂(真实情况是先分裂,然后插入元素,这里是为了直观而先插入元素,下面的操作都一样,不再赘述),取节点中间元素【7】,加入到父节点,左右分裂为2个节点,如图(3)
接着插入元素【5】,【11】,【17】时,不需要任何分裂操作,如图(4)
插入元素【13】
节点元素超出最大数量,进行分裂,提取中间元素【13】,插入到父节点当中,如图(6)
接着插入元素【6】,【12】,【20】,【23】时,不需要任何分裂操作,如图(7)
插入【26】时,最右的叶子结点空间满了,需要进行分裂操作,中间元素【20】上移到父节点中,注意通过上移中间元素,树最终还是保持平衡,分裂结果的结点存在2个关键字元素。
插入【4】时,导致最左边的叶子结点被分裂,【4】恰好也是中间元素,上移到父节点中,然后元素【16】,【18】,【24】,【25】陆续插入不需要任何分裂操作
最后,当插入【19】时,含有【14】,【16】,【17】,【18】的结点需要分裂,把中间元素【17】上移到父节点中,但是情况来了,父节点中空间已经满了,所以也要进行分裂,将父节点中的中间元素【13】上移到新形成的根结点中,这样具体插入操作的完成。
删除
首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其结点中进行删除;删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素(“左孩子最右边的节点”或“右孩子最左边的节点”)到父节点中,然后是移动之后的情况;如果没有,直接删除。
- 某结点中元素数目小于(m/2)-1,(m/2)向上取整,则需要看其某相邻兄弟结点是否丰满;
- 如果丰满(结点中元素个数大于(m/2)-1),则向父节点借一个元素来满足条件;
- 如果其相邻兄弟都不丰满,即其结点数目等于(m/2)-1,则该结点与其相邻的某一兄弟结点进行“合并”成一个结点;
以5阶B树为例,详细讲解删除的动作;
- 关键要领,元素个数小于 2(m/2 -1)就合并,大于4(m-1)就分裂
如图依次删除依次删除【8】,【20】,【18】,【5】
首先删除元素【8】,当然首先查找【8】,【8】在一个叶子结点中,删除后该叶子结点元素个数为2,符合B树规则,操作很简单,咱们只需要移动【11】至原来【8】的位置,移动【12】至【11】的位置(也就是结点中删除元素后面的元素向前移动)
下一步,删除【20】,因为【20】没有在叶子结点中,而是在中间结点中找到,咱们发现他的继承者【23】(字母升序的下个元素),将【23】上移到【20】的位置,然后将孩子结点中的【23】进行删除,这里恰好删除后,该孩子结点中元素个数大于2,无需进行合并操作。
下一步删除【18】,【18】在叶子结点中,但是该结点中元素数目为2,删除导致只有1个元素,已经小于最小元素数目2,而由前面我们已经知道:如果其某个相邻兄弟结点中比较丰满(元素个数大于ceil(5/2)-1=2),则可以向父结点借一个元素,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中,在这个实例中,右相邻兄弟结点中比较丰满(3个元素大于2),所以先向父节点借一个元素【23】下移到该叶子结点中,代替原来【19】的位置,【19】前移;然【24】在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除【24】,后面元素前移。
最后一步删除【5】, 删除后会导致很多问题,因为【5】所在的结点数目刚好达标,刚好满足最小元素个数(ceil(5/2)-1=2),而相邻的兄弟结点也是同样的情况,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。所以在该实例中,咱们首先将父节点中的元素【4】下移到已经删除【5】而只有【6】的结点中,然后将含有【4】和【6】的结点和含有【1】,【3】的相邻兄弟结点进行合并成一个结点。
也许你认为这样删除操作已经结束了,其实不然,在看看上图,对于这种特殊情况,你立即会发现父节点只包含一个元素【7】,没达标(因为非根节点包括叶子结点的元素K必须满足于2=<K<=4,而此处的K=1),这是不能够接受的。如果这个问题结点的相邻兄弟比较丰满,则可以向父结点借一个元素。而此时兄弟节点元素刚好为2,刚刚满足,只能进行合并,而根结点中的唯一元素【13】下移到子结点,这样,树的高度减少一层。
5.B+树
B+树的定义
B+树是应文件系统所需而出的一种B树的变型树。一棵m阶的B+树和m阶的B树的差异在于:
1.有n棵子树的结点中含有n个key,每个key不保存数据,只用来索引,所有数据都保存在叶子节点。
非叶根节点至少有两棵子树,其他分支结点至少有Math.ceil(m/2)棵子树。
2.所有的叶子结点中包含了全部key的信息,及指向含这些key记录的指针,且叶子结点本身依key的大小自小而大顺序链接。
3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)key。
通常在B+树上有两个头指针,一个指向根结点,一个指向key最小的叶子结点。因些,对于B+树进行查找两种运算:一种是从最小关键字起顺序查找,另一种是从根结点开始,进行随机查找。
”卫星数据“ ,指的是索引元素所指向的数据记录(比如数据库中的某一行),在B树中,无论中间节点还是叶子节点都带有卫星数据。
而在B+树中,只有叶子节点带有卫星数据,其余中间节点仅仅是索引,没有任何数据关联。
B+树比B树的优势有三个:
1、单一节点存储更多的元素,使得查询的IO次数减少;
2、所有查询都要查找到叶子节点,查询性能稳定;
3、所有叶子节点形成有序链表,便于范围查询。
6.应用场景选择
AVL和红黑树
AVL又称 ( 严格)高度平衡的二叉搜索树,也叫二叉查找树、平衡二叉树。左右子树高度差<=1。AVL树的话提供了更快的 lookups(查询),比红黑树来说的话,它AVL优秀的地方是更快的 lookups。因为它是更加严格平衡的二叉搜索树,也就是说读或者是查找性能来说的话,AVL更好。
红黑树是非严格平衡二叉树,可以能确保树的最长路径不大于两倍的最短路径的长度。由于它的设计,任何不平衡都会在三次旋转之内解决O(1),红黑树提供来更快的插入和删除的操作,因为AVL的旋转操作会更多O(logn)。
AVL为了保持平衡要存额外的信息,如factor(平衡因子)占用更多的内存,而红黑树要的信息非常少。
如果在读操作非常非常多,写操作比较少的情况下就用AVL就好了。AVL的问题是插入删除调整的比较繁琐,但它的好处是非常平衡,查询快。如果是插入操作比较多的话,或者插入操作和查询操作一半一半的话,一般来说是用红黑树。因为红黑树的话比较简洁比较好实现。
B树和B+树
B树中关键字集合分布在整棵树中,叶节点中不包含任何关键字信息,而B+树关键字集合分布在叶子结点中,非叶节点只是叶子结点中关键字的索引;
不同于B树只适合随机检索,B+树同时支持随机检索和顺序检索;
B+树的磁盘读写代价更低。B+树的内部结点并没有指向关键字具体信息的指针,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素。
B+树的查询效率更加稳定。B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当。
数据库索引采用B+树的主要原因是:B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。