对于红黑树的删除,看了数据结构的书,也看了很多网上的讲解和实现,但都不满意。很多讲解都是囫囵吞枣,知其然,不知其所以然,讲的晦涩难懂。

红黑树是平衡二叉树的一种,其删除算法是比较复杂的,因为删除后还要保持红黑树的特性。红黑树的特性如下:

    1. 节点是红色或黑色。
    2. 根是黑色。
    3. 所有叶子都是黑色(叶子是NIL节点)。
    4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
    5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点(简称黑高)。

因此,从红黑树最基础的特性出发,抛开教科书和网上的算法,画了无数张图,分析了多种可能的情况以后,经过归纳提炼,实现了不同于教科书上的删除算法。

经过多次画图证明以后,笔者发现,红黑树的删除算法不是唯一的,不管如何调整,只要保证删除后还是一颗红黑树即可。

因此,笔者实现的 删除思路和算法如下:

  1. 删除转移:(这部分是大路货,不是自己实现的)

    • 如果被删除节点有两个非空子节点,则用后继节点的值代替该节点的值,这样演变成了删除后继节点;否则转下一条;
    • 如果被删除节点一个或两个孩子都为空:若有非空孩子,则用非空孩子节点替代之;若无,直接删除;
    • 删除后继节点:后继节点的左孩子节点一定为空,右孩子可能为空;处理如上一条;

  删除转移的目的是为了简化删除操作,更是为了简化修复操作。因为删除转移后,最终待删除的节点最多只会有一个非空孩子。

  2. 删除后修复:

  2.1 简单的情况:

    • 若被删除节点为红色节点,不需修复;此时该节点一定为红色的叶子节点(可根据红黑树的特性证明);
    • 若被删除的节点为黑色节点,且有一个非空子节点,则将其非空子节点颜色涂黑即可;

    对于以上两种简单的情况,做个说明:根据红黑树特性,非空子节点一定为红色节点,否则将违反特性;根据红黑树特性,在删除前,一颗红黑树不可能出现以下几种情况:

