当前位置:   article > 正文

红黑树算法

红黑树算法

点击上方“小白学视觉”,选择加"星标"或“置顶

  1. 重磅干货,第一时间送达

本文转自:机器学习算法工程师

背景

红黑树是AVL树里最流行的变种,有些资料甚至说自从红黑树出来以后,AVL树就被放到博物馆里了。红黑树是否真的有那么优秀,我们一看究竟。红黑树遵循以下5点规则,需要我们理解并熟记。

规则: 

1.树节点要么是红的,要么是黑的

2.树的根节点是黑的

3.树的叶节点链接的空节点都是黑的,即nullNode为黑

4.红色节点的左右孩子必须是黑的

5.从某节点到null节点所有路径都包含相同数目的黑节点

正是因为作为二叉查找树的红黑树满足这些性质,才使得树的节点是相对平衡的。由归纳法得知,如果一颗子树的根到nullNode节点的路径都包含B个黑色节点,则这棵子树含有除nullNode节点外的2^B-1个节点,而由性质4得从根到nullNode节点的路径上至少有一半的节点是黑色的,从而得到树包含的节点数n>=2^(h/2)-1,h是树的深度,从而得到h<=2*log(n+1)。即可以保证红黑树的深度是对数的,可以保证对树的查找、插入删除等操作满足对数级的时间复杂度。

下边我们将讨论红黑树最主要的两个算法,插入和删除。

红黑树的插入

为了不违反规则5,所以我们将带插入的节点先染成红色,再进行调整以满足其他性质。为了能够清晰的解决问题,我们可以将红黑树的插入分为以下五种情况:

情况1:插入空树中,插入的节点是根节点

情况2:插入的节点的父亲是黑色的

情况3:插入的节点的父亲是红色的,而父节点的兄弟为黑色,且插入节点为外部节点(要找到它要么一直遍历做节点,要么一直遍历右节点)

情况4:插入的节点的父亲是红色的,而父节点的兄弟为黑色,且插入节点为内部节点(不是外外部节点的节点)

情况5:插入的节点的父亲是红色的,而父节点的兄弟也是红色的

对于第一种情况:插入后将根节点再染成黑色即可。

对于第二种情况:直接插入依然满足性质。首先设X是新增节点,P是其父节点,S是其父节点的兄弟节点,G是其的祖父节点。

对于第三种情况:违反了性质4,我们可以通过对P进行右旋和节点的重新着色对树进行修复(应对三、四两种情况我们的着色方式都是:在旋转前先将要旋转的根节点染红,然后旋转,最后将新的根节点染黑)见下面的“图2”。

对于第四种情况:亦是如此,只不过我们需要两次旋转,先对P做左旋再对G做右旋,并重新着色,见下面的“图3”。

对于第五种情况:我们如果按照三四两种情况的修复方式是无法满足性质的,我们就考虑旋转后将新的根节点染红,未插入之前的父节点的兄弟节点染红,新根节点的孩子节点染黑。这样出现的问题是新根节点的父节点可能是红的。此时,我们只能向着根的方向逐步过滤这种情况,不再有连续的两个红色节点或遇到根节点为止(把根重染成黑色)。这种策略自底向上,逐步递归完成,见下面的“图4”。

       我们还有一种策略是自顶向下,如果遇到一个黑色节点有两个红色节点,我们将进行颜色翻转(如果节点X有两个红色孩子,我们将X染成红色,将它的两个孩子染成黑色),如果X的父节点也是红色的呢?我们这又归于3、4两种情况。X的父节点的兄弟节点会不会是红色的呢?答案是不会,应为我们自顶向下已经排除了这种情况。此时我们可以排除情况5,将所有情况都转换为情况一到四的处理,除了特殊情况,就剩下情况三和情况四了。

