core java(五)treeMap的原理和使用(1)

来源:互联网 发布:剑灵捏脸数据导入图片 编辑:程序博客网 时间:2024/06/06 12:59

对于 TreeMap 而言,它采用一种被称为“红黑树”的排序二叉树来保存 Map 中每个 Entry

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

{

   privatefinalComparator<?superK>comparator;

private transient Entry<K,V> root = null;

……………………….

    privatetransient EntrySetentrySet =null;

   privatetransientKeySet<K>navigableKeySet =null;

   privatetransientNavigableMap<K,V>descendingMap =null;

 

}

treemap 中数据是以Entry为基础的,而Entry的定义如下:

 

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

        booleancolor =BLACK;

由此可以看出每一个entry都被当成红黑树的一个节点对待。

 

对于如下程序而言:

 public class TreeMapTest

 {

    public static void main(String[] args)

    {

        TreeMap<String , Integer> map =

            newTreeMap<String , Integer>();

        map.put("c" , 3);

        map.put("a" , 1);

        map.put("e" , 5);

        map.put("b" , 2);

        map.put("F" , 6);

        System.out.println(map);

    }

 }

 

我们来一步一步的看这个例子

第一步就是treemap的创建工作:在创建之前,我们必须了解一下红黑树的知识。

红黑树

红黑树是一种自平衡排序二叉树,树中每个节点的值,都大于或等于在它的左子树中的所有节点的值,并且小于或等于在它的右子树中的所有节点的值,这确保红黑树运行时可以快速地在树中查找和定位的所需节点。

对于 TreeMap 而言,由于它底层采用一棵“红黑树”来保存集合中的 Entry,这意味这 TreeMap 添加元素、取出元素的性能都比 HashMap 低:当 TreeMap 添加元素时,需要通过循环找到新增 Entry 的插入位置,因此比较耗性能;当从 TreeMap 中取出元素时,需要通过循环才能找到合适的 Entry,也比较耗性能。但 TreeMap、TreeSet 比 HashMap、HashSet 的优势在于:TreeMap 中的所有 Entry 总是按 key 根据指定排序规则保持有序状态,TreeSet 中所有元素总是根据指定排序规则保持有序状态。

为了理解 TreeMap 的底层实现,必须先介绍排序二叉树和红黑树这两种数据结构。其中红黑树又是一种特殊的排序二叉树。

排序二叉树是一种特殊结构的二叉树,可以非常方便地对树中所有节点进行排序和检索。

排序二叉树要么是一棵空二叉树,要么是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 它的左、右子树也分别为排序二叉树。

图 1 显示了一棵排序二叉树:



1. 排序二叉树
 

对排序二叉树,若按中序遍历就可以得到由小到大的有序序列。如图 1 所示二叉树,中序遍历得:

{2,3,4,8,9,9,10,13,15,18}

 

创建排序二叉树的步骤,也就是不断地向排序二叉树添加节点的过程,向排序二叉树添加节点的步骤如下:

1.     以根节点当前节点开始搜索。

2.     拿新节点的值和当前节点的值比较。

3.     如果新节点的值更大,则以当前节点的右子节点作为新的当前节点;如果新节点的值更小,则以当前节点的左子节点作为新的当前节点。

4.     重复 2、3 两个步骤,直到搜索到合适的叶子节点为止。

5.     将新节点添加为第 4 步找到的叶子节点的子节点;如果新节点更大,则添加为右子节点;否则添加为左子节点。

掌握了上面的理论之后:再看treemap的创建:

 

Treemap提供了四种创建的方法:

public TreeMap() {

        comparator=null;

   }

使用键的自然顺序构造一个新的、空的树映射。

 

public TreeMap(Comparator<?superK> comparator) {

        this.comparator = comparator;

    }

构造一个新的、空的树映射,该映射根据给定比较器进行排序。

 

public TreeMap(SortedMap<K,? extendsV> m) {

        comparator = m.comparator();

        try {

            buildFromSorted(m.size(),m.entrySet().iterator(), null, null);

        } catch (java.io.IOException cannotHappen) {

        } catch (ClassNotFoundException cannotHappen) {

        }

}

构造一个与指定有序映射具有相同映射关系和相同排序顺序的新的树映射。

public TreeMap(Map<?extends K, ?extendsV> m) {

        comparator = null;

        putAll(m);

}

构造一个与给定映射具有相同映射关系的新的树映射,该映射根据其键的自然顺序进行排序。插入此新映射的所有键都必须实现Comparable接口。另外,所有这些键都必须是可互相比较的:对于映射中的任意两个键k1k2,执行k1.compareTo(k2) 都不得抛出ClassCastException。此方法的运行时间为n*log(n)。

这个方法中的putAll的源码:

 

