图解TreeMap是如何使用红黑树的

TreeMap与红黑树
本文详细解析了红黑树的基本概念及其在Java TreeMap中的应用,包括节点插入、旋转及自平衡过程,并提供了手写红黑树的指导。

1 红黑树概念

红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求

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

后面继续分析一下TreeMap中是如何运用红黑树的

2 红黑树在TreeMap中的应用

2.1 新建TreeMap并插入第一个节点

  TreeMap treeMap = new TreeMap();
  treeMap.put(5,5);

表面看起来很简单:存了一个5:5的键值对,接下来分析看一下具体做了什么。
第一步初始化:只构造了一个TreeMap对象,comparator 赋值为空。

 public TreeMap() {
        comparator = null;
    }

这时候整棵树没有任何元素。

第二步存入5:5

public V put(K key, V value) {
		// 这里root是null
        Entry<K,V> t = root;
        if (t == null) {
            // 进入if,这里主要为了判断key是不是null
            compare(key, key); // type (and possibly null) check
			// 新插入的5:5作为根节点
            root = new Entry<>(key, value, null);
            // size是元素个数
            size = 1;
            // modCount作为记录map增删改查次数
            modCount++;
            return null;
        }
        ....

这里顺便看一下Entry中包含什么

static final class Entry<K,V> implements Map.Entry<K,V> {
        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;
        }
        ....

当前节点的键值信息,还有左右子节点以及父节点。默认颜色为黑色,所以刚才的根节点不需要染色操作。
这时候整棵树是这样的情况:
在这里插入图片描述

2.2 插入6:6

直接跟一下源码看看做了什么

treeMap.put(6,6);


public V put(K key, V value) {
        Entry<K,V> t = root;
        ...
        int cmp;
        Entry<K,V> parent;
        Comparator<? super K> cpr = comparator;
        // 这里的判断是为了区分使用comparator还是用key直接进行比较。if和else中的两个循环内容都已一样的操作。因为我们的comparator是null,所以看一下else中的。
        if (cpr != null) {
        ...
        }
        else {
            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
                	// 这里如果发现key已经存在,则直接覆盖原来的值
                    return t.setValue(value);
            } while (t != null);
        }
        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的操作:已有根节点的情况下,put方法主要内容是找到要挂靠的父节点,然后跳出循环根据cmp判断是左节点还是右侧节点。
这时候树形结构是这样:
在这里插入图片描述
可以发现已经违背了上面的“要求5:从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点”。继续看fixAfterInsertion中做了什么

/** From CLR */
    private void fixAfterInsertion(Entry<K,V> x) {
    	// 首先给新插入的节点染色为红色
        x.color = RED;
		// 这里6:6的父节点5:5是黑色,所以不会走入循环
        while (x != null && x != root && x.parent.color == RED) {
        ...
        }
        // 根节点染黑后直接返回
        root.color = BLACK;
    }

到这一步put才算是完整走完
这时候的树形结构:
在这里插入图片描述
这里可以总结出:新插入的节点都是红色

2.3 插入7:7

在7:7刚插入并且染色为红色时树形结构是这样:
在这里插入图片描述
这时候的红红相连,红黑树已经不平衡了,需要进行自平衡操作。(不符合 4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点))

/** From CLR */
    private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;
		// 7:7的父节点是红色,进行自平衡操作
        while (x != null && x != root && x.parent.color == RED) {
            // 这个判断是意思是:当前节点的父节点是不是一个左侧的节点
            // 7:7的父节点6:6,是5:5的右侧节点,所以当前判断是false
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            ...
            } else {
            	// 这里是拿到当前节点的父节点的兄弟节点,所以这里6:6的兄弟节点是null
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                // 获取y的颜色,叶子结点默认黑色,这里是false
                if (colorOf(y) == RED) {
                ...
                } else {
                    // 这里判断一下当前节点是否是一个左侧节点,7:7是6:6的右节点,所以不会走到这里
                    if (x == leftOf(parentOf(x))) {
                    ...
                    }
                    // 父节点染色为黑色
                    setColor(parentOf(x), BLACK);
                    // 父节点的父节点染色为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 针对父节点的父节点左旋
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }
2.3.1 左旋和右旋

看一下左旋做了什么:

	
    private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            // 1 拿到当前节点的右侧节点
            Entry<K,V> r = p.right;
            // 2 把当前节点的右侧节点,改为自己右侧节点的左侧节点
            p.right = r.left;
            // 2.1 迎合上一步,把子节点的指针重新知道正确的父节点
            if (r.left != null)
                r.left.parent = p;
            // 3 当前右侧节点的父节点改为当前节点的父节点
            r.parent = p.parent;
            // 3.1 没有父节点说明是root节点,把右节点作为root
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                // 3.2 迎合3 修改父节点到子节点的指针
                p.parent.left = r;
            else
            	// 3.3 和3.2一样只是一左一右
                p.parent.right = r;
            // 4 右节点的左侧节点改为子节点
            r.left = p;
            // 5 当前节点的父节点改为右节点
            p.parent = r;
        }
    }