(图片来自网络,感谢原作者。)

  2.2 复杂的情况:删除后需要修复的。

    只有当被删除的节点为黑色叶子节点时,导致该节点所在的分支,少了一个黑色节点,树不再平衡,因此需要修复。

    修复的整体思路是:

    • 如果该节点的父节点、或兄弟节点、或兄弟节点的特定方向的子节点 中,有红色节点,则将此红色节点旋转过来,通过旋转、涂黑操作,保持自父节点以来的树的平衡;
    • 如果不满足上述条件,则通过旋转和变色操作,使其兄弟分支上也减少一个黑色节点,这样自父节点以来的分支保持了平衡,满足了条件,但对于父节点来说,其整个分支减少了一个黑色节点,需要递归向上处理,直至重新平衡,或者到达根节点;

  掌握了整体思路以后,就可编码实现了,编码中用了一些小技巧,合并了一些情况,代码比较简单易懂,阅读者可以根据代码的情况自己画图证明:

  说明:代码为dart语言实现,dart语法基本与Java一致,不清楚的地方可以参考:

    https://www.dartlang.org/guides/language/language-tour

  1. // 删除
  2. bool delete(E value) {
  3. var node = find(value);
  4. if (node == null) return false;
  5. _delete(node);
  6. _nodeNumbers--;
  7. return true;
  8. }
  9.  
  10. // 删除转移 并修复
  11. void _delete(RBTNode<E> d) {
  12. if (d.left != null && d.right != null) {
  13. var s = _successor(d);
  14. d.value = s.value;
  15. d = s;
  16. }
  17.  
  18. var rp = d.left ?? d.right;
  19. rp?.parent = d.parent;
  20. if (d.parent == null)
  21. _root = rp;
  22. else if (d == d.parent.left)
  23. d.parent.left = rp;
  24. else
  25. d.parent.right = rp;
  26.  
  27. if (rp != null)
  28. rp.paintBlack();
  29. else if (d.isBlack && d.parent != null)
  30. _fixAfterDelete(d.parent, d.parent.left == null);
  31. }
  32.  
  33. RBTNode<E> _successor(RBTNode<E> d) =>
  34. d.right != null ? _minNode(d.right) : d.left;
  35.  
  36. RBTNode<E> _minNode(RBTNode<E> r) => r.left == null ? r : _minNode(r.left);
  37.  
  38. // fix up after delete
  39. void _fixAfterDelete(RBTNode<E> p, bool isLeft) {
  40. var ch = isLeft ? p.right : p.left;
  41. if (isLeft) { // 如果被删除节点是父节点p的左分支;
  42. if (p.isRed) { // 如果父节点为红,则兄弟节点ch一定为黑;
  43. if (ch.left != null && ch.left.isRed) {
  44. p.paintBlack();
  45. _rotateRight(ch);
  46. }
  47. _rotateLeft(p);
  48. } else if (ch.isRed) { // 兄弟节点为红,此时兄弟节点一定有两个非空黑色子节点;
  49. p.paintRed();
  50. ch.paintBlack();
  51. _rotateLeft(p);
  52. _fixAfterDelete(p, true); // 变换为父节点为红的情况,递归处理;
  53. } else if (ch.left != null && ch.left.isRed) { // 父、兄均为黑,兄有红色左孩子;
  54. ch.left.paintBlack();
  55. _rotateRight(ch);
  56. _rotateLeft(p);
  57. } else { // 父兄均为黑,将父分支左右均减少一个黑节点,然后递归向上处理;
  58. p.paintRed();
  59. _rotateLeft(p);
  60. if (ch.parent != null) _fixAfterDelete(ch.parent, ch == ch.parent.left);
  61. }
  62. } else { // symmetric
  63. if (p.isRed) {
  64. if (ch.right != null && ch.right.isRed) {
  65. p.paintBlack();
  66. _rotateLeft(ch);
  67. }
  68. _rotateRight(p);
  69. } else if (ch.isRed) {
  70. p.paintRed();
  71. ch.paintBlack();
  72. _rotateRight(p);
  73. _fixAfterDelete(p, false);
  74. } else if (ch.right != null && ch.right.isRed) {
  75. ch.right.paintBlack();
  76. _rotateLeft(ch);
  77. _rotateRight(p);
  78. } else {
  79. p.paintRed();
  80. _rotateRight(p);
  81. if (ch.parent != null) _fixAfterDelete(ch.parent, ch == ch.parent.left);
  82. }
  83. }
  84. }

  旋转操作的代码:

  1. void _rotateLeft(RBTNode<E> node) {
  2. var r = node.right, p = node.parent;
  3. r.parent = p;
  4. if (p == null)
  5. _root = r;
  6. else if (p.left == node)
  7. p.left = r;
  8. else
  9. p.right = r;
  10.  
  11. node.right = r.left;
  12. r.left?.parent = node;
  13. r.left = node;
  14. node.parent = r;
  15. }
  16.  
  17. void _rotateRight(RBTNode<E> node) {
  18. var l = node.left, p = node.parent;
  19. l.parent = p;
  20. if (p == null)
  21. _root = l;
  22. else if (p.left == node)
  23. p.left = l;
  24. else
  25. p.right = l;
  26.  
  27. node.left = l.right;
  28. l.right?.parent = node;
  29. l.right = node;
  30. node.parent = l;
  31. }

