平衡二叉查找树
平衡二叉树中任意一个节点的左右子树的高度相差不能大于1
完全二叉树、满二叉树都是平衡二叉树,但非完全二叉树也有可能是平衡二叉树
平衡二叉查找树满足上面平衡二叉树的定义,还满足二叉查找树的特点
平衡二叉查找树为了解决普通二叉查找树频繁插入、删除等动态更新导致时间复杂度退化
平衡二叉查找树中“平衡”:
“平衡”可以等价为性能不退化
让整棵树左右子树高度比较“平衡”,相应的插入、删除、查找等操作的效率高一些
如设计一个平衡二叉查找树,只要树高度不比logn大很多,尽管不符合定义仍然可以算合格
红黑树(Red-Black Tree)
是一种不严格的平衡二叉查找树,属于最常用平衡二叉查找树
满足红黑树前提条件
所有节点只有黑色和红色
根节点是黑色
每个叶子节点都是黑色的空节点(NIL) (主要为了简化代码实现而设置)
添加黑色的空叶子节点,可以在具体实现的时候公用一个空节点,不会太浪费空间
任何相邻的节点都不能同时为红色
每个节点从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点
红黑树是“近似平衡”
“平衡”可以等价为性能不退化,而“近似平衡”等价为性能不会退化的太严重
二叉查找树操作的性能都跟树的高度成正比,极其平衡的二叉树高度大约为logn
特点
只要按照这些固定的调整规则来操作,就能将一个非平衡的红黑树调整成平衡
左旋(rotate left): 围绕某个节点的左旋
右旋(rotate right): 围绕某个节点的右旋
红黑树的插入、删除操作会破坏红黑树的平衡,如何调整平衡
名词介绍
叔叔节点: 父节点的兄弟节点,
祖父节点: 父节点的父节点
关注节点: 正在处理的节点
前驱节点: 中序遍历的序列中,当前节点上一个节点
后继节点: 中序遍历的序列中,当前节点下一个节点
插入操作的平衡调整
红黑树定义: 插入的节点必须是红色,而且树中新插入的节点都放在叶子节点上
具体情况如下:
如果插入节点的父节点是黑色,那不需要任何调整,它仍然满足红黑树的定义
如果插入的节点是根节点,那直接改变它的颜色,把它变成黑色就可以了
其他情况都会违背红黑树的定义,需要进行两种基础的操作: 左右旋转和改变颜色
实现过程
红黑树的平衡调整过程是一个迭代的过程,
关注节点随着不停地迭代处理,而不断发生变化。最开始的关注节点是新插入的节点
新节点插入后,如果红黑树的平衡被打破,一般会有下面三种情况 & 如何实现再平衡
Case 1.如果关注节点是a,它的叔叔节点d是红色,依次执行
将关注节点a的父节点b、叔叔节点d的颜色都设置成黑色
将关注节点a的祖父节点c的颜色设置成红色
关注节点变成a的祖父节点c
跳到CASE 2 或 CASE 3
Case 2.如果关注节点是a,叔叔节点d是黑色,关注节点a是其父节点b的右子节点
关注节点变成节点a的父节点b;
围绕新的关注节点b左旋;
跳到 CASE 3
Case 3.如果关注节点是a,叔叔节点d是黑色,关注节点a是其父节点b的左子节点
围绕关注节点a的祖父节点c右旋;
将关注节点a的父节点b、兄弟节点c的颜色互换。
删除操作的平衡调整
实现过程
1.针对删除节点初步调整
红黑树定义中“只包含红色节点和黑色节点”,经过初步调整之后,
为了保证这一条要求,有些节点会被标记成两种颜色,“红-黑”或者“黑-黑”。
如果一个节点被标记为“黑-黑”,那在计算黑色节点个数的时候,要算成两个黑色节
如果一个节点既可以是红色,也可以是黑色,在下图用一半红色一半黑色来表示
如果一个节点是“红-黑”或者“黑-黑”,我会用左上⻆的一个小黑点来表示额外的黑色
Case 1:如果要删除的节点是a,它只有一个子节点b
删除节点a,并且把节点b替换到节点a的位置
节点a只能是黑色,节点b也只能是红色,不符合红黑树定义,把节点b改为黑色
Case 2:如果要删除的节点a有两个非空子节点,且后继节点是节点a的右子节点c
如果节点a后继节点是右子节点c,把节点a删除且将节点c替换到节点a的位置
然后把节点c的颜色设置为跟节点a相同的颜色
如果节点c是黑色,给节点c右子节点d多加一个黑色,则节点d变成“红-黑”或“黑-黑”
这时关注节点变成了节点d,第二步的调整操作就会针对关注节点d来做
Case 3:如果要删除是节点a有两个非空子节点,且节点a后继节点不是右子节点
找到后继节点d,并将它删除,删除后继节点d的过程参照CASE 1
将节点a替换成后继节点d
把节点d的颜色设置为跟节点a相同的颜色
如果节点d是黑色,给节点d右子节c多加一个黑色,则节点c就成“红-黑”或“黑-黑”
这时关注节点变成节点c,第二步的调整操作就会针对关注节点c来做
2.针对关注节点进行二次调整
经过初步调整之后,关注节点变成“红-黑”或“黑-黑”节点
二次调整是为了让红黑树中不存在相邻的红色节点
针对这个关注节点,我们再分四种情况来进行二次调整
Case 1: 如果关注节点是a,它的兄弟节点c是红色的
围绕关注节点a的父节点b左旋
关注节点a的父节点b和祖父节点c交换颜色
关注节点不变
继续从四种情况中选择适合的规则来调整
Case 2: 如果关注节点是a,兄弟节点c是黑色,且节点c左右子节点d、e都是黑色
将关注节点a的兄弟节点c的颜色变成红色
从关注节点a中去掉一个黑色,这个时候节点a就是单纯的红色或者黑色
给关注节点a的父节点b添加一个黑色,这时节点b就变成了“红-黑”或者“黑黑”
关注节点从a变成其父节点b
继续从四种情况中选择符合的规则来调整
Case 3: 如果关注节点是a,兄弟节点c是黑色,c左子节点d红色,c右子节点e黑色
围绕关注节点a的兄弟节点c右旋
节点c和节点d交换颜色
关注节点不变跳转到Case 4,继续调整
Case 4: 如果关注节点a的兄弟节点c是黑色的,并且c的右子节点是红色的
围绕关注节点a的父节点b左旋
将关注节点a的兄弟节点c的颜色,跟关注节点a的父节点b设置成相同的颜色;
将关注节点a的父节点b的颜色设置为黑色;
从关注节点a中去掉一个黑色,节点a就变成了单纯的红色或者黑色;
将关注节点a的叔叔节点e设置为黑色;
调整结束。
总结操作过程
第一点,把红黑树的平衡调整的过程比作魔方复原,不要过于深究这个算法的正确性
只要按照固定的操作步骤,保持插入、删除的过程,不破坏平衡树的定义即可
第二点,找准关注节点,不要搞丢、搞错关注节点
每种操作规则,都是基于关注节点来做的
在迭代的调整过程中,关注节点在不停地改变
第三点,插入操作的平衡调整比较简单,但是删除操作就比较复杂
针对删除操作,我们有两次调整,
第一次针对要删除的节点做初步调整,让调整后的红黑树继续满足第四条定义
“每个节点到可达叶子节点的路径都包含相同个数的黑色节点”
但是不满足第三条定义,有可能会存在两个红色节点相邻的情况
第二次调整就是解决让红黑树不存在相邻的红色节点的问题
几种平衡二叉查找树对比
Treap(树堆)
Treap是二叉搜索树和堆合并构成的数据结构,每个节点数据域包含2个值
key值: 满足左子树<根节点<右子树 (满足二叉搜索树特性)
weight值:小于等于(或大于等于)左右子节点 (满足堆特性)
利用weight值作为随机因子来调整二叉树形状,所以在大部分情况下更平衡,性能更高
但无法避免极端情况下时间复杂度的退化,不适用对于操作时间非常敏感场景
Splay Tree(伸展树)
是一种能够自我平衡的二叉查找树
每次查找后对树进行调整,把被查找的条目搬移到离根节点近一些的地方
它会沿着从某个节点到根节点之间的路径,通过一系列的旋转把这个节点搬移到根节点
良好的的性能,同时存储所需的内存少
但无法避免极端情况下时间复杂度的退化(特别在多线程环境)
AVL树
一种高度平衡的二叉树,查找的效率非常高;
但是AVL树为了维持高度的平衡,每次插入、删除等操作都要调整,就比较复杂、耗时;
对于有频繁的插入、删除操作的数据集合,使用AVL树的代价就有点高
红黑树
红黑树的插入、删除、查找等操作性能都比较稳定
因近似平衡,在维护平衡的成本上,要比AVL树要低
综合对比
为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树