集合框架源码分析(jdK1.7)(三)TreeMap


1.红黑树简介

TreeMap的实现是红黑树算法的实现了解TreeMap首先要了解红黑树。

1.1二叉树

概念:n(n≥0)个结点的有限集合,由一个根节点以及两颗互不相交的、分别称为左子树右子树的二叉树组成。

基本特征:

每个结点最多只有两颗子树(不存在度>2的结点)。

左子树和右子树次序不能颠倒(有序树)。

1.2平衡二叉树

概念:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

1.3红黑树

概念:  红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。

特点:

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

       2、根节点是黑色

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

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

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

注意:

(01) 特性(3)中的叶子节点,是只为空(NILnull)的节点。

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

下面是一个红黑树所有叶子节点都为NULL节点没有表示出来

自动地址  https://sandbox.runjs.cn/show/2nngvn8w

1.4红黑树添加操作

首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:

第一步: 将红黑树当作一颗二叉查找树,将节点插入

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

第二步:将插入的节点着色为"红色"。

      为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性

(1) 每个节点或者是黑色,或者是红色。

(2) 根节点是黑色。

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

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

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

      将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。

第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。

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

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

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

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

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

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

增加方案的解决方法:

根据被插入节点的父节点的情况,可以将"当节点z被着色为红色节点,并插入二叉树"划分为三种情况来处理。

①情况说明:被插入的节点是根节点。

    处理方法:直接把此节点涂为黑色。

②情况说明:被插入的节点的父节点是黑色。

    处理方法:什么也不需要做。节点被插入后,仍然是红黑树。

③情况说明:被插入的节点的父节点是红色。

处理方法:那么,该情况与红黑树的“特性(5)”相冲突。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。理解这点之后,我们依据"叔叔节点的情况",

将这种情况进一步划分为3种情况(Case)。

 

现象说明

处理策略

Case 1

当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色。

(01) 父节点设为黑色。
(02) 叔叔节点设为黑色。
(03) 祖父节点设为红色
(04) 祖父节点设为当前节点”(红色节点);即,之后继续对当前节点进行操作。

Case 2

当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子

(01) 父节点作为新的当前节点
(02) 新的当前节点为支点进行左旋。

Case 3

当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子

(01) 父节点设为黑色
(02) 祖父节点设为红色
(03) 祖父节点为支点进行右旋。

1.5左旋、右旋

根据二叉树的特点插入节点,插入结束后为了维持平衡二叉树采用左旋、右旋的方式如图:

 

左右旋的原理相同

1.6删除操作

将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。

删除方案:

  按照待删除节点有几个儿子的情况来进行分类:

   情况一、无子节点(红色节点)

       这种情况对该节点直接删除即可,不会影响树的结构。

       情况二、有一个子节点

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

       情况三、有两个子节点

       这种情况可能会稍微有点儿复杂。它需要找到一个替代待删除节点(N)来替代它,然后删除N即可。它主要分为四种情况。

       1N的兄弟节点W为红色

       2N的兄弟w是黑色的,且w的俩个孩子都是黑色的。

       3N的兄弟w是黑色的,w的左孩子是红色,w的右孩子是黑色。

       4N的兄弟w是黑色的,且w的右孩子时红色的。

//todo

原理图:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html

参考:https://blog.csdn.net/zhangyuan19880606/article/details/51234420/

http://www.cnblogs.com/skywang12345/p/3245399.html

2.TreeMap的数据结构

.TreeMap内部是有Entry<K,V>维护的,可以看出Entry<K,V>的结构就是一个红黑树的结构,

包含:左子树右子树,和父节点节点颜色(默认颜色为黑色)

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;

   
/**
    
* Make a new cell with given key, value, and parent, and with
     * {@code null} child links, and BLACK color.
     */
   
Entry(K key, V value, Entry<K,V> parent) {
       
this.key = key;
       
this.value = value;
       
this.parent = parent;
   
}

3.主要参数:

//比较器,因为TreeMap是有序的,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制

private final Comparator<? super K> comparator;

//TreeMap-黑节点,为TreeMap的内部类
private transient Entry<K,V> root = null;

//容器大小
private transient int size = 0;

//TreeMap修改次数
private transient int modCount = 0;

4.Treem主要构造方法:

public TreeMap() {

//始化的时候先初始化一个为Null的比较器
   
comparator = null;
}

TreeMap是红黑树无初始化容量等参数

5.TreeMap的Put方法

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,表示TreeMap集合中存在一个元素
       
size = 1;

        modCount++; //修改次数 + 1
       
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
           