下边是Java代码具体实现:

  1. public void insert(E item){
  2. current = parent = grand = header;
  3.    nullNode.element = item;
  4.    //当未找到正确插入位置时,一直向下查找,
  5.    //并对树中一个黑节点有两个红节点的情况进行调整  
  6.    while(compare(item,current)!=0){
  7. great = grand;
  8.        grand = parent;
  9.        parent = current;
  10.        current = compare(item,current)<0?current.left:current.right;
  11.        if(current.left.color==RED&t.right.color==RED)
  12. handleReorient(item);
  13.    }
  14. if(current!=nullNode){
  15. System.out.println("该项已存在");
  16.        return;
  17.    }
  18. //创建节点并插入修复  
  19.    current = new RedBlackNode<E>(item, nullNode, nullNode);
  20.    if(compare(item,parent)<0){
  21. parent.left = current;
  22.    }
  23. else{
  24. parent.right = current;
  25.    }
  26. current.parent = parent;
  27.    handleReorient(item);
  28.    nullNode.element = null;
  29. }
  30. //对要插入的节点的链进行调整修复  
  31. private void handleReorient(E item){
  32. current.color = RED;
  33.    current.left.color = BLACK;
  34.    current.right.color = BLACK;
  35.    if(parent.color == RED){
  36. grand.color = RED;//先把要旋转的树的根节点染红  
  37.        // 如果不是外节点,则需要旋转两次,
  38.        // 第一次旋转以parent为根,这里传过去的参数树根的父节点  
  39.        if((compare(item,parent)<0)!=(compare(item,grand)<0))
  40. parent = rotate(item,grand);
  41.        current = rotate(item,great);
  42.        current.color = BLACK;//将新的根节点染黑  
  43.    }
  44. header.right.color = BLACK;//将整棵树的根节点染黑  
  45. }
  46. /**
  47. * 明确旋转树在根的左边还是右边后,
  48. * 我们将旋转树父节点指向根,如果
  49. * 最终项在左边就右旋,在右边就左旋
  50. * @param item 最终项  
  51. * @param parent 旋转树的根的父节点  
  52. * @return 旋转树的根节点
  53. */
  54. private RedBlackNode<E> rotate(E item,RedBlackNode<E> parent){
  55. if(compare(item,parent)<0){
  56. parent.left = compare(item,parent.left)<0?
  57. rotateWithLeftChild(parent.left):rotateWithRightChild(parent.left);
  58.        parent.left.parent = parent;
  59.        return parent.left;
  60.    }
  61. else{
  62. parent.right = compare(item,parent.right)<0?
  63. rotateWithLeftChild(parent.right):
  64. rotateWithRightChild(parent.right);
  65.        parent.right.parent = parent;
  66.        return parent.right;
  67.    }
  68. }
  69. //以左孩子为支点旋转,即我们所说的右旋(形象理解
  70. //为以支点为中心向右旋转),t1为旋转树的根.返回新根  
  71. private RedBlackNode<E> rotateWithLeftChild(RedBlackNode t1){
  72. RedBlackNode<E> t2 = t1.left;//得到左孩子  
  73.    t1.left = t2.right;//将左孩子的右子树作为原根的左子树  
  74.    t2.right = t1;//此时t2作为新根  
  75.    t1.parent = t2;
  76.    t2.left.parent = t2;
  77.    t1.left.parent = t1;
  78.    t1.right.parent = t1;
  79.    return t2;
  80. }
  81. //以右孩子为支点旋转,即左旋  
  82. private RedBlackNode<E> rotateWithRightChild(RedBlackNode t1){
  83. RedBlackNode<E> t2 = t1.right;
  84.    t1.right = t2.left;
  85.    t2.left = t1;
  86.    t1.parent = t2;
  87.    t2.right.parent = t2;
  88.    t1.left.parent = t1;
  89.    t1.right.parent = t1;
  90.    return t2;
  91. }

红黑树的删除

1三种情况

红黑树的删除相对复杂些,但只要我们思路明确,问题就迎刃而解。我们先回忆普通二叉树的删除操作,可分为三种情况:

1.没有孩子节点:直接删掉该节点

2.只有一个孩子节点:将要删除节点的父节点直接与该孩子节点相链

3.有两个孩子节点:将中序遍历的后继,即待删除节点的右子树中的最小节点赋给待删除节点,然后将该后继删掉。实际最终都会归于1、2两种情况。

2三个问题

        对于红黑树来说,我们不仅要满足二叉树的性质,而且要满足着色要求,所以讨论的情况会比较多,我们从简单的情况开始讨论。如果待删除的实际节点是红色的,我们可以用普通方法进行删除,因为删除过后树依然满足红黑树的性质。如果待删除的实际节点是黑色的,就会出现三个问题:

1.如果删除的节点是根节点,而他的红色孩子成了根节点,这就违反了“规则2”。

2.如果删除的节点的父节点是红色的,而该节点的孩子也是红色的,删除之后就会违反“规则4”。

3.删除了一个黑色节点后,包含该节点的任何路径黑色节点数都会少1,从而违反了“规则5”,我们的任务就是把这些问题解决。

3解决思路

