zoukankan      html  css  js  c++  java
  • 【数据结构】11.java源码关于TreeMap

    目录


    1.TreehMap的内部结构
    2.TreehMap构造函数
    3.元素新增策略
    4.元素删除
    5.元素修改和查找
    6.特殊操作
    7.扩容
    8.总结

    1.TreeMap的内部结构

    首先确认一点,treemap是一个基于红黑树的map,这个集合的一个特点就是排序,是的如果不是排序,那么hashmap可以完美取代

    再开始前我们要熟悉一个红黑树的概念:

    对于红黑树的定义:

    1.节点是红色或黑色。
    2.根是黑色。
    3.所有叶子都是黑色(叶子是NIL节点)。
    4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
    5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

    2.TreeMap构造函数

    通过已排序的map进行新的treemap的构造生成,这里用到了一个buildFromSorted函数,这个方法会递归数据

    参考buildFromSorted实现,这个实现的红黑树,说白了,就是把最后一层变成红色,以上全作为黑色

    3.元素新增策略

    Put操作

    这个操作没啥,就是遍历这颗树,左边小,右边大,遍历到合适的位置设置值,或者创建新的节点插入,并默认设置为黑色
    重点在于后面的变动之后,如果进行红黑树的修复
    针对红黑树的变动,可以参考以上总结的规则:https://www.cnblogs.com/cutter-point/p/10976416.html

    针对于红黑树的操作主要就是左旋和右旋的操作

    public V put(K key, V value) {
            Entry<K,V> t = root;
            if (t == null) {
                //如果根节点为空,然后这个比较其实是起一个类型检查作用
                compare(key, key);
                //创建root节点
                root = new Entry<>(key, value, null);
                size = 1;
                modCount++;
                return null;
            }
            //如果根节点存在
            int cmp;
            Entry<K,V> parent;
            //判断是否设置了比较器
            Comparator<? super K> cpr = comparator;
            if (cpr != null) {
                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();
                //就提前key的比较器
                @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);
            }
            //如果一直到t为空还没找到,那么就创建新值
            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;
        }
    
    
        //获取对应节点的父节点
        private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
            return (p == null ? null: p.parent);
        }
    
        //求当前节点的左右节点
        private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
            return (p == null) ? null: p.left;
        }
    
        private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
            return (p == null) ? null: p.right;
        }
    
        //获取当前节点的颜色,为空即为黑
        private static <K,V> boolean colorOf(Entry<K,V> p) {
            return (p == null ? BLACK : p.color);
        }
    
        private static <K,V> void setColor(Entry<K,V> p, boolean c) {
            if (p != null)
                p.color = c;
        }
    
    
    红黑树的修复操作,主要重点就是对从一个节点到叶子节点的黑色节点个数相同为基准
    
    //进行红黑树修复
        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 = parentOf(parentOf(x));
                    } else {
                        //如果是黑色,说明两边的分支走到叶子节点的不是相同数目的黑色节点
                        //如果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 {
                    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;
        }
    
    
    左旋右旋操作就是把以对应的节点为核心进行节点的上升和下降,然后要复合红黑树的规范
    private void rotateLeft(Entry<K,V> p) {
            //左旋操作
            if (p != null) {
                //如果节点不为空,进行左旋的时候,获取节点的右节点
                Entry<K,V> r = p.right;
                p.right = r.left;
                if (r.left != null)
                    r.left.parent = p;
                r.parent = p.parent;
                if (p.parent == null)
                    root = r;
                else if (p.parent.left == p)
                    p.parent.left = r;
                else
                    p.parent.right = r;
                r.left = p;
                p.parent = r;
            }
        }
    
        /** From CLR */
        private void rotateRight(Entry<K,V> p) {
            if (p != null) {
                Entry<K,V> l = p.left;
                p.left = l.right;
                if (l.right != null) l.right.parent = p;
                l.parent = p.parent;
                if (p.parent == null)
                    root = l;
                else if (p.parent.right == p)
                    p.parent.right = l;
                else p.parent.left = l;
                l.right = p;
                p.parent = l;
            }
        }

    4.元素删除

    进行元素删除的思想是:
    如果我需要删除这个节点,那么首选判断右子树是否存在,如果存在那么就找到右边子树的最小值,也就是最小的叶子节点,用一个最靠近的节点的数据替换需要删除的节点的数据,这样就保障二叉索引树的特性,然后根据变动的节点颜色重新修复这颗红黑树的颜色

        //节点删除操作
        public V remove(Object key) {
            //获取到这个节点
            Entry<K,V> p = getEntry(key);
            if (p == null)
                return null;
    
            //获取旧值
            V oldValue = p.value;
            //删除节点,然后返回旧值
            deleteEntry(p);
            return oldValue;
        }
    
    
    
    final Entry<K,V> getEntry(Object key) {
            //如果有设置比较器,那么就优先使用比较器进行寻找
            if (comparator != null)
                return getEntryUsingComparator(key);
            if (key == null)
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            Entry<K,V> p = root;
            //因为是红黑树,可以判断节点值的大小,然后判断是左右节点的遍历
            while (p != null) {
                int cmp = k.compareTo(p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
            return null;
        }
    
    
    //进行节点的删除
        private void deleteEntry(Entry<K,V> p) {
            modCount++;
            size--;
    
            // 当前节点有两个子节点,找到右子树中最小元素作为后继节点;将后继节点信息替换到当前节点
            if (p.left != null && p.right != null) {
                //说白了就是找这个p元素相邻大小的元素,优先找大的,其次找小的
                //1.找右子树的最小节点  因为上面有判断p的左右节点必须存在,所以结果肯定是右子树的最小值
                Entry<K,V> s = successor2(p);
                p.key = s.key;
                p.value = s.value;
                //吧引用指向s,吧p的值设置为s的值
                //这个时候需要删除的那个节点的值变成了一个新的最靠近的值,这样就不会破坏索引树的条件,然后把那个用来替换的节点干掉即可
                p = s;
                //替换删除的元素
            }
    
            // 开始修复,优先取这个节点的左边,否则取右边作为replacement节点对象
            //除非是p.right和left有一个为空,不然一般肯定走的是p.right并且是个null对象
            Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    
            //如果要进行取代的节点为空,那么就不用操作
    //        1、有两个儿子。这种情况比较复杂,但还是比较简单。上面提到过用子节点C替代代替待删除节点D,然后删除子节点C即可。
    //        2、没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
    //        3、只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
            if (replacement != null) {
                //把需要替换的节点的父节点设置为p的父节点
                replacement.parent = p.parent;
                //判断p的父节点是否Wie空,或者判断p是否是作为左节点,否则判断是否是右节点
                //说白了就是用replacement 取代P节点
                if (p.parent == null)
                    root = replacement;
                else if (p == p.parent.left)
                    p.parent.left  = replacement;
                else
                    p.parent.right = replacement;
    
                //设置p的左右和父节点为空,也就是吧p节点剥离开
                p.left = p.right = p.parent = null;
    
                // 修复颜色分配,因为最后一个要删除的节点是黑色,那么删除这个节点之后,这条线的黑色节点个数肯定会减去1
                if (p.color == BLACK)
                    //那么我们就需要对这个变动过的节点进行调整
                    fixAfterDeletion(replacement);
            } else if (p.parent == null) { // return if we are the only node.
                root = null;
            } else {
                //一般情况是这个,也就是说吧需要删除的节点移动到末尾叶子节点,然后把key,value替换掉,最后删除调最后一个叶子即可
                //然后如果叶子的颜色正好是黑色的,那么要重新修复颜色
                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;
                }
            }
        }
    
    
    
    static <K,V> TestTreeMap.Entry<K,V> successor2(Entry<K,V> t) {
            //找到右边节点的最小元素,也就是仅仅比T大一点的元素
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        }
    
    
    private void fixAfterDeletion(Entry<K,V> x) {
            //判断替换过的节点是否是黑色,删除节点需要一直迭代,直到 x 不是根节点,且 x 的颜色是黑色
            while (x != root && colorOf(x) == BLACK) {
                //如果这个节点是左节点
                if (x == leftOf(parentOf(x))) {
                    //获取兄弟节点
                    Entry<K,V> sib = rightOf(parentOf(x));
    
                    if (colorOf(sib) == RED) {
                        //兄弟节点为红,设置为黑,父辈设置为红,然后左旋
                        setColor(sib, BLACK);
                        setColor(parentOf(x), RED);
                        //左旋
                        rotateLeft(parentOf(x));
                        sib = rightOf(parentOf(x));
                    }
    
                    if (colorOf(leftOf(sib))  == BLACK &&
                            colorOf(rightOf(sib)) == BLACK) {
                        setColor(sib, RED);
                        x = parentOf(x);
                    } else {
                        if (colorOf(rightOf(sib)) == BLACK) {
                            setColor(leftOf(sib), BLACK);
                            setColor(sib, RED);
                            //右旋
                            rotateRight(sib);
                            sib = rightOf(parentOf(x));
                        }
                        setColor(sib, colorOf(parentOf(x)));
                        setColor(parentOf(x), BLACK);
                        setColor(rightOf(sib), BLACK);
                        rotateLeft(parentOf(x));
                        x = root;
                    }
                } else { // symmetric
                    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);
        }

    5.元素修改和查找

    修改可以参考put操作,对key无影响

    查找因为是二叉索引树,所以查找方式和remove中的getEntry操作类似


    6.特殊操作

    6.1BuildFromSorted

    这函数用来根据一级拍好顺序的map构建treemap

    再这个函数之前还有一个computeRedLevel函数,这个用来计算当前节点所在的层数

     //计算红色节点应该在红黑树哪一层,因为二叉树,因为每层二叉树要填满的话必须是2的倍数
        //每层数据叠加是1,1+2,1+2+4,1+2+4+8.。。 基本就是每层就是每层/2
        //sz指树中节点的个数,为了确保是一个红黑树,那么需要把前面几层全部看成黑色,最后一层设置为红色即可
        //因为sz是节点的个数,所以最后一个节点所在的层数即是红色
        public static int computeRedLevel(int sz) {
            int level = 0;
            //从0开始计算0,2,6,14
            //可以看出m=(m+1)*2 前一个和后一个的递推关系 每一层计算
            //那么反过来就是m/2-1就是上一层的位置,最后一个m>=0的时候还要计算一次
            for (int m = sz - 1; m >= 0; m = m / 2 - 1)
                level++;
            return level;
        }
    
    
        //通过迭代构造一个新的排序的map,递归将SortedMap中的元素逐个关联
        //str: 如果不为空,则从流里读取key-value,defaultVal:见名知意,不为空,则value都用这个值
        private void buildFromSorted(int size, Iterator<?> it,
                                     java.io.ObjectInputStream str,
                                     V defaultVal)
                throws  java.io.IOException, ClassNotFoundException {
            this.size = size;
            //递归,添加到root节点上
            root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
                    it, str, defaultVal);
        }
    
    
       /**
         * level: 当前树的层数,注意:是从0层开始
         * lo: 子树第一个元素的索引
         * hi: 子树最后一个元素的索引
         * redLevel: 上述红节点所在层数
         * 剩下的3个就不解释了,跟上面的一样
         */
        private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
                                                 int redLevel,
                                                 Iterator<?> it,
                                                 java.io.ObjectInputStream str,
                                                 V defaultVal)
                throws  java.io.IOException, ClassNotFoundException {
            /*
             * Strategy: The root is the middlemost element. To get to it, we
             * have to first recursively construct the entire left subtree,
             * so as to grab all of its elements. We can then proceed with right
             * subtree.
             *
             * The lo and hi arguments are the minimum and maximum
             * indices to pull out of the iterator or stream for current subtree.
             * They are not actually indexed, we just proceed sequentially,
             * ensuring that items are extracted in corresponding order.
             */
            if (hi < lo) return null;
    
            //这相当于除以二,取中间位置,相当于除以2
            int mid = (lo + hi) >>> 1;
    
            Entry<K,V> left  = null;
            //子树第一个元素的索引开始到中间的位置作为左子树,右边剩下递归又右子树
            if (lo < mid) //递归左边部分节点
                left = buildFromSorted(level+1, lo, mid - 1, redLevel,
                        it, str, defaultVal);
    
            K key;
            V value;
            //通过迭代器遍历所有节点
            if (it != null) {
                if (defaultVal==null) {
                    Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
                    key = (K)entry.getKey();
                    value = (V)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<>(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;
        }

    这里可以总结一点:左旋和右旋的判断主要依据是=》从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。因为我们每次新增的节点都是红色,所以这个红色的节点就会破坏原来的结构,会再红色的null节点新增一个黑色null节点,为了修复这种情况,那么就需要对父节点下的另外一个节点进行修复

    7.扩容

    不存在扩容问题,二叉树嘛,更类似链表的结构

    8.总结

    总结就是不论是新增还是删除,再修复颜色的时候,维持从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点这个原则
    所以一般会校验这个节点的兄弟,以及父辈的兄弟节点,至于进行右旋还是左旋,这个参考我之前的博客的内容以及规则


    参考:

    https://blog.csdn.net/cyywxy/article/details/81151104
    https://www.jianshu.com/p/e11fe1760a3d

  • 相关阅读:
    【LeetCode】456. 132 Pattern
    【Python&Sort】QuickSort
    Python虚拟环境的配置
    【LeetCode】459. Repeated Substring Pattern
    【LeetCode】462. Minimum Moves to Equal Array Elements II
    【LeetCode】20. Valid Parentheses
    radio 获取选中值
    页面加载时自动执行(加载)js的几种方法
    加一个字段: updateTime 更新时间
    多用户同时处理同一条数据解决办法
  • 原文地址:https://www.cnblogs.com/cutter-point/p/11587453.html
Copyright © 2011-2022 走看看