TreeMap及其红黑树算法个人解析

本文主要探讨了Java中TreeMap的实现原理,它利用红黑树进行排序。文章详细解释了红黑树的基本概念,并分析了TreeMap的put方法,包括插入操作如何维护红黑树的性质。通过对一系列节点插入的示例,展示了不同情况下红黑树的调整过程,阐述了为何TreeMap选择红黑树作为数据结构的原因,强调了其在查找、插入和删除操作上的高效性。
摘要由CSDN通过智能技术生成

  工作中,有时会遇到需要对map进行排序的情况,java中常用的两种带排序的map,一种是LinkedHashMap,而另外种就是TreeMap了,LinkedHashMap是基于散列进行存储的,这里不过多讨论了。而TreeMap则是使用红黑树来实现排序的,这里我们重点研究下TreeMap的实现。

红黑树的基本概念
百度百科红黑树
  
TreeMap基本数据结构
为了实现红黑树,TreeMap定义的静态内部类entry如下

    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        //左孩子节点,存放比当前节点小的节点
        Entry<K,V> left = null;
        //右孩子节点,存放比当前节点大的节点
        Entry<K,V> right = null;
       /**
        *父节点,理论上只需要左右孩子指针,就可以生成一棵树,但是红黑树进行排序时,
        *涉及到节点换位,所以引入父指针来实现双向指针,方便换位。
        */
        Entry<K,V> parent;
        //当前节点颜色
        boolean color = BLACK;

        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }
 }

而TreeMap中的一些基本属性如下

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    /**
     * 排序器,方便使用者自定义treeMap排序规则
     * 如果为null,则使用自然排序
     */
    private final Comparator<? super K> comparator;
    //树的根节点
    private transient Entry<K,V> root = null;
    //TreeMao中数据的个数
    private transient int size = 0;
    //对TreeMap更新的次数
    private transient int modCount = 0;
}

