红黑树的删除详解与思路分析——不同于教科书上的算法(dart语言实现)
对于红黑树的删除,看了数据结构的书,也看了很多网上的讲解和实现,但都不满意。很多讲解都是囫囵吞枣,知其然,不知其所以然,讲的晦涩难懂。
红黑树是平衡二叉树的一种,其删除算法是比较复杂的,因为删除后还要保持红黑树的特性。红黑树的特性如下:
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NIL节点)。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点(简称黑高)。
因此,从红黑树最基础的特性出发,抛开教科书和网上的算法,画了无数张图,分析了多种可能的情况以后,经过归纳提炼,实现了不同于教科书上的删除算法。
经过多次画图证明以后,笔者发现,红黑树的删除算法不是唯一的,不管如何调整,只要保证删除后还是一颗红黑树即可。
因此,笔者实现的 删除思路和算法如下:
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语言实现)的更多相关文章
- stl map底层之红黑树插入步骤详解与代码实现
转载注明出处:http://blog.csdn.net/mxway/article/details/29216199 本篇文章并没有详细的讲解红黑树各方面的知识,只是以图形的方式对红黑树插入节点需要进 ...
- POJ-2590-Steps题目详解,思路分析及代码,规律题,重要的是找到规律~~
Steps Time Limit: 1000MS Memory Limit: 65536K http://poj.org/problem?id=2590 Description One ...
- RB-Tree删除详解
红黑树的删除操作较于插入操作,情况更为复杂: 考虑到红黑节点的差异性,我们在此通过红黑节点来考虑这个问题,即仅仅通过要删除的节点是红节点,还是黑节点来讨论不同的情况: 1 删除的红节点为叶子结点(此 ...
- LinkedList详解-源码分析
LinkedList详解-源码分析 LinkedList是List接口的第二个具体的实现类,第一个是ArrayList,前面一篇文章已经总结过了,下面我们来结合源码,学习LinkedList. 基于双 ...
- 关于syslog日志功能详解 事件日志分析、EventLog Analyzer
关于syslog日志功能详解 事件日志分析.EventLog Analyzer 一.日志管理 保障网络安全 Windows系统日志分析 Syslog日志分析 应用程序日志分析 Windows终端服务器 ...
- Java性能分析之线程栈详解与性能分析
Java性能分析之线程栈详解 Java性能分析迈不过去的一个关键点是线程栈,新的性能班级也讲到了JVM这一块,所以本篇文章对线程栈进行基础知识普及以及如何对线程栈进行性能分析. 基本概念 线程堆栈也称 ...
- 使用IDEA详解Spring中依赖注入的类型(上)
使用IDEA详解Spring中依赖注入的类型(上) 在Spring中实现IoC容器的方法是依赖注入,依赖注入的作用是在使用Spring框架创建对象时动态地将其所依赖的对象(例如属性值)注入Bean组件 ...
- Block详解二(底层分析)
Block专辑: Block讲解一 MRC-block与ARC-block Block详解一(底层分析) 今天讲述Block的最后一篇,后两篇仅仅是加深1,2篇的理解,废话少说,开始讲解! __blo ...
- ArrayList详解-源码分析
ArrayList详解-源码分析 1. 概述 在平时的开发中,用到最多的集合应该就是ArrayList了,本篇文章将结合源代码来学习ArrayList. ArrayList是基于数组实现的集合列表 支 ...
随机推荐
- Linux学习---linux下的彩蛋和各种有趣的命令
[原文]https://www.toutiao.com/i6596596897392099844/ screenfetch 一个显示系统信息和主题信息的命令 使用方法 输入screenfetch 效果 ...
- 2.2Python基础语法(二)之运算符
返回总目录 目录: 1.Python运算符的分类 2.算数运算符 3.复合运算符 4.比较运算符 5.逻辑运算符 (一)Python运算符的分类: (二)算数运算符: 注意下面三种算数符号: 1.** ...
- DevExpress04、LayoutControl、GalleryControl
首先需求是通过LayoutControl控件设计下图所示的窗体: 从该界面的设计过程 1.向窗体中添加LayoutControl控件 在将该控件拖入窗体后,最好立即设置该控件的尺寸和位置.拖入后,在如 ...
- PyQt5--QLineEdit
# -*- coding:utf-8 -*- ''' Created on Sep 20, 2018 @author: SaShuangYiBing Comment: ''' import sys f ...
- JSONP方法解决跨域请求
Ajax跨域请求的问题 跨域:跨域名, 一个域名下的文件去请求了和他不一样的域名下的资源文件(注意是请求文件,而不是数据接口),那么就会产生跨域请求,下面来写一个ajax来跨域请求的例子 <!D ...
- 【转】PHP----JS相互调用
JS调用PHP 1.取值: 执行html,得到一个弹窗,提示:I from PHP <script type="text/javascript" src="http ...
- single number和变体
给array of integers. 裡面有一个数字是单独出现 其他都会出现两次(而且一起出现)ex: [1,2,2,3,3]要判断哪个数字是单独出现的. 以这个例子的话就是 1 LZ 一开始先说 ...
- Kubernetes1.91(K8s)安装部署过程(六)--node节点部署
hi,everybody,我回来了,之前安装到flannel之后,文章一直没有更新,甚至不少小伙伴都来加qq询问是否继续更新了, 这里说明下原因,我在部署1.91node的时候的确出现了各种各样的问题 ...
- linux服务器关闭ipv6 方法
第一个文件: /etc/sysconfig/network 第二个文件:如无此文件,vim添加 /etc/modprobe.d/disable_ipv6.conf
- [转]VS 2012环境下使用MFC进行OpenGL编程
我就不黏贴复制了,直接给出原文链接:VS 2012环境下使用MFC进行OpenGL编程 其它好文链接: 1.OpenGL系列教程之十二:OpenGL Windows图形界面应用程序