红黑树从根出发,溯本求源

上一篇写HashMap底层数据结构因为偷懒(嘘!)  因为篇幅原因没有展开红黑树的部分,所以周末特意补了一篇简单介绍一下红黑树弥补一下内心的空缺。

说到红黑树,它其实已经是一棵比较高级的树了,它的起源需要追述到最最基础的二叉树。

 

1 二叉树

1.1 定义

In computer science, a binary tree is a tree data structure in which each node has at most two children, which are referred to as the left child and the right child. A recursive definition using just set theory notions is that a (non-empty) binary tree is a triple (L, S, R), where L and R are binary trees or the empty set and S is a singleton set.Some authors allow the binary tree to be the empty set as well.

                                                                                                                                                                          --维基百科

1.2 图解

1.3 二叉树遍历

1.3.1 前序遍历

  • 输出当前节点;
  • 如果左子树不为空,则遍历左子树;
  • 如果右子树不为空,则遍历右子树;

 

1.3.2 中序遍历

  • 如果左子树不为空,则遍历左子树;
  • 输出当前节点;
  • 如果右子树不为空,则遍历右子树;

1.3.3 后续遍历

  • 如果左子树不为空,则遍历左子树;
  • 如果右子树不为空,则遍历右子树;
  • 输出当前节点;

1.3.4 二叉树遍历简易实现

public class BinaryTreeTest{
    public static void main(String[] args) {
        HeroNode root = new HeroNode(0,"美国队长");
        HeroNode h1 = new HeroNode(1,"钢铁侠");
        HeroNode h2 = new HeroNode(2,"绿巨人");
        HeroNode h3 = new HeroNode(3,"蚁人");
        HeroNode h4 = new HeroNode(4,"黑寡妇");
        HeroNode h5 = new HeroNode(5,"雷神");

        root.setLeft(h1);
        root.setRight(h2);
        h1.setLeft(h3);
        h1.setRight(h4);
        h2.setLeft(h5);

        Tree tree = new Tree();
        tree.setRoot(root);

        System.out.println("------------二叉树遍历-----------");
        System.out.println("前序遍历:");
        tree.preOrder();
        System.out.println();
        System.out.println("中序遍历:");
        tree.midOrder();
        System.out.println();
        System.out.println("后序遍历:");
        tree.sufOrder();
        System.out.println();
    }

}

class Tree {
    private HeroNode root;

    //前序遍历
    public void preOrder(){
        if(this.root!=null){
            this.root.preOrder();
        }
    }

    //中序遍历
    public void midOrder(){
        if(this.root!=null){
            this.root.midOrder();
        }
    }

    //后序遍历
    public void sufOrder(){
        if (this.root!=null){
            this.root.sufOrder();
        }
    }


    public HeroNode getRoot() {
        return root;
    }

    public void setRoot(HeroNode root) {
        this.root = root;
    }
}


class HeroNode{
    private int no;
    private String name;
    private HeroNode left;
    private HeroNode right;

    //前序遍历:先输出当前节点,在输出当前节点的左子树,在输出当前节点的右子树
    public void preOrder(){
        System.out.println(this);
        if(this.left!=null){
            this.left.preOrder();
        }
        if(this.right!=null){
            this.right.preOrder();
        }
    }

    //中序遍历:先输出左子树,再输出当前节点,再输出右子树
    public void midOrder(){
        if(this.left !=null){
            this.left.midOrder();
        }
        System.out.println(this);
        if(this.right!=null){
            this.right.midOrder();
        }
    }


    //后续遍历:先输出左子树,再输出右子树,再输出当前节点
    public void sufOrder(){
        if(this.left !=null){
            this.left.sufOrder();
        }
        if(this.right!=null){
            this.right.sufOrder();
        }
        System.out.println(this);
    }


    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }
}

执行结果:

