zoukankan      html  css  js  c++  java
  • 【深入理解java集合】-TreeMap实现原理

    一、红黑树介绍
    1、R-B Tree概念
    红黑树(Red Black Tree,简称R-B Tree) 是一种自平衡二叉查找树,它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。

    红黑树是特殊的二叉查找树,意味着它满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。

    除了具备该特性之外,红黑树还包括许多额外的信息。

    2、红黑树5个特性
    (1) 每个节点或者是黑色,或者是红色。

    (2) 根节点是黑色。

    (3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]

    (4) 如果一个节点是红色的,则它的子节点必须是黑色的。

    (5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

    关于它的特性,需要注意的是:

    第一,特性(3)中的叶子节点,是只为空(NIL或null)的节点。

    第二,特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

    这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

    要知道为什么这些特性确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

    3、红黑树的操作
    红黑树的基本操作是添加、删除和旋转。在对红黑树进行添加或删除后,会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。
    旋转包括两种:左旋 和 右旋。

    3.1 左旋

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

    TreeMap中左旋代码如下:

    //Rotate Left
    private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
    Entry<K,V> r = p.right;
    //1.将B子树与x(p)链接
    p.right = r.left;
    if (r.left != null)
    r.left.parent = p;
    //2.将y(r)与x(p)的父树链接
    r.parent = p.parent;
    if (p.parent == null) //根节点
    root = r;
    else if (p.parent.left == p) // x(p)原为左支树
    p.parent.left = r;
    else
    p.parent.right = r; // x(p)原为左支树
    //3.将y(r)与x(p)链接
    r.left = p;
    p.parent = r;
    }
    }
    3.2 右旋

    右旋的过程是将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;
    }
    }
    二、TreeMap实现原理
    1、TreeMap的基本概念:

    TreeMap集合是基于红黑树(Red-Black tree)的 NavigableMap实现。TreeMap继承AbstractMap,实现NavigableMap、Cloneable、Serializable三个接口。其中AbstractMap表明TreeMap为一个Map即支持key-value的集合, NavigableMap(更多)则意味着它支持一系列的导航方法,具备针对给定搜索目标返回最接近匹配项的导航方法 。实现SortedMap,支持遍历时按元素的大小有序遍历。

    该集合最重要的特点就是可排序,该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator比较器进行排序,具体取决于使用的构造方法。

    根据上一条,我们要想使用TreeMap存储并排序我们自定义的类(如User类),那么必须自己定义比较机制:一种方式是User类去实现java.lang.Comparable接口,并实现其compareTo()方法。另一种方式是写一个类(如MyCompatator)去实现java.util.Comparator接口,并实现compare()方法,然后将MyCompatator类实例对象作为TreeMap的构造方法参数进行传参(当然也可以使用匿名内部类)。

    对TreeMap核心源码的理解,实质上就是对“如何维持红黑树数据结构的特性的理解”。下面的解析,都会以红黑树特性为引子来深入剖析TreeMap源码。

    1、内部成员数据结构

    TreeMap中包含了如下几个重要的属性:

    public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
    //比较器,因为TreeMap是有序的,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制
    private final Comparator<? super K> comparator;
    //红-黑树根节点
    private transient Entry<K,V> root = null;
    //容器大小
    private transient int size = 0;
    //树结构被修改次数
    private transient int modCount = 0;
    //红黑树的节点颜色--红色
    private static final boolean RED = false;
    //红黑树的节点颜色--黑色
    private static final boolean BLACK = true;
    ...

    }
    TreeMap采用红黑树的数据结构来实现。树节点Entry实现了Map.Entry,采用内部类的方式实现

    static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

    // 其他省略
    }
    节点很简单,存储了父节点,左右子节点,以及红黑颜色,元素的key以及value信息,也就是三查。

    2、构造方法
    // 1,无参构造方法
    public TreeMap() {
    comparator = null; // 默认比较机制(自然比较)
    }

    // 2,自定义比较器的构造方法
    public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
    }

    // 3,构造已知Map对象为TreeMap
    public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null; // 默认比较机制
    putAll(m);
    }

    // 4,构造已知的SortedMap对象为TreeMap
    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) {
    }
    }
    3、get()方法
    get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value。因此getEntry()是方法的核心。方法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0的entry。

    public V get(Object key) {//根据key值查找value

    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
    }

    //根据key值查找元素方法;final方法不允许被子类重写
    final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)//存在比较器,按比较器进行比较查找
    return getEntryUsingComparator(key);
    if (key == null)//key值为null抛空指针异常
    throw new NullPointerException();

    //没有比较强,自然排序比较
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
    //从root开始循环查找,一直到叶子节点
    int cmp = k.compareTo(p.key); //采用key的compareTo方法进行比较
    if (cmp < 0) //小于继续查找左边
    p = p.left;
    else if (cmp > 0) //大于继续查找右边
    p = p.right;
    else
    return p;//等于返回当前元素
    }
    return null;
    }
    前面只是集合通用的简单知识,下面才是重点,了解红黑树增加、删除节点的核心算法。
    4、put方法
    红黑树在新增节点过程中比较复杂,一般经历三个步骤。第一步: 将红黑树当作一颗二叉查找树,将节点插入。第二步:将插入的节点着色为"红色"。第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。下面会以红黑树特性为基础解析put方法源码,以帮助理解代码的设计。

    4.1 红黑树增加节点的特性

    对于新节点的插入有如下四个关键地方:

    1、插入新节点会对map做一次查找。

    2、插入新节点总是红色节点 。      

    3、如果插入节点的父节点是黑色, 能维持性质 。      

    4、如果插入节点的父节点是红色, 破坏了性质. 故插入算法就是通过重新着色或旋转, 来维持性质 。      

    红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。

    为什么着色成红色,而不是黑色呢?将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了。

    第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?

    对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。

    对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。

    对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。

    对于"特性(4)",是有可能违背的!

    那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。

    这里将红黑树的五点规定再贴一遍:

    (1)每个节点都只能是红色或者黑色

    (2)根节点是黑色

    (3)每个叶节点(NIL节点,空节点)是黑色的。

    (4)如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。

    (5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

    4.2 红黑树增加节点的情况

    假设我们这里有一棵最简单的树,我们规定新增的节点为新、它的父节点为父、父的兄弟节点为叔、父的父节点为祖父。

    1)情况1:无父

    若新插入的节点N没有父节点,则直接当做根据节点插入即可,同时将颜色设置为黑色。

    2)情况2 :黑父

    插入节点后可维持红黑树性质。红黑树插入的新节点肯定为原来的某支的末尾,时颜色为红色,由于根据规则四它会存在两个黑色的叶子节点,值为null,满足规则3和4。同时新增节点为红色,所以通过它的子节点的路径依然会保存着相同的黑色节点数,同样满足规则5。

    3)情况3 红父、红叔

    插入节点后不能维持红黑树性质4,故可能需要进行着色或旋转操作。

    下面假设父亲为左节点进行分析(右节点的情况类似,只是旋转方向相反)。

    根据红黑树的性质5可推得,红叔肯定没有孩子,因为既然可以插入,那父原本也没孩子。插入新节点只破坏了颜色的性质,所以将父、叔节点变黑、祖节点变红。这时由于经过节点父、叔的路径都必须经过G所以在这些路径上面的黑节点数目还是相同的。但是经过上面的处理,可能G节点的父节点也是红色,这个时候我们需要将G节点当做新增节点迭代处理。也就是:进行重新着色再按祖向上继续判断即可。

    4)情况4:红父、黑叔(Null)或缺失、插入左节点

    由于红父没有孩子,根据红黑树的性质5可推得,该情况的黑叔实际只可能为空的NIL节点。插入新节点不仅破坏了颜色的性质,还破坏了平衡,故需要进行重新着色和旋转。此时新节点的左右位置影响具体的旋转方式。

    插入左节点这种情况有可能是由于情况五而产生的,也有可能不是。对于这种情况它违反了规则4,所以我们先将父P、组G节点的颜色进行交换,但这样并不平衡,违反了规则5(黑色节点数相同),所以还需要以祖G节点为中心进行右旋转,在旋转后产生的树中,节点父P是新节点N、祖G的父节点,这样就满足开始时所有的路径经过祖G节点到他们叶子的黑色节点数一样,满足规范5,同时由于颜色相同,也不用再向上迭代。即重新着色并按祖右旋

    5)情况4:红父、黑叔(Null)或缺失、插入右节点

    对于插入位置为右节点这种情况,我们对新增节点N、父节点P进行一次左旋转。这里所产生的结果其实并没有完成,还不是平衡的(违反了规则四),这是我们需要进行情况4的操作。即按父左旋,并指向父继续判断(即接着按情况3处理)

    4.3 put方法源码解析

    put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束,还需要进行调整(旋转,改变某些节点的颜色)。put算法时间复杂度约为O(log2N),并且其与普通查找二叉树比较,插入的旋转次数很少。

    注意看注释

    public V put(K key, V value) {
    //用t表示二叉树的查找入口,即根节点
    Entry<K,V> t = root;
    //t为null表示一个空树,即TreeMap中没有任何元素,直接插入
    if (t == null) {
    //检查key的类型
    compare(key, key); // type (and possibly null) check
    //将新的key-value键值对创建为一个Entry节点,并将该节点赋予给root
    root = new Entry<>(key, value, null);
    size = 1;
    //修改次数 + 1
    modCount++;
    return null;
    }
    int cmp; //cmp表示key排序的返回结果
    Entry<K,V> parent; //父节点
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator; //指定的排序算法
    //如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合
    if (cpr != null) {
    do {
    parent = t; //parent指向上次循环后的t
    //比较新增节点的key和当前节点key的大小
    cmp = cpr.compare(key, t.key);
    //新增节点的key小于当前节点的key,则以当前节点的左子节点作为新的当前节点
    if (cmp < 0)
    t = t.left;
    //新增节点的key大于当前节点的key,则以当前节点的右子节点作为新的当前节点
    else if (cmp > 0)
    t = t.right;
    //两个key值相等,则新值覆盖旧值,并返回新值
    else
    return t.setValue(value);
    } while (t != null);
    }
    //如果cpr为空,则采用默认的排序算法进行创建TreeMap集合
    else {
    if (key == null) //key值为空抛出异常
    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);
    }
    //将新增节点当做parent的子节点
    Entry<K,V> e = new Entry<>(key, value, parent);
    //如果新增节点的key小于parent的key,则当做左子节点
    if (cmp < 0)
    parent.left = e;
    //如果新增节点的key大于parent的key,则当做右子节点
    else
    parent.right = e;
    /*
    *上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置
    *下面fixAfterInsertion()方法就是对这棵树进行调整、平衡,具体过程参考上面的4种情况
    */
    fixAfterInsertion(e);
    //TreeMap元素数量 + 1
    size++;
    //TreeMap容器修改次数 + 1
    modCount++;
    return null;
    }
    }

    fixAfterInsertion(e),调整的过程务必会涉及到红黑树的左旋、右旋、着色三个基本操作。代码如下:

    /**
    * 新增节点后的修复操作
    * x 表示新增节点
    */
    private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED; //新增节点的颜色为红色
    //循环直到x不是根节点,且x的父节点不为红色
    while (x != null && x != root && x.parent.color == RED) {
    //如果X的父节点(P)是其父节点的父节点(G)的左节点
    if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
    //获取X的叔节点(U)
    Entry<K,V> y = rightOf(parentOf(parentOf(x)));
    //如果X的叔节点(U) 为红色(情况三)
    if (colorOf(y) == RED) {
    //将X的父节点(P)设置为黑色
    setColor(parentOf(x), BLACK);
    //将X的叔节点(U)设置为黑色
    setColor(y, BLACK);
    //将X的父节点的父节点(G)设置红色
    setColor(parentOf(parentOf(x)), RED);
    x = parentOf(parentOf(x));
    }
    //如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五)
    else {
    //如果X节点为其父节点(P)的右子树,则进行左旋转(情况四)
    if (x == rightOf(parentOf(x))) {
    //将X的父节点作为X
    x = parentOf(x);
    //右旋转
    rotateLeft(x);
    }
    //(情况五)
    //将X的父节点(P)设置为黑色
    setColor(parentOf(x), BLACK);
    //将X的父节点的父节点(G)设置红色
    setColor(parentOf(parentOf(x)), RED);
    //以X的父节点的父节点(G)为中心右旋转
    rotateRight(parentOf(parentOf(x)));
    }
    }
    //如果X的父节点(P)是其父节点的父节点(G)的右节点
    else {
    //获取X的叔节点(U)
    Entry<K,V> y = leftOf(parentOf(parentOf(x)));
    //如果X的叔节点(U) 为红色(情况三)
    if (colorOf(y) == RED) {
    //将X的父节点(P)设置为黑色
    setColor(parentOf(x), BLACK);
    //将X的叔节点(U)设置为黑色
    setColor(y, BLACK);
    //将X的父节点的父节点(G)设置红色
    setColor(parentOf(parentOf(x)), RED);
    x = parentOf(parentOf(x));
    }
    //如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五)
    else {
    //如果X节点为其父节点(P)的右子树,则进行左旋转(情况四)
    if (x == leftOf(parentOf(x))) {
    //将X的父节点作为X
    x = parentOf(x);
    //右旋转
    rotateRight(x);
    }
    //(情况五)
    //将X的父节点(P)设置为黑色
    setColor(parentOf(x), BLACK);
    //将X的父节点的父节点(G)设置红色
    setColor(parentOf(parentOf(x)), RED);
    //以X的父节点的父节点(G)为中心右旋转
    rotateLeft(parentOf(parentOf(x)));
    }
    }
    }
    //将根节点G强制设置为黑色
    root.color = BLACK;
    }
    5、remove()方法
    对于红黑树的增加节点而言,删除显得更加复杂,使原本就复杂的红黑树变得更加复杂。同时删除节点和增加节点一样,同样是找到删除的节点,删除之后调整红黑树。

    5.1 红黑树删除节点的特性

    真正删除的节点并不一定是指定的节点,当其有两个孩子时,会查找下一个节点作为真正的删除点,这里先称为后继节点,由迭代循环方法可知,该节点一定是没有孩子或只有一个孩子。

    ①该节点如果为红色,必然为叶子节点

    ②该节点如果为黑色,只可能有一个红色孩子或无孩子

    这是因为删除节点并不是直接删除,而是被其后继(树种比大于D的最小的那个元素)替代。这是通过走了“弯路”的方式来删除的:找到被删除的节点D的子节点F,用F来替代D,不是直接删除D,因为D被F替代了,直接删除F即可,若F还有子节点就迭代。所以这里就将删除父节点D的事情转变为了删除子节点F的事情,这样处理就将复杂的删除事件简单化了。子节点F的规则是:右分支最左支树,或者 左分支最右边祖树。

    那被删除节点有没有后继节点就会出现不同的删除方式:

    ① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。

    ② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。

    ③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给"被删除节点"之后,再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了,下面就考虑后继节点。 在"被删除节点"有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然"后继节点"不可能双子都非空,就意味着"该节点的后继节点"要么没有儿子,要么只有一个儿子” (如下图)。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。

    并且,该节点如果为红色,必然为叶子节点,即无儿子,因为如果该节点的子节点为黑,则必然存在令一个子节点(两个儿子不符合迭代查询的结果),否则违反黑节点数原则(规定5),为红,则违反“颜色”原则(规定4)。上下图对比理解。

    红-黑二叉树删除节点,最大的麻烦是要保持各分支黑色节点数目相等。 因为是删除,所以不用担心存在颜色冲突问题(值的替换)——插入才会引起颜色冲突。

    5.2 红黑树删除节点的情况

    红黑树删除节点同样会分成几种情况,这里是按照待删除节点有几个儿子的情况来进行分类:

    1)情况1:无子节点(红色节点)

    这种情况对该节点直接删除即可,不会影响树的结构。因为该节点为叶子节点它不可能存在子节点-----如子节点为黑,则违反黑节点数原则(规定5),为红,则违反“颜色”原则(规定4)。

    2)情况2:有一个子节点

    这种情况处理也是非常简单的,用子节点替代待删除节点,然后删除子节点即可。

    3)情况3:有两个子节点

    这种情况可能会稍微有点儿复杂。它需要找到一个后继节点N来替代本来该被删除的节点(覆盖值),然后真正删除N(N的删除方法上面已讲到),再完善红黑树的约束,这就牵扯到N的父与兄了。它主要分为四种情况。

    N的兄弟节点W为红色
    N的兄弟w是黑色的,且w的俩个孩子都是黑色的。
    N的兄弟w是黑色的,w的左孩子是红色,w的右孩子是黑色。
    N的兄弟w是黑色的,且w的右孩子是红色的。
    4)情况3.1:N的兄弟节点W为红色

    此情况红兄必然有两个孩子,删除后同时影响平衡及颜色性质,故需要重新着色及旋转操作。策略改变兄节点W、父节点P的颜色,然后进行一次左旋转。这样处理就可以使得红黑性质得以继续保持。N的新兄弟是旋转之前w的某个孩子,为黑色。这样处理后将情况3.1、转变为3.2、3.3、3.4中的一种。如下:

    5)情况3.2:N的兄弟节点w是黑色的,且w只有一个右孩子

    该情况需要重新着色及旋转

    6)情况3.3:N的兄弟节点w是黑色的,且w只有一个左孩子

    该情况需要重新着色,然后以兄为中心右旋转得出3.2的情况

    5.3 remove()方法源码分析

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

    public V remove(Object key) {
    Entry<K,V> p = getEntry(key);//先找到需要删除的元素
    if (p == null)
    return null;
    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
    }
    这里重点放deleteEntry()上,该函数删除指定的entry并在红黑树的约束被破坏时进行调用fixAfterDeletion(Entry<K,V> x)进行调整。

    private void deleteEntry(Entry<K,V> p) {
      modCount++; //修改次数 +1

      size--; //元素个数 -1

      /*

      * 被删除节点的左子树和右子树都不为空,那么就用 p节点的中序后继节点代替 p 节点

      * successor(P)方法为寻找P的替代节点。规则是右分支最左边,或者 左分支最右边的节点

      * ---------------------(1)

      */

      if (p.left != null && p.right != null) {
             Entry<K,V> s = successor(p);

                  //仅仅是Key-Value键值对的替换

             p.key = s.key;

             p.value = s.value;
                         //p指向后继节点,后续都是对后继节点s进行操作

             p = s;

      }

      //先删除后继节点,replacement为替代节点(后继节点孩子),如果P的左子树存在那么就用左子树替代,否则用右子树替代

      Entry<K,V> replacement = (p.left != null ? p.left : p.right);

        /*

      * 删除节点,分为上面提到的三种情况

      * -----------------------(2)

      */

      //如果替代节点不为空

      if (replacement != null) {
             replacement.parent = p.parent;

             /*

             *replacement来替代P节点

             */

             //若P没有父节点,则跟节点直接变成replacement

             if (p.parent == null)

                    root = replacement;

             //如果P为左节点,则用replacement来替代为左节点

             else if (p == p.parent.left)

                    p.parent.left = replacement;

             //如果P为右节点,则用replacement来替代为右节点

             else

                    p.parent.right = replacement;

        //同时将P节点从这棵树中剔除掉

             p.left = p.right = p.parent = null;

        /*

             * 若P为红色直接删除,红黑树保持平衡

             * 但是若P为黑色,则需要调整红黑树使其保持平衡

              */

             if (p.color == BLACK)

             fixAfterDeletion(replacement);

    } else if (p.parent == null) { //p没有父节点,表示为P根节点,直接删除即可

             root = null;

    } else { //P节点不存在子节点,直接删除即可

             if (p.color == BLACK) //如果P节点的颜色为黑色,对红黑树进行调整

                    fixAfterDeletion(p);

        //删除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;

             }

      }

    }

    (2)处是删除该节点过程。它主要分为上面提到的三种情况,它与上面的if…else if… else一一对应 。如下:      

    1、有两个儿子。这种情况比较复杂,但还是比较简单。上面提到过用子节点C替代代替待删除节点D,然后删除子节点C即可。      

    2、没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。      

    3、只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。

    删除完节点后,就要根据情况来对红黑树进行复杂的调整:fixAfterDeletion()。

    private void fixAfterDeletion(Entry<K,V> x) {
      // 删除节点需要一直迭代,直到 x 不是根节点,且 x 的颜色是黑色

      while (x != root && colorOf(x) == BLACK) {
             if (x == leftOf(parentOf(x))) { //若X节点为左节点

                    //获取其兄弟节点

                    Entry<K,V> sib = rightOf(parentOf(x));

               /*

                    * 如果兄弟节点为红色----(情况3.1)

                    * 策略:改变W、P的颜色,然后进行一次左旋转

                    */

                    if (colorOf(sib) == RED) {
                           setColor(sib, BLACK);

                           setColor(parentOf(x), RED);

                           rotateLeft(parentOf(x));

                           sib = rightOf(parentOf(x));

                    }

               /*

                    * 若兄弟节点的两个子节点都为黑色----(情况3.4)

                    * 策略:将兄弟节点变成红色

                    */

                    if (colorOf(leftOf(sib)) == BLACK &&

                           colorOf(rightOf(sib)) == BLACK) {
                           setColor(sib, RED);

                           x = parentOf(x);

                    }

                    else {
                           /*

                           * 如果兄弟节点只有右子树为黑色----(情况3.2)

                           * 策略:将兄弟节点与其左子树进行颜色互换然后进行右转

                           * 这时情况会转变为3.3

                           */

                           if (colorOf(rightOf(sib)) == BLACK) {
                                  setColor(leftOf(sib), BLACK);

                                  setColor(sib, RED);

                                  rotateRight(sib);

                                  sib = rightOf(parentOf(x));

                           }

                           /*

                           *----情况3.3

                           *策略:交换兄弟节点和父节点的颜色,

                           *同时将兄弟节点右子树设置为黑色,最后左旋转

                           */

                           setColor(sib, colorOf(parentOf(x)));

                           setColor(parentOf(x), BLACK);

                           setColor(rightOf(sib), BLACK);

                           rotateLeft(parentOf(x));

                            x = root;

                    }

             }

        /**

             * X节点为右节点与其为做节点处理过程差不多,这里就不在累述了

             */

             else {
                    Entry<K,V> sib = leftOf(parentOf(x));

                if (colorOf(sib) == RED) {
                       setColor(sib, BLACK);

                       setColor(parentOf(x), RED);

                        rotateRight(parentOf(x));

                        sib = leftOf(parentOf(x));

                   }

               if (colorOf(rightOf(sib)) == BLACK &&

        colorOf(leftOf(sib)) == BLACK) {
                         setColor(sib, RED);

                         x = parentOf(x);

                   } else {
                        if (colorOf(leftOf(sib)) == BLACK) {
                        setColor(rightOf(sib), BLACK);

                        setColor(sib, RED);

                        rotateLeft(sib);

                        sib = leftOf(parentOf(x));

                        }

                       setColor(sib, colorOf(parentOf(x)));

                       setColor(parentOf(x), BLACK);

                       setColor(leftOf(sib), BLACK);

                       rotateRight(parentOf(x));

                       x = root;

                  }

               }

           }

        setColor(x, BLACK);

     
    ————————————————
    版权声明:本文为CSDN博主「Wonder ZH」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/qq_42022528/article/details/82932591

  • 相关阅读:
    php中数组排序的基本方法
    mysql的锁--行锁,表锁,乐观锁,悲观锁
    php数组内数据个数统计函数
    《自我介绍》
    结对编项目作业
    《结对-英文词频检测程序-需求分析》
    对软件工程课程的期望
    挑战图像处理100问(4)——Otsu
    挑战图像处理100问(3)——二值化
    挑战图像处理100问(2)——灰度化
  • 原文地址:https://www.cnblogs.com/shoshana-kong/p/14596049.html
Copyright © 2011-2022 走看看