【算法】红黑树删除数据(寻找继承人)(四)

我的前三篇文章讲红黑树的插入介绍完毕,并且也解释了TreeMap的put的源码,接下来我们一起看下remove,红黑树如何删除节点?

该系列已经全部更完,有5篇文章:

【算法】红黑树(二叉树)概念与查询(一):https://blog.csdn.net/lsr40/article/details/85230703

【算法】红黑树插入数据(变色,左旋、右旋)(二):https://blog.csdn.net/lsr40/article/details/85245027

【算法】红黑树插入数据的情况与实现(三):https://blog.csdn.net/lsr40/article/details/85266069

【算法】红黑树删除数据(寻找继承人)(四):https://blog.csdn.net/lsr40/article/details/85322371

【算法】红黑树删除数据(最后一步,平衡红黑树)(五):https://blog.csdn.net/lsr40/article/details/86711889

插入节点分为两步,第一步是通过key的大小,来判断插入数据应该加入到哪个位置;第二步就是平衡红黑树

删除节点也分为两步,第一步的通过key找到要删除的节点,然后寻找该节点的继承人,然后删除继承人,第二步就是平衡红黑树。

本篇文章,我们就来讲讲继承人的故事!

今天,我们从代码入手来学习删除!

最开始是调用remove方法,传入要删除的key

    //删除方法,remove
    public V remove(Object key) {
        //通过getEntry方法来寻找要删除的key的节点是否存在,存在就返回该节点,不存在就返回null
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;
        //将该节点的value作为返回值return出去
        V oldValue = p.value;
        //进入到删除节点的方法中
        deleteEntry(p);
        return oldValue;
    }

然后进入到deleteEntry方法中。

1、会遇到的情况:

总共会遇到3种情况!(以下均用p来代表要删除的那个节点)

情况1:p节点一个子节点也没有

情况2:p节点有且仅有一个子节点

情况3:p节点两个子节点都有

2、对于不同情况的处理

情况1:直接删除该节点

情况2:直接将该节点的唯一的子节点接到该节点的父节点上 ,删除该节点

情况3:找一个继承节点(因为寻找继承节点的方式导致了继承节点一定是情况1或者情况2),把继承节点的值写入p节点,根据情况1或者情况2来处理继承节点。

对于情况1和情况2自然不用多说,很简单。比较麻烦的就是找继承人这个过程到底是怎么找的,为什么要这么找,这么找是否合理?

我这里想先贴上java写的代码!(successor这个方法是TreeMap用来找继承节点的方法)

 private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // If strictly internal, copy successor's element to p and then make p
        // point to successor.
        //判断要删除的节点,有两个孩子!!!
        if (p.left != null && p.right != null) {
            //通过successor继承节点
            Entry<K,V> s = successor(p);
            //将继承节点的值,放在原来要删除的那个节点的位置
            p.key = s.key;
            p.value = s.value;
            //完全用继承节点替换原来要删除的节点
            p = s;
        } // p has 2 children

        //....这里的代码先省略,先看怎么选继承人
    }    

    /**
     * Returns the successor of the specified Entry, or null if no such.
     */
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        //要删除的节点为t,该节点为空,直接返回null
        if (t == null)
            return null;
            //该节点右子节点存在
        else if (t.right != null) {
            Entry<K,V> p = t.right;
            //通过循环,拿到t节点的右孩子p的最左边的节点
            while (p.left != null)
                p = p.left;
            return p;
            //否则该节点的右孩子节点不存在
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            //通过循环,拿到t节点的第一个向右的父亲
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

先不管其他的,单纯看这段这段代码,它做了是否有右孩子的判断。

如果有,返回的后继节点就如下图:

如果没有,返回的后继节点如下图:

但是这里我又要明确的声明了,我们确实看到deleteEntry方法中,判断要删除的节点p,必须有两个孩子才去寻找继承人,但是successor这个方法却又判断p节点有右孩子和没有右孩子的情况???原因是successor这个方法并不是只有在删除的时候用到!在containsValue的方法中也使用到了,而且你认真看下,successor这个方法,是不是寻找比p节点大的数中最小的那个数!那不就是中序遍历吗??所以再次认真看下containsValue方法,该方法就是使用中序遍历红黑树上的每一个节点的value,判断是否包含。

我想这里应该又会有同学有疑惑,啊,为什么containsValue要使用中序遍历红黑树,明明按照红黑树的遍历方法会更快,为什么要用这种最差的方式遍历?很简单,我们的红黑树是按照key排序的,但是这里是判断value是否包含,没办法用上key的大小来判断value在哪里,所以只能一个个遍历!当然如果是containsKey,肯定就是用红黑树的遍历方式来查询(封装成getEntry方法)

    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }   
 
    public boolean containsValue(Object value) {
        for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
            if (valEquals(value, e.value))
                return true;
        return false;
    }

