当前位置: 移动技术网 > IT编程>开发语言>Java > TreeMap原理实现及常用方法

TreeMap原理实现及常用方法

2019年07月21日  | 移动技术网IT编程  | 我要评论

前面我们分别讲了map接口的两个实现类hashmaplinkedhashmap,本章我们讲一下map接口另一个重要的实现类treemap,treemap或许不如hashmap那么常用,但存在即合理,它也有自己的应用场景,treemap可以实现元素的自动排序。

一. treemap概述

  1. treemap存储k-v键值对,通过红黑树(r-b tree)实现;
  2. treemap继承了navigablemap接口,navigablemap接口继承了sortedmap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要treemap自己去实现;
  3. treemap实现了cloneable接口,可被克隆,实现了serializable接口,可序列化;
  4. treemap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过key值的自然顺序进行排序;

二. 红黑树回顾

因为treemap的存储结构是红黑树,我们回顾一下红黑树的特点以及基本操作,红黑树的原理可参考关于红黑树(r-b tree)原理,看这篇如何。下图为典型的红黑树:

红黑树规则特点:

  1. 节点分为红色或者黑色;
  2. 根节点必为黑色;
  3. 叶子节点都为黑色,且为null;
  4. 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
  5. 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
  6. 新加入到红黑树的节点为红色节点;

红黑树自平衡基本操作:

  1. 变色:在不违反上述红黑树规则特点情况下,将红黑树某个node节点颜色由红变黑,或者由黑变红;
  2. 左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点
  3. 右旋:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点

三. treemap构造

我们先看一下treemap中主要的成员变量

/**
 * 我们前面提到treemap是可以自动排序的,默认情况下comparator为null,这个时候按照key的自然顺序进行排
 * 序,然而并不是所有情况下都可以直接使用key的自然顺序,有时候我们想让map的自动排序按照我们自己的规则,
 * 这个时候你就需要传递comparator的实现类
 */
private final comparator<? super k> comparator;

/**
 * treemap的存储结构既然是红黑树,那么必然会有唯一的根节点。
 */
private transient entry<k,v> root;

/**
 * map中key-val对的数量,也即是红黑树中节点entry的数量
 */
private transient int size = 0;

/**
 * 红黑树结构的调整次数
 */
private transient int modcount = 0;

上面的主要成员变量根节点root是entry类的实体,我们来看一下entry类的源码

static final class entry<k,v> implements map.entry<k,v> {
    //key,val是存储的原始数据
    k key;
    v value;
    //定义了节点的左孩子
    entry<k,v> left;
    //定义了节点的右孩子
    entry<k,v> right;
    //通过该节点可以反过来往上找到自己的父亲
    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;
    }

    /**
     * 获取节点的key值
     */
    public k getkey() {return key;}

    /**
     * 获取节点的value值
     */
    public v getvalue() {return value;}

    /**
     * 用新值替换当前值,并返回当前值
     */
    public v setvalue(v value) {
        v oldvalue = this.value;
        this.value = value;
        return oldvalue;
    }

    public boolean equals(object o) {
        if (!(o instanceof map.entry))
            return false;
        map.entry<?,?> e = (map.entry<?,?>)o;
        return valequals(key,e.getkey()) && valequals(value,e.getvalue());
    }

    public int hashcode() {
        int keyhash = (key==null ? 0 : key.hashcode());
        int valuehash = (value==null ? 0 : value.hashcode());
        return keyhash ^ valuehash;
    }

    public string tostring() {
        return key + "=" + value;
    }
}

entry静态内部类实现了map的内部接口entry,提供了红黑树存储结构的java实现,通过left属性可以建立左子树,通过right属性可以建立右子树,通过parent可以往上找到父节点。

大体的实现结构图如下:

treemap构造函数:

//默认构造函数,按照key的自然顺序排列
public treemap() {comparator = null;}
//传递comparator具体实现,按照该实现规则进行排序
public treemap(comparator<? super k> comparator) {this.comparator = comparator;}
//传递一个map实体构建treemap,按照默认规则排序
public treemap(map<? extends k, ? extends v> m) {
    comparator = null;
    putall(m);
}
//传递一个map实体构建treemap,按照传递的map的排序规则进行排序
public treemap(sortedmap<k, ? extends v> m) {
    comparator = m.comparator();
    try {
        buildfromsorted(m.size(), m.entryset().iterator(), null, null);
    } catch (java.io.ioexception cannothappen) {
    } catch (classnotfoundexception cannothappen) {
    }
}

