平衡二叉查找树
平衡二叉查找树的严格定义:二叉树中任意一个节点的左右子树的高度相差不能大于1。
设计平衡二叉查找树的初衷:为了解决普通二叉查找树在频繁的插入、删除等动态更新的情况下(尤其是插入的一组数据是有序的情况),出现时间复杂度退化的问题。
但是在实际的开发中,并不能完全严格的按照定义去做,只要保证平衡即可,也就是让整棵树左右看起来比较对称、比较平衡、更新数据时不要造成一边很高、一边又很低的情况。这样就能让整棵树的高度相对来说低一些(如树的高度是对数量级的,不必log2n大很多)。只要能保证这样,就可以说它是一个合格的平衡二叉查找树。
平衡二叉查找树-红黑树(R-B Tree)
红黑树中的节点,一类被标记为黑色,一类被标记为红色。并且要求:
-
- 每个节点不是黑色就是红色,根节点必须是黑色的(可以在节点数据结构中加一个数据字段代表颜色);
-
- 每个叶子节点都是黑色的空节点(nil)(为了保证除了根以外,每个节点都有兄弟节点,也就是每个父节点都有两个叉),也就是说叶子节点不存储数据;
-
- 任何相邻(线连着)的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
-
- 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点(说明黑色节点是要成对出现的,否则失去平衡了)。
为什么说红黑树是“近似平衡的”
近似平衡就是说查找等动态更新的性能不会退化太严重。
二叉查找树很多操作的性能都很树的高度成正比,因此只要证明红黑树的高度能比较稳定的趋近log2n就好。
而红色节点必须是被黑色节点隔开的,一红一黑,因此红色节点加回去,树的高度应该不大于2log2n。高度仅是大了一倍,性能上下降不多,可以近似O(logn)。
同时红黑树是近似平衡,因此维护平衡的成本较低,性能比较稳定。对于工程应用来说,要面对各种异常情况,稳定性很重要,因此红黑树的应用范围较广。
动态数据结构
动态数据结构是指支持动态的更新操作,里面存储的数据是时刻在变化的,它支持查询、删除、插入等操作,并且这些操作是非常高效。这样的数据结构才能算作动态数据结构。
如何维护红黑树的近似平衡
红黑树规定插入的节点必须是红色的且在修改过程中不能改变颜色,因为插入红色节点比黑色节点违背规则的可能性更小。插入黑色节点一定会改变黑色高度(违背规则4),而插入红色只有一半机会违背规则3。且3比4更易修正。
当插入一个新节点(二叉查找树插入新节点会插到叶子节点上,那么在红黑树上也就是插入到黑色空节点上层了)就可能会破坏红黑树原本结构,打破平衡,那么如何修正呢?
主要有三种方式:改变节点颜色、左旋、右旋。
-
变色 假设原本只有节点E,然后插入了节点A和S,再插入F:
-
左旋 通常左旋操作用于将一个向右倾斜的红色链接转为向左链接。
动图效果: -
左旋
红黑树的操作
- 节点数据
typedef struct TreeNode {
int key;
struct TreeNode *left;
struct TreeNode *right;
struct TreeNode *father; // 双向链表
BOOL color;
} Node;
- 左旋的实现
/*
* 左旋示意图:对节点x进行左旋
* p p
* / /
* x y
* / \ / \
* lx y -----> x ry
* / \ / \
* ly ry lx ly
* 左旋做了三件事:
* 1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
* 2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
* 3. 将y的左子节点设为x,将x的父节点设为y
*/
- 右旋的实现
/*
* 左旋示意图:对节点y进行右旋
* p p
* / /
* y x
* / \ / \
* x ry -----> lx y
* / \ / \
* lx rx rx ry
* 右旋做了三件事:
* 1. 将x的右子节点赋给y的左子节点,并将y赋给x右子节点的父节点(x右子节点非空时)
* 2. 将y的父节点p(非空时)赋给x的父节点,同时更新p的子节点为x(左或右)
* 3. 将x的右子节点设为y,将y的父节点设为x
*/
- 插入操作
- 如果第一次插入,原本树是空的,则只需要将它涂黑即可。
- 如果最终插入后,插入节点的父节点是黑色的,则不违反红黑树规则,不需改变。
- 如果最终插入后,插入节点的父节点是红色的,就违反规则了,需要修改保持树的平衡了。此时又有三种情况:
- ①插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的;
- ②插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;
- ③插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。
具体过程为例: 插入新节点4,如图1(也就是情况①)。此时违反3,因此将它的父节点涂黑,但是如果只涂5,就会造成黑色节点不平衡了,违反了4。因此新节点的父节点5和叔叔节点8都要涂黑。这时758都是黑色(黑色太多了?)将祖父7涂红。这时又变成情况②,把7当做新节点 ,以新节点 为支点做左旋操作。如图2->3。此时27节点仍有问题继续修改,此时那2作为新节点,将父节点7涂黑,祖父节点11涂红。在以11为支点右旋。这样整棵树就又平衡。
从上面的步骤可以看出,如果插入数据最终是情况1,则需要走完2、3步骤;如果插入数据出现的是情况2,则只需走完3;插入后是3,则完成3即可了。
总体:变色->左旋->右旋