public voidputAll(Map<? extends K, ?extends V> map) {

        int mapSize = map.size();

//要求map必须是sortedMap的实例

        if (size==0 && mapSize!=0 && mapinstanceof SortedMap) {

            Comparator c = ((SortedMap)map).comparator();

            if (c ==comparator || (c !=null && c.equals(comparator))) {

       ++modCount;

       try {

           buildFromSorted(mapSize,map.entrySet().iterator(),

                  null,null);

       }catch(java.io.IOException cannotHappen) {

       }catch(ClassNotFoundException cannotHappen) {

       }

       return;

            }

        }

        super.putAll(map);//①

    }

 

buildFromSorted,就是根据已经一个排好序的map创建一个treemap

就是一步一步添加节点的过程。

private voidbuildFromSorted(int size,Iteratorit,

               java.io.ObjectInputStream str,

               V defaultVal)

        throws java.io.IOException, ClassNotFoundException {

        this.size = size;

        root = buildFromSorted(0, 0, size-1,computeRedLevel(size),

                  it, str, defaultVal);

}

//level树的层次初始化为0

//lo当前子树第一个元素的下标初始化为0

//hi当前子树最后一个元素的下表,初始化为length-1

private finalEntry<K,V> buildFromSorted(int level,int lo,int hi,

                       int redLevel,

                      Iterator it,

                       java.io.ObjectInputStream str,

                       V defaultVal)

        throws java.io.IOException, ClassNotFoundException {

        if (hi < lo)returnnull;

        //确定中间的元素

        int mid = (lo + hi) / 2;

        Entry<K,V> left  = null;

        if (lo < mid)//防止递归到最后一个元素的情况

            left = buildFromSorted(level+1, lo, mid - 1, redLevel,

                 it, str, defaultVal);

        // extract key and/or value from iterator or stream

        K key;

        V value;

        if (it !=null) {

            if (defaultVal==null) {

                Map.Entry<K,V> entry = (Map.Entry<K,V>)it.next();

                key = entry.getKey();

                value = entry.getValue();

            } else {

                key = (K)it.next();

                value = defaultVal;

            }

        } else {// use stream

            key = (K) str.readObject();

            value = (defaultVal != null ? defaultVal :(V)str.readObject());

        }

        Entry<K,V> middle =  new Entry<K,V>(key, value,null);

        // color nodes in non-full bottommost level red

        if (level == redLevel)//非最底层的节点都是红色

            middle.color = RED; //插入的节点都是红色的。

        if (left !=null) {

            middle.left = left;

            left.parent = middle;

        }

       if (mid < hi) {

            Entry<K,V> right = buildFromSorted(level+1,mid+1, hi, redLevel,

                         it, str, defaultVal);

            middle.right = right;

            right.parent = middle;

        }

 

        return middle;

    }

建树的策略:

根就是中间的元素(对于已经排好顺序的map而言),建树首先我们递归的构造做字数,然后处理友子树。

Lo和hi这两个变量就是迭代器或者子树流的最小值和最大值。

上面的算是提供的构造参数是sortmap的实例的,还有的不是的情况就是上面代码标记①中的super.putALL(map)。这个逻辑是:

public voidputAll(Map<? extends K, ?extendsV> m) {

        for (Map.Entry<?extends K, ?extends V> e : m.entrySet())

            put(e.getKey(), e.getValue());

}

这个就比较的好理解了,这个是一个一个的插入元素。

