在看了数据结构书上的一些内容之后,涉及到了红黑树、AVL树以及树堆的一些点,然后自己也不理解,就学习一下并写博客记录一下。
目录
3.1 我们要把一个点挪到根,那我们首先要知道怎么让一个点挪到它的父节点
①树有且仅有一个根节点
②当n>1时,其余节点可分为m个互不相交的有限集,每一个集合本身又是一个树,并称为跟的子树
二叉树,顾名思义就是每个节点只有两个子节点的树(最多)
所有非叶子节点都存在子节点且所有的子节点都在同一层
完全二叉树指的是对一个有n个节点的二叉树,按照层级顺序编号。如果这棵树所有节点和同样深度的满二叉树的编号为1到n的节点位置一样,则这个树称为完全二叉树。
Nil
或Null
)一些简单的说明
一些规则:
情况一:父亲和叔叔都是红色
情况二:叔叔为黑色,自己为父亲的左儿子
父亲为祖父的左儿子,叔叔为黑色,自己是父亲的左儿子
(1)父亲变成黑色,祖父变成红色(右子树的黑色高度变低)
(2)对祖父进行右旋,让父节点成为新的祖父,以恢复右子树的黑色高度
(3)不满足循环条件,退出循环
情况三:叔叔为黑色,自己为父亲节点的右子树
情况一:父亲和叔叔都是红色
情况二:叔叔为黑色,自己是父亲的右儿子
情况三:叔叔为黑色,自己为父亲节点的左儿子
- public void fixAfterInsert(RedBlackTreeNode x) {
- // 新插入的节点,默认为红色
- x.color = RED;
-
- // p不为null、不是整棵树的根节点、父亲为红色,需要调整
- while (x != null && this.root != x && x.parent.color == RED) {
- // 父亲是祖父的左儿子
- if (parentOf(x) == parentOf(parentOf(x)).left) {
- // 父亲和叔叔都是红色
- RedBlackTreeNode uncle = parentOf(parentOf(x)).right;
- if (uncle.color == RED) {
- // 父亲和叔叔都变成黑色
- parentOf(x).color = BLACK;
- uncle.color = BLACK;
- // 祖父变成红色,继续从祖父开始进行调整
- parentOf(parentOf(x)).color = RED;
- x = parentOf(parentOf(x));
- } else { // 叔叔为黑色
- // 自己是父亲的右儿子,需要对父亲左旋
- if (x == parentOf(x).right) {
- x = parentOf(x);
- leftRotate(x);
- }
- // 自己是父亲的左儿子,变色后右旋,保持黑色高度
- parentOf(x).color = BLACK;
- parentOf(parentOf(x)).color = RED;
- rightRotate(parentOf(parentOf(x)));
- }
- } else { //父亲是祖父的右儿子
- RedBlackTreeNode uncle = parentOf(parentOf(x)).left;
- // 父亲和叔叔都是红色
- if (uncle.color == RED) {
- // 叔叔和父亲变成黑色
- parentOf(x).color = BLACK;
- uncle.color = BLACK;
- // 祖父变为红色,从祖父开始继续调整
- parentOf(parentOf(x)).color = RED;
- x = parentOf(parentOf(x));
- } else {
- // 自己是父亲的左儿子,以父亲为中心右旋
- if (parentOf(x).left == x) {
- x = parentOf(x);
- rightRotate(x);
- }
- // 自己是父亲的右儿子,变色后左旋,保持黑色高度
- parentOf(x).color = BLACK;
- parentOf(parentOf(x)).color = RED;
- leftRotate(parentOf(parentOf(x)));
- }
- }
- }
-
- // 最后将根节点置为黑色,以满足红黑规则1,又不会破坏规则5
- this.root.color = BLACK;
- }
-
- private static RedBlackTreeNode parentOf(RedBlackTreeNode p) {
- return (p == null ? null : p.parent);
- }
一些规则:
情况一:兄弟为红色
情况二:兄弟为黑色,左右侄子也是黑色
情况三:兄弟为黑色,右侄子为黑色
此时,自己和兄弟均为黑色,父节点为红色或黑色;右侄子为黑色、左侄子为红色;
(1)将左侄子变成黑色,兄弟变为红色;这时,以兄弟为起点的右子树黑色高度降低
(2)将兄弟节点右旋,以恢复右子树的黑色高度;这时,左侄子将成为新的右兄弟
此时,兄弟的右儿子为红色,满足情况4;继续按照情况4,对节点x进行调整
情况四:兄弟为黑色,右侄子为红色
情况一:兄弟是红色节点
此时,兄弟是红色节点,父节点必为黑色;若兄弟有左右儿子,左右儿子必为黑色(满足红黑规则4)
(1)将兄弟变成黑色节点,父节点变成红色;这时,以父节点为起点的右子树黑色高度降低
(2)将父节点右旋,以恢复右子树的黑色高度;这时,兄弟的右孩子成为新的兄弟
此时,自己和兄弟都是黑色,将满足情况2、3和4、4
情况二:兄弟是黑色,左右侄子也是黑色
此时,自己和兄弟是黑色,父节点可以为红色或黑色
(1)将兄弟变成红色,x指向父节点,继续对父节点进行调整
情况三:兄弟为黑色,左侄子为黑色
此时,自己和兄弟均为黑色,父节点为黑色或红色;左侄子为黑色,右侄子为红色
(1)将右侄子变成黑色,兄弟变成红色;这是,以兄弟为起点的左子树黑色高度降低
(2)将兄弟左旋,以恢复左子树的黑色高度;这时,右侄子成为新的兄弟
此时,将满足情况4,可以按照情况4,继续进行调整
情况四:兄弟为黑色,左侄子为红色
此时,自己和兄弟均为黑色,父节点为红色或黑色;左侄子为红色,右侄子为红色或黑色
(1)将兄弟变成与父节点一样的颜色,左侄子和父节点变成黑色
(2)为了保证父节点变成黑色,不会影响所有路径的黑色高度,需要将父节点右旋(兄弟上提)
(3)x指向根节点,退出循环
- public void fixAfterDeletion(RedBlackTreeNode x) {
- // x不是根节点且颜色为黑色,开始循环调整
- while (x != root && x.color == BLACK) {
- // x是父亲的左儿子
- if (x == parentOf(x).left) {
- RedBlackTreeNode brother = parentOf(x).right;
- // 兄弟为红色
- if (brother.color == RED) {
- // 兄弟变成黑色,父节点变成红色
- brother.color = BLACK;
- parentOf(x).color = RED;
- // 父节点左旋,恢复左子树的黑色高度
- leftRotate(parentOf(x));
- // 更新兄弟
- brother = parentOf(x).right;
- }
-
- // 兄弟为黑色,左右侄子为黑色
- if (brother.left.color == BLACK && brother.right.color == BLACK) {
- // 兄弟变成红色
- brother.color = RED;
- // 从父节点开始继续调整
- x = parentOf(x);
- } else {
- // 右侄子为黑色(左侄子为红色)
- if (brother.right.color == BLACK) {
- // 左侄子变为黑色,兄弟变成红色
- brother.left.color = BLACK;
- brother.color = RED;
- // 兄弟右旋,恢复右子树黑色高度
- rightRotate(brother);
- // 左侄子成为新的兄弟
- brother = parentOf(x).right;
- }
- // 右侄子为红色,兄弟变成父节点颜色
- brother.color = parentOf(x).color;
- // 父节点和右侄子变成黑色
- parentOf(x).color = BLACK;
- brother.right.color = BLACK;
- // 父节点左旋
- leftRotate(parentOf(x));
- // x指向根节点
- x = root;
- }
- } else {
- RedBlackTreeNode brother = parentOf(x).left;
- // 兄弟为红色
- if (brother.color == RED) {
- // 兄弟变黑色,父亲变红色
- brother.color = BLACK;
- parentOf(x).color = RED;
- // 父亲右旋,恢复红黑色高度
- rightRotate(parentOf(x));
- // 更新兄弟为右侄子
- brother = parentOf(x).left;
- }
-
- // 兄弟的左右儿子为黑色
- if (brother.left.color == BLACK && brother.right.color == BLACK) {
- // 兄弟变为红色
- brother.color = RED;
- // x指向父节点,继续进行调整
- x = parentOf(x);
- } else {
- // 左侄子为黑色(右侄子为红色)
- if (brother.left.color == BLACK) {
- // 右侄子变黑色,兄弟变红色
- brother.right.color = BLACK;
- brother.color = RED;
- // 对兄弟左旋
- leftRotate(brother);
- // 右侄子成为新的兄弟
- brother = parentOf(x).left;
- }
-
- // 左侄子为红色,兄弟改为父节点颜色
- brother.color = parentOf(x).color;
- // 父节点和左侄子变成黑色
- brother.left.color = BLACK;
- parentOf(x).color = BLACK;
- // 兄弟节点上提(右旋父节点)
- rightRotate(parentOf(x));
- // x指向根节点
- x = root;
- }
-
- }
- }
- // 更新x为黑色
- x.color = BLACK;
- }
旋转式Treap不支持区间操作,但是相比于无旋式Treap效率要高一些。
它的基本操作有“左旋”、“右旋”
首先来看一个合法的Treap
从图中我们可以看到此处满足小根堆的情况
【图片来自数据结构-Treap(树堆) 详解_HeartFireY的博客-CSDN博客_treap】
我们现在假设执行插入操作,节点键值key=7,随机生成priority值为7。
我们按照二叉搜索树的方式进行插入【按照Key值大小放在左边还是右边】
显然,并不满足堆的条件
1. 右旋操作
当我们发现当前节点左儿子的priority小于自身priority的时候,我们可以通过右旋操作解决问题。我们首先通过上面的样例来理解右旋操作的原理:
对于新插入的节点,我们检索其根节点,发现根节点左儿子的priority小于自身priority,因此执行右旋操作
我们将左字节点提到根节点作为新的根,原根节点右旋至新根节点的右子节点。
旋转完之后的情况如下:
此时还是不满足堆,这个时候可以发现是根节点的左子树的右子树不满足情况,我们将其进行左旋
还是根据上面的案例进行讲解
1. 分裂操作
对无旋式Treap执行分裂操作有两种含义,即按照权值进行分裂或按照排名进行分裂。我们首先来讲解按照权值分裂:
所谓按照权值分裂即:将根指针指向的Treap分裂为两个Treap,第一个Treap所有节点的权值小于等于给定key值,第二个Treap所有节点的权值大于等于给定k e y keykey值。
定义操作pair
那么操作的大致思路如下:
判断root所指向的节点是否为空;
将当前root节点所指向的值与给定的key值进行比较:
根据上述两个条件判断递归向左子树分裂or右子树分裂,并继续递归分裂子树,待子树分裂完成后按刚刚的判断情况连接 的左子树或右子树到递归分裂所得的子树中。
我们通过具体的例子来理解这个操作:以样例所示Treap为例,指定key=7进行分裂。
第一次:root→val=11>key,因此可以判断当前root指向的节点及右子树应该属于第二个Treap,向左子树分裂;
第二次:root→val=6≤key,因此可以判断当前root指向的节点及其左子树全部属于第一个Treap,向右子树分裂;
第三次,root→val=8>key,因此可以判断当前root指向的节点及其右子树全部属于第二个Treap,此时分裂到叶子节点,无法继续分裂,分裂操作到此终止。
最终,上述二叉树分裂为两棵二叉树:
2. 合并操作
必须满足u中所有结点的关键值小于等于v中所有结点的关键值。因为两个Treap已经有序,我们只需要考虑priority来决定哪个 Treap 应与另一个Treap的儿子合并。
定义操作node *merge(node *u, node *v),其中u和v均为待合并的树根指针。
那么操作的大致思路如下:
指针判空:如果u指针为空,则返回v;如果v指针为空,则返回u指针;
若 u 的根结点的priority 小于 v 的,那么 u 即为新根结点,v 应与 u 的右子树合并;反之,则 v 作为新根结点,然后让 u 与 v 的左子树合并。
不难发现,这样合并所得的树依然满足priority 的大根堆性质。
我们依然通过样例来对这个过程进行展示:在先前的分裂操作中,我们得到了两棵子树,设其根节点指针分别为u、v。
函数入口node * new_tree = merge(u, v);
第一次,①.判断u和v均为非空指针 ②u指向节点的priority=12,v vv指向节点的priority=6,则v vv应该作为新根节点,u与v的左子树执行合并,执行merge(u, v -> lc);
第二次,①.判断u和v均为非空指针 ②u指向节点的priority=12,v vv指向节点的priority=18,则u uu应该作为新根节点,v与u的右子树执行合并,执行merge(u -> rc, v);
第三次,①.判断u为空指针,则返回v指针,此后逐层返回。
如此,两个树就重新合并在了一起。
之前学习二叉搜索树的时候,最坏的时间复杂度为O(n),如果二叉搜索树尽可能的平衡,它的时间复杂度就会转换为O(logN),这就是平衡二叉树是基于二叉搜索树提出来的原因
平衡因子:在AVL树中,要求树之间的高度差不超过1,所以平衡因子<=1
最小不平衡子树:导致树变成不平衡的条件
使用场景:LL
实现过程:
①将58作为根节点抽出来
②把根节点的左子树向上移动,根节点顺势成为其左子树的右子树
③此时根节点会和左子树的原来的右子树冲突,将左子树的原来的右子树变为现在根节点的左子树即可
新插入节点37的插入位置:根节点58的左儿子的左子树上,这种情况称为LL
使用场景:RR
实现过程:
①将根节点向左下方移动
②将根节点的右子树向左上方移动变为其根节点
③将现在根节点(15)原来的左子树(13)变为原来根节点(12)的右子树并将原来的根节点变为现在的根节点的左子树
新插入节点18的位置:根节点12的右儿子的右子树,这种情况称为RR
使用场景:LR
实现过程:
①将根节点的左子树整体进行左旋
②将树整体进行右旋
新的节点插入在左子树的右子树上称为LR
使用场景:RL
①将右子树右旋
②整棵树左旋
新插入的节点在右子树的左子树上称为RL
splay树是平衡树的一种,中文叫做伸展树
它的主要思想是:对于查找频率较高的节点,使其处于离根节点相对较近的节点。
你可以认为每次被查找的点查找频率相对较高,说白了就是你把每次查找到的点搬到根节点去
当然你也可以每次查找之后随机一个点作为根,于是Treaplay这种数据结构就诞生啦
这是Splay需要解决的问题
splay规定:每访问一个节点后都要强制将其旋转到根节点
情况一:当X为Y的左孩子
这个时候,我们让X成为Y的父亲节点,其实就类似于AVL树的右旋,操作后结果如下所示
情况二:X是Y的右孩子
类似于AVL的左旋
情况三:当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。
当x和p同为左孩子时,依次将p和x右旋;
情况四:当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。
当x和p同为右孩子时,依次将p和x左旋。
情况五:当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。
当p为左孩子,x为右孩子时,将x左旋后再右旋。
情况六:当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。
当p为右孩子,x为左孩子时,将x右旋后再左旋。