四. put方法

put方法为map的核心方法,treemap的put方法大概流程如下:

我们来分析一下源码

public v put(k key, v value) {
    entry<k,v> t = root;
    /**
     * 如果根节点都为null,还没建立起来红黑树,我们先new entry并赋值给root把红黑树建立起来,这个时候红
     * 黑树中已经有一个节点了,同时修改操作+1。
     */
    if (t == null) {
        compare(key, key); 
        root = new entry<>(key, value, null);
        size = 1;
        modcount++;
        return null;
    }
    /**
     * 如果节点不为null,定义一个cmp,这个变量用来进行二分查找时的比较;定义parent,是new entry时必须
     * 要的参数
     */
    int cmp;
    entry<k,v> parent;
    // cpr表示有无自己定义的排序规则,分两种情况遍历执行
    comparator<? super k> cpr = comparator;
    if (cpr != null) {
        /**
         * 从root节点开始遍历,通过二分查找逐步向下找
         * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法
         * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的key<root.key,那么
         * 继续在root的左子树中找,从root的左孩子节点(root.left)开始:如果传入的key>root.key,
         * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key,
         * 那么直接根据root节点的value值即可。
         * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
         *
         * 需要注意的是:这里并没有对key是否为null进行判断,建议自己的实现comparator时应该要考虑在内
         */
        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 {
        //从这里看出,当默认排序时,key值是不能为null的
        if (key == null)
            throw new nullpointerexception();
        @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);
    }
    /**
     * 能执行到这里,说明前面并没有找到相同的key,节点已经遍历到最后了,我们只需要new一个entry放到
     * parent下面即可,但放到左子节点上还是右子节点上,就需要按照红黑树的规则来。
     */
    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;
}

put方法源码中通过fixafterinsertion(e)方法来进行自平衡处理,我们回顾一下插入时自平衡调整的逻辑,下表中看不懂的名词可以参考关于红黑树(r-b tree)原理,看这篇如何

无需调整 【变色】即可实现平衡 【旋转+变色】才可实现平衡
情况1: 当父节点为黑色时插入子节点 空树插入根节点,将根节点红色变为黑色 父节点为红色左节点,叔父节点为黑色,插入左子节点,那么通过【左左节点旋转】
情况2: - 父节点和叔父节点都为红色 父节点为红色左节点,叔父节点为黑色,插入右子节点,那么通过【左右节点旋转】
情况3: - - 父节点为红色右节点,叔父节点为黑色,插入左子节点,那么通过【右左节点旋转】
情况4: - - 父节点为红色右节点,叔父节点为黑色,插入右子节点,那么通过【右右节点旋转】

接下来我们看一看这个方法

