本文以Java TreeMap为例,从源代码层面,结合详细的图解,剥茧抽丝地讲解红黑树(Red-Black tree)的插入,删除以及由此产生的调整过程。

总体介绍

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。

TreeMap底层通过红黑树(Red-Black tree)实现,也就意味着containsKey()get()put()remove()都有着log(n)的时间复杂度。其具体算法实现参照了《算法导论》。

出于性能原因,TreeMap是非同步的(not synchronized),如果需要在多线程环境使用,需要程序员手动同步;或者通过如下方式将TreeMap包装成(wrapped)同步的:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一陪。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
  4. 对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。

在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。

预备知识

前文说到当查找树的结构发生改变时,红黑树的条件可能被破坏,需要通过调整使得查找树重新满足红黑树的条件。调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操作:左旋(Rotate Left),右旋(RotateRight)

左旋

左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

TreeMap中左旋代码如下:

//Rotate Left
private void rotateLeft(Entry<K,V> p) {if (p != null) {Entry<K,V> r = p.right;p.right = r.left;if (r.left != null)r.left.parent = p;r.parent = p.parent;if (p.parent == null)root = r;else if (p.parent.left == p)p.parent.left = r;elsep.parent.right = r;r.left = p;p.parent = r;}
}

右旋

右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

TreeMap中右旋代码如下:

//Rotate Right
private void rotateRight(Entry<K,V> p) {if (p != null) {Entry<K,V> l = p.left;p.left = l.right;if (l.right != null) l.right.parent = p;l.parent = p.parent;if (p.parent == null)root = l;else if (p.parent.right == p)p.parent.right = l;else p.parent.left = l;l.right = p;p.parent = l;}
}

方法剖析

get()

get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value。因此getEntry()是算法的核心。算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0entry

具体代码如下:

//getEntry()方法
final Entry<K,V> getEntry(Object key) {......if (key == null)//不允许key值为nullthrow new NullPointerException();Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序Entry<K,V> p = root;while (p != null) {int cmp = k.compareTo(p.key);if (cmp < 0)//向左找p = p.left;else if (cmp > 0)//向右找p = p.right;elsereturn p;}return null;
}

put()

put(K key, V value)方法是将指定的keyvalue对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束,还需要进行调整(旋转,改变某些节点的颜色)。