百花齐放:

        首先,我们解决问题的总体思路很简单:将节点删除,然后通过旋转和适当的着色来修复树使之重新满足红黑树的性质。我们的入手点就是我们之后所说的当前节点。我们知道实际删除的节点要么只有一个孩子,要么没有孩子,如果该删除节点有一个孩子,则将这个孩子作为当前节点,如果没有孩子,则将nullNode节点作为当前节点。当前节点的父节点就是原删除节点的父节点。我们第一次调整树就是从上边描述的当前节点x开始。(要记住第一个当前节点,要不然后边的描述会变得含糊不清)。

       对于问题1我们很好解决,最后再把根节点涂黑即可。而对于问题2,我们可以把当前节点涂黑就可以让树满足红黑树的性质。现在我们需要关注的问题是问题3,即让删除节点后的树依然满足红黑树的性质5——各个节点到根节点到叶节点nullNode所包含的的黑节点数相同。

       现在你有什么思路呢?如果像先前我们执行插入的思路那样,自顶向下,保证删除的节点是红色的,这看起来是可行的,但要怎么处理呢?要处理的情况是不是太多了?我们可不可以加上一些条件限制来减少对情况的处理,比如左节点不能是红色的?

要点核心:

      这里的处理的思路是:既然删除节点后经过该节点的路径上黑色节点都少一个,我们可不可以将这黑色下推到他的子节点,这样子节点就有了两重颜色。这样就可以满足性质5了。而又违反了性质1,即节点要么是红色的要么是黑色的。我们要做的在保持性质的情况下去掉。(实际上这一层黑色是我们处理问题所转换的标记,并非节点的颜色属性,当前节点指向谁,谁就有了这一层黑色,最终我们在保证基本性质的情况下去掉这一层黑色的影响,问题解决)要想去掉这一层黑色,我们处理的方式有:

1.将这层黑色推向一个包含删除节点路径上的一个红色节点,在满足其他性质的情况下将其染成黑色。

2.一直推至根节点,减掉这层黑色。(因为我们每次的调整最后都是满足性质的)

3.在某些情况下,通过调整和重新着色,我们就可以保住性质,当然这一点有点难以凭空想象,那就看看下边的情况分析吧。

具体操作:

      首先我们可以将情况分为两类,即当前节点是其父节点的右节点或左节点,他们是对称的,只需要对一种情况进行详细讨论,另一种情况也就是以此类推了。一般的资料都是对当前节点x是做节点的情况进行分析,在这里我们就先对x是右节点的情况一一画图进行分析。这里先对图中的标记进行解释:x表示当前节点,w表示当前节点的,p表示x的父节点,c表示某个确定的颜色(可能是红,也可能是黑,就看实际情况了)对应于逻辑中的存在,c'表示任意颜色(才不管你是什么颜色咧,对讨论无影响)对应于逻辑中的任意。

情况1:

当前节点x的兄弟w是红色的。这种情况我们可以确定x的父节点为黑色,w的两个孩子为黑。如图所示,我们先对p染红,再将w染黑,然后对p进行一次右旋,红黑性质得以保持。而这时新的兄弟节点是黑色的,进而可以将情况一转换成情况2、3、4中的一种。

情况2:

当前节点x的兄弟节点w是黑色的,且w的两个孩子都是黑色的。这种情况我们无法确定父节点p的颜色,所以其颜色标记为c,情况3、4同。在这个情况下,我们可以将x、w同时去掉一层黑色,将这一层黑色指向根节点p,p变成新的当前节点x。此时如果该标记颜色c为红色,则可以将节点染成黑色,此时指针x的那一层黑色被去掉,同时红黑性质得到满足,调整完毕;如果c为黑色,则需要对新的当前节点x的情况进行处理,直到调整完毕。

情况3:

当前节点x的兄弟节点为黑色,且w的左孩子是黑色,右孩子是红色的。在这种情况下,我们将w和其右孩子的颜色交换,并对w进行左转,红黑性质得以保持。此时已将情况3转换成情况4。

情况4:

当前节点x的兄弟节点w为黑色,且w的左孩子是红的,有孩子可以为任意颜色。将p的颜色赋给w,然后将p,和w的左节点染黑,并对p做右旋转。这是可以去掉x的额外的黑色,而且可以保持红黑性质。最后将树的根赋给x后,调整结束。

我们发现,情况1、3、4最多经过三次旋转调整就可以结束。情况二在最坏的情况下一直向上推最多也是树的层数log(n),这就是红黑树删除操作的性能优势。