------------二叉树遍历-----------
前序遍历:
HeroNode{no=0, name='美国队长'}
HeroNode{no=1, name='钢铁侠'}
HeroNode{no=3, name='蚁人'}
HeroNode{no=4, name='黑寡妇'}
HeroNode{no=2, name='绿巨人'}
HeroNode{no=5, name='雷神'}

中序遍历:
HeroNode{no=3, name='蚁人'}
HeroNode{no=1, name='钢铁侠'}
HeroNode{no=4, name='黑寡妇'}
HeroNode{no=0, name='美国队长'}
HeroNode{no=5, name='雷神'}
HeroNode{no=2, name='绿巨人'}

后序遍历:
HeroNode{no=3, name='蚁人'}
HeroNode{no=4, name='黑寡妇'}
HeroNode{no=1, name='钢铁侠'}
HeroNode{no=5, name='雷神'}
HeroNode{no=2, name='绿巨人'}
HeroNode{no=0, name='美国队长'}

1.3.5 小结

二叉树前中后序遍历主要看当前节点什么时候输出,当前节点最先输出是前序,当前节点中间输出是中序,当前节点最后输出是后续。

 

1.4 二叉树查找

二叉树查找就是在二叉树遍历的基础上加上等值判断而已,不赘述,直接上代码。

public class BinaryTreeTest{
    public static void main(String[] args) {
        HeroNode root = new HeroNode(0,"美国队长");
        HeroNode h1 = new HeroNode(1,"钢铁侠");
        HeroNode h2 = new HeroNode(2,"绿巨人");
        HeroNode h3 = new HeroNode(3,"蚁人");
        HeroNode h4 = new HeroNode(4,"黑寡妇");
        HeroNode h5 = new HeroNode(5,"雷神");

        root.setLeft(h1);
        root.setRight(h2);
        h1.setLeft(h3);
        h1.setRight(h4);
        h2.setLeft(h5);

        Tree tree = new Tree();
        tree.setRoot(root);

        System.out.println("-------------二叉树查找-----------");
        System.out.println("前序查找:");
        System.out.println(tree.preSearch(3));
        System.out.println();
        System.out.println("中序查找:");
        System.out.println(root.midSearch(3));
        System.out.println();
        System.out.println("后续查找:");
        System.out.println(root.sufSearch(3));
    }

}

class Tree {
    private HeroNode root;

    //前序查找
    public HeroNode preSearch(int no){
       return this.root.preSearch(no);
    }

    //中序查找
    public HeroNode midSearch(int no){ return  this.root.midSearch(no); }

    //后序查找
    public HeroNode sufSearch(int no) { return this.root.sufSearch(no);}

    public HeroNode getRoot() {
        return root;
    }

    public void setRoot(HeroNode root) {
        this.root = root;
    }
}


class HeroNode{
    private int no;
    private String name;
    private HeroNode left;
    private HeroNode right;

    //前序查找
    public HeroNode preSearch(int no){
        if(this.no==no){
            return this;
        }else if(this.left!=null){
            return this.left.preSearch(no);
        }else if(this.right!=null){
            return this.right.preSearch(no);
        }else {
            return null;
        }
    }

    //中序查找
    public HeroNode midSearch(int no){
        if(this.left!=null){
            return this.left.preSearch(no);
        } else if(this.no==no){
            return this;
        } else if(this.right!=null){
            return this.right.preSearch(no);
        }else {
            return null;
        }
    }

    //后序查找
    public HeroNode sufSearch(int no){
        if(this.left!=null){
            return this.left.preSearch(no);
        } else if(this.right!=null){
            return this.right.preSearch(no);
        } else if(this.no==no){
            return this;
        }else {
            return null;
        }
    }

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }
}

 

执行结果:

-------------二叉树查找-----------
前序查找:
HeroNode{no=3, name='蚁人'}

中序查找:
HeroNode{no=3, name='蚁人'}

后续查找:
HeroNode{no=3, name='蚁人'}

 

2 二叉查找树(BST)

