平衡二叉树的旋转_数据结构|红黑树是一棵平衡的二叉查找树,每个节点颜色或红或黑...

红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。简单说就是可用于二分查找的(二叉查找树),且高度是平衡的(二叉平衡树)二叉树。红黑树的用途有很多,常用来构造关联数组和集合,STL的关联容器的底层数据结构就是黑红树。

我们知道,对于n个元素的数组arr[n],如果要查找元素i:

情况1:如果arr[n]没有排序,我们做顺序查找,其时间复杂度是n;

情况2:如果arr[n]有排序,我们做二分法查找,其时间复杂度是logn;

如果数据结构是二叉树,并能实现查找的时间复杂度是logn,则这棵二叉树必须是一棵红黑树。

1 二叉查找树

1.1 左子数上所有的节点的值都小于或等于他的根节点上的值;

1.2 右子树上所有节点的值均大于或等于他的根节点的值;

1.3 左、右子树也分别为二叉查找树;

如下图就是一棵二叉查找树:

b212bc04011d628c7fd0a67bbd4350b2.png

可以看到如果要查询10的话,10>9,在右子树查找:

838ad7165a24d578cb3cdd5c8178c066.png

右子树根节点为13,10<13,在左子树查找:

439181abb602aa62c812479ec6069564.png

左子树根节点为11>10,继续在左子树查找

2f235afe580c86ce8d37778de7bccc51.png

有左子树找到相应的节点值10:

db088af713a75dc96849017e34a62583.png

不过二叉查找树有一些问题,当不断插入一些元素时,可能会出现不平横的情况,即如下图所示:

dfdd993fc0461309bd9f1c9078c3f96f.png

从这种情况可以看出,明显存在左子树和右子树深度相差过多,在使用平衡情况下的二叉查找树是时间复杂度为logn,而出现这种极端情况的话,想要查9的位置就需要每一次都遍历下一个右子树,很有可能时间复杂度变为n(与数组普通查询的时间复杂度相同)。

基于上述情况,引入了平衡二叉树,红黑树即为平衡二叉树(是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。)和二叉查找树的结合。

2 红黑树

红黑树是每个节点都带有颜色属性的二叉查找树:

2.1 节点是红色或黑色;

2.2 根节点一定是黑色;

2.3 每个叶节点都是黑色的空节点(NIL节点);

2.4 每个红节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不能有两个连续的红节点,即对于每层来说,除了NIL节点,红黑节点是交替的,第一层是黑节点那么其下一层肯定都是红节点,反之一样);

2.5 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点;

这五条性质约束了红黑树,可以通过数学证明来证明,满足这五条性质的二叉树可以将查找、删除维持在对数时间内。

正是由于这些原因使得红黑树是一个平衡二叉树。

当我们进行插入或者删除操作时所作的一切操作都是为了调整树使之符合这五条性质(达到平衡,且符合二叉查找树的要求)。

这些约束强制了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

要知道为什么这些特性确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

红黑树在函数式编程中也特别有用,在这里它们是最常用的持久数据结构之一,它们用来构造关联数组和集合,在突变之后它们能保持为以前的版本。除了O(log n)的时间之外,红黑树的持久版本对每次插入或删除需要O(log n)的空间。

如下图就是一棵红黑树:

64e20e635263fb17e51aa7b058ae27d7.png

向红黑树中插入节点14(一般默认插入节点是红色的):

4fc314e349fa65cbbd9a8b4af32585fb.png

因为以上插入点的特殊性,插入后的整棵树还是平衡的(插入点所在位置的节点的最高深度为5,与其它节点的最高深度相等)。

在原树上插入20:

8628ac0a439604cbc8b15febb93061a4.png

可以看到,插入以后树已经不是一个平衡的二叉树(最高深度成了6),而且并不满足红黑树的要求,因为20和21均为红色,这种情况下就需要对红黑树进行变色,21需要变为黑色,22就会变成红色,如果22变成红色,则需要17和25都变成黑色:

946fc87d1a213a8680ce7868eb5673ce.png

而17变成黑色显然是不成立的,因为如果17变为黑色,那么13就会变为红色,不满足二叉树的规则,因此此处需要进行另一个操作---------左旋操作

12fc0471b3fac2423b48769c7d5598a4.gif

左旋:下图就是一个左旋的例子,一般情况下,如果左子树深度过深,那么便需要进行左旋操作以保证左右子树深度差变小

acd80cd81449b48e52afa31d73423b99.png

对于上图由于右子树中17变为黑色以后需要把13变成红色,因此进行一次左旋,将17放在根节点,这样既可保证13为红色,左旋后结果:

8148eca4a7444f0d38cd05d46cda94d0.png

而后根据红黑树的要求进行颜色的修改:

