开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情
TreeMap的实现
写在前面
文章摘要
- 初识映射Map
- Map的实现
- 添加相关方法
- 查询相关方法
- 删除相关方法
- 遍历方法
阅读准备
- 建议阅读时间:10 ~ 15 分钟
- 阅读Tip:本文红黑树的实现是从零开始的,可以不用阅读其它的分析,若有什么不是很明白的地方,可以看笔者主页的文章哟,欢迎留言~
- 例如树的旋转、如何构建二叉搜索树、树的上溢和下溢、红黑树的添加和删除...
- 当然了,这些在此篇也会有分析~
一、初识Map映射
-
在复习红黑树之前,我们先来看看映射
Map
-
相信大家大大小小都使用过这本书:
-
没错,就是《新华字典》,而我们接下来要介绍的
Map映射
,其实就和字典差不多,查询到的每一个汉字,都有一个与它对应的详细解释 -
因此,
Map
在有些编程语言中也叫做字典(dictionary
) -
就和字典一样,在编程里的体现就是形如
<Key, Value>
这样的键值对,通过一个Key
,就有与它对应的Value
- 而实现这种映射的关系,有很多的实现方案,我们下面先用红黑树来实现
二、TreeMap的实现
(1)接口定义 & 基本构造
- 直接上代码,先来切切菜~
- 就不解释这些方法了。因为要从零实现一个
Map
,所以还是将这些接口贴出来了
① 接口定义
public interface Map<K, V> {
int size();
boolean isEmpty();
void clear();
V put(K key, V value);
V get(K key);
V remove(K key);
/**
* 查看 Key 存不存在
*/
boolean containsKey(K key);
/**
* 查看 Value 存不存在
*/
boolean containsValue(V value);
/**
* 遍历
* @param visitor:访问器
*/
void traversal(Visitor<K, V> visitor);
/**
* 访问器抽象类
*/
abstract class Visitor<K, V> {
boolean stop;
public abstract boolean visit(K key, V value);
}
}
复制代码
② 基本构造
- 之前实现的红黑树只有一个泛型,而我们的Map,有<K, V>两个泛型参数,我们应该如何处理呢?
- 当然,聪明的你肯定能想到这个办法,再提供一个类,形如这样的:
private static class KV<K, V> {
K key;
V value;
// ....
}
复制代码
- 到时候,红黑树就可以用此类作为泛型的参数了:
RBTree<KV> rbTree
- 这样确实可以做到,接收两个泛型的参数。虽然外界使用没有什么影响,可是内部在使用的时候,还需要中转一层,显得很麻烦,而且也要增加额外的内存
- 况且,如果这样实现的话,那就是继续组合以前写的红黑树了,并不是从零实现一棵红黑树~
- 那下面跟我一起来改造一下,从零利用红黑树来实现
Map
改造节点
- 因为之前从零分析并且实现过一遍了,所以很多基础性的代码,就直接贴出来了,代码中也配有注释,若有描述不清楚的地方,欢迎讨论,我努力改正~
/**
* 内部节点
* @param <K>:键 key
* @param <V>:值 value
*/
private static class Node<K, V> {
boolean color = RED; // 节点的颜色,默认为红色
K key;
V value;
Node<K, V> parent;
Node<K, V> left;
Node<K, V> right;
public Node(K key, V value, Node<K, V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
/*--------------------------↓ ↓ ↓节点的辅助函数---------------------------------*/
/**
* 是否是左子节点
*/
public boolean isLeftChild() {
return parent != null && this == parent.left;
}
/**
* 是否是右子节点
*/
public boolean isRightChild() {
return parent != null && this == parent.right;
}
/**
* 是否是叶子节点
*/
public boolean isLeaf() {
return left == null && right == null;
}
/**
* 是否是度为2的节点
*/
public boolean hasTowChildren() {
return left != null && right != null;
}
/**
* 获取兄弟节点
*/
public Node<K, V> sibling() {
if (isLeftChild()) { // 自己在左边,返回右边
return parent.right;
}
if (isRightChild()) { // 自己在右边,返回左边
return parent.left;
}
return null; // 没有父节点,那么也没有兄弟节点
}
/*--------------------------↑ ↑ ↑节点的辅助函数-------------------------------*/
@Override
public String toString() { // 方便调试
String str = "";
if (color == RED) {
str = "RED_";
}
return str + "【K:" + key.toString() + "】【V:" + value.toString() + "】【P:" + parent + "】";
}
}
复制代码
- 既然觉得用以前的节点不方便,那我们重新写一个节点就行了呗~
- 并且提供一些之后会用到的辅助函数
红黑树的辅助函数
/*--------------------------↓ ↓ ↓红黑树的辅助函数---------------------------------*/
/**
* 染色
* @param node:待染色节点
* @param color:颜色
* @return :染色后的节点
*/
private Node<K, V> color(Node<K, V> node, boolean color) {
if (node == null) return node;
node.color = color;
return node;
}
/**
* 将节点染成红色
*/
private Node<K, V> red(Node<K, V> node) {
return color(node, RED);
}
/**
* 将节点染成黑色
*/
private Node<K, V> black(Node<K, V> node) {
return color(node, BLACK);
}
/**
* 查看节点的颜色
* @param node:待查询节点
* @return :节点的颜色
*/
private boolean colorOf(Node<K, V> node) {
return node == null ? BLACK : node.color;
}
/**
* 查看节点是否是红色
*/
private boolean isRed(Node<K, V> node) {
return colorOf(node) == RED;
}
/**
* 查看节点是否是黑色
*/
private boolean isBlack(Node<K, V> node) {
return colorOf(node) == BLACK;
}
/*--------------------------↑ ↑ ↑红黑树的辅助函数-------------------------------*/
复制代码
- 用于方便维护红黑树的性质,提供的几个辅助函数
简单方法的实现
public class TreeMap<K, V> implements Map<K, V> {
private int size;
private Node<K, V> root;
private Comparator<K> comparator;
private static final boolean RED = false;
private static final boolean BLACK = true;
public TreeMap() { this(null); }
public TreeMap(Comparator<K> comparator) {
this.comparator = comparator;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void clear() {
size = 0;
root = null;
}
}
复制代码
- 这就是TreeMap内部的基本构造,因为要用红黑树来实现,所以做了很多铺垫
- 有了这些铺垫之后,我们来看看其它方法是如何实现的
(2)添加相关方法
- 先来添加两个元素进入
Map
,看看该如何添加
map.put("Ciusyan", 666);
map.put("Zhiyan", 999);
复制代码
① put(K key, V value)
方法
-
因为底层使用红黑树来实现Map,红黑树的前提是二叉搜索树,所以要先构建二叉搜索树,再维护红黑树的性质
-
而构建二叉搜索树的前提就是:元素必须是可比较的
-
外界给我们Key和Value,我们选用
Key
来构建,所以,在执行此方法前,必须要进行对Key的非空判断 -
当然,Value可以存储空值,不需要判断
private void keyNotNullCheck(K key) {
if (key == null) {
throw new InvalidParameterException("Key 不能为空");
}
}
复制代码
- 非空判断后,我们就可以开始构建二叉搜索树了:
- 如果是第一次添加,那么直接构建根节点即可
- 如果不是第一次添加,那么先比较节点的大小,找出待添加节点的父节点与该放置它的位置
- 再根据父节点构建出新的节点,最后将其放在父节点的左边或右边即可
- 将其思路转换为代码:
public V put(K key, V value) {
keyNotNullCheck(key);
if (root == null) { // 添加的是根节点
root = new Node<>(key, value, null);
size++;
afterPut(root);
return null;
}
// 来到这里说明不是根节点
// 记录大小、父节点
int compare = 0;
Node<K, V> parent = root;
Node<K, V> currentNode = root;
while (currentNode != null) {
compare = compare(key, currentNode.key); // 比较大小
parent = currentNode; // 给父节点赋值,之后需要用父节点构建新的节点
if (compare > 0) { // 插入的值在右边
currentNode = currentNode.right;
} else if (compare < 0) { // 插入的值在左边
currentNode = currentNode.left;
} else { // 相等的情况
V oldValue = currentNode.value;
// 将 Key Value 都覆盖
currentNode.key = key;
currentNode.value = value;
return oldValue;
}
}
// 构建新节点
Node<K, V> node = new Node<>(key, value, parent);
if (compare > 0) { // 应该插入右边
parent.right = node;
} else { // 应该插入左边
parent.left = node;
}
size++;
// 插入后的操作
afterPut(node);
return null;
}
复制代码
- 当然,我们为此方法增加了一个返回值:旧节点的Value,也可以不用此返回值
- 而且这个返回值,只有在节点相等的时候,才可能不会返回
null
- 而我们里面如果遇到节点相等的情况,会先将
新Key和新Value
覆盖掉旧Key和旧Value
,至于为什么要覆盖,在前面的文章已经详细解释过了 - 至此,节点就已经添加好了,但是在我们添加节点之后,还需要维护红黑树的性质
② afterPut(Node<K, V> node)
方法
- 在维护性质前,先来复习一下红黑树的性质:
- 看完这些性质,我们再来复习一下关于红黑树的添加的分析:
-
上面是将其红黑树转化为了它等价的4阶B树,来看看思路:
- 如果父节点是黑色,那么不需要做额外的处理
- 如果父节点是红色,需要查看它叔父节点的颜色
- 如果叔父节点是红色,那么在添加后会出现上溢的现象。需要将父节点和叔父节点染成黑色,祖父节点染成红色,再将祖父节点当做新添加的节点,递归调用
afterPut()
方法 - 如果叔父节点是黑色,添加后不会出现上溢现象。利用染色 + 旋转即可维护性质
- 如果叔父节点是红色,那么在添加后会出现上溢的现象。需要将父节点和叔父节点染成黑色,祖父节点染成红色,再将祖父节点当做新添加的节点,递归调用
-
关于叔父节点是黑色的情况,我单拎出来解释,因为他们需要对树进行旋转:
- 需要双旋才能解决的两种情况(
LR、RL
),染色是:将自己染成黑色,祖父节点染成红色 - 需要单旋即可解决的两种情况(
LL、RR
),染色是:将父节点染成黑色,祖父节点染成红色
- 需要双旋才能解决的两种情况(
-
将其思路转换为代码:
private void afterPut(Node<K, V> node) {
Node<K, V> parent = node.parent;
if (parent == null) { // 添加的是根节点
black(node); // 将其染色即可
return;
}
// 1、父节点是黑色。不需要处理
if (isBlack(parent)) return;
// 来到这里说明父节点都是红色的情况了
Node<K, V> uncle = parent.sibling(); // 取出叔父节点
Node<K, V> grandparent = red(parent.parent); // 将祖父节点染成红色
if (isRed(uncle)) { // 2、叔父节点是红色,会产生上溢现象
// 将父节点和叔父节点染黑
black(parent);
black(uncle);
// 将它当做新添加的节点,递归解决
afterPut(grandparent);
return;
}
// 来到这里说明,叔父节点不是红色、添加后通过【旋转 + 染色】解决
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
// 将祖父节点染成红色、父节点染成黑色
black(parent);
} else { // LR
// 将祖父节点染成红色、自己染成黑色
black(node);
rotateLeft(parent); // 父节点左旋
}
rotateRight(grandparent); // 祖父节点右旋
} else { // R
if (node.isRightChild()) { // RR
// 将祖父节点染成红色、父节点染成黑色
black(parent);
} else { // RL
// 将祖父节点染成红色、自己染成黑色
black(node);
rotateRight(parent); // 父节点右旋
}
rotateLeft(grandparent); // 祖父节点左旋
}
}
复制代码
- 配合着思路来看代码,是不是感觉红黑树的添加也不是很难~
- 我们再将其中的旋转操作拎出来复习复习
③ 树的旋转
左旋:rotateLeft(Node<K, V> node)
private void rotateLeft(Node<K, V> node) {
final Node<K, V> child = node.right; // 取出子节点【左旋、在右边】
final Node<K, V> grandchild = child.left; // 取出孙子节点
node.right = grandchild; // 自己的右子节点指向孙子节点
child.left = node; // 将自己旋转到下方
afterRotate(node, child, grandchild); // 旋转后的操作
}
复制代码
右旋:rotateRight(Node<K, V> node)
private void rotateRight(Node<K, V> node) {
final Node<K, V> child = node.left; // 取出子节点【右旋、在左边】
final Node<K, V> grandchild = child.right; // 取出孙子节点
node.left = grandchild; // 自己的左子节点指向孙子节点
child.right = node; // 将自己旋转到下方
afterRotate(node, child, grandchild); // 旋转后的操作
}
复制代码
公共代码:afterRotate(Node<K, V> node, Node<K, V> child, Node<K, V> grandchild)
private void afterRotate(Node<K, V> node, Node<K, V> child, Node<K, V> grandchild) {
child.parent = node.parent; // 更新子节点的父节点
// 将子节点旋转到上方
if (node.isLeftChild()) { // 待旋转节点在父节点的左边
node.parent.left = child;
} else if (node.isRightChild()) { // 待旋转节点在父节点的右边
node.parent.right = child;
} else { // 没有父节点
root = child;
}
// 如果有孙子节点,更新孙子节点的父节点
if (grandchild != null) {
grandchild.parent = node;
}
node.parent = child; // 更新原先待旋转节点的父节点
}
复制代码
- 配合着图,看看树的旋转,其实也不是很难,就是将某些节点上移、某些节点下移。交换它们的子节点,维护它们的父节点
(3)查询相关方法
- 写完了添加相关的方法,并且成功添加了几个元素进去,我们该如何查询它们呢?
map.get("Ciusyan");
map.containsKey("Zhiyan");
map.containsValue(666);
复制代码
① get(K key)
- 根据
Key
,获取对应的Value
,外部想要获取值,内部得先去获取节点,再通过节点取出Value
- 那我们还得提供一个根据
Key
,查找Node
节点的方法:
private Node<K, V> node(K key) {
if (key == null) return null;
Node<K, V> node = root;
while (node != null) {
int compare = compare(key, node.key); // 比较大小
if (compare == 0) return node;
if (compare > 0) { // 传入值大
node = node.right; // 可能在右子树
} else { // 传入值小
node = node.left; // 可能在左子树
}
}
return null;
}
复制代码
- 思路也不是很难,就是从根节点开始查找。因为红黑树也是一棵二叉搜索树,所以可以比较节点
Key
的大小,来二分查找 - 所以,我们还需要提供一个,比较节点
Key
大小的方法:
private int compare(K k1, K k2) {
if (comparator != null) { // 有传比较器就使用比较器
return comparator.compare(k1, k2);
}
return ((Comparable<K>)k1).compareTo(k2); // 没有比较器,就默认当做是可比较的
}
复制代码
-
如果外界在使用
TreeMap
的时候,有传入比较器,优先使用Comparator
的方法 -
如果外界没有传入比较器,那我们就默认它是可以比较的,使用
Comparable
的方法 -
至此,
node(K key)
方法写完了,我们就可实现get(K key)
方法了
public V get(K key) {
final Node<K, V> node = node(key); // 根据 key 获取节点
return node == null ? null : node.value;
}
复制代码
② containsKey(K key)
- 这个方法是用于查看
Key
,是否存在与容器中 - 那我们完全可以套用刚刚写的
node(K key)
方法来实现
public boolean containsKey(K key) {
return node(key) != null;
}
复制代码
③ containsValue(V value)
- 这个方法是用于查看
Value
,是否存在与容器中 - 因为构建红黑树是利用
Map
的Key
来构建的,所以不能套用上面的node方法了 - 只能自己遍历,确保每一个节点都被访问
public boolean containsValue(V value) {
if (root == null) return false;
// 准备开始层序遍历
Queue<Node<K, V>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
// 队头出队
final Node<K, V> node = queue.poll();
if (node.value.equals(value)) return true;
if (node.left != null) { // 左子树不为空放入队列
queue.offer(node.left);
}
if (node.right != null) { // 右子树不为空放入队列
queue.offer(node.right);
}
}
return false;
}
复制代码
- 之前学习的四种遍历方式,其实都可以。我这里就选择使用层序遍历的方式了
- 这一段代码,应该都能倒背如流了吧!!!
(4)删除相关方法
- 查询也完成了,来看看如何删除?
map.remove("Zhiyan");
复制代码
- 外界是根据key来删除对应的value
- 而内部则是根据key,来删除node
- 所以内部是使用:
remove(node(key))
来删除对应的节点
① remove(Node<K, V> node)
- 删除二叉搜索树中的节点,应该也不陌生了,我们来看看思路:
- 查看节点的度是否为
2
,若为2,先将其转换为删除度为0 或 1
的节点- 找到前驱或后继节点,这里以前驱节点为例,将前驱节点的key和value赋值给待删除节点。然后将前驱节点变成待删除节点
- 经过上面的逻辑,来到这里说明待删除节点的度要么为
0
,要么为1
- 先取出用于替代它的子节点,如果不为
null
,说明度为1。如果为null
,说明度为0 - 再找出待删除节点在它父节点的哪一边,就将那一边用取代它的子节点赋值
- 先取出用于替代它的子节点,如果不为
- 查看节点的度是否为
- 将其思路转换为代码:
private V remove(Node<K, V> node) {
if (node == null) return null;
size--;
// 取出被删除节点的值
final V oldValue = node.value;
// 1、将度为 2 的节点,转换为删除度为 0 或 1 的节点
if (node.hasTowChildren()) {
// 查找前驱or后继节点
final Node<K, V> predecessor = predecessor(node);
// 交换它们的 K,V【度为 2 必然有前驱、后继节点】
node.key = predecessor.key;
node.value = predecessor.value;
node = predecessor; // 将其转换为度为 0 or 1 的节点
}
// 来到这里说明度肯定为 0 or 1
Node<K, V> child = node.left != null ? node.left : node.right; // 取出替代它的子节点(如果有)
if (child != null) { // 2、说明待删除节点的度为 1
child.parent = node.parent; // 需要改变子节点的父节点
if (node.parent == null) { // 删除的是根节点
root = child;
} else if (node == node.parent.left) { // 待删除节点在左子树
node.parent.left = child;
} else { // node == node.parent.right
node.parent.right = child;
}
afterRemove(child); // 将用于取代的子节点传入,删除后的逻辑
} else { // 3、度为 0
if (node.parent == null) { // 删除根节点
root = null;
} else if (node == node.parent.left) { // 待删除节点在左子树
node.parent.left = null;
} else { // node == node.parent.right
node.parent.right = null;
}
afterRemove(node); // 将被删的节点传入删除后的逻辑
}
return oldValue;
}
复制代码
- 看上面的代码,二叉搜索树的删除应该不是很难理解。最主要的是其中在删除节点后,对红黑树性质的维护
- 那我们先将上面查找前驱节点的逻辑复习之后,重点来看看
afterRemove()
② predecessor(Node<K, V> node)
- 查找某一节点的前驱节点(如果你查找的是后继节点,思路于此方法相反,可自行实现)
- 先查看该节点是否拥有左子树:
- 若有左子树,前驱节点必然在左子树的最右边。那么找到左子树后,一直向右遍历,直到为
null
- 若无左子树,前驱节点可能是某一祖先节点,也可能没有前驱节点。那么拿到该节点的父节点向上遍历,直至父节点为空或者该节点位于父节点的右子树。返回此时该节点的父节点即可
- 若有左子树,前驱节点必然在左子树的最右边。那么找到左子树后,一直向右遍历,直到为
- 先查看该节点是否拥有左子树:
- 将思路转换为代码:
private Node<K, V> predecessor(Node<K, V> node) {
if (node == null) return node;
Node<K, V> predecessor = node.left;
if (predecessor != null) { // 说明前驱节点在左子树
while (predecessor.right != null) { // 在左子树的最右边
predecessor = predecessor.right;
}
return predecessor;
}
// 来到这里说明不在左子树 1、是某一祖先节点 2、没有前驱节点
while (node.parent != null && node == node.parent.left) { // 向父节点上找
node = node.parent;
}
// 来到这里,1、说明要么 parent == null 2、要么 node == node.parent.right
return node.parent; // 1、【没有前驱节点】 2、【前驱节点是node.parent】
}
复制代码
- 感兴趣的朋友,可以自己实现一下如何查找后继节点~
- 至此,节点就已经被删除了,但是在我们删除节点之后,还需要维护红黑树的性质
③ afterRemove(Node<K, V> node)
- 之前实现删除,可是费了九牛二虎之力啊,相信现在会轻松许多,我们先来看看被删除节点可能出现的情况:
- 如图所示,我们来看看思路:
- 如果删除的节点是红色的和删除的黑色节点有两个红色的子节点。那么不需要做任何处理,直接返回即可
- 如果删除的节点是黑色的,并且它有一个红色的子节点:那么将用于取代它的子节点,也就是那个唯一的子节点染成黑色即可
- 如果删除的节点是黑色,并且一个红色的子节点都没有,删除后会出现下溢现象:取出兄弟节点
- 若为黑色:查看兄弟节点有没有红色的子节点
- 若有:通过旋转 + 染色,向兄弟借一个元素
- 若没有:将父节点向下合并,如果父节点原先就是黑色。那么还要将它当做是被删除的节点,递归执行
afterRemove()
方法
- 若为红色:将其转换为兄弟节点为黑色的情况。通过旋转 + 染色,将侄子变成兄弟。然后执行兄弟节点为黑色的逻辑
- 若为黑色:查看兄弟节点有没有红色的子节点
- 将其思路转换为代码:
private void afterRemove(Node<K, V> node) {
/*
1、删除的是红色的节点
2、删除的是有一个红色子节点的黑色节点【删除的是BST中度为 1 的节点】
*/
if (isRed(node)) {
black(node); // 1、都被删除了,染色也没关系 2、将其取代的子节点染成黑色即可
return;
}
// 来到这里,被删除的节点都是度为0的黑色节点
Node<K, V> parent = node.parent; // 取出父节点
if (parent == null) return; // 说明删除的是根节点
/*
查看被删除的节点是否位于左子树
1、parent.left == null【传入前左边被删除了,所以是左边】
2、node.isLeftChild()【下面解决下溢时,可能会递归调用此方法,node是被当做删除节点传入】
*/
boolean isLeft = parent.left == null || node.isLeftChild();
Node<K, V> sibling = isLeft ? parent.right : parent.left; // 取出兄弟节点
if (isLeft) { // 被删除节点位于左子树【与右子树操作对称】
if (isRed(sibling)) { // 兄弟节点是红色
// 将兄弟节点染黑、父节点染红
black(sibling);
red(parent);
rotateLeft(parent); // 父节点左旋
sibling = parent.right; // 旋转后兄弟变了,将侄子变成兄弟
}
// 来到这里,兄弟节点肯定是黑色的情况了
if (isRed(sibling.left) || isRed(sibling.right)) { // 兄弟节点至少有一个红色的子节点,可以借用
if (isRed(sibling.left)) { // RL 的情况
rotateRight(sibling); // 将兄弟节点右旋
sibling = parent.right; // 旋转后兄弟节点变换了
}
// 来到这里,说明都能看成是 RR的情况了
rotateLeft(parent); // 将父节点左旋
color(sibling, colorOf(parent)); // 新父节点【中心节点】继承旧父节点的颜色
// 将新父节点的子节点都染成黑色
black(sibling.right);
black(sibling.left);
} else { // 兄弟节点一个红色的子节点也没有,需要向下合并
boolean parentBlack = isBlack(parent); // 记录原先父节点的颜色
black(parent); // 父节点染成黑色
red(sibling); // 兄弟节点染成红色
if (parentBlack) { // 如果以前就是黑色,父节点向下合并后,也会参数下溢
afterRemove(parent); // 将父节点当做被删除的节点,递归调用此函数
}
}
} else { // 被删除节点位于右子树【与左子树操作对称】
if (isRed(sibling)) { // 兄弟节点是红色
// 将兄弟节点染黑、父节点染红
black(sibling);
red(parent);
rotateRight(parent); // 父节点右旋
sibling = parent.left; // 旋转后兄弟变了,将侄子变成兄弟
}
// 来到这里,兄弟节点肯定是黑色的情况了
if (isRed(sibling.left) || isRed(sibling.right)) { // 兄弟节点至少有一个红色的子节点,可以借用
if (isRed(sibling.right)) { // LR 的情况
rotateLeft(sibling); // 将兄弟节点左旋
sibling = parent.left; // 旋转后兄弟节点变换了
}
// 来到这里,说明都能看成是 LL的情况了
rotateRight(parent); // 将父节点右旋
color(sibling, colorOf(parent)); // 新父节点【中心节点】继承旧父节点的颜色
// 将新父节点的子节点都染成黑色
black(sibling.right);
black(sibling.left);
} else { // 兄弟节点一个红色的子节点也没有,需要向下合并
boolean parentBlack = isBlack(parent); // 记录原先父节点的颜色
black(parent); // 父节点染成黑色
red(sibling); // 兄弟节点染成红色
if (parentBlack) { // 如果以前就是黑色,父节点向下合并后,也会参数下溢
afterRemove(parent); // 将父节点当做被删除的节点,递归调用此函数
}
}
}
}
复制代码
- 一定要结合思路和注释,自己分析分析代码~ 相信红黑树对你来说,也不会特别难
(5)遍历方法
① traversal(Visitor<K, V> visitor)
- 说到二叉树的遍历,你应该至少能想到四种方式:前序遍历、中序遍历、后序遍历、层序遍历
- 而外界想要遍历Map中的元素。他们可不知道内部用的什么遍历方式
public void traversal(Visitor<K, V> visitor) {
if (visitor == null) return;
inorder(root, visitor);
}
复制代码
- 很显然,我这里使用了中序遍历,因为中序遍历是有顺序的。对外界来说,可能会有些许作用。(当然,使用其他方式也是可以的)
② 中序遍历:inorder(Node<K, V> node, Visitor<K, V> visitor)
- 方便起见,我这里就直接使用递归的方式了
private void inorder(Node<K, V> node, Visitor<K, V> visitor) {
if (node == null || visitor.stop) return; // 注:visitor.stop 用于停止递归调用
inorder(node.left, visitor);
if (visitor.stop) return; // 注:visitor.stop 用于取消调用访问逻辑
visitor.stop = visitor.visit(node.key, node.value);
inorder(node.right, visitor);
}
复制代码
- 至此,我们利用红黑树从零实现了映射
Map
,其实也并不是很难很难,是吧~ - 如果还有人问起你TreeMap的底层,红黑树的实现之类的问题,你是不是能够很自信的跟他说!我看过Ciusyan的文章,当然知道了~ balabala....
写在后面
- ✌️✌️✌️完整代码
本篇收获
- 初识映射Map
- 实现TreeMap
- 复习二叉搜索树的添加、删除、查询
- 复习树的旋转
- 复习树的上溢和下溢
- 复习红黑树的性质、添加、删除