//这个方法就是获取红黑树最小的key,也就是中序遍历的第一个值
    final Entry<K,V> getFirstEntry() {
        Entry<K,V> p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }

所以,在删除的时候,寻找继承人,我们只考虑一种情况:

找到的后继节点是右孩子中最左边(小)的那个元素!

可以从两种方向来理解:

1、按照中序的思想,我找的那个值,就是把红黑树上所有值按照从小到大排序之后,p点的后一个值

2、我需要拿一个值来放在当前的p节点上,但是这个继承节点的key值,替换上来之后必须不破坏红黑树的key的大小关系,因此找了比p大的数中(p的右子节点下的数,全部都比p大)最小的一个来替换p

注:其实大家认真想一下,我找p的左子节点的最右边(最大)的值来代替p,不是也没有问题吗?就是从小到大排序后p的前一个值。

我的想法是:如果我是java代码的编写人,successor需要是一个通用的方法,所以我会选择写一个取p点后一个值的方法,因为方法刚好是中序遍历树结构,那么这个方法就也可以用到其他地方,避免我又单独为删除节点去写另一个方法!!(当然这只是我的个人见解,但我觉得是很接近真相的想法)

 

 private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // If strictly internal, copy successor's element to p and then make p
        // point to successor.
        //判断要删除的节点,有两个孩子!!!
        if (p.left != null && p.right != null) {
            //通过successor继承节点
            Entry<K,V> s = successor(p);
            //将继承节点的值,放在原来要删除的那个节点的位置
            p.key = s.key;
            p.value = s.value;
            //完全用继承节点替换原来要删除的节点
            p = s;
        } // p has 2 children

        // Start fixup at replacement node, if it exists.
        //要删除的节点p有可能没有子节点,或者就只有一个子节点,
        //所以replacement要么是p的左子节点,要么是p的右子节点(但是右节点也有可能为空)
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        //所以这里再次判断replacement是否为空,如果为空证明要删除的节点没有子节点
        if (replacement != null) {
            // Link replacement to parent
            //p节点的父节点变成replacement的父节点
            replacement.parent = p.parent
            //如果p没有父节点,那么p就是根节点,所以replacement就变成根节点
            if (p.parent == null)
                root = replacement;
                //设置完replacement的父节点,还要设置下父节点的左右孩子指向replacement
                //如果p是他老爸的左孩子
            else if (p == p.parent.left)
                //那么replacement也就变成左孩子
                p.parent.left  = replacement;
            else
                //否则replacement就是右孩子
                p.parent.right = replacement;

            // Null out links so they are OK to use by fixAfterDeletion.
            //将要删除的p节点全部设置为空,该对象会被JVM回收
            p.left = p.right = p.parent = null;

            // Fix replacement
            // 如果删除的节点颜色是黑色,就会破坏规则5,那么就需要重新平衡
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
            //p节点没有父节点,证明p点是根节点
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
            //否则replacement为空,证明p没有子节点
        } else { //  No children. Use self as phantom replacement and unlink.
            //如果是黑色,就需要重新平衡
            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;
            }
        }
    }

所以,我做一下总结:

先判断要删除的p点:

(1)、如果有两个孩子,如果有两个孩子,就找继承人,然后将继承人的值写到要删除的那个p的位置上,然后做如下两步判断

(2)、如果没有孩子,直接删除该节点,p.parent指向p节点的引用也去掉

(3)、如果有一个孩子,删除该节点,判断孩子是p的左孩子还是右孩子,把这个孩子对应的接到p的父节点上

 

所以,下一篇,应该就是红黑树的最后一篇了,我会说明删除节点之后,如何变换!

如果大家能把我的文章一篇一篇看下来(这需要一定的耐心),相信你也会对红黑树有一定的见解!

好了,本人菜鸡一个,如果有说错的地方,请大家批评指出,尽力做到我的说明没有歧义,没有错误!!有任何问题欢迎留言讨论~

猜你喜欢

转载自blog.csdn.net/lsr40/article/details/85322371
今日推荐