public V put(K key, V value) {......int cmp;Entry<K,V> parent;if (key == null)throw new NullPointerException();Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序do {parent = t;cmp = k.compareTo(t.key);if (cmp < 0) t = t.left;//向左找else if (cmp > 0) t = t.right;//向右找else return t.setValue(value);} while (t != null);Entry<K,V> e = new Entry<>(key, value, parent);//创建并插入新的entryif (cmp < 0) parent.left = e;else parent.right = e;fixAfterInsertion(e);//调整size++;return null;
}

上述代码的插入部分并不难理解:首先在红黑树上找到合适的位置,然后创建新的entry并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整往往需要1.改变某些节点的颜色,2.对某些节点进行旋转。

调整函数fixAfterInsertion()的具体代码如下,其中用到了上文中提到的rotateLeft()rotateRight()函数。通过代码我们能够看到,情况2其实是落在情况3内的。情况4~情况6跟前三种情况是对称的,因此图解中并没有画出后三种情况,读者可以参考代码自行理解。

//红黑树调整函数fixAfterInsertion()
private void fixAfterInsertion(Entry<K,V> x) {x.color = RED;while (x != null && x != root && x.parent.color == RED) {if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {Entry<K,V> y = rightOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {//如果y为null,则视为BLACKsetColor(parentOf(x), BLACK);              // 情况1setColor(y, BLACK);                        // 情况1setColor(parentOf(parentOf(x)), RED);      // 情况1x = parentOf(parentOf(x));                 // 情况1} else {if (x == rightOf(parentOf(x))) {x = parentOf(x);                       // 情况2rotateLeft(x);                         // 情况2}setColor(parentOf(x), BLACK);              // 情况3setColor(parentOf(parentOf(x)), RED);      // 情况3rotateRight(parentOf(parentOf(x)));        // 情况3}} else {Entry<K,V> y = leftOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {setColor(parentOf(x), BLACK);              // 情况4setColor(y, BLACK);                        // 情况4setColor(parentOf(parentOf(x)), RED);      // 情况4x = parentOf(parentOf(x));                 // 情况4} else {if (x == leftOf(parentOf(x))) {x = parentOf(x);                       // 情况5rotateRight(x);                        // 情况5}setColor(parentOf(x), BLACK);              // 情况6setColor(parentOf(parentOf(x)), RED);      // 情况6rotateLeft(parentOf(parentOf(x)));         // 情况6}}}root.color = BLACK;
}

remove()

remove(Object key)的作用是删除key值对应的entry,该方法首先通过上文中提到的getEntry(Object key)方法找到key值对应的entry,然后调用deleteEntry(Entry<K,V> entry)删除对应的entry。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束,因此有可能要进行调整。

有关remove()的具体讲解将放到下一篇博文当中,敬请期待!

史上最清晰的红黑树讲解(上)(转自CarpenterLee,纯学习用)相关推荐

  1. 史上最清晰的红黑树讲解(上)

    本文github地址 本文以Java TreeMap为例,从源代码层面,结合详细的图解,剥茧抽丝地讲解红黑树(Red-Black tree)的插入,删除以及由此产生的调整过程. 总体介绍 Java T ...

  2. 查找(一)史上最简单清晰的红黑树讲解 http://blog.csdn.net/yang_yulei/article/details/26066409

    查找(一)史上最简单清晰的红黑树讲解 2014-05-18 00:05 4037人阅读 评论(6) 收藏 举报 分类: 数据结构(7) 算法(4) 版权声明:本文为博主原创文章,未经博主允许不得转载. ...

  3. 红黑树 键值_查找(一)史上最简单清晰的红黑树讲解

    http://blog.csdn.net/yang_yulei/article/details/26066409 查找(一) 我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然 ...

  4. 【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树

    目录 一.红黑树简介 二.为什么需要红黑树? 三.红黑树的特性 四.红黑树的效率 4.1 红黑树效率 4.2 红黑树和AVL树的比较 五.红黑树的等价变换 六.红黑树的操作 6.1 旋转操作 6.2 ...

  5. 【动态图文详解-史上最易懂的红黑树讲解】手写红黑树(Red Black Tree)

    红黑树:一棵自平衡(AVL)+二叉查找树(BST) 什么是红黑树 红黑树,Red-Black Tree 「RBT」是一个自平衡(不是绝对的平衡)的二叉查找树(BST). 红黑树是在1972年由Rudo ...

  6. 【tree】红黑树(上)

    本文目录 一.基本概念 红黑树的定义 NIL叶节点的讨论 引用值为null 引用值为特殊节点NIL 红黑树和AVL树 二.查询节点 三.插入节点 插入节点的颜色 插入逻辑 fixup逻辑 fixup原 ...

  7. 史上最易懂的红黑树动态图解!

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 本文转载自公众号:日拱一兵 红黑树,对很多童鞋来说,是既熟悉又陌生.学校中学过,只了解大概: ...

  8. 清晰理解红黑树的演变---红黑的含义

    前言 红黑树,对不少人来说是个比较头疼的名字,在网上搜资料也很少有讲清楚其演变来源的,多数一上来就给你来五条定义,红啊黑啊与根节点距离相等之类的,然后就开始进行旋转.插入.删除这些操作.一通操作下来, ...

  9. 清晰理解红黑树的演变-红黑的含义

    前言 红黑树,对不少人来说是个比较头疼的名字,在网上搜资料也很少有讲清楚其演变来源的,多数一上来就给你来五条定义,红啊黑啊与根节点距离相等之类的,然后就开始进行旋转.插入.删除这些操作.一通操作下来, ...

最新文章

  1. 第二阶段冲刺第六天(6月5号)
  2. Matlab中typecast函数由int8转换为int32
  3. linux下gdb使用core文件调试程序,解决“段错误核心已转储“的问题
  4. 题解 T28305 【yizimi的旅游景点】
  5. 计算机意外重启或遇错误无法继续,计算机意外地重新启动或遇到错误如何解决?...
  6. 初识ABP vNext(4):vue用户登录菜单权限
  7. [luogu P4198] 楼房重建(线段树 + 思维)
  8. 系统容灾备份选型的决策表
  9. python同时输出两个数组_python中实现将多个print输出合成一个数组
  10. 小汤学编程之JAVA基础day13——I/O流
  11. Android实现蝴蝶动画,蝴蝶飞舞- (补间动画+逐帧动画)
  12. 第23章:MongoDB-聚合操作--聚合命令
  13. PyTorch 学习笔记(六):PyTorch的十八个损失函数
  14. 软件测试52讲-安全第一:渗透测试
  15. CheetahLab:2018中国人工智能报告
  16. 中标麒麟(linux)下Qt调用python数据转换
  17. ENVI5.3.1使用Landsat 8影像进行图像融合
  18. 华为手机热点无法连接_华为手机开热点,连不上怎么破
  19. PROXMOX 开源虚拟服务器系统安装及配置
  20. GAE—图自编码器/Graph RNN/Graph RL

热门文章

  1. 大学计算机专业个人介绍 英语翻译,哪位高手帮我翻译一下个人简历个人简介br/作为一名计算机专业 爱问知识人...
  2. 08.数据结构:第一个动态结构-链表
  3. sql批量更新update嵌套select更新
  4. Unity学习笔记:私有变量private如何在编译器可见 公有变量public在编译器隐藏
  5. Arduino开发(一)_软件开发IDE工具的安装
  6. 微信小程序:全新独家云开发微群人脉
  7. 动态规划之线性DP题集
  8. 开发历程之让暴风雨来得更猛烈些吧!
  9. Do it for success
  10. 刘汝佳 9.2.1 硬币问题