本章节主要分析红黑树的“插入算法”和“获取算法”,也就是put()方法和get()方法的底层实现。
在学习TreeMap的put()方法之前,我们首先应该创建一个叶子节点Entry类,叶子节点Entry是TreeMap的内部类,它有几个重要的属性:节点的颜色、左子节点指针、右子节点指针、父节点指针、节点的值。
class MyTreeMap<K, V> {
// 节点类
class Entry<K, V> {
// 键
K key;
// 值
V value;
// 左孩子
Entry<K, V> left;
// 右孩子
Entry<K, V> right;
// 父亲
Entry<K, V> parent;
// 颜色
boolean color = BLACK;
// 构造方法
public Entry(K key, V value, Entry<K, V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
}
TreeMap中同时也包含了如下几个重要的属性:
class MyTreeMap<K, V> {
// 比较器,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制
private final Comparator<? super K> comparator;
// 红-黑树根节点
private Entry<K, V> root;
// 容器大小
private int size;
// 红黑树的节点颜色--红色
private static final boolean RED = false;
// 红黑树的节点颜色--黑色
private static final boolean BLACK = true;
// 此处省略Entry节点类
}
最后,我们再添加两个构造方法,一个无参构造,另外一个有参构造。
class MyTreeMap<K, V> {
// 此处省略MyTreeMap属性
// 无参构造方法
public MyTreeMap() {
this.comparator = null; // 默认比较机制
}
// 有参构造方法
public MyTreeMap(Comparator<? super K> comparator) {
this.comparator = comparator; // 自定义比较器的构造方法
}
// 此处省略Entry节点类
}
到此处,TreeMap的put()方法的准备工作就已经完成,那么接下来我们就开始分析并实现put()方法。
- **put()**方法实现
在TreeMap的put()的实现方法中主要分为两个步骤,第一步:构建排序二叉树,第二步:保持红黑树平衡。
- 第一步:构建排序二叉树
对于排序二叉树的创建,其添加节点的过程如下:
(1) 以根节点为初始节点进行检索。
(2) 与当前节点进行比对,若新增节点值较大,则以当前节点的右子节点作为新的当前节点。否则以当前节点的左子节点作为新的当前节点。
(3) 循环递归2步骤知道检索出合适的叶子节点为止。
(4) 将新增节点与3步骤中找到的节点进行比对,如果新增节点较大,则添加为右子节点;否则添加为左子节点。
按照这个步骤我们就可以将一个新增节点添加到排序二叉树中合适的位置。如下:
class MyTreeMap<K, V> {
// 此处省略MyTreeMap属性和构造方法
/**
* 添加元素
* 步骤一:构建排序二叉树
* 步骤二:保持红黑树平衡
*/
public V put(K key, V value) {
// 步骤一:构建排序二叉树
// 1.当红-黑树是空树,把新创建的节点设置为跟节点
if (null == root) { // 当root为null,证明是空树
// 创建一个Entry节点,并设置为根节点
root = new Entry<K, V>(key, value, null);
// 结合元素个数设置为1
size = 1;
return null;
}
// 2.当红-黑数不为空树,则找到新添加节点的父节点
// 用t表示二叉树的当前节点
Entry<K, V> t = root;
// 用parent表示父节点,并作为新添加节点的父节点
Entry<K, V> parent;
// 使用compare表示key排序的返回结果
int compare;
// 2.1如果comparator不为null,则采用外部比较器进行创建TreeMap集合
if (null != comparator) {
do {
// parent指向上次循环后的t
parent = t;
// 比较新增节点的key和当前节点key的大小
compare = comparator.compare(key, t.key);
// compare返回值小于0,表示新增节点的key小于当前节点的key
// 则以当前节点的左子节点作为新的当前节点
if (compare < 0) {
t = t.left;
}
// compare返回值大于0,表示新增节点的key大于当前节点的key
// 则以当前节点的右子节点作为新的当前节点
else if (compare > 0) {
t = t.right;
}
//compare返回值等于0,表示两个key值相等,则新值覆盖旧值,并返回新值
else {
return t.value = value;
}
} while (null != t); // 当前节点为null,则跳出循环
}
// 2.2当果comparator为null,则采用自然排序进行创建TreeMap集合
else {
// 判断key是否为空,因为key需要调用compareTo方法
if (null == key)
throw new NullPointerException(); // 抛出空指针异常
// 自然排序的处理过程和外部比较器类似
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
// 比较新增节点的key和当前节点key的大小
compare = k.compareTo(t.key);
if (compare < 0) {
t = t.left;
} else if (compare > 0) {
t = t.right;
} else {
return t.value = value;
}
} while (null != t);
}
// 3.插入新增节点
// 创建新节点
Entry<K, V> entry = new Entry<K, V>(key, value, parent);
// 如果新增节点的key小于parent的key,则当做左子节点
if (compare < 0) {
parent.left = entry;
}
// 如果新增节点的key大于parent的key,则当做右子节点
else {
parent.right = entry;
}
// 步骤二:保持红黑树平衡【该步骤后面再实现,此处省略】
return null;
}
// 此处省略Entry节点类
}
- 第二步:保持红黑树平衡
红黑树在新增节点过程中比较复杂,复杂归复杂它同样必须要依据上面提到的五点规范,为了保证下面的阐述更加清晰和根据便于参考,我这里将红黑树的五点规范再贴一遍:
-
每个节点都只能是红色或者黑色。
-
根节点是黑色。
-
每个叶节点(NIL节点,NULL空节点)是黑色的。
-
每个红色节点的两个子节点都是黑色 (****从每个叶子到根的路径上不会有两个连续的红色节点) 。
-
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
由于规则1、2、3基本都会满足,下面我们主要讨论规则4、5。
假设我们这里有一棵最简单的树,我们规定新增的节点为N、它的父节点为P、P的兄弟节点为U、P的父节点为G。
对于新节点的插入有如下三个关键地方:
1、插入新节点总是红色节点。
2、如果插入节点的父节点是黑色,能维持性质 。
3、如果插入节点的父节点是红色,破坏了性质。
故插入算法就是通过重新着色或旋转,来维持性质,可能出现的情况如下:
【情况一】为跟节点
若新插入的节点N没有父节点,则直接当做根据节点插入即可,同时将颜色设置为黑色。
【情况二】父结点为黑色
那么插入的红色节点将不会影响红黑树的平衡,直接插入即可。
【情况三】父节点和叔节点都为红色
当叔父结点为红色时,无需进行旋转操作,只要将父和叔结点变为黑色,将祖父结点变为红色即可。
但是经过上面的处理,可能G节点的父节点也是红色,这个时候我们需要将G节点当做新增节点递归处理。
【情况四】父红,叔黑,并且新增节点和父节点都为左子树
对于这种情况先已P节点为中心进行右旋转,在旋转后产生的树中,节点P是节点N、G的父节点。
但是这棵树并不规范,所以我们将P、G节点的颜色进行交换,使之其满足规范。
【情况五】父红,叔黑,并且新增节点和父节点都为右子树
对于这种情况先已P节点为中心进行左旋转,在旋转后产生的树中,节点P是节点G、N的父节点。但是这棵树并不规范,所以我们将P、G节点的颜色进行交换,使之其满足规范。
【情况六】父红,叔黑,并且新增节点为左子树,父节点为右子树
对于这种情况先以N节点为中心进行右旋转,在旋转后产生的树中,节点N是节点P、X的父节点。然后再以N节点为中心进行左旋转,在旋转后产生的树中,节点N是节点P、G的父节点。但是这棵树并不规范,所以我们将N、G节点的颜色进行交换,使之其满足规范。
【情况七】父红,叔黑,并且新增节点为右子树,父节点为左子树
对于这种情况先以N节点为中心进行左旋转,在旋转后产生的树中,节点N是节点P、Y的父节点。然后再以N节点为中心进行右旋转,在旋转后产生的树中,节点N是节点P、G的父节点。但是这棵树并不规范,所以我们将N、G节点的颜色进行交换,使之其满足规范。
插入节点后,保持红黑树平衡的实现:
class MyTreeMap<K, V> {
// 此处省略MyTreeMap属性和构造方法
/**
* 添加元素
* 步骤一:构建排序二叉树
* 步骤二:保持红黑树平衡
*/
public V put(K key, V value) {
// 步骤一:构建排序二叉树【该步骤省略】
// 步骤二:保持红黑树平衡
// 设置新增节点为红色
entry.color = RED;
// 保持红黑树平衡
fixAfterInsertion(entry);
// 添加元素累加
size++;
return null;
}
/**
* 保持红黑树平衡
* 当父节点为黑色,那么插入的红色节点不影响平衡
* 当父节点为红色,那么插入的红色节点会影响平衡
* @param entry 表示新增节点
*/
private void fixAfterInsertion(Entry<K, V> entry) {
// 循环直到entry不是根节点,并且entry的父节点是红色
while (null != entry && entry != root && colorOf(entry.parent) == RED) {
// 当entry的父节点属于左侧节点时
if (parentOf(entry) == leftOf(parentOf(parentOf(entry)))) {
// 获取entry的右侧叔叔节点
Entry<K, V> uncle = rightOf(parentOf(parentOf(entry)));
// 当叔叔节点为红色时(父红&叔红)
if (colorOf(uncle) == RED) { // 【情况三】
// 将父节点设置为黑色
setColor(parentOf(entry), BLACK);
// 将叔节点设置为黑色
setColor(uncle, BLACK);
// 将父节点的父节点设置为红色
setColor(parentOf(uncle), RED);
// 更新entry,通过循环继续遍历处理
// 因为有可能“父节点的父节点的父节点”还是为红色
entry = parentOf(parentOf(entry));
}
// 当叔叔节点为黑色时(父红&叔黑)
else { // 【情况四】和【情况七】
// 1.当新增节点为右子树时(父红&叔黑&且新增节点为右子树&父左子树)
if (entry == rightOf(parentOf(entry))) { // 【情况七】
// 把entry的父节点进行左旋
rotateLeft(entry.parent);
}
// 2.新增节点为左子树时
// 父红&叔黑&并且新增节点和父节点都为左子树 【情况四】
// 设置entry的父节点为黑色
setColor(parentOf(entry), BLACK);
// 设置entry的父节点的父节点为红色
setColor(parentOf(parentOf(entry)), RED);
// 设置entry父节点的父节点右旋
rotateRight(parentOf(parentOf(entry)));
}
}
// 当entry的父节点属于右侧节点时
else {
// 获取entry的左侧叔叔节点
Entry<K, V> uncle = leftOf(parentOf(parentOf(entry)));
// 当叔叔节点为红色时(父红&叔红)
if (colorOf(uncle) == RED) {// 【情况三】
// 将父节点设置为黑色
setColor(parentOf(entry), BLACK);
// 将叔节点设置为黑色
setColor(uncle, BLACK);
// 将父节点的父节点设置为红色
setColor(parentOf(uncle), RED);
// 更新entry,通过循环继续遍历处理
// 因为有可能“父节点的父节点的父节点”还是为红色
entry = parentOf(parentOf(entry));
}
// 当叔叔节点为黑色时(父红&叔黑)
else { // 【情况五】和【情况六】
// 1.当新增节点为左子树时(父红&叔黑&且新增节点为左子树&父右子树)
if (entry == leftOf(parentOf(entry))) { // 【情况六】
// 把entry的父节点进行右旋
rotateRight(entry.parent);
}
// 2.新增节点为右子树时
// 父红&叔黑&并且新增节点和父节点都为右子树 【情况五】
// 设置entry的父节点为黑色
setColor(parentOf(entry), BLACK);
// 设置entry的父节点的父节点为红色
setColor(parentOf(parentOf(entry)), RED);
// 设置entry父节点的父节点左旋
rotateLeft(parentOf(parentOf(entry)));
}
}
}
// 将根节点强制设置为黑色
setColor(root, BLACK);
}
// 此处省略Entry节点类
}
完成保持红黑树平衡,还需要用到几个很重要的操作,例如:获取父节点(parentOf)、获取右子树(rightOf)和左子树(leftOf)、设置色(setColor)和获取颜色(colorOf)、左旋(rotateLeft)、右旋(rotateRight)。
获取父节点方法:
/**
* 获取entry的父节点
*/
private Entry<K, V> parentOf(Entry<K, V> entry) {
return (null == entry) ? null : entry.parent;
}
获取左子树和右子树方法:
/**
* 获取entry的左节点
*/
private Entry<K, V> leftOf(Entry<K, V> entry) {
return (null == entry) ? null : entry.left;
}
/**
* 获取entry的右节点
*/
private Entry<K, V> rightOf(Entry<K, V> entry) {
return (null == entry) ? null : entry.right;
}
设置和获取颜色方法:
/**
* 设置entry节点的颜色
*/
private void setColor(Entry<K, V> entry, boolean color) {
if (entry != null)
entry.color = color;
}
/**
* 获取entry节点的颜色
*/
private boolean colorOf(Entry<K, V> entry) {
// 因为叶子默认颜色为黑色
return (entry == null ? BLACK : entry.color);
}
左旋方法实现:
代码实现如下:
/**
* 左旋转
*/
private void rotateLeft(Entry<K, V> entry) {
if (null != entry) {
// 1.获取entry的右子节点,其实这里就相当于新增节点
Entry<K, V> right = entry.right;
// 2.左旋之后,设置entry和right左子树的连线
// 2.1将right的左子树设置为entry的右子树
// 指向关系:entry-->right.left
entry.right = right.left;
// 2.2若right的左子树不为空,则将entry设置为right左子树的父亲
// 指向关系:right.left-->entry
if (null != right.left)
right.left.parent = entry; //
// 3.左旋之后,设置right和entry父节点之间的连线
// 3.1把entry的父节点设置为right的父节点
// 指向关系:right-->entry.parent
right.parent = entry.parent;
// 3.2当entry就是根节点时
if (null == entry.parent) {
root = right; // 把right设置为根节点
}
// 3.3当entry不是根节点时
// 指向关系:entry.parent-->right
// 3.3.1当旋转之前,entry为父节点的左子树时
else if (entry.parent.left == entry) {
entry.parent.left = right;
}
// 3.3.2当旋转之前,entry为父节点的右子树时
else {
entry.parent.right = right;
}
// 4.左旋之后,设置right和entry之间的连线
// 指向关系:right-->entry
right.left = entry;
// 指向关系:entry-->right
entry.parent = right;
}
}
右旋方法实现:
代码实现如下:
/**
* 右旋转
*/
private void rotateRight(Entry<K, V> entry) {
if (null != entry) {
// 1.获取entry的左子节点,其实这里就相当于新增节点的父节点
Entry<K, V> left = entry.left;
// 2.右旋之后,设置entry和left右子树的连线
// 指向关系:entry-->left.right;
entry.left = left.right;
// 指向关系:left.right-->entry
if (null != left.right) {
left.right.parent = entry;
}
// 3.右旋之后,left和它的父节点的连线
// 3.1设置left和它父节点的连线
// 指向关系:left-->entry.parent
left.parent = entry.parent;
// 3.2当entry为跟节点的时候
// 指向关系:设置left为根节点
if (null == entry.parent) {
root = left;
}
// 3.3当entry不为根节点的时候
// 指向关系:entry.parent-->left
// 3.3.1当entry为左子树的时候
else if (entry.parent.left == entry) {
entry.parent.left = left;
}
// 3.3.2当entry为右子树的时候
else {
entry.parent.right = left;
}
// 4.右旋之后,entry和left的连线
// 指向关系:left-->entry
left.right = entry;
// 指向关系:entry-->left
entry.parent = left;
}
}
- **get()**方法实现
当TreeMap 根据key来取出value 时,TreeMap对应的方法如下:
class MyTreeMap<K, V> {
// 此处省略MyTreeMap属性和构造方法
/**
* 根据key,获取value值
*/
public V get(Object key) {
// 获取key对应的entry对象
Entry<K, V> p = getEntry(key);
// 返回key对应的value值
return (p == null ? null : p.value);
}
// 此处省略Entry节点类
}
从上面程序的粗体字代码可以看出,get(Object key) 方法实质是由于 getEntry() 方法实现的,这个 getEntry() 方法的代码如下:
/**
* 根据key,获取key对应的Entry节点
*/
public Entry<K, V> getEntry(Object key) {
// 1.如果comparator不为null,则采用外部比较器进行创建TreeMap集合
if (null != comparator) {
return getEntryUsingComparator(key);
}
// 2.否则,采用了自然比排序进行创建TreeMap集合
// 当key为null时,抛出NullPointerException异常
if (null == key) {
throw new NullPointerException();
}
// 把key向上提升为Comparable类型
Comparable<? super K> cmp = (Comparable<? super K>) key;
// 用于保存父节点
Entry<K, V> parent = root;
do {
// 获取compareTo方法之后的返回值
int compare = cmp.compareTo(parent.key);
//当compare小于0,证明key小于parent.key,那么继续朝着parent的左子树查找
if (compare < 0) {
// 更新parent的值
parent = parent.left;
}
//当compare大于0,证明key大于parent.key,那么继续朝着parent的右子树查找
else if (compare > 0) {
// 更新parent的值
parent = parent.right;
}
// 当compare等于0,证明key等于parent.key,那么证明找到key对应的节点
else {
return parent;
}
} while (parent != null);//当parent为null时,结束循环,证明TreeMap中不存在
return null;
}
上面的getEntry(Object obj) 方法也是充分利用排序二叉树的特征来搜索目标Entry,程序依然从二叉树的根节点开始,如果被搜索节点大于当前节点,程序向“右子树”搜索;如果被搜索节点小于当前节点,程序向“左子树”搜索;如果相等,那就是找到了指定节点。
当TreeMap里的comparator != null即表明该TreeMap采用了定制排序,在采用定制排序的方式下,TreeMap采用getEntryUsingComparator(key) 方法来根据key获取Entry。
下面是该方法的代码:
public Entry<K, V> getEntryUsingComparator(Object key) {
K k = (K) key;
// 用于保存父节点
Entry<K, V> parent = root;
do {
// 获取compare方法之后的返回值
int compare = comparator.compare(k, parent.key);
//当compare小于0,证明key小于parent.key,那么继续朝着parent的左子树查找
if (compare < 0) {
// 更新parent的值
parent = parent.left;
}
//当compare大于0,证明key大于parent.key,那么继续朝着parent的右子树查找
else if (compare > 0) {
// 更新parent的值
parent = parent.right;
}
// 当compare等于0,证明key等于parent.key,那么证明找到key对应的节点
else {
return parent;
}
} while (parent != null);//当parent为null时,结束循环,证明TreeMap中不存在
return null;
}
其实getEntry、getEntryUsingComparator两个方法的实现思路完全类似,只是前者对自然排序的TreeMap获取有效,后者对定制排序的TreeMa有效。
通过上面源代码的分析不难看出,TreeMap这个工具类的实现其实很简单。或者说:从内部结构来看,TreeMap本质上就是一棵“红黑树”,而TreeMap的每个Entry就是该红黑树的一个节点。
ps:如需最新的免费文档资料和教学视频,请添加QQ群(627407545)领取。