46f3444cb7b49368798aa5636df30b76.png

进行左旋后,发现从根节点17,到1左子树的叶子节点经过了两个黑节点,而到6的左叶子节点或者右叶子节点要经历3个黑节点,很显然也不满足红黑树,因此还需要进行下一步操作,需要进行右旋操作:

1ab44e2d12f939f70c830f8c52941047.gif

右旋:与左旋正好相反:

a8b5dec532b268619d3fbe18c99f894e.png

由于是从13节点出现的不平衡,因此对13节点进行右旋,得到结果:

830e0bc7785cb8616fa7587909c9713d.png

而后再对其节点进行变色,得到结果:

25f22d1c776993368e501cc6b95e6da8.png

折半查找法要求的是一个有序的序列,红黑树对应的也是一个有序的、平衡的序列:

templatestruct TreeNode{ T data; int balance; //左子树深度 - 右子树深度 TreeNode *parent; TreeNode *LeftChild; TreeNode *RightChild;};bool FindElem(TreeNode * node,ElemType elem){ if(node != NULL) { if(node->data == elem)//找到元素 return true; else if(node->data > elem)//只需继续查找左子树 return FindElem(node->LeftChild, elem); else if(node->data < elem)//只需继续查找右子树 return FindElem(node->RightChild, elem); } else return false;}

每次添加/删除一个元素后,势必会影响到某处的深度,就有可能使得操作之后的二叉树不再处于平衡状态。把不平衡的地方进行某种所谓的“旋转”使得重新变成平衡的状态。

红黑树的用途有很多,例如STL的关联容器的底层数据结构就是黑红树:

af6b6a1a0f53dcf432c9e04dd120efa7.png

是为每个节点添加了一个字段int balance。

balance = 左子树深度 - 右子树深度

(两个数相减的值可以作为这两个数的一种比较)

1)当balance的绝对值<=1的时候就认为“差距不大”,为“平衡状态”;

2)当balance的绝对值>=2的时候就认为“差距过大”,为“不平衡状态”;

3)定义二叉平衡树:所有节点的balance的绝对值均<=1;

每次添加/删除一个元素,势必会影响到某处的深度,进而影响到balance的值,就有可能使得操作之后的二叉树不再处于平衡状态。

解决办法就是:

每次添加/删除一个元素后,根据balance的变化,把不平衡的地方进行某种所谓的“旋转”使得重新变成平衡的状态。

每次的添加/删除操作都是针对一个平衡的二叉树,如果操作完成后不平衡了则重新整理成平衡状态,这就保证了二叉树时刻处于平衡状态。

对于一棵已经处于平衡状态的二叉树,添加/删除一个元素对其的平衡状态的影响实际上非常有限。

由于原先的balance只有三个值“0,1,-1”,所以一个元素的影响只有使得“-1变成-2”或“1变成2”才可以破坏平衡。

旋转操作代码:

void Swing(TreeNode*p) { if (p->balance == 2) //balance=左子树深度 - 右子树深度 { if (p->LeftChild->balance == 1)//LL型旋转 SwingLL(p, p->LeftChild); else if (p->LeftChild->balance == -1)//LR型旋转 SwingLR(p, p->LeftChild, p->LeftChild->RightChild); } else if (p->balance == -2) { if (p->RightChild->balance == -1)//RR型旋转 SwingRR(p, p->RightChild); else if (p->RightChild->balance == 1)//RL型旋转 SwingRL(p, p->RightChild, p->RightChild->LeftChild); } } void SwingLL(TreeNode*A, Tree Node*B) { TreeNode *parent = A->parent; if (parent != NULL) { if (IsLeftChild(A)) parent->LeftChild = B; else parent->RightChild = B; } else root = B; A->LeftChild = B->RightChild; if (B->RightChild != NULL)B->RightChild->parent = A; B->RightChild = A; A->parent = B; B->parent = parent; A->balance = 0; B->balance = 0; } void SwingLR(TreeNode*A, TreeNode*B, TreeNode*C) { A->LeftChild = C; C->parent = A; B->RightChild = C->LeftChild; if (C->LeftChild != NULL) C->LeftChild->parent = B; C->LeftChild = B; B->parent = C; TreeNode *parent = A->parent; if (parent != NULL) { if (IsLeftChild(A)) parent->LeftChild = C; else parent->RightChild = C; } else root = C; A->LeftChild = C->RightChild; if (C->RightChild != NULL) C->RightChild->parent = A; C->RightChild = A; A->parent = C; C->parent = parent; if (C->balance == 1) { C->balance = 0; B->balance = 0; A->balance = -1; } else { C->balance = 0; B->balance = 1; A->balance = 0; } }

-End-

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值