Skip to main content

源码分析[4] HashMap之红黑树

源码hashmap红黑树About 6 min

二叉搜索树BST

先介绍一下二叉搜索树. 之后再引出红黑树.

BST的特点

  1. 当前节点的左子节点小于当前节点.
  2. 当前节点的右子节点大于当前节点.

缺陷

我们先计算一下理想情况下的BST的时间复杂度. 对于节点数为n的二叉树, 其深度m满足:
2m112^{m-1}-1 < n <= 2m12^m-1
如果遍历一个节点的时间花费为O(1), 那么遍历到我们想要的节点就大约需要: O(log_2n)
性能还是不错的.
但是当二叉树倾斜后时间复杂度将变为O(n), 此时的查找为链表查找, 每次都会寻址, 查询效率大大降低.

下面是我写的一段BST实现的代码:

package com.nikola.treetest.tree;

import java.util.Objects;

import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

/**
 * 二叉树的实现
 * 
 * @author nikolazhang
 */
@Slf4j
public class BST {

    public static void main(final String[] args) {
        final BST bst = new BST();
        final Node<Integer> head = bst.new Node<>(7);
        head.addChild(2);
        head.addChild(6);
        head.addChild(7);
        head.addChild(9);
        head.addChild(10);
        head.addChild(11);
        head.addChild(12);
        head.addChild(13);
        head.addChild(14);
        head.addChild(15);
        head.addChild(16);
        log.debug("输出信息为: {}", head);
    }

    @Data
    @ToString(exclude = "parent")
    public class Node<T extends Comparable<T>> {
        private T data;
        private Node<T> left;
        private Node<T> right;
        private Node<T> parent;

        public Node(final T t) {
            this.data = Objects.requireNonNull(t);
        }

        /**
         * 向二叉树中添加节点
         * 
         * @param t 将要放入二叉树中的值
         */
        public void addChild(final T t) {
            // 创建一个新节点
            final Node<T> child = new Node<>(Objects.requireNonNull(t));
            // 遍历当前节点信息并比较两个节点
            Node<T> e;
            int res;
            for (e = this; e != null;) {
                // 比较当前节点和新节点, 如果新节点小于等于当前节点则添加到左子树, 否则添加到右子树
                if ((res = e.compareTo(child)) < 0) {
                    if (e.left == null) {
                        e.left = child;
                        child.setParent(e);
                        break;
                    } else {
                        e = e.left;
                    }
                } else if (res == 0) {
                    log.debug("和当前节点数值相同, 不做处理");
                    break;
                } else {
                    if (e.right == null) {
                        e.right = child;
                        child.setParent(e);
                        break;
                    } else {
                        e = e.right;
                    }
                }
            }
        }

        /**
         * 删除元素
         * 
         * @param t
         */
        public boolean deleteChild(T t) {
            Node<T> targetNode = findNode(t);
            if (Objects.nonNull(targetNode)) {
                Node<T> parentNode = targetNode.getParent();
                parentNode.setLeft(targetNode.getLeft());
                parentNode.setRight(targetNode.getRight());
                targetNode = null;
                return true;
            } else {
                log.debug("{} 不存在", t);
                return false;
            }
        }

        /**
         * 判断t是否存在
         * 
         * @param t
         * @return
         */
        public boolean isExist(final T t) {
            // 返回节点不为空则存在
            return Objects.nonNull(findNode(t));
        }

        /**
         * 查询t所在的节点
         * 
         * @param t
         * @return
         */
        public Node<T> findNode(final T t) {
            Node<T> e;
            int res;
            for (e = this; e != null;) {
                if ((res = compareTo(t, e.getData())) < 0) {
                    if (e.left == null) {
                        return null;
                    } else {
                        e = e.left;
                    }
                } else if (res == 0) {
                    return e;
                } else {
                    if (e.right == null) {
                        return null;
                    } else {
                        e = e.right;
                    }
                }
            }
            return null;
        }

        /**
         * 比较两个节点
         */
        public int compareTo(final Node<T> o) {
            return compareTo(o.getData(), this.data);
        }

        /**
         * 比较数值
         * 
         * @param t1
         * @param t2
         * @return
         */
        private int compareTo(T t1, T t2) {
            return t1.compareTo(t2);
        }

    }
}

输出结果为(这里我手动格式化了一下):

