可能许多人会有疑问,为什么要使用二叉树,有那么多的数据结构,比如数组、链表等
简单看下数组和链表的优缺点
数组
链表
那么二叉树则是结合了上面两种数据结构的优势,并且它是有序的,而且在处理大批量的动态数据是比较有用的。它的时间复杂度O(logN)
先来看看二叉查找树的定义:
每个节点都符合这个特性,所以它是有序的,也便于查找,如下图:
但是在一种极端的情况下,二叉查找树会出现不平衡。如果一棵二叉树,只有左子树或者右子树,就变成了一个链表,查找的效率就变的很慢,如下图:
对于查找而言,二叉查找树的查找是跟树的高度是有关系的,如果一棵树的高度为N,那么最多可以在N步内完成查找,所以树的高度越矮,那么查询的效率就越高。考虑到一般情况,左子树和右子树的高度不能相差太大,所以我们都希望二叉查找树两边子树是平衡的,而不是只有一边子树。为了优化因左右子树高度不稳定对查找效率的影响,于是出现了平衡二叉树
先看平衡二叉树的定义:
在构造平衡二叉树时,新增一个节点,可能会造成二叉树的失衡,失衡调整主要是通过旋转最小失衡树来实现。
失衡调整主要分为4种情况:
当插入“7”节点,是最小失衡树的左子树的“8”左节点。很显然,是“9”的左子树过高,那么以"9"节点为轴心右旋
当插入"8"节点,是最小失衡树左子树的“7”的右节点。首先以“7”为轴心,然后左旋,变成了LL型,然后以“9”为轴心右旋。
当插入"11"节点,是最小失衡树右子树的“12”的左节点。首先以“12”为轴心,右旋,变成了RR型,然后以“10”为轴心右旋。
先来看看红黑树的定义
注意:
当对红黑树进行插入和删除时,可能会破坏红黑树的性质,那么就需要通过修改某些节点颜色和树的旋转来恢复红黑树的性质
树的旋转,分为左旋和右旋,如下图
对A节点进行右旋,首先找到A节点的左孩子节点B,让B节点的右孩子节点D执行A节点的右孩子节点,在把A节点执行B节点的右孩子节点。
向一棵含有n个节点的红黑树插入一个新节点的操作可以在O(lgn)时间内完成。
在继续插入操作分析前,再来复习下红黑树的特性:
规则:
插入节点可以分为7种情况进行处理
违反:性质2
修复策略:把插入节点修改为黑色即可
违反:未违反任何性质
修复策略:什么都不做
违反:性质4
修复策略:
违反:性质4
修复策略:
违反:性质4
修复策略:
图就不画,跟情况三类型
违反:性质4
修复策略:
图就不画,跟情况四类型
违反:性质4
修复策略:
删除节点对节点的调整,我们在TreeMap在进行分析
Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的判定通过其本身自带的自然排序,也可以通过构造器传入Comparator比较器。
TreeMap底层是通过红黑树实现,也就意味着containsKey(),get(),put(),remove()的时间复杂度都为O(log(n))
首先来看看TreeMap构造器
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
TreeMap的成员变量
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
// key的比较器
private final Comparator<? super K> comparator;
// 树的根节点
private transient Entry<K,V> root;
// 树的节点个数
private transient int size = 0;
// 对树的修改次数
private transient int modCount = 0;
````省略代码
}
下面我们依次来看get()、put()、remove()方法
public V get(Object key) {
// get方法实际上调用的是getEntry()
Entry<K,V> p = getEntry(key);
// 如果p节点存在,则返回p节点的value,否则返回null
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
// 如果创建TreeMap的时候传入了比较器,那么调用getEntryUsingComparator(key)
// getEntryUsingComparator(key)跟getEntry(key)逻辑差不多,只不过一个使用了自定义比较器去比较key,一个使用自身的比较器去比较key
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
// 把key强转为比较器
Comparable<? super K> k = (Comparable<? super K>) key;
// 获取根节点
Entry<K,V> p = root;
while (p != null) {
// key与根节点的key进行比较
int cmp = k.compareTo(p.key);
if (cmp < 0)
// key小,则把左节点赋给p进行循环
p = p.left;
else if (cmp > 0)
// key大,则把右节点赋给p进行循环
p = p.right;
else
// 相等,直接返回p节点
return p;
}
return null;
}
get()方法还是比较简单的,从根节点开始,依次对节点的key进行判断,如果大于节点的key则继续判断节点的右孩子节点,以此类推,直到找到相等key的节点。上面都有注释讲的非常清楚了。
public V put(K key, V value) {
Entry<K,V> t = root;
// 如果根节点为空
if (t == null) {
compare(key, key); // type (and possibly null) check
// 直接创建一个Entry,赋给根节点
root = new Entry<>(key, value, null);
// 树节点的大小赋值为1
size = 1;
// 修改次数+1
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
// 获取比较器
Comparator<? super K> cpr = comparator;
// 判断该key是否存在,如果存在直接找到该节点,把节点的值修改为新value,然后直接返回
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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);
}
// 如果key不存在
// 创建一个新的Entry节点
Entry<K,V> e = new Entry<>(key, value, parent);
// key与parent的key进行比较
if (cmp < 0)
// key小,把新节点指向parent的左节点
parent.left = e;
else
// key大,把新节点指向parent的右节点
parent.right = e;
// 添加了一个红色的新节点,可能会破坏原来的红黑树结构,那么需要进行修复
fixAfterInsertion(e);
// 节点+1
size++;
// 修改次数+1
modCount++;
return null;
}
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) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
put()方法总结:
fixAfterInsertion()跟我们在上面讲红黑树插入的情况,已经讲的非常清楚了。
public V remove(Object key) {
// 首先判断该key是否存在
Entry<K,V> p = getEntry(key);
if (p == null)
// 不存在直接返回null
return null;
V oldValue = p.value;
// 调用deleteEntry()删除节点
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
// 修改次数+1
modCount++;
// 树节点个数-1
size--;
// 如果p节点的左右孩子节点都不为空,那么调用successor(p)寻找后继节点
if (p.left != null && p.right != null) {
// 寻找后继节点逻辑很简单
// 即为:p节点的右子树的最小的那个元素,即为p的后继节点
Entry<K,V> s = successor(p);
// 把p替换成后继节点
p.key = s.key;
p.value = s.value;
p = s;
}
// 获取p节点的左孩子节点,如果左孩子节点不存在,则获取p节点的右孩子节点
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
// 如果p节点的孩子节点不为空
if (replacement != null) {
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
p.left = p.right = p.parent = null;
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {
// 如果p的父节点为null,则为root节点
root = null;
} else {
// 如果p节点没有孩子节点
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
successor(),找到后继节点
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
Entry<K,V> p = t.right;
// 沿着t的右节点的左子树找到最小的元素
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
找到后继节点原理很简单,就是沿着右孩子节点的左子树找到最小的元素
fixAfterDeletion(),对删除节点的树,进行修复
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK); 情况1
setColor(parentOf(x), RED); 情况1
rotateLeft(parentOf(x)); 情况1
sib = rightOf(parentOf(x)); 情况1
}
if (colorOf(leftOf(sib)) == BLACK && 情况2
colorOf(rightOf(sib)) == BLACK) { 情况2
setColor(sib, RED); 情况2
x = parentOf(x); 情况2
} else {
if (colorOf(rightOf(sib)) == BLACK) { 情况3
setColor(leftOf(sib), BLACK); 情况3
setColor(sib, RED); 情况3
rotateRight(sib); 情况3
sib = rightOf(parentOf(x)); 情况3
}
setColor(sib, colorOf(parentOf(x))); 情况4
setColor(parentOf(x), BLACK); 情况4
setColor(rightOf(sib), BLACK); 情况4
rotateLeft(parentOf(x)); 情况4
x = root; 情况4
}
} else { // symmetric
Entry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) { 情况5
setColor(sib, BLACK); 情况5
setColor(parentOf(x), RED); 情况5
rotateRight(parentOf(x)); 情况5
sib = leftOf(parentOf(x)); 情况5
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) { 情况6
setColor(sib, RED); 情况6
x = parentOf(x); 情况6
} else {
if (colorOf(leftOf(sib)) == BLACK) { 情况7
setColor(rightOf(sib), BLACK); 情况7
setColor(sib, RED); 情况7
rotateLeft(sib); 情况7
sib = leftOf(parentOf(x)); 情况7
}
setColor(sib, colorOf(parentOf(x))); 情况8
setColor(parentOf(x), BLACK); 情况8
setColor(leftOf(sib), BLACK); 情况8
rotateRight(parentOf(x)); 情况8
x = root; 情况8
}
}
}
setColor(x, BLACK);
}
下面以remove(12)进行2个图解解释fixAfterDeletion()
fixAfterDeletion()的中心思想:将情况1首先转换为情况2或者情况3和情况4,当前调整情况并不一定从情况1开始。情况5~情况8跟前四种情况是对称的,就没有画出这四种情况了,可以参考代码自行理解。
到此为止TreeMap已经分析完了,其实大多数时间都在讲红黑树的着色、旋转、修复,因为TreeMap底层就是红黑树。在数据结构中红黑树是比较难懂的,其算法也比较复杂,所以对于树的理解一定要多看图画图。
那是因为平衡二叉是高度平衡的树, 而每一次对树的修改, 都要 rebalance, 这里的开销会比红黑树大。 如果插入一个node引起了树的不平衡,平衡二叉树和红黑树都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,平衡二叉树需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而根据fixAfterDeletion(),我们可知红黑树最多只需3次旋转,只需要O(1)的复杂度, 所以平衡二叉树需要rebalance的频率会更高,因此红黑树在大量插入和删除的场景下效率更高