TreeMap源码解析

TreeMap是基于红黑树结构实现的一种Map,要分析TreeMap的实现首先就要对红黑树有所了解。

1、二叉查找树、红黑树介绍

什么是二叉查找树呢?它有什么特点呢?

(1)若左子树不空,则左子树所有节点的值均小于它的根节点的值;

(2)若右子树不空,则右子树所有节点的值均小于它的根节点的值;

(3)左、右子树也分别为二叉查找树;

(4)没有键值相等的节点。

二叉查找树

按照二叉查找树存储的数据,对元素的搜索效率是非常高的,比如上图中如果要查找值为48的节点,只需要遍历4个节点就能完成。理论上,一棵平衡二叉查找树的任一节点平均查找效率为树的高度h,即O(lgn)。但是如果二叉查找树失去平衡(元素全在一侧),搜索效率就会退化为O(n),因此二叉查找树的平衡是搜索效率的关键所在。而红黑树就是靠红黑规则来维持二叉查找树的平衡性。

失去平衡的二叉查找树

(一棵失去平衡的二叉查找树)

简单用代码描述上面的二叉查找树,试试看:

package com.test;

/**
 * Created by universe on 9/15/16.
 */
public class BinaryTree {
    // 二叉树的根节点
    public TreeNode rootNode;
    // 记录搜索深度
    public int count;

    /**
     * 利用传入一个数组来建立二叉树
     */
    public BinaryTree(int[] data) {
        for(int i = 0; i < data.length; i++) {
            addNodeToTree(data[i]);
        }
    }

    /**
     * 将指定的值加入到二叉树中适当的节点
     */
    private void addNodeToTree(int value) {
        TreeNode currentNode = rootNode;
        // 建立树根
        if(rootNode == null) {
            rootNode = new TreeNode(value);
            return;
        }
        // 建立二叉树
        while(true) {
            // 新增的value比节点的value小,则在左子树
            if(value < currentNode.value) {
                if(currentNode.leftNode == null) {
                    currentNode.leftNode = new TreeNode(value);
                    return;
                } else {
                    currentNode = currentNode.leftNode;
                }
            } else { // 新增的value比节点的value大,在右子树
                if(currentNode.rightNode == null) {
                    currentNode.rightNode = new TreeNode(value);
                    return;
                } else {
                    currentNode = currentNode.rightNode;
                }
            }
        }
    }

    /**
     * 中序遍历(左子树-根-右子树)
     */
    public void inOrder(TreeNode node) {
        if(node != null) {
            inOrder(node.leftNode);
            System.out.print("[" + node.value + "]");
            inOrder(node.rightNode);
        }
    }

    /**
     * 前序遍历(根-左子树-右子树)
     */
    public void preOrder(TreeNode node) {
        if(node != null) {
            System.out.print("[" + node.value + "]");
            preOrder(node.leftNode);
            preOrder(node.rightNode);
        }
    }

    /**
     * 后序遍历(左子树-右子树-根)
     */
    public void postOrder(TreeNode node) {
        if(node != null) {
            postOrder(node.leftNode);
            postOrder(node.rightNode);
            System.out.print("[" + node.value + "]");
        }
    }

    /**
     * 从二叉树中查找指定的value
     */
    public boolean findTree(TreeNode node, int value) {
        if(node == null) {
            System.out.println("共搜索" + count + "次");
            return false;
        } else if(node.value == value) {
            System.out.println("共搜索" + count + "次");
            return true;
        } else if(value < node.value) {
            count++;
            return findTree(node.leftNode, value);
        } else {
            count++;
            return findTree(node.rightNode, value);
        }
    }

    /**
     * 利用中序遍历进行排序
     */
    public void sort() {
        this.inOrder(rootNode);
    }

    class TreeNode {
        int value;
        TreeNode leftNode;
        TreeNode rightNode;

        public TreeNode(int value) {
            this.value = value;
            this.leftNode = null;
            this.rightNode = null;
        }
    }

    public static void main(String[] args) {
        int[] content = {50, 35, 27, 45, 40, 48, 78, 56, 90};

        BinaryTree tree = new BinaryTree(content);
        System.out.println("前序遍历");
        tree.preOrder(tree.rootNode);
        System.out.println("\n\n中序遍历");
        tree.inOrder(tree.rootNode);
        System.out.println("\n\n后序遍历");
        tree.postOrder(tree.rootNode);

        System.out.println("\n\n\n开始搜索");
        boolean isFind = tree.findTree(tree.rootNode, 48);
        System.out.println("是否搜索到" + 48 + ":" + isFind);

        System.out.println("\n\n\n进行排序");
        tree.sort();
    }
}