2.1 定义

A binary search tree is a rooted binary tree, whose internal nodes each store a key (and optionally, an associated value) and each have two distinguished sub-trees, commonly denoted left and right. The tree additionally satisfies the binary search tree property, which states that the key in each node must be greater than all keys stored in the left sub-tree, and not greater than any key in the right sub-tree. (The leaves (final nodes) of the tree contain no key and have no structure to distinguish them from one another. Leaves are commonly represented by a special leaf or nil symbol, a NULL pointer, etc.)

                                                                                                                                                                          --维基百科

2.2 图解

二叉查找树要求:每个节点必须大于自己的左子节点并且小于或等于自己的右子节点。e.g.将【5,8,3,4,9,7,6】构建成一棵二叉查找树如图所示:

 

2.3 二叉查找树简易实现

/**
 * 二叉查找树
 */
public class BSTTest {
    public static void main(String[] args) {
        BSTNode root = new BSTNode(5, "穆克什·安巴尼");
        BSTNode node1 = new BSTNode(8, "拉里·埃里森");
        BSTNode node2 = new BSTNode(3, "伯纳德·阿诺特");
        BSTNode node3 = new BSTNode(4, "马克·扎克伯格");
        BSTNode node4 = new BSTNode(9, "史蒂夫·鲍尔默");
        BSTNode node5 = new BSTNode(7, "沃伦·巴菲特");
        BSTNode node6 = new BSTNode(6, "埃隆·马斯克");

        BST bst = new BST();
        bst.add(root);
        bst.add(node1);
        bst.add(node2);
        bst.add(node3);
        bst.add(node4);
        bst.add(node5);
        bst.add(node6);

        System.out.println("----------二叉查找树中序遍历----------");
        bst.midOrder();
    }

}

class BST {
    private BSTNode root;

    public BSTNode getRoot() {
        return root;
    }

    public void setRoot(BSTNode root) {
        this.root = root;
    }

    public void add(BSTNode node) {
        if(this.root == null){
            root = node;
        }else {
            this.root.add(node);
        }
    }

    public void midOrder() {
        this.root.midOrder();
    }

}


class BSTNode {
    private int no; //编号
    private String name; //姓名
    private BSTNode left; //左子节点
    private BSTNode right; //右子节点

    public BSTNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BSTNode getLeft() {
        return left;
    }

    public void setLeft(BSTNode left) {
        this.left = left;
    }

    public BSTNode getRight() {
        return right;
    }

    public void setRight(BSTNode right) {
        this.right = right;
    }

    //中序遍历
    public void midOrder() {
        if (this.left != null) {
            this.left.midOrder();
        }
        if (this != null) {
            System.out.println(this);
        }
        if (this.right != null) {
            this.right.midOrder();
        }
    }

    //添加节点
    public void add(BSTNode node) {
        if(node.getNo() < this.getNo()){ //小于当前节点,往左走
            if(this.left == null){
                this.setLeft(node); //插入节点
            }else {
                this.left.add(node); //递归左子树
            }
        } else { //大于或等于当前节点,往右走
            if(this.right == null){
                this.setRight(node); //插入节点
            }else {
                this.right.add(node); //递归右子树
            }
        }

    }

    @Override
    public String toString() {
        return "BSTNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }


}

运行结果: 

----------二叉查找树中序遍历----------
BSTNode{no=3, name='伯纳德·阿诺特'}
BSTNode{no=4, name='马克·扎克伯格'}
BSTNode{no=5, name='穆克什·安巴尼'}
BSTNode{no=6, name='埃隆·马斯克'}
BSTNode{no=7, name='沃伦·巴菲特'}
BSTNode{no=8, name='拉里·埃里森'}
BSTNode{no=9, name='史蒂夫·鲍尔默'}

 

2.4 优点和不足

优点:理想状态下,二叉查找树能够高效实现数据的添加和查询(对比有序数组添加慢和链表查询慢)。

缺点:特殊(有序)情况下,二叉查找树会退化成链表,导致查询效率降低。如:给定数据【1,2,3,4,5,6,7】构造一棵二叉查找树,此时二叉查找树退化成了链表,查找效率甚至还不如单链表(与链表相比还多了一次左子树的比较)。

 

3 红黑树

3.1 定义

A red–black tree is a kind of self-balancing binary search tree. Each node of the binary tree has an extra bit, and that bit is often interpreted as the color (red or black) of the node. These color bits are used to ensure the tree remains approximately balanced during insertions and deletions.