private void fixafterinsertion(entry<k,v> x) {
    //新插入的节点为红色节点
    x.color = red;
    //我们知道父节点为黑色时,并不需要进行树结构调整,只有当父节点为红色时,才需要调整
    while (x != null && x != root && x.parent.color == red) {
        //如果父节点是左节点,对应上表中情况1和情况2
        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 {
                //如果插入节点是黑色,插入的是右子节点,通过【左右节点旋转】(这里先进行父节点左旋)
                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;
}

源码中通过 rotateleft 进行【左旋】,通过 rotateright 进行【右旋】。都非常类似,我们就看一下【左旋】的代码,【左旋】规则如下:“逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点”。

private void rotateleft(entry<k,v> p) {
    if (p != null) {
        /**
         * 断开当前节点p与其右子节点的关联,重新将节点p的右子节点的地址指向节点p的右子节点的左子节点
         * 这个时候节点r没有父节点
         */
        entry<k,v> r = p.right;
        p.right = r.left;
        //将节点p作为节点r的父节点
        if (r.left != null)
            r.left.parent = p;
        //将节点p的父节点和r的父节点指向同一处
        r.parent = p.parent;
        //p的父节点为null,则将节点r设置为root
        if (p.parent == null)
            root = r;
        //如果节点p是左子节点,则将该左子节点替换为节点r
        else if (p.parent.left == p)
            p.parent.left = r;
        //如果节点p为右子节点,则将该右子节点替换为节点r
        else
            p.parent.right = r;
        //重新建立p与r的关系
        r.left = p;
        p.parent = r;
    }
}

就算是看了上面的注释还是并不清晰,看下图你就懂了

五. get 方法

get方法是通过二分查找的思想,我们看一下源码

public v get(object key) {
    entry<k,v> p = getentry(key);
    return (p==null ? null : p.value);
}
/**
 * 从root节点开始遍历,通过二分查找逐步向下找
 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过k.compareto(p.key)比较传入的key和
 * 根节点的key值;
 * 如果传入的key<root.key, 那么继续在root的左子树中找,从root的左孩子节点(root.left)开始;
 * 如果传入的key>root.key, 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;
 * 如果恰好key==root.key,那么直接根据root节点的value值即可。
 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
 */
//默认排序情况下的查找
final entry<k,v> getentry(object key) {
    
    if (comparator != null)
        return getentryusingcomparator(key);
    if (key == null)
        throw new nullpointerexception();
    @suppresswarnings("unchecked")
    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;
}
/**
 * 从root节点开始遍历,通过二分查找逐步向下找
 * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法
 * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的key<root.key,那么
 * 继续在root的左子树中找,从root的左孩子节点(root.left)开始:如果传入的key>root.key,
 * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key,
 * 那么直接根据root节点的value值即可。
 * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
 */
//自定义排序规则下的查找
final entry<k,v> getentryusingcomparator(object key) {
    @suppresswarnings("unchecked")
    k k = (k) key;
    comparator<? super k> cpr = comparator;
    if (cpr != null) {
        entry<k,v> p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

六. remove方法

remove方法可以分为两个步骤,先是找到这个节点,直接调用了上面介绍的getentry(object key),这个步骤我们就不说了,直接说第二个步骤,找到后的删除操作。

public v remove(object key) {
    entry<k,v> p = getentry(key);
    if (p == null)
        return null;

    v oldvalue = p.value;
    deleteentry(p);
    return oldvalue;
}

通过deleteentry(p)进行删除操作,删除操作的原理我们在前面已经讲过

  1. 删除的是根节点,则直接将根节点置为null;
  2. 待删除节点的左右子节点都为null,删除时将该节点置为null;
  3. 待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可;
  4. 待删除节点的左右子节点都不为null,则找前驱或者后继,将前驱或者后继的值复制到该节点中,然后删除前驱或者后继(前驱:左子树中值最大的节点,后继:右子树中值最小的节点);
private void deleteentry(entry<k,v> p) {
    modcount++;
    size--;
    //当左右子节点都不为null时,通过successor(p)遍历红黑树找到前驱或者后继
    if (p.left != null && p.right != null) {
        entry<k,v> s = successor(p);
        //将前驱或者后继的key和value复制到当前节点p中,然后删除节点s(通过将节点p引用指向s)
        p.key = s.key;
        p.value = s.value;
        p = s;
    } 
    entry<k,v> replacement = (p.left != null ? p.left : p.right);
    /**
     * 至少有一个子节点不为null,直接用这个有值的节点替换掉当前节点,给replacement的parent属性赋值,给
     * parent节点的left属性和right属性赋值,同时要记住叶子节点必须为null,然后用fixafterdeletion方法
     * 进行自平衡处理
     */
    if (replacement != null) {
        //将待删除节点的子节点挂到待删除节点的父节点上。
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;
        p.left = p.right = p.parent = null;
        /**
         * p如果是红色节点的话,那么其子节点replacement必然为红色的,并不影响红黑树的结构
         * 但如果p为黑色节点的话,那么其父节点以及子节点都可能是红色的,那么很明显可能会存在红色相连的情
         * 况,因此需要进行自平衡的调整
         */
        if (p.color == black)
            fixafterdeletion(replacement);
    } else if (p.parent == null) {//这种情况就不用多说了吧
        root = null;
    } else { 
        /**
         * 如果p节点为黑色,那么p节点删除后,就可能违背每个节点到其叶子节点路径上黑色节点数量一致的规则,
         * 因此需要进行自平衡的调整
         */ 
        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;
        }
    }
}

操作的操作其实很简单,场景也不多,我们看一下删除后的自平衡操作方法fixafterdeletion

private void fixafterdeletion(entry<k,v> x) {
    /**
     * 当x不是root节点且颜色为黑色时
     */
    while (x != root && colorof(x) == black) {
        /**
         * 首先分为两种情况,当前节点x是左节点或者当前节点x是右节点,这两种情况下面都是四种场景,这里通过
         * 代码分析一下x为左节点的情况,右节点可参考左节点理解,因为它们非常类似
         */
        if (x == leftof(parentof(x))) {
            entry<k,v> sib = rightof(parentof(x));

            /**
             * 场景1:当x是左黑色节点,兄弟节点sib是红色节点
             * 兄弟节点由红转黑,父节点由黑转红,按父节点左旋,
             * 左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点
             */
            if (colorof(sib) == red) {
                setcolor(sib, black);
                setcolor(parentof(x), red);
                rotateleft(parentof(x));
                sib = rightof(parentof(x));
            }

            /**
             * 场景2:节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变
             * 红,同时将x指向当前x的父节点
             */
            if (colorof(leftof(sib))  == black &&
                colorof(rightof(sib)) == black) {
                setcolor(sib, red);
                x = parentof(x);
            } else {
                /**
                 * 场景3:节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时,
                 * 需要将sib左子节点设置为黑色,sib节点设置为红色,同时按sib右旋,再将sib指向x的
                 * 兄弟节点
                 */
                if (colorof(rightof(sib)) == black) {
                    setcolor(leftof(sib), black);
                    setcolor(sib, red);
                    rotateright(sib);
                    sib = rightof(parentof(x));
                }
                /**
                 * 场景4:节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、
                 * 左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色,
                 * 设置x的父节点为黑色,设置sib右子节点为黑色,左旋x的父节点p,然后将x赋值为root
                 */
                setcolor(sib, colorof(parentof(x)));
                setcolor(parentof(x), black);
                setcolor(rightof(sib), black);
                rotateleft(parentof(x));
                x = root;
            }
        } else {//x是右节点的情况
            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);
}

当待操作节点为左节点时,上面描述了四种场景,而且场景之间可以相互转换,如deleteentry后进入了场景1,经过场景1的一些列操作后,红黑树的结构并没有调整完成,而是进入了场景2,场景2执行完成后跳出循环,将待操作节点设置为黑色,完成。我们下面用图来说明一下四种场景帮助理解,当然大家最好自己手动画一下。

场景1:

当x是左黑色节点,兄弟节点sib是红色节点,需要兄弟节点由红转黑,父节点由黑转红,按父节点左旋,左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点。

但经过这一系列操作后,并没有结束,而是可能到了场景2,或者场景3和4

场景2:

节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变红,同时将x指向当前x的父节点

经过场景2的一系列操作后,循环就结束了,我们跳出循环,将节点x设置为黑色,自平衡调整完成。

场景3:

节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时,需要将sib左子节点设置为黑色,sib节点设置为红色,同时按sib右旋,再将sib指向x的兄弟节点

并没有完,场景3的一系列操作后,会进入到场景4

场景4:

节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色,设置x的父节点颜色为黑色,设置sib右孩子的颜色为黑色,左旋x的父节点p,然后将x赋值为root

四种场景讲完了,删除后的自平衡操作不太好理解,代码层面的已经弄明白了,但如果让我自己去实现的话,还是差了一些,还需要再研究。

七. 遍历

遍历比较简单,treemap的遍历可以使用map.values(), map.keyset(),map.entryset(),map.foreach(),这里不再多说。

八. 总结

本文详细介绍了treemap的基本特点,并对其底层数据结构红黑树进行了回顾,同时讲述了其自动排序的原理,并从源码的角度结合红黑树图形对put方法、get方法、remove方法进行了讲解,最后简单提了一下遍历操作,若有不对之处,请批评指正,望共同进步,谢谢!

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网