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

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

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

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

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

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

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

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

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

  2. 删除后修复:

  2.1 简单的情况:

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

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

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

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

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

    修复的整体思路是:

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

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

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

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

   // 删除
bool delete(E value) {
var node = find(value);
if (node == null) return false;
_delete(node);
_nodeNumbers--;
return true;
} // 删除转移 并修复
void _delete(RBTNode<E> d) {
if (d.left != null && d.right != null) {
var s = _successor(d);
d.value = s.value;
d = s;
} var rp = d.left ?? d.right;
rp?.parent = d.parent;
if (d.parent == null)
_root = rp;
else if (d == d.parent.left)
d.parent.left = rp;
else
d.parent.right = rp; if (rp != null)
rp.paintBlack();
else if (d.isBlack && d.parent != null)
_fixAfterDelete(d.parent, d.parent.left == null);
} RBTNode<E> _successor(RBTNode<E> d) =>
d.right != null ? _minNode(d.right) : d.left; RBTNode<E> _minNode(RBTNode<E> r) => r.left == null ? r : _minNode(r.left); // fix up after delete
void _fixAfterDelete(RBTNode<E> p, bool isLeft) {
var ch = isLeft ? p.right : p.left;
if (isLeft) { // 如果被删除节点是父节点p的左分支;
if (p.isRed) { // 如果父节点为红,则兄弟节点ch一定为黑;
if (ch.left != null && ch.left.isRed) {
p.paintBlack();
_rotateRight(ch);
}
_rotateLeft(p);
} else if (ch.isRed) { // 兄弟节点为红,此时兄弟节点一定有两个非空黑色子节点;
p.paintRed();
ch.paintBlack();
_rotateLeft(p);
_fixAfterDelete(p, true); // 变换为父节点为红的情况,递归处理;
} else if (ch.left != null && ch.left.isRed) { // 父、兄均为黑,兄有红色左孩子;
ch.left.paintBlack();
_rotateRight(ch);
_rotateLeft(p);
} else { // 父兄均为黑,将父分支左右均减少一个黑节点,然后递归向上处理;
p.paintRed();
_rotateLeft(p);
if (ch.parent != null) _fixAfterDelete(ch.parent, ch == ch.parent.left);
}
} else { // symmetric
if (p.isRed) {
if (ch.right != null && ch.right.isRed) {
p.paintBlack();
_rotateLeft(ch);
}
_rotateRight(p);
} else if (ch.isRed) {
p.paintRed();
ch.paintBlack();
_rotateRight(p);
_fixAfterDelete(p, false);
} else if (ch.right != null && ch.right.isRed) {
ch.right.paintBlack();
_rotateLeft(ch);
_rotateRight(p);
} else {
p.paintRed();
_rotateRight(p);
if (ch.parent != null) _fixAfterDelete(ch.parent, ch == ch.parent.left);
}
}
}

  旋转操作的代码:

   void _rotateLeft(RBTNode<E> node) {
var r = node.right, p = node.parent;
r.parent = p;
if (p == null)
_root = r;
else if (p.left == node)
p.left = r;
else
p.right = r; node.right = r.left;
r.left?.parent = node;
r.left = node;
node.parent = r;
} void _rotateRight(RBTNode<E> node) {
var l = node.left, p = node.parent;
l.parent = p;
if (p == null)
_root = l;
else if (p.left == node)
p.left = l;
else
p.right = l; node.left = l.right;
l.right?.parent = node;
l.right = node;
node.parent = l;
}

红黑树的删除详解与思路分析——不同于教科书上的算法(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. 第五次作业 hql查询

    hql查询是基于对象的查询,不是基于表的查询. 1.hql的简单查询 @Test public void queryUsers() { //简单查询 SessionFactory sf = null; ...

  2. November 17th, 2017 Week 46th Friday

    If you shut the door to all errors, truth will be shut out. 你如果拒绝面对错误,真相也会被挡在门外. Sometimes being a f ...

  3. 阿里八八Alpha阶段Scrum(12/12)

    今日进度 俞鋆: 和前端对接,完成了注册接口的对接写了一部分数据同步接口. 李嘉群: 完成了注册界面与服务器的对接 黄梅玲: json数据解析并在listview中显示(还未完成) 王国超: 今天开始 ...

  4. ModuleNotFoundError No module named urllib2

    ModuleNotFoundError No module named urllib2?那么在进行编辑的来进行代码上开发,那就会出现的来代码的上错误,也是版本的上差异导致的问题.   工具/原料   ...

  5. 'No Transport' Error w/ jQuery ajax call in IE

    I need to use foursquare API to search venues. Of course it is cross-domain. It has no any problems ...

  6. python第三十六课——2.迭代器对象

    满足前提: 1).必须是一个可迭代对象 2).可以被next()所作用的 举例: generator... 高效的检测一个对象是否是迭代器对象 需要使用collections模块中的Iterator类 ...

  7. apache不能启动LoadModule php5_module modules/ph

    apache不能启动LoadModule php5_module modules/php5apache2.dll的问题 主要是版本问题!!有点不爽!! apache不能启动 加入下面两行,apache ...

  8. Android APP的字体设置

    Android系统自带有对字体的设置,这些设置是对字体的显示方式的设置,比如加粗,倾斜,下划线,字号等,但是对于字体本身,比如设置为楷体,隶书等貌似没有.Android系统只有一种默认的,如果需要修改 ...

  9. JAVA框架 Mybaits 动态sql

    动态sql 一:if标签使用: 我们在查询的时候,有时候由于查询的条件的不确定性,导致where的后面的条件的不同,这时候就需要我们进行where后面的条件进行拼接. Mapper配置文件: < ...

  10. 卢卡斯定理 Lucas (p为素数)

    证明摘自:(我网上唯一看得懂的证明) https://blog.csdn.net/alan_cty/article/details/54318369 结论:(显然递归实现)lucas(n,m)=luc ...