看一下运行结果:

前序遍历
[50][35][27][45][40][48][78][56][90]

中序遍历
[27][35][40][45][48][50][56][78][90]

后序遍历
[27][40][48][45][35][56][90][78][50]


开始搜索
共搜索3次
是否搜索到48:true



进行排序
[27][35][40][45][48][50][56][78][90]

通过上面的代码是否对二叉查询树有所认识,那么,红黑树的红黑规则到底是什么?

(1)节点是红色或黑色。

(2)根节点是黑色。

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

(4)每个红色节点的两个子节点都是黑色的。(从每个叶节点到根的所有路径上不能有两个连续的红色节点)

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

红黑树

上面的规则前4条都好理解,第5条规则到底是什么情况,下面简单解释下,比如图中红8到1左边的叶子节点的路径包含两个黑色节点,到6下面的叶子节点的路径也包含两个黑色节点,到11下面的叶子节点的路径也包含两个黑色节点。

但是在添加或删除节点后,红黑树就发生了变化,可能不能满足上面的5个特性,为了保持红黑树的5个特性,我们需要做3个动作:左旋、右旋、着色。

下面就来看看什么是红黑树的左旋和右旋:

左旋

对x进行左旋,意味着“将x变为一个左节点”

右旋

对y进行右旋,意味着“将y变为一个右节点”

如果还是不明白,下面找两张左旋和右旋的动态图:

左旋动图右旋动图

OK,对红黑二叉树的概念有所了解后,我们来看下红黑树的两个主要逻辑添加和删除,看看TreeMap是怎么实现的。


2、TreeMap的底层实现

首先来看一下TreeMap的定义:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

可以看到,TreeMap继承了AbstractMap的抽象类,并实现了NavigableMap、Cloneable、Serializable接口。NavigableMap接口扩展了SortedMap,主要是提供了给定搜索目标返回最接近匹配项的导航方法。

下面我们再看下TreeMap的底层存储相关定义:

    // 比较器
    private final Comparator<? super K> comparator;
    // 红黑树根节点
    private transient Entry<K,V> root;
    // 集合元素数量
    private transient int size = 0;
    // "fail-fast"集合修改记录
    private transient int modCount = 0;

这里的Comparator是一个比较器,一个类实现了Comparator接口并重写其compare方法,就能进行比较大小。Entry是树的节点类,我们来看一下Entry的定义:

    private static final boolean RED   = false;
    private static final boolean BLACK = true;

    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和父节点构造一个Entry,默认为黑色
         */
        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }
        
        public V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
        }

        public int hashCode() {
            int keyHash = (key==null ? 0 : key.hashCode());
            int valueHash = (value==null ? 0 : value.hashCode());
            return keyHash ^ valueHash;
        }

        public String toString() {
            return key + "=" + value;
        }
    }

Entry类理解起来比较简单,主要是定义了树的孩子和父亲节点引用,和红黑色属性,并对equals和hashCode进行重写,以利于比较是否相等。