就像上面例子中的map.put(“a”,8)一样的逻辑。Treemap的put的源代码如下:

 

   publicV put(K key, V value) {

        Entry<K,V> t = root;

        if (t ==null) {//如果root为空,则表示是一个空树,直接赋予根元素即可。

           //新建一个key-value的Entry作为根元素

            root = new Entry<K,V>(key, value,null);

            size = 1;

            modCount++;

            return null;

        }

        int cmp;//大小比较的值

        Entry<K,V> parent;//上一级

        Comparator<? super K> cpr =comparator;

       if (cpr !=null) {// 如果比较器cpr 不为 null,即表明采用定制排序

 

            do {//循环的确认位置

                parent = t;

// 拿新插入 key 和 t 的 key 进行比较

                cmp = cpr.compare(key, t.key);

              //新插入的key小于t的key值,t就为t的左节点

                if (cmp < 0)

                    t = t.left;

              //新插入的key大于t的key值,t就为t的右节点

                else if (cmp > 0)

                   t = t.right;

                else

                  //如果相等的话,就是新值替换旧值

                    return t.setValue(value);

            } while (t !=null);

        }

        else {

            if (key ==null)

                throw new NullPointerException();

            Comparable<? super K> k =(Comparable<?superK>) 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<K,V>(key,value, parent);

        if (cmp < 0)

            parent.left = e;

        else

            parent.right = e;

       //修复红黑树

        fixAfterInsertion(e);//①

        size++;

        modCount++;

        return null;

    }

代码的逻辑:每当程序希望添加新节点时:系统总是从树的根节点开始比较(即将根节点当成当前节点),如果新增节点大于当前节点、并且当前节点的右子节点存在,则以右子节点作为当前节点;如果新增节点小于当前节点、并且当前节点的左子节点存在,则以左子节点作为当前节点;如果新增节点等于当前节点,则用新增节点覆盖当前节点,并结束循环 —— 直到找到某个节点的左、右子节点不存在,将新节点添加该节点的子节点 —— 如果新节点比该节点大,则添加为右子节点;如果新节点比该节点小,则添加为左子节点。

 

 

修复红黑树的方法源代码:这里还是首先了解一下红黑树的性质:

RED-BLACK树的性质:
1、根元素永远是显色。
2、除根节点外,所有新插入的节点都是红色的。
3、Red规则:红色节点的子节点都是黑色的,不能有两个红色节点相邻。
4、Path规则:从根元素到某个子节点或是只有一个子节点的元素的所有路径中,黑色元素的个数必须相同。

红黑树在插入新的结点的时候,规定新结点的颜色是红色。和普通二叉搜索树一样,首先找到插入的位置,之后红黑树需要维持自己的性质,因此需要进行修正。

         当插入结点N的父节点是黑色结点的时候,显然根本不需要任何修正,上述5个性质仍旧成立。所以只需要考虑插入结点N的父节点同样是红色的情况,一共有6种情况,实际上是3种情况,因为有3种情况是对称的。

         情况1N是其父节点的左孩子,N的父节点是红色,父节点的兄弟结点是红色。

         情况2N是其父节点的左孩子,N的父节点是红色,父节点的兄弟结点是黑色。但父节点是爷爷结点的右孩子。

         情况3N是其父节点的左孩子,N的父节点是红色,父节点的兄弟结点是黑色。父节点是爷爷结点的左孩子。

 情况456只是把N换成是其父节点的右孩子。

可以看出情况1并不关心N、父节点和爷爷结点的关系。但是情况23是关心的。

 下面给出情况123的调整办法:

 情况1,调整N的父节点和N的父节点的兄弟结点(称叔叔结点)的颜色为黑色,爷爷结点的颜色为红色,并设爷爷结点为新的N。这样N相当于向上调整了两层。之后再根据N所处位置判断是哪种情况继续调整。




 
G代表爷爷结点Grandfather,P代表父节点FatherN代表新结点New

情况2



 
此时,违规的结点变成了P结点,相当于新的N,实际上这就转化为了情况3.

情况3

 
下面以一个具体的例子介绍:

 



 
当前该红黑树满足其5个性质,现在我们需要插入一个新得结点15.

 

 


 
 
可以看出上述满足情况1.于是我们进行了调整,之后变成了右边的情形。此时违规的是结点10.



 结点10违规,检查之后发现属于情况2.此时需要右旋转。一旦实行右旋转就转化为了情况3,情况3也意味着结束。

 


private void  fixAfterInsertion(Entry<K,V> x) {

//新插入的节点的为红色

        x.color = RED;

//如果父节点为红色

        while (x !=null && x !=root && x.parent.color ==RED){

//父节点是爷爷节点的左节点

            if (parentOf(x) ==leftOf(parentOf(parentOf(x)))){

//取爷爷节点的右节点

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

//如果这个时候爷爷节点的右节点,既是父节点的兄弟节点为红色,就是第一种情况。

                if (colorOf(y) ==RED) {

//把父节点和父节点的兄弟节点,变为黑色

                    setColor(parentOf(x),BLACK);

                    setColor(y, BLACK);

//把爷爷节点变为红色

                    setColor(parentOf(parentOf(x)),RED);

//这个时候x变为爷爷节点

//完成以后发现,x节点向上面移动了两层,因此循环的最大次数是树的高度的一半,这也就是TreeMap中的//put方法的最大执行时间为 O(logn) 原因所在了  

                    x = parentOf(parentOf(x));

                } else {

//如果父节点的兄弟节点为黑色或者为空,根据条件判断此时如果父节点的兄弟节点为黑色的话,那么爷爷节点为红色,父节点也是红色这种情况是不可能出现的,所以此时的父节点的兄弟节点只能为空。

//x为父节点的右孩子

                    if (x ==rightOf(parentOf(x))) {

                        x = parentOf(x);

//左单旋转,转化为情况三

                        rotateLeft(x);

                    }

//x为左孩子

                    setColor(parentOf(x),BLACK);//父节点为黑色

                    setColor(parentOf(parentOf(x)),RED);//爷爷节点为红色

                    rotateRight(parentOf(parentOf(x)));//以爷爷节点右旋

                }

            } else {//对称的说法,把上面的左变为右

                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;

   }

 

原创粉丝点击