4Java代码实现

  1. /**
  2. * 1.没有孩子节点:直接删掉该节点
  3. * 2.只有一个孩子节点:将要删除节点的
  4.      父节点直接与该孩子节点相链
  5. * 3.有两个孩子节点:将中序遍历的后继,即待删除节点的右
  6.     子树中的最小节点赋给待删除节点,然后将该后继删掉。
  7. * @param e 要删除的元素
  8. * @param t 删除元素的树的根节点
  9. * @return  被删除的节点
  10. */
  11. private RedBlackNode<E> remove(E e,RedBlackNode<E> t){
  12. if(t==nullNode)
  13. return null;
  14.    //找到要删除的节点  
  15.    while(compare(e,t)!=0){
  16. if(compare(e,t)<0&&t.left!=nullNode)
  17. t = t.left;
  18.        else if(compare(e,t)>0&&t.right!=nullNode)
  19. t = t.right;
  20.        else
  21.            return null;
  22.    }
  23. RedBlackNode<E> x;//当前节点  
  24.    RedBlackNode<E> y;//要删除的节点  
  25.    //如果待删除节点只有一个孩子或没有孩子,则实际删除的节点
  26.      //就是所指节点,如果有两个孩子则实际删除节点是右子树的最小项  
  27.    //先确定删除节点  
  28.    if(t.left==nullNode||t.right==nullNode)
  29. y = t;
  30.    else
  31.        y = findMin(t.right);
  32.     //再确定当前节点,如果实际删除节点的左节点不为nullNode,
  33.      //则当前节点为左节点,如果删除节点只有右节点或没有孩子  
  34.    //我们都将右孩子赋给他,因为没有孩子时右节点为nullNode  
  35.    if(y.left!=nullNode)
  36. x = y.left;
  37.    else
  38.        x = y.right;
  39.    //当前节点的父亲指向删除节点的父亲  
  40.    x.parent = y.parent;
  41.    //我们根据删除节点在其父节点的子树方向,将父节点直接链接到当前节点  
  42.   //需要注意的是我们引用的伪根节点就是为了减少对特殊情况的讨论,
  43.    //不然有这行代码if(y.parent==header)header.right = x;  
  44.    if(y==y.parent.left)
  45. y.parent.left = x;
  46.    else
  47.        y.parent.right = x;
  48.    //如果实际删除的节点不是我们找到的具有该键的节点,它属于有两个
  49.     //孩子的情况,我们将实际删除的节点的值赋给它  
  50.    if(y!=t)
  51. t.element = y.element;
  52.    if(y.color==BLACK)
  53. removeFixUp(x);
  54.    return y;
  55. }
  56. /**
  57. * 删除修复方法
  58. * @param x 当前节点
  59. */
  60. private void removeFixUp(RedBlackNode<E> x){
  61. RedBlackNode<E> w;
  62.    while(x!=header.right&&x.color==BLACK){
  63. //当前节点是父节点的左节点  
  64.    if(x==x.parent.left){
  65. w = x.parent.right;
  66.          //case1,如上详细分析,如果x的兄弟节点为红色,
  67.          //调整后是兄弟节点为黑后再往下走  
  68.       if(w.color==RED){
  69. x.parent.color = RED;
  70.          w.color = BLACK;
  71.          rightRotate(x.parent,x.parent.parent);
  72.          w = x.parent.right;
  73.       }
  74. //case2,新的x如果为红色,循环终止,否则继续循环  
  75.    if(w.left.color==BLACK&&w.right.color==BLACK){
  76. w.color = RED;
  77.           x = x.parent;
  78.       }else {
  79. //case3,兄弟节点的右孩子是黑色的,
  80.          //左孩子时红色的,调整后进入case4  
  81.       if(w.right.color==BLACK){
  82. w.left.color = BLACK;
  83.           w.color = RED;
  84.           rightRotate(w, w.parent);
  85.           w = x.parent.right;
  86.       }
  87. //case4,调整完成,满足红黑性质  
  88.           w.color = x.parent.color;
  89.           x.parent.color = BLACK;
  90.           w.right.color = BLACK;
  91.      leftRotate(x.parent, x.parent.parent);
  92.           x = header.right;
  93.        }
  94. }else{
  95. //对称  
  96.        }
  97. x.color = BLACK;
  98.    }
  99. }

下载1:OpenCV-Contrib扩展模块中文版教程

在「小白学视觉」公众号后台回复:扩展模块中文教程即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。

下载2:Python视觉实战项目52讲

在「小白学视觉」公众号后台回复:Python视觉实战项目即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。

下载3:OpenCV实战项目20讲

在「小白学视觉」公众号后台回复:OpenCV实战项目20讲即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。

交流群

欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/682378
推荐阅读
相关标签
  

闽ICP备14008679号