                                                                                                                                                                          --维基百科

前面说到二叉查找树可能退化成为链表,平衡二叉树(AVL树)对此进行了优化以保证二叉树较高的查询效率。平衡二叉树要求任意节点的两个子树的高度最多相差1,不满足上述要求时需要通过再平衡(rebalance)从而达到上述要求。红黑树是常见的平衡二叉树的一种实现方法,除此之外还有AVL、替罪羊树、Treap、伸展树等。

红黑树的特性

  • 每个节点要么是红色的,要么是黑色的;

  • 根节点是黑色的;

  • 叶子节点都是黑色的;

  • 如果一个节点是红色的,那么它的子节点都是黑色的(注意:黑色节点的子节点也可以是黑色的);

  • 从给定节点到其子叶子节点的每条路径都包含相同数量的黑色节点。

 

3.2 图解

 

红黑树本质是一棵二叉查找树,所以满足左子节点<当前节点<=右子节点;另外还需要满足平衡二叉树的特性,所以任意两棵子树高度差不能超过1,即平衡特性。红黑树通过变色左右旋转来达到平衡的目的。需要注意的是每次新插入的节点默认是红色的。新插入一个节点时:

  • 变色:当父节点是红色,且叔叔节点是红色。

(1)把父节点和叔叔节点变为黑色

(2)把祖父节点变为红色

(3)若祖父节点是根节点,重新变为黑色

  • 左旋:当父节点是红色,叔叔节点是黑色,且当前节点是右子树,进行左旋。

  • 右旋:当父节点是红色,叔叔节点是黑色,且当前节点是左子树,进行右旋。

(1)把父节点变为黑色

(2)把祖父节点变为红色

(3)右旋

如果感兴趣,可以到旧金山大学的数据结构可视化网站玩一玩,相信自己动手尝试过会加深理解。 

3.3 红黑树简易实现

public class Read_Black_TreeTest {
    public static void main(String[] args) {

        //插入
        System.out.println("-------红黑树插入----------");
        Read_Black_Tree<Integer,String> rbt = new Read_Black_Tree<>();
        rbt.put(5, "牛筋丸");
        rbt.put(9, "流沙包");
        rbt.put(3, "凤爪");
        rbt.put(8, "红米肠");
        rbt.put(1, "虾饺");

        //中序遍历
        System.out.println("-------红黑树中序遍历----------");
        rbt.midOrder();
        
    }

}

class Read_Black_Tree<K, V> {
    private TreeNode<K, V> root;


    //遍历
    public void midOrder() {
        this.root.midOrder();
    }

    //添加节点
    public void put(K key, V val) {
        if (key == null) throw new NullPointerException();
        TreeNode<K, V> t = this.root;
        if (t == null) {
            this.root = new TreeNode<>(key, val, null);
            return;
        }

        //寻找插入位置
        TreeNode parent;
        do {
            parent = t;
            if (key.hashCode() < t.key.hashCode()){ //小于,往左走
                t = t.left;
            }else if(key.hashCode() > t.key.hashCode()){ //大于,往右走
                t =  t.right;
            }else { //等于,替换val
                t.value = val;
                return;
            }
        }while (t!=null);

        //新建一个节点
        TreeNode<K,V> e = new TreeNode<>(key,val,parent);
        if(key.hashCode()<parent.key.hashCode()){
            parent.left = e;
        }else {
            parent.right = e;
        }

        this.root = e.balanceInsertion(root,e);

    }