3、TreeMap的构造方法

    /**
     * 默认构造方法,comparator为空,代表使用key的自然顺序来维持TreeMap的顺序
     * 这里要求key必须实现Comparable接口
     */
    public TreeMap() {
        comparator = null;
    }

    /**
     * 用指定的比较器构造一个TreeMap
     */
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

    /**
     * 构造一个指定map的TreeMap,同样比较器comparator为空,使用key的自然顺序排序
     */
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }

    /**
     * 构造一个指定SortedMap的TreeMap,根据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) {
        }
    }

从构造方法可以看出,要创建一棵红黑树实现的TreeMap必须要有一个用于比较大小的比较器,因为只有能够比较大小才能够实现红黑树的左孩子<树根<右孩子的特点。


4、红黑树的添加原理及TreeMap的put实现

将一个节点添加到红黑树中,通常需要下面几个步骤:

(1)将红黑树当成一棵二叉查找树,将节点插入。

这一步比较简单,就像开始我们自己写的二叉查找树的操作一样,至于为什么可以这样插入,是因为红黑树本身就是一个二叉查找树。

(2)将新插入的节点设置为红色。

有没有疑问,为什么新插入的节点一定要是红色的,因为新插入的节点为红色,不会违背红黑规则第(5)条,少违背一条规则就少处理一种情况。

(3)通过旋转和着色,使它恢复平衡,重新变成一棵符合规则的红黑树。

要想知道怎么进行左旋和右旋,首先就要知道为什么要进行左旋和右旋。

我们来对比一下红黑树的规则和新插入节点后的情况,看下新插入节点会违背哪些规则。

(1)节点是红色或黑色。

这一点肯定是不会被违背的。

(2)根节点是黑色的。

这一点也不会违背,如果是根节点,只需将根节点插入即可,因为默认是黑色。

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

这一点也不会违背,我们插入的是非空节点,不会影响空节点。

(4)每个红色节点的两个子节点都是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

这一点是有可能违背的,我们将新插入的节点都设置成红色,如果其父节点也是红色的话,那就产生冲突了。

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

这一点不会违背,因为我们将新插入的节点都设置成红色。


了解了红黑树的左旋和右旋操作,以及新插入节点主要是可能会违背红黑树的规则(4)后,我们来分析下,添加新节点的过程有哪几种情况:

(1)新插入节点为根节点。这种情况直接将新插入节点设置为根节点即可,无须进行后续的旋转和着色处理。

(2)新插入节点的父节点是黑色。这种情况直接将新节点插入即可,不会违背规则(4)。

(3)新插入节点的父节点为红色。这种情况下会违背规则(4),而这种情况下又分为以下几种情况,下面进行图解:

a、新插入节点N的父节点P和叔叔节点U都是红色。方法是:将祖父节点G设置为红色,父节点P和叔叔节点U设置为黑色,这时候看似平衡了,但是,如果祖父节点G的父节点也是红色,这时候又违背了规则(4),怎么办?方法是:将GPUN这一组看作一个新的节点,按照前面的方案递归;但是递归到最后根节点为红色怎么办(违反了规则(2))?方法是,直接将根节点设置为黑色(两个连续的黑色是没有问题的)。

插入:父节点和叔节点都是红色

b、新插入节点N的父节点P是红色,叔叔节点U是黑色或者缺少,且新节点N是P的右孩子。方法是:左旋父节点P。左旋后N和P角色互换,但是P和N还是连续的两个红色节点,还没有平衡,怎么办?看第三种情况。

插入:父节点红色,叔节点黑色,新节点是父节点的右孩子

c、新插入节点N的父节点P是红色,叔叔节点U是黑色或者缺少,且新节点N是P的左孩子。方法是:右旋祖父节点G,然后P设置为黑色,G设置为红色,达到平衡。此时父节点P是黑色,所以不用担心P的父节点是红色。

插入:父节点红色,叔叔节点黑色,新节点是父节点的左孩子

当然上面说的三种情况都是基于一个前提:新插入节点N的父节点P是祖父节点G的左孩子,如果P是G的右孩子又是什么情况呢?其实情况和上面是相似的,只需要调整一下左旋还是右旋。

上面分析了这么多,到底TreeMap是怎么实现的?我们看一下:

    public V put(K key, V value) {
        // 根节点
        Entry<K,V> t = root;
        // 如果节点为空,就直接创建一个根节点,返回
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        // 记录比较结果
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        // 当前使用的比较器
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            // do while循环,查找key要插入的位置(也就是新节点的父节点是谁)
            do {
                // 记录上次循环的节点t
                parent = t;
                // 比较当前节点的key和新插入的key的大小
                cmp = cpr.compare(key, t.key);
                // 新插入的key小的话,那以当前节点的左孩子节点作为新的比较节点
                if (cmp < 0)
                    t = t.left;
                // 新插入的key大的话,那以当前节点的右孩子节点作为新的比较节点
                else if (cmp > 0)
                    t = t.right;
                // 如果当前节点的key和新插入的key相等的话,则覆盖map的value,返回
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            // 如果比较器为空,则使用key作为比较器进行比较
            // 这里要求key不能为空,并且必须实现Comparable接口
            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);
        }
        // 找到新节点的父节点之后,创建节点对象
        Entry<K,V> e = new Entry<>(key, value, parent);
        // 如果新节点的key值小于父节点的key值,则插在父节点的左侧
        if (cmp < 0)
            parent.left = e;
        // 如果新节点的key值大于父节点的key值,则插在父节点的右侧
        else
            parent.right = e;
        // 插入新的节点后,为了保持红黑树的平衡,对红黑树进行调整
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

    /**
     * 新增节点后对红黑树的调整方法
     */
    private void fixAfterInsertion(Entry<K,V> x) {
        // 将新插入节点的颜色设置为红色
        x.color = RED;

        // while循环,保证新插入节点x不是根节点或者新插入节点x的父节点不是红色(这两种情况不需要调整)
        while (x != null && x != root && x.parent.color == RED) {
            // 如果新插入节点x的父节点是祖父节点的左孩子
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                // 取得新插入节点x的叔叔节点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                // 如果新插入x的叔叔节点是红色---情况a
                if (colorOf(y) == RED) {
                    // 将x的父节点设置为黑色
                    setColor(parentOf(x), BLACK);
                    // 将x的叔叔节点设置为黑色
                    setColor(y, BLACK);
                    // 将x的祖父节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 将x指向祖父节点,如果x的祖父节点的父节点是红色,按照上面的步骤继续循环
                    x = parentOf(parentOf(x));
                } else {
                    // 如果新插入x的叔叔节点是黑色或者缺少,且x节点是父节点的右孩子---情况b
                    if (x == rightOf(parentOf(x))) {
                        // 将x指向父节点
                        x = parentOf(x);
                        // 左旋父节点
                        rotateLeft(x);
                    }
                    // 如果新插入的x的叔叔节点是黑色或者缺少,且x节点是父节点的左孩子---情况c
                    // 将x的父节点设置为黑色
                    setColor(parentOf(x), BLACK);
                    // 将x的祖父节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 右旋x的祖父节点
                    rotateRight(parentOf(parentOf(x)));
                }
            } else { // 如果新插入节点x的父节点是祖父节点的右孩子,下面的步骤和上面类似,只不过左旋与右旋有所变化
                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;
    }

    /**
     * 对红黑树的节点(p)进行左旋
     * 
     */
    private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            // 取得要旋转节点的右孩子
            Entry<K,V> r = p.right;
            // p 和 r的左孩子 的相互指向……
            // 将r的左孩子设为p的右孩子
            p.right = r.left;
            // 如果r的左孩子非空,将p设为r的左孩子的父亲
            if (r.left != null)
                r.left.parent = p;
            // p的父亲 和 r 的相互指向……
            // 将p的父亲设为r的父亲
            r.parent = p.parent;
            // 如果p的父亲是空节点,则将r设置为根节点
            if (p.parent == null)
                root = r;
            // 如果p是父节点的左孩子,则将r设置为p的父节点的左孩子
            else if (p.parent.left == p)
                p.parent.left = r;
            // 如果p是父节点的右孩子,则将r设置为p的父节点的右孩子
            else
                p.parent.right = r;
            // r 和 p 的相互指向……
            // 将p设为r的左孩子
            r.left = p;
            // 将r设成p的父亲
            p.parent = r;
        }
    }

    /**
     * 对红黑树的节点(p)进行右旋
     */
    private void rotateRight(Entry<K,V> p) {
        if (p != null) {
            // 获取旋转节点p的左子节点
            Entry<K,V> l = p.left;
            // p 和 l的右孩子 相互指向……
            // 将l的右孩子设为p的左孩子
            p.left = l.right;
            // 如果l的右孩子不为空,将右孩子的父亲设为p
            if (l.right != null) l.right.parent = p;
            // p的父节点 和 l 相互指向……
            // 将p的父节点设为l的父节点
            l.parent = p.parent;
            // 如果p的父节点为空,则将l设为根节点
            if (p.parent == null)
                root = l;
            // 如果p是父节点的右孩子,则将l设为p父节点的右孩子
            else if (p.parent.right == p)
                p.parent.right = l;
            // 如果p是父节点的左孩子,则将l设为p父节点的左孩子
            else p.parent.left = l;
            // l 和 p 相互指向……
            // 将p设置为l的右孩子
            l.right = p;
            // 将l设置为p的父节点
            p.parent = l;
        }
    }