cmp = cpr.compare(key, t.key); //比较新增节点的key和当前节点key的大小

        //cmp返回值小于0,表示新增节点的key小于当前节点的key,则以当前节点的左子节点作为新的当前节点
            if (cmp < 0)
               
t = t.left;

        //cmp返回值大于0,表示新增节点的key大于当前节点的key,则以当前节点的右子节点作为新的当前节点
           
else if (cmp > 0)
               
t = t.right;

        //cmp返回值等于0,表示两个key值相等,则新值覆盖旧值,并返回新值
            else
               
return
t.setValue(value);
       
} while (t != null);
   
}

//如果cpr为空,则采用默认的排序算法进行创建TreeMap集合(默认构造方法时cpr为null)
   
else {
       
if (key == null) //key值为空抛出异常
           
throw new NullPointerException();

/* 下面处理过程用compareTo比较和上面一样 */
       
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;
   
else

  //如果新增节点的key小于parent的key,则当做左子节点
       
parent.right = e;

    /*

     上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置

     下面fixAfterInsertion()方法就是对这棵树进行调整、平衡

     */
   
fixAfterInsertion(e);
    size++; //TreeMap元素数量 + 1
   
modCount++; //TreeMap容器修改次数 + 1
   
return null;
}

5.1 fixAfterInsertion(e)把二叉树调整为红黑树;

上面代码中do{}代码块是实现排序二叉树的核心算法,通过该算法我们可以确认新增节点在该树的正确位置。找到正确位置后将插入即可,这样做了其实还没有完成,因为我知道TreeMap的底层实现是红黑树,红黑树是一棵平衡排序二叉树,普通的排序二叉树可能会出现失衡的情况,所以下一步就是要进行调整。fixAfterInsertion(e); 调整的过程务必会涉及到红黑树的左旋、右旋、着色三个基本操作。代码如下:

private void fixAfterInsertion(Entry<K,V> x) { // 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)));
           
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;
}

 

 

 

解释:对这段代码的研究我们发现,其处理过程完全符合红黑树新增节点的处理过程。所以在看这段代码的过程一定要对红黑树的新增节点过程有了解。在这个代码中还包含几个重要的操作。左旋(rotateLeft())、右旋(rotateRight())、着色(setColor())。

 

左旋:rotateLeft(): 所谓左旋转,就是将新增节点(N)当做其父节点(P),将其父节点P当做新增节点(N)的左子节点。

即:G.left ---> N ,N.left ---> P

右旋:P.right ---> G、G.parent ---> P

5.2rotateRight()左旋方法

private void rotateLeft(Entry<K,V> p) {

        if (p != null) {

            //获取P的右子节点,其实这里就相当于新增节点N(情况四而言)

            Entry<K,V> r = p.right;

            //R的左子树设置为P的右子树

            p.right = r.left;

            //R的左子树不为空,则将P设置为R左子树的父亲

            if (r.left != null)

                r.left.parent = p;

            //P的父亲设置R的父亲

            r.parent = p.parent;

            //如果P的父亲为空,则将R设置为跟节点

            if (p.parent == null)

                root = r;

            //如果P为其父节点(G)的左子树,则将R设置为P父节点(G)左子树

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

                p.parent.left = r;

            //否则R设置为P的父节点(G)的右子树

            else

                p.parent.right = r;

            //P设置为R的左子树

            r.left = p;

            //R设置为P的父节点

            p.parent = r;

        }

}

4.3rotateRight()右旋转方法

private void rotateRight(Entry<K,V> p) {

        if (p != null) {

            //L设置为P的左子树

            Entry<K,V> l = p.left;

            //L的右子树设置为P的左子树

            p.left = l.right;

            //L的右子树不为空,则将P设置L的右子树的父节点

            if (l.right != null)

                l.right.parent = p;

            //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;

            //否则将L设置为P的父节点的左子树

            else

                p.parent.left = l;

            //P设置为L的右子树

            l.right = p;

            //L设置为P的父节点

            p.parent = l;

        }

    }

5.4 setColor()着色:

着色就是改变该节点的颜色,在红黑树中,它是依靠节点的颜色来维持平衡的

private static <K,V> void setColor(Entry<K,V> p, boolean c) {
   
if (p != null)
       
p.color = c;
}


5remove(Object key)方法

//todo

6.总结

1.TreeMap和HashMap,HashTable的数据结构完全不同所以性质完全不同

2. TreeMap为红黑树,所有节点是有顺序的,(按顺序插入)

3.无初始容量和扩容



猜你喜欢

转载自blog.csdn.net/zpoison/article/details/80884643