TreeMap put方法
描述完TreeMap的基本结构,现在步入正题,解析put方法吧,
TreeMap的put方法代码如下

    public V put(K key, V value) {
        Entry<K,V> t = root;
        //如果根节点为null,这当前节点直接设置成根节点,然后返回。
        if (t == null) {
            compare(key, key); 
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        //cmp即为2个节点key比较后的大小返回值,<0为小,=0为相等,>0为大
        int cmp;
        //新插入节点的父节点
        Entry<K,V> parent;
        // 获取排序器
        Comparator<? super K> cpr = comparator;
        //排序器不为null情况下,进行设置节点
        if (cpr != null) {
           /**
            *此循环用来查找节点适合插入的位置,从根节点进行遍历,
            *如果插入新节点的key小于遍历的当前节点的key,则向左子节点
            *继续进行遍历,如果大于则向右子节点进行遍历,等于则用新插入
            *的节点替换当前遍历的节点,然后返回,如遍历到叶子节点(注:
            *这里的叶子节点指的是有数据,但是子节点都为null的节点,而
            *TreeMap的红黑树算法中,默认红黑树叶子节点都为null,以满
            *足红黑树性质,同时方便运算),TreeMap中不存在插入key,
            *则循环结束。定位出新插入节点的父节点
            */
            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();
            //排序器为null情况下,采用key的自然排序
            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);
        }
        //如果put运行到这里,则代表树中不存在与插入key相同key的节点,
        //所以需要新增节点,新增节点都是作为树的叶子节点添加
        Entry<K,V> e = new Entry<>(key, value, parent);
        //判断新增节点是父节点的左节点还是右节点
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //核心算法,用来对二叉树进行平衡,也即是红黑树算法。
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

TreeMap 红黑树插入算法
这里我们先讲完fixAfterInsertion方法是怎么实现新增节点,保证红黑树性质不变的,然后再描述为何不使用普通的二叉树树或平衡二叉树(avl树)。fixAfterInsertion代码如下,虽然代码不多,但是理解起来颇为艰难。
红黑树的性质还是简单在这里提一下吧,方便对照代码
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

/**
*注意此算法为CLR算法(按根左右的方式遍历)
*注意此算法中,每个树节点都有左右两个子节点,尽管其中的一个或两个可能是空叶子
*请注意分析put方法,添加的x节点一定位于树的最后一层
*/
private void fixAfterInsertion(Entry<K,V> x) {
        //每个新增非null节点先默认为红
        x.color = RED;
        /**
        *此循环判断遍历节点x(初始为新增节点)的位置及颜色是否合理,不合理则调整,调整完后,为了保证调整后的树也满足红黑树性质,需再向上遍历将x赋值为x.parent或x.parent.parant,再次遍历调整。循环终止条件为遍历的节点为根节点,或x的父节点不为红(因为父节点为黑时,直接在父节点下添加红色的子节点,依旧满足红黑树的性质),当满足此条件时,整棵树已经满足了红黑树性质。此算法循环中的操作最多只涉及到三层,即x层,x.parent层,x.parent.parent层,如果x.parant.parent存在,则是调整以x.parant.parant为根的三层树结构中的节点,保证这棵子树维持红黑树性质。
        */
        while (x != null && x != root && x.parent.color == RED) {
            //条件a:判断x的父节点是否为当前子树的左节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                /**
                设置y为x父节点的右兄弟节点(因为条件1已经决定如果进入此分支,
                *则x父节点一定位于以x.parent.parent为根的左子树上面)
                */
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                //条件b:判断父节点右兄弟节点是否为红
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //条件c:判断x节点是否为父节点的右节点
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                /**
                设置y为x父节点的左兄弟节点(因为条件1已经决定如果进入此分支,
                *则x父节点一定位于以x.parent.parent为根的右子树上面)
                */
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                //条件d:判断父节点左兄弟节点是否为红
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //条件e:判断x节点是否为父节点的左节点
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        //保证红黑树根节点始终为黑色,维持性质2
        root.color = BLACK;
    }

分析上述代码由于while循环体内,最多只对以x.parent.parent为根的三层树进行操作
(如果不存在,则操作的是x.parent为根的两层树),而由条件abcde则分出6种可能
分支(注意这些分支隐含默认条件为x父节点为红)

分支1.x父左,x父兄红(处理:将x父节点,x的父兄节点,x父节点的父节点变色,使以x.parent.parent为根的三层子树满足红黑树性质,因为x.parent.parent的颜色发生了改变,如果x.parent.parent存在父节点且颜色为红,会导致红黑树性质改变,所以将x赋值为x.parent.parent,再次执行循环判断)
分支2.x父左,x父兄黑,x右(处理:将x赋值为x.parent,然后对x进行使树的节点相对位置颜色不变,此时已经将x赋值为)
分支3.x父左,x父兄黑,x左(处理:先修改相应节点颜色,然后对x.parent.parent右旋转,最终使树的节点相对位置颜色不变,但是部分节点位置发生改变)
分支4.x父右,x父兄红
分支5.x父右,x父兄黑,x右
分支6.x父右,x父兄黑,x左
这6种分支已经包含了在一个三层的红黑树中,在父节点为红色的情况下(在TreeMapd的插入算法中,因为插入的新节点都默认为红色,如果父节点为黑色直接插入节点不影响红黑树性质,而如果父节点为红色,则不满足红黑树性质4,所以需要调整),新增节点所产生的所有可能性,现在我们就通过具体的例子来更加深入理解这个方法吧,这里我选取的是按一定规律插入一组key,来覆盖每一种可能分支,这组key是:128,64,192,32,48,16,8,160,224,232,228,240,230,236
插入过程如图

这里写图片描述

图中圈住的图,代表插入节点破坏了红黑树性质,然后调整节点满足红黑树性质的过程。(图片中只给出了每次循环的初始状态和最终状态)
插入128:作为根节点直接插入
插入64:父节点为黑,直接插入
插入192:父节点为黑,直接插入
插入32:x=32,满足分支1,按分支1进行处理,对128,64,192进行颜色反转,然后x=128,因为128为根节点,循环结束,结束循环后将root设置为黑色,虽然左右图对比,128颜色未变,实际上代码中128进行2次颜色反转,最终树形态如右图。
插入48:x=48,满足分支2,分支2进行了3步处理,第一步x=32,然后对x进行左旋操作,第二步对x.parant(48),x.parant.parant(64)进行颜色反转,第三步对x.parent.parent(64)进行右旋操作。x.parent(48).color为黑色,循环结束。
插入16:x=16,满足分支1,按分支1处理,过程就不再描述,大家直接看结果图即可
插入8:x=8,满足分支3,按分支3处理,第一步对x.parent(16),x.parent.parent(32)进行颜色反转,第二步对x.parent.parent(32)进行右旋操作。x.parent(16).color为黑色,循环结束。因分支三操作实际上是分支2后两步操作,所以不再给出中间图,直接给出开始和结果图,过程参照插入48第二图开始。

父节点为左的分支插入我们已经全部了解了,现在让我们看看父节点为右的插入,接上图最终形态继续插入160,224,232,228,240,230,236,对于左边不再变化的节点树,直接用子树代替了。

这里写图片描述

插入160:父节点为黑,直接插入
插入224:父节点为黑,直接插入
插入232:x=232,满足分支4,对192,160,224进行颜色反转,然后x=192,x.parent(128)为黑,循环结束
插入228:x=228,满足分支5,第一步x=x.parent(232),对x(232)右旋,第二步x.parent(228),x.parent.parent(224)进行颜色反转,第三步x.parent.parent(224)左旋
,此时x.parent(228)为黑,循环结束。

下面是插入240,230,236

这里写图片描述

插入240:x=240,满足分支4,x.parent(232),x.parent.parent(228),x.parent.parent.left(224)进行颜色反转,x=x.parent.parent(228),继续循环,满足分支4,x.parent(192),x.parent.parent(128),x.parent.parent.left(48)颜色反转,x=x.parent.parent(128),x为root,循环结束,root颜色设置为黑色。
插入230:父节点为黑,直接插入
插入236:x=236,满足分支4,x.parent(240),x.parent.parent(232),x.parent.parent.left(230)进行颜色反转,x=x.parent.parent(232),继续循环,满足分支6,x.parent(228),x.parent.parent(192)颜色反转,x=x.parent.parent(192)左旋,x.parent(228)为黑色,循环结束。

TreeMap remove
TreeMap使用红黑树的理由
至此,TreeMap中的红黑树插入算法可以算解析完毕了,我们已经了解TreeMap中是怎么实现红黑树的插入的。排序算法有多种,为何TreeMap采用红黑树算法呢,红黑树的本身性质是其中很重要的因素,由于红黑树的性质,在查找,插入,及删除时,红黑树的时间复杂度都为O(logn),此外,由于它的设计,任何不平衡都会在三次旋转之内解决。虽然还存在一些更复杂的数据结构,能够在一次旋转就实现平衡,但是红黑树本身在性能和复杂度上相对而言更均衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值