    static class TreeNode<K, V> {
        K key; //键
        V value; //值
        TreeNode<K, V> parent; //父节点
        TreeNode<K, V> left; //左子节点
        TreeNode<K, V> right;  //右子节点
        boolean red; //颜色标识


        //构造方法
        TreeNode(K key, V val, TreeNode<K, V> parent) {
            this.key = key;
            this.value = val;
            this.parent = parent;
            red = true;
        }

        /**
         * 查询根节点
         */
        final TreeNode<K, V> root() {
            for (TreeNode<K, V> r = this, p; ; ) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }

        /**
         * 中序遍历
         */
        void midOrder() {
            if (this.left != null) {
                this.left.midOrder();
            }
            if (this != null) {
                System.out.println(this);
            }
            if (this.right != null) {
                this.right.midOrder();
            }
        }


        /**
         * 左旋
         *
         * @param root
         * @param p
         * @param <K>
         * @param <V>
         * @return
         */
        <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root,
                                         TreeNode<K, V> p) {
            TreeNode<K, V> r, pp, rl;
            if (p != null && (r = p.right) != null) {
                if ((rl = p.right = r.left) != null)
                    rl.parent = p;
                if ((pp = r.parent = p.parent) == null)
                    (root = r).red = false;
                else if (pp.left == p)
                    pp.left = r;
                else
                    pp.right = r;
                r.left = p;
                p.parent = r;
            }
            return root;
        }

        /**
         * 右旋
         *
         * @param root
         * @param p
         * @param <K>
         * @param <V>
         * @return
         */
        <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root,
                                          TreeNode<K, V> p) {
            TreeNode<K, V> l, pp, lr;
            if (p != null && (l = p.left) != null) {
                if ((lr = p.left = l.right) != null)
                    lr.parent = p;
                if ((pp = l.parent = p.parent) == null)
                    (root = l).red = false;
                else if (pp.right == p)
                    pp.right = l;
                else
                    pp.left = l;
                l.right = p;
                p.parent = l;
            }
            return root;
        }


        /**
         * 插入节点后平衡处理
         *
         * @param root
         * @param x
         * @param <K>
         * @param <V>
         * @return
         */
        <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
                                               TreeNode<K, V> x) {
            x.red = true;
            for (TreeNode<K, V> xp, xpp, xppl, xppr; ; ) {
                if ((xp = x.parent) == null) { //插入节点的父节点是空,说明插入的是根节点
                    x.red = false; //根节点设为黑色
                    return x;
                } else if (!xp.red || (xpp = xp.parent) == null) //父节点是黑色或爷爷节点是根节点,不需要调整
                    return root;
                if (xp == (xppl = xpp.left)) { //父节点是红色且为爷爷节点的左节点
                    if ((xppr = xpp.right) != null && xppr.red) {  //叔叔节点不为空且为红色,变色
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    } else { //叔叔节点是黑色
                        if (x == xp.right) { //当前节点是父节点的右子节点
                            root = rotateLeft(root, x = xp);//左旋
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateRight(root, xpp);//右旋
                            }
                        }
                    }
                } else { //父节点是红色且为爷爷节点的右节点
                    if (xppl != null && xppl.red) { //叔叔节点为红色,变色
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    } else { //叔叔节点为黑色
                        if (x == xp.left) {
                            root = rotateRight(root, x = xp); //右旋
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateLeft(root, xpp); //左旋
                            }
                        }
                    }
                }
            }
        }
        

        @Override
        public String toString() {
            return "TreeNode{" +
                    "key=" + key +
                    ", value=" + value +
                    '}';
        }
    }
}

3.4 总结

红黑树的关键是在二叉查找树(BST)的基础上加入了平衡算法,即通过改变着色、左旋、右旋达到任意两棵子树高度差不超过1。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值