如果没有理解的话,可以看一位大神的视频: 点击打开视频链接


5、红黑树的删除原理及TreeMap的remove实现

相比添加,红黑树的删除显得更加复杂了。看一下红黑树的删除需要哪几个步骤:

(1)将红黑树当成一棵二叉查找树,将节点删除。

(2)通过旋转和着色,使它恢复平衡,重新变成一棵符合规则的红黑树。

删除节点的关键是:

(1)如果删除的是红色节点,不会违背红黑树的规则。

(2)如果删除的是黑色节点,那么这个路径上就少了一个黑色节点,则违背了红黑树的规则(5)。

来看一下红黑树删除节点会有哪几种情况:

(1)被删除的节点没有孩子节点,即叶子节点,可直接删除。

(2)被删除的节点只有一个孩子节点,那么直接删除该节点,然后用它的孩子节点顶替它的位置。

(3)被删除的节点有两个孩子节点。这种情况下二叉树的删除有一个技巧,就是查找到要删除的节点x,接着我们找到它左子树的最大元素M,或者它右子树的最小元素M,交换X和M的值,然后删除节点M。此时M就最多只有一个子节点N(若是左子树则没有右子节点,若是右子树则没有左子节点),若M没有孩子则进入(1)的情况,否则进入(2)的情况。