这里就是完整的一个左旋操作。
示例一下途中是针对节点5的左旋操作,右旋不说了换个方向而已。

在这里插入图片描述

在结构上理解的话,对一个节点的左旋就是如下几步:

  • 1 右侧子节点代替自己原来的位置
  • 2 节点和它的换到左侧子节点的位置
  • 3 节点的右侧节点改为原子节点的左侧节点

在我们的树形结构中5:5,6:6,7:7相当于5,7,8的位置,这个染色和移动的结果如下。
在这里插入图片描述
这一步通过两次染色和一次左旋完成了平衡。

2.4 插入8:8

插入8:8之后的结构:
在这里插入图片描述
红红相连,又一次打破平衡了。看一下自平衡这次做了那些

/** From CLR */
    private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;

        while (x != null && x != root && x.parent.color == RED) {
        	// 当前父节点仍然是个右侧节点,进入else
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            ...
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                // 这里父节点的兄弟节点是红色,进入if
                if (colorOf(y) == RED) {
                	// 7:7变为黑色
                    setColor(parentOf(x), BLACK);
                    // 5:5变为黑色
                    setColor(y, BLACK);
                    // 6:6变为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 当前节点改为6:6,发现不符合循环条件了
                    x = parentOf(parentOf(x));
                } else {
                ...
                }
            }
        }
        // 6:6染色为黑
        root.color = BLACK;
    }

这一次平衡发现只有染色操作也达到了了平衡的目的,整体流程如下图
在这里插入图片描述
这一步可以总结出:染色平衡优先于左旋和右旋

2.5 插入7.5:7.5

插入7.5:7.5之后的结构
在这里插入图片描述
依然需要平衡

/** From CLR */
    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)))) {
            ...
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                ...
                } else {
                   	// 这一次终于遇到了当前节点是左侧节点的情况
                    if (x == leftOf(parentOf(x))) {
                        // 直接针对当前节点的父节点,也就是8:8进行一个右旋
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    // 继续染色,左旋
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }

一共进行了三步,右旋,染色,左旋。最终达到平衡,如下图
在这里插入图片描述

这一步可以总结出:当前节点属于右侧子树,插入了左侧节点时。先右旋,满足左旋条件然后再染色左旋
到这一步已经走完了右侧子树所有情况,也就是if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {的else全部走完了。左侧子树就是换个方向,其他都同理。

2.6 删除操作

红黑树中删除节点的操作主要思想是找出一个节点来代替被删除的节点,然后进行平衡。
在这里插入图片描述
以上图举例,删除6:6这个节点。来跟一下代码

private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

       	// 这一部分的意思是:如果删除的节点拥有两个子节点,则找到右侧子树最小的那个节点。然后代替自己的位置,再删除被找到的节点。
       	// 这里明细6:6有两个子节点,找到替换的节点也就是7:7来代替6:6。然后开始删除7:7
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } 

        // 这里准备找出替代自己的子节点,7:7没有,所以是null
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

		// 下面的就是删除操作了,主要内容就是替换指针。如果被删除的是黑色则进行自平衡操作
        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;

            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
        } else { //  No children. Use self as phantom replacement and unlink.
            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;
            }
        }
    }

3 如何手写红黑树

回头看一下红黑树的5个要求,如果只看这个还是无法自己手写。但是理清TreeMap中的实现之后发现,要实现红黑树也很简单。代码就不写了JDK里都有,这里简单总结几个手写红黑树的要点(参考JDK的版本)

  • 1 新插入的节点全部是红色
  • 2 如果新插入节点父节点是红色且不是根节点,开始自平衡循环处理
  • 3 如果父节点的兄弟节点是红色,则进行染色处理。然后把祖父节点当做新插入节点循环
  • 4 如果当前插入到右侧子树的右侧节点,左旋。如果是左侧节点,先右旋再左旋。把原来的父节点作为新插入节点循环。左侧的话相反操作
  • 5 最后根节点染色为黑色

对应一下自平衡的代码

/** From CLR */
    private void fixAfterInsertion(Entry<K,V> x) {
       // 第1步
        x.color = RED;
		// 第2步
        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) {
                	// 左侧子树第3步
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                	// 左侧子树第4步
                    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) {
                	// 右侧子树第3步
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                	// 右侧子树第4步
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        // 第5步
        root.color = BLACK;
    }
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值