红黑树的删除详解与思路分析——不同于教科书上的算法(dart语言实现)的更多相关文章

  1. stl map底层之红黑树插入步骤详解与代码实现

    转载注明出处:http://blog.csdn.net/mxway/article/details/29216199 本篇文章并没有详细的讲解红黑树各方面的知识,只是以图形的方式对红黑树插入节点需要进 ...

  2. POJ-2590-Steps题目详解,思路分析及代码,规律题,重要的是找到规律~~

    Steps Time Limit: 1000MS   Memory Limit: 65536K       http://poj.org/problem?id=2590 Description One ...

  3. RB-Tree删除详解

    红黑树的删除操作较于插入操作,情况更为复杂: 考虑到红黑节点的差异性,我们在此通过红黑节点来考虑这个问题,即仅仅通过要删除的节点是红节点,还是黑节点来讨论不同的情况: 1  删除的红节点为叶子结点(此 ...

  4. LinkedList详解-源码分析

    LinkedList详解-源码分析 LinkedList是List接口的第二个具体的实现类,第一个是ArrayList,前面一篇文章已经总结过了,下面我们来结合源码,学习LinkedList. 基于双 ...

  5. 关于syslog日志功能详解 事件日志分析、EventLog Analyzer

    关于syslog日志功能详解 事件日志分析.EventLog Analyzer 一.日志管理 保障网络安全 Windows系统日志分析 Syslog日志分析 应用程序日志分析 Windows终端服务器 ...

  6. Java性能分析之线程栈详解与性能分析

    Java性能分析之线程栈详解 Java性能分析迈不过去的一个关键点是线程栈,新的性能班级也讲到了JVM这一块,所以本篇文章对线程栈进行基础知识普及以及如何对线程栈进行性能分析. 基本概念 线程堆栈也称 ...

  7. 使用IDEA详解Spring中依赖注入的类型(上)

    使用IDEA详解Spring中依赖注入的类型(上) 在Spring中实现IoC容器的方法是依赖注入,依赖注入的作用是在使用Spring框架创建对象时动态地将其所依赖的对象(例如属性值)注入Bean组件 ...

  8. Block详解二(底层分析)

    Block专辑: Block讲解一 MRC-block与ARC-block Block详解一(底层分析) 今天讲述Block的最后一篇,后两篇仅仅是加深1,2篇的理解,废话少说,开始讲解! __blo ...

  9. ArrayList详解-源码分析

    ArrayList详解-源码分析 1. 概述 在平时的开发中,用到最多的集合应该就是ArrayList了,本篇文章将结合源代码来学习ArrayList. ArrayList是基于数组实现的集合列表 支 ...

随机推荐

  1. 乘风破浪:LeetCode真题_041_First Missing Positive

    乘风破浪:LeetCode真题_041_First Missing Positive 一.前言 这次的题目之所以说是难,其实还是在于对于某些空间和时间的限制. 二.First Missing Posi ...

  2. C++进阶书籍(转)

    推荐的阅读顺序:level 1从<<essential c++>>开始,短小精悍,可以对c++能进一步了解其特性以<<c++ primer>>作字典和课 ...

  3. jsencrypt加密

    在登录时,前台页面的用户名.密码都是明文传输到后台的,漏洞扫描认为这样子不安全,就想到了前台js加密,后台解密的方法,要用到jsencrypt.js,具体代码如下: 前台页面: <!DOCTYP ...

  4. Priority Queue

    优先队列 集合性质的数据类型离不开插入删除这两操作,主要区别就在于删除的时候删哪个,像栈删最晚插入的,队列删最早插入的,随机队列就随便删,而优先队列删除当前集合里最大(或最小)的元素.优先队列有很多应 ...

  5. linux禁止非法用户试探登录

    当我们的linux主机一旦暴露在互联网上,就会遭受到来自网络上的一些非法用户的骚扰.如弱口令扫描,试探性登录:这些行为对linux主机构成一定的威胁.那怎样防范此类的攻击了,这里写了一个脚本,功能就是 ...

  6. requirejs原理深究以及r.js和gulp的打包【转】

    转自:http://blog.csdn.net/why_fly/article/details/75088378 requirejs原理 requirejs的用法和原理分析:https://githu ...

  7. Python csv.md

    csv csv模块可以用于处理从电子表格和数据库导出的数据到带有字段和记录格式的文本文件,通常称为逗号分隔值(csv)格式,因为逗号通常用于分隔记录中的字段. Reading csv.reader(c ...

  8. POJ2104 K-th Number(整体二分)

    嘟嘟嘟 整体二分是一个好东西. 理解起来还行. 首先,需要牢记的是,我们二分的是答案,也就是在值域上二分,同时把操作分到左右区间中(所以操作不是均分的). 然后我就懒得讲了-- 李煜东的<算法竞 ...

  9. 选择当天一周一月导出excel表格

    <a href="javascript:;" class="fr btn btn-primary radius mt-6 mr-10" onclick=& ...

  10. CSS属性书写顺序及命名规则

    /* mozilla.org Base Styles * maintained by fantasai */ /* Suggested order: * display * list-style * ...