删除技巧

如上,我们假定节点X是要删除的节点,而节点M是找到X右子树的最小元素,所以节点M是X的替代节点,也就是说M是真正要删除的节点。上面我们分析了此时的M只会有一个子节点N,当删除节点M后,N将替代M作为M节点的父节点的子节点。删除的节点M是黑色(删除红色不影响上面分析),此时如果N是红色,只需将N设置为黑色,就会重新达到平衡,不会出现该路经上少了一个黑色节点的情况;但是如果N是黑色,情况就比较复杂了,需要对红黑树进行调整,而这种情况又分为以下几种,下面进行图解:

a、N的兄弟节点B是红色。方法是:交换P和B的颜色,左旋父节点P。此时并未完成平衡,左子树仍然少了一个黑色节点,进入情况c。(B为红色,P必然为黑色)。

删除:N的兄弟节点B是红色

b、N的父节点P是黑色,且兄弟节点B和它的两个孩子节点也都是黑色。方法是:将N的兄弟节点B改为红色。这样从P出发到叶子节点的路径都包含了相同的黑色节点,但是,对于节点P这个子树,P的父节点G到P的叶子节点路径上的黑色节点就少了一个,此时需要将P整体看作一个节点,继续调整。

删除:N的父节点P是黑色,兄弟节点B和它的两个孩子也是黑色

c、N的父节点P为红色,兄弟节点B和它的两个孩子节点也都是黑色。此时只需要交换P和B的颜色,将P改为黑色,B改为红色,则可到达平衡。这相当于既然节点N路径少了一个黑色节点,那么B路径也少一个黑色节点,这两个路径达到平衡,为了防止P路径少了一个黑色节点,将P节点置黑,则达到最终平衡。

删除:父亲节点P为红色,兄弟节点B和它的两个孩子是黑色

d、N的兄弟节点B是黑色,B的左孩子节点BL是红色,B的右孩子节点BR是黑色,P为任一颜色。方法是:交换B和BL的颜色,右旋节点B。此时N子树路径并没有增加黑色节点,也就是说没有达到平衡,此时进入下一种情况e。

删除:兄弟节点B是黑色,B的左孩子红色,右孩子黑色

e、N的兄弟节点B是黑色,B的右孩子节点BR是红色,B的左孩子节点BL任意颜色,P任意颜色。方法是:BR变为黑色,P变为黑色,B变为P的颜色;左旋节点B。首先给N路径增加了一个黑色节点P,P原位置上的颜色不变;BR路径上少了一个黑色节点,于是将BR改为黑色,最终达到平衡。

兄弟节点B黑色,BR红色,BL颜色任意,P颜色任意