BST.Node(
    data=7,
    left=BST.Node(
        data=2,
        left=null,
        right=BST.Node(
            data=6,
            left=null,
            right=null)),
    right=BST.Node(
        data=9,
        left=null,
        right=BST.Node(
            data=10,
            left=null,
            right=BST.Node(
                data=11,
                left=null,
                right=BST.Node(
                    data=12,
                    left=null,
                    right=BST.Node(
                        data=13,
                        left=null,
                        right=BST.Node(
                            data=14,
                            left=null,
                            right=BST.Node(
                                data=15,
                                left=null,
                                right=BST.Node(
                                    data=16,
                                    left=null,
                                    right=null)))))))))

很明显添加形如: 10, 11, 12, 13, 14......这样的递增数字的时候, 会导致二叉树不断右斜(所有的子节点都在右子节点上).
进而性能向链表退化.

红黑树

平衡二叉树可以解决BST的倾斜退化问题, 红黑树就是这其中的一种解决方案. (另外如AVL等)

红黑树性质

  1. 任何一个节点都有颜色,黑色或者红色
  2. 根节点是黑色的
  3. 父子节点之间不能出现两个连续的红节点
  4. 任何一个节点向下遍历到其子孙的叶子节点,所经过的黑节点个数必须相等
  5. 空节点被认为是黑色的

红黑树的自平衡

红黑树进行自平衡有以下几种处理方法: 变色, 左旋, 右旋.

这里简单说一下左旋和右旋的步骤, 请看下图(分别以R,H 和 L,H做左旋和右旋演示, 图画的有点拙劣 X_X):
2020-01-12-20-18-49

对于新手,建议你先有这几点意识:

  1. 旋转只会发生在父子节点之间
  2. 旋转之后, 父子节点关系发生变化, 父节点变成子节点, 子节点变成父节点.
  3. 旋转方向上的子节点的子节点会改变父级指向. 比如: 右旋则子级节点的右子节点的父级会指向到子节点的父级上, 成为其左子节点.
  4. 左旋只会发生在右子节点和父节点之间; 右旋只会发生在左子节点和父节点之间.
  5. 左旋其父节点最终要变为左子节点, 右旋父节点最终要变成右子节点.

演示

下面演示加入10, 5, 20, 1, 3生成红黑树的过程:

  1. 添加10时, 父节点为空, 则将10设置为头节点, 并将其涂成黑色.
    2020-01-12-09-38-14
  2. 当新的节点插入时, 颜色要设置为红色, 插入红色时, 不会影响到达新的节点的黑色数量的计算. 但是插入红色唯一出现的问题是会违反规则3, 这个时候就需要进行变色和旋转. 使红黑树重新达到自平衡.
  3. 当插入节点5时, 我们设置其节点为红色, 校验父节点的颜色, 发现为黑色. 则红黑树条件满足. 插入节点20的情况与之相同
    2020-01-12-09-47-292020-01-12-09-48-19
  4. 当插入节点1时, 遍历红黑树此时1应该放在5的左子树上. 当1设置为红色后, 发现父节点5也为红色. 这时我们需要将父节点5变为黑色,这时到达20的子节点的黑色节点数量和到达5的子节点的黑色数量不一致, 因此兄弟节点20也需要变成黑色, 以满足条件4.
    2020-01-12-10-07-19
  5. 之后再添加节点3, 3要先放到1的右子树上(如图所示). 并且颜色为红色.
    2020-01-12-13-16-01
    首先我们先进行旋转让树达到平衡, 首先肯定不能对1和5进行右旋,因为3占据了右子节点. 因此先对1,3进行左旋, 如下图.
    2020-01-12-13-58-44
    之后再进行3,5右旋,如下图.
    2020-01-12-21-09-59
    进行完以上处理树已经缴纳满足平衡二叉树了. 但是仍旧不满足条件3,4. 此时我们进行变色处理. 将5变成红色, 3变成黑色.
    2020-01-12-21-08-33
    这样就满足了红黑树的5个条件

end

这里有一个数据结构的模拟网站: www.cs.usfca.eduopen in new window有兴趣可以自己尝试一下.
这篇文章我也是边学习边探索边写的(主要是探索), 有些坑还没有填上(比如红黑树的删除). 下一篇继续再说说hashmap中的红黑树的实现吧.

What do you think?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v3.0.0-alpha.10