上面对红黑树删除的原理和删除过程中遇到的情况进行了说明,我们得到的结论是红黑树的删除遇到的主要问题就是被删除路径上的黑色节点减少,于是需要进行一系列旋转和着色,当然上面的情况是基于M是X右子树的最小元素,而M如果是X左子树的最大元素和上面的情况是相似的,我们具体看一下TreeMap的代码是怎么实现的:

    public V remove(Object key) {
        // 根据key查找到对应的节点对象
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        // 记录key对应的value,供返回时用
        V oldValue = p.value;
        // 删除节点
        deleteEntry(p);
        return oldValue;
    }

    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--; // map容器的元素个数减1

        // If strictly internal, copy successor's element to p and then make p
        // point to successor.
        // 如果被删除的节点p的左孩子和右孩子都不为空,则查找其替代节点-----这里表示要删除的节点有两个孩子
        if (p.left != null && p.right != null) {
            // 找出p的替代节点
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            // 将p指向替代节点,从此之后的p不再是原先要删除的节点p,而是替代者p(就是图中讲解的M)
            p = s;
        } // p has 2 children

        // Start fixup at replacement node, if it exists.
        // replacement为替代节点p的继承者(就是图解里的N,p的左孩子存在则用p的左孩子替代,否则用p的右孩子)
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

        if (replacement != null) { // 这里表示要删除的节点有一个孩子(2)
            // Link replacement to parent
            // 将p的父节点拷贝给替代节点
            replacement.parent = p.parent;
            // 如果替代节点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;

            // Null out links so they are OK to use by fixAfterDeletion.
            // 将替代节点p的left、right、parent的指针都指向空,即解除前后引用关系(相当于将p从树中删除,使得GC可以回收)
            p.left = p.right = p.parent = null;

            // Fix replacement
            // 如果替代节点p的颜色是黑色,则需要调整红黑树以保持其平衡
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.
            // 如果要替代的节点p没有父节点,代表p为根节点,直接删除即可
            root = null;
        } else { //  No children. Use self as phantom replacement and unlink.
            /* 这里之所以先fix再删除,是因为先删除的话,null将不能引用其父节点,从而无法获知兄弟节点等信息 */
            // 判断进入这里,说明替代节点p没有孩子-----表示没有孩子可以直接删除(1)
            // 如果p的颜色是黑色,则调整红黑树
            if (p.color == BLACK)
                fixAfterDeletion(p);

            // 下面删除替代节点p
            if (p.parent != null) {
                // if-else解除p的父节点对p的引用
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                // 解除p对p的父节点的引用
                p.parent = null;
            }
        }
    }

    /**
     * 查找要删除节点的替代节点
     */
    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;
            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;
        }
    }

    private void fixAfterDeletion(Entry<K,V> x) {
        // while循环,保证要删除的节点x不是根节点,并且是黑色(根节点和红色不需要调整)
        while (x != root && colorOf(x) == BLACK) {
            // 如果要删除节点x是其父节点的左孩子
            if (x == leftOf(parentOf(x))) {
                // 取出要删除节点x的兄弟节点
                Entry<K,V> sib = rightOf(parentOf(x));

                // 如果删除节点x的兄弟节点是红色-----a
                if (colorOf(sib) == RED) {
                    // 将x的兄弟节点颜色设置为黑色
                    setColor(sib, BLACK);
                    // 将x的父节点设置为红色
                    setColor(parentOf(x), RED);
                    // 左旋x的父节点
                    rotateLeft(parentOf(x));
                    // 将sib重新指向旋转后x的兄弟节点,进入else的步骤-----c
                    sib = rightOf(parentOf(x));
                }

                // 如果x的兄弟节点的两个孩子都是黑色-----c
                if (colorOf(leftOf(sib))  == BLACK &&
                        colorOf(rightOf(sib)) == BLACK) {
                    // 将兄弟节点的颜色设置为红色
                    setColor(sib, RED);
                    // 将x的父节点指向x,如果x的父节点是黑色的,需要将x的父节点整体看作一个节点继续调整-----b
                    x = parentOf(x);
                } else {
                    // 如果x的兄弟节点右孩子是黑色的,左孩子是红色的----d
                    if (colorOf(rightOf(sib)) == BLACK) {
                        // 将x的兄弟节点的左孩子设置为黑色
                        setColor(leftOf(sib), BLACK);
                        // 将x的兄弟节点设置为红色
                        setColor(sib, RED);
                        // 右旋x的兄弟节点
                        rotateRight(sib);
                        // 将sib指向旋转后x的兄弟节点,进入步骤e
                        sib = rightOf(parentOf(x));
                    }
                    // 如果x的兄弟节点右孩子是红色-----e
                    // 将兄弟节点的设置为父节点的颜色
                    setColor(sib, colorOf(parentOf(x)));
                    // 将父节点设置为黑色
                    setColor(parentOf(x), BLACK);
                    // 将兄弟节点的右孩子设置为黑色
                    setColor(rightOf(sib), BLACK);
                    // 左旋x的父节点
                    rotateLeft(parentOf(x));
                    // 达到平衡,将x指向root,退出循环
                    x = root;
                }
            } else { // symmetric // 如果要删除节点x是其父亲节点的右孩子,和上面情况一样,这里不在细讲
                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);
    }

对照图解看代码,慢慢理解。




原文地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值