【学习总结】在Unity中用C#写一棵红黑树

34 篇文章 5 订阅

红黑树作为基本的二叉查找数据结构,大概是属于游戏客户端这边必须知道的东西。所以一不做二不休,干脆手写一个出来。

参考文章或视频:

红黑树的特性

首先需要知道的是红黑树的特性

  1. 红黑树的每一个节点都必须是红色或者是黑色
  2. 红黑树的根节点一定是黑色的
  3. 每个叶子节点(这里指的是NULL的那些节点)是黑色
  4. 如果一个节点是红色的,则其子节点一定是黑色的,即不存在两个连续的红色节点
  5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
    在这里插入图片描述
    与AVL树相同,红黑树由于也是一棵平衡二叉树,所以自然也需要旋转操作来帮助整个树进行平衡,于是这里引出平衡二叉树的两个基本操作,左旋和右旋。

左旋与右旋的父子节点变化(与AVL并无不同):
在这里插入图片描述
(在文末的代码部分,在RBTree中可以找到该Rotate算法对照着看)

知道了旋转的基本原理之后,就会对应两个比较重要的,会产生红黑树平衡冲突的操作,添加节点和删除节点。

红黑树添加节点

添加节点的大致步骤如下:

  1. 先将红黑树当作一棵二叉查找树,将节点插入树中
  2. 将该插入的节点颜色设置为红色(设置为红色可以有效减少旋转次数)
  3. 通过解决冲突,即通过旋转和变换着色的一系列操作,将红黑树变回标准的红黑树

而根据被插入节点造成的冲突,又可以分为三种情况:

  1. 被插入的节点是根节点,此时直接将该节点变为黑色即可
  2. 被插入的节点的父节点是黑色,由于当前插入节点是红色,所以不会影响黑色高度(即特性五),不处理
  3. 被插入节点的父节点是红色,产生了连续的两个红色节点,此时才需要解决冲突

而产生的冲突类型概括起来又会分为三种:
CASE1:当父节点是红色,且父节点的兄弟节点也是红色
解决方案:
1.将父节点和父节点的兄弟节点都设置为黑色
2.将祖父节点设置为红色
3.将祖父节点设置为当前需要解决冲突的节点向上递归
在这里插入图片描述
CASE2:当父节点是红色,且父节点的兄弟节点是黑色,并且父节点的兄弟节点和当前节点在异侧(一个是左节点,一个是右节点)
1.将父节点变更为黑色
2.将祖父节点变更为红色
3.旋转父节点和祖父节点即可
在这里插入图片描述

CASE3:当父节点是红色,且父节点的兄弟节点是黑色,并且父节点的兄弟节点和当前节点在同侧(两个都是右节点或者左节点)

  1. 首先不改变任何颜色,旋转当前节点和父节点(这样就会变成CASE2的情况)
  2. 按照CASE2的情况进行进一步的旋转
    PS:CASE3的处理方式在引用的视频和文章中并不相同,我这里采用的是视频中的解法,因为文章中的解法还需要进一步递归,看着挺别扭的。
    在这里插入图片描述

红黑树删除节点

删除节点的操作步骤:

  1. 先按照二叉排序的方式进行删除(直接删除叶子节点,或者是找到一个可替换的子节点)
  2. 进行重新的着色和旋转

所有的删除操作又可以归纳为一下几种情况

  1. 删除的是一个红色的节点,整个红黑树的性质不发生变化
  2. 删除的节点是一个黑色的节点,但是它是根节点,整个红黑树的性质不发生变化
  3. 删除的节点是一个黑色的节点,且不是根节点
    对于第三种情况,是最复杂的情况,也是最常见的情况。因为对应路径上少了一个黑色节点,因此无法维持特性五,即所有节点黑色高度一样。这种情况我们一般称之为DOUBLE BLACK。一般还可以分为以下三种情况:

CASE1:Double Black产生的节点的兄弟节点至少有一个红色子节点
CASE1.1:红色子节点在DoubleBlack节点的异侧:

  1. 旋转父节点和兄弟节点
  2. 交换原本的父节点和兄弟节点的颜色(父为黑色,兄弟不变;父为红色,兄弟变红)
  3. 将红色子节点的颜色变为黑色
    在这里插入图片描述

CASE1.2:红色子节点在DoubleBlack节点的同侧

  1. 旋转红色的子节点和兄弟节点
  2. 交换两个节点的颜色
  3. 此时被转变为了CASE1.1,做进一步处理
    在这里插入图片描述

CASE2:DoubleBlack产生的节点的兄弟节点的两个子节点都是黑色
CASE2.1:父节点是红色

  1. 父节点变为黑色
  2. 兄弟节点变为红色
    在这里插入图片描述

CASE2.2:父节点是黑色

  1. 将兄弟节点替换为红色
  2. 父节点标记为DoubleBlack并且进一步向上递归
    在这里插入图片描述

CASE3:DoubleBlack产生的节点的兄弟节点是红色

  1. 旋转父节点和兄弟节点
  2. 交换这两个节点的颜色
  3. 此时以A为根节点的子树就被转换成了CASE1/CASE2的情况
    在这里插入图片描述

红黑树维持平衡的核心原则

综上,从插入和删除的思路算法不难看出,红黑树调整平衡的核心原则是维持特性五,即每一个节点的黑色高度应该是平衡的。
所有的调整都是为了保持每一个路径上黑色的子节点个数相同而存在的

代码部分

总共有三个文件:
红黑树接口文件:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;


public interface BSTADT<IndexType,DataType> where IndexType : IComparable<IndexType>
{
    void Add(IndexType index, DataType data);
    DataType Get(IndexType index);
    void Remove(IndexType index);
    bool Contains(IndexType index);
    int GetSize();
    bool IsEmpty();
}

红黑树本体实现文件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public enum NodeColor { RED, BALCK};
public class RedBlackTree<IndexType,DataType> : MonoBehaviour,BSTADT<IndexType,DataType>
    where IndexType : IComparable<IndexType>
{
    public class RBNode//红黑树的节点
    {
        public IndexType index;
        public DataType data;
        public RBNode parent;
        public RBNode leftchild;
        public RBNode rightchild;
        public NodeColor nodecolor;

        public bool IsLeftChild
        {
            get
            {
                if(parent==null)
                {
                    return false;
                }

                if(parent.leftchild!=this)
                {
                    return false;
                }

                return true;
            }
        }

        public RBNode(IndexType index,DataType data)
        {
            this.index = index;
            this.data = data;
            this.nodecolor = NodeColor.RED;
        }
        public override string ToString()
        {
            return this.nodecolor.ToString() + "-" + this.index.ToString() + "-" + this.data.ToString();
        }
    }

    private RBNode root;
    private int size;
    public RBNode getRoot()
    {
        return this.root;
    }
    public void Add(IndexType index, DataType data)
    {
        if(this.root==null)//如果根节点为空的话
        {
            this.root = new RBNode(index, data);
            this.root.nodecolor = NodeColor.BALCK;//根节点一定是黑色

            return; //不用再向下插入了
        }

        RBNode newNode = new RBNode(index, data);//重新创建一个节点
        RBNode currentNode = this.root;//从根节点开始遍历插入
        while(true)
        {
            int res = newNode.index.CompareTo(currentNode.index);
            if(res==0)
            {
                throw new ArgumentException("The index already exit");
            }
            else if(res>0)//插入值比当前节点值大
            {
                if(currentNode.rightchild==null)
                {
                    //简单放入
                    currentNode.rightchild = newNode;
                    newNode.parent = currentNode;

                    //需要调整红黑树结构
                    if (CheckInsertConflict(newNode))
                    {
                        SolveInsertConflict(newNode);
                    }


                    this.size++;//增加节点数
                    return;
                }
                else
                {
                    currentNode = currentNode.rightchild;//继续往下递归遍历
                }

            }
            else//插入值比当前节点要小
            {
                if(currentNode.leftchild==null)
                {
                    currentNode.leftchild = newNode;
                    newNode.parent = currentNode;
                    //需要调整红黑树结构
                    if (CheckInsertConflict(newNode))
                    {
                        SolveInsertConflict(newNode);
                    }

                    this.size++;
                    return;
                }
                else
                {
                    currentNode = currentNode.leftchild;
                }

            }

        }
    }

    public DataType Get(IndexType index)
    {
        throw new NotImplementedException();
    }

    #region "Remove function"
    public void Remove(IndexType index)//1.使用传统二叉排序树的方式进行删除  2.解决冲突
    {
        RBNode currentNode = root;
        while(true)
        {
            int res = index.CompareTo(currentNode.index);
            if (res == 0)//说明找到了当前节点
            {
                RemoveHelper(currentNode);
                return;
            }
            else if (res > 0)
            {
                if (currentNode.rightchild == null)
                {
                    throw new ArgumentException("没有找到需要删除的那个节点");
                }
                else
                {
                    currentNode = currentNode.rightchild;
                }
            }
            else
            {
                if (currentNode.leftchild == null)
                {
                    throw new ArgumentException("没有找到需要删除的那个节点");
                }
                else
                {
                    currentNode = currentNode.leftchild;
                }
            }
        }
    }

    private void RemoveHelper(RBNode node)
    {
        //1.如果当前节点是一个叶子节点
        if(node.leftchild==null && node.rightchild==null)
        {
            if(node.nodecolor == NodeColor.BALCK)
            {
                SolveDoubleBlack(node);
            }

            if(node.IsLeftChild)
            {
                node.parent.leftchild = null;
            }
            else
            {
                node.parent.rightchild = null;
            }
            //之后会自动触发GC回收机制

            return;
        }
        //2.如果有两个子节点
        if(node.leftchild!=null && node.rightchild != null)
        {
            //这里采用的方法是找到右子树中最左边的那个节点进行替换
            RBNode successor = node.rightchild;
            while(successor.leftchild!=null)
            {
                successor = successor.leftchild;
            }

            node.index = successor.index;
            node.data = successor.data;

            RemoveHelper(successor);
        }
        //3.只有一个子节点,这样子写会比较简洁
        else
        {
            //先得到唯一的子节点
            RBNode child;
            if (node.leftchild != null)
            {
                child = node.leftchild;
            }
            else
            {
                child = node.rightchild;
            }

            //删除掉原本的节点
            if(node.IsLeftChild)
            {
                node.parent.leftchild = child;
            }
            else
            {
                node.parent.rightchild = child;
            }
            child.parent = node.parent;

            //3.1如果颜色相同
            if(node.nodecolor == child.nodecolor)
            {
                child.nodecolor = NodeColor.BALCK;
            }
            else//如果不具有相同颜色
            {
                SolveDoubleBlack(node);
            }
        }
    }
    private void SolveDoubleBlack(RBNode node)
    {
        //Case0:Double Black出现在根节点上
        if (node.parent == null)
        {
            return;
        }
        //其它情况先预存一些数据
        bool currentNodeIsLeft = node.IsLeftChild;
        RBNode parent = node.parent;
        RBNode sibling;//根据红黑树原则,这里的兄弟节点不可能是空节点
        if (currentNodeIsLeft)//根据左右节点得到相应的兄弟节点
            sibling = node.rightchild;
        else
            sibling = node.leftchild;

        RBNode siblingSameSideChild;//这里的两个节点有可能是Null
        RBNode siblingOppoSideChild;
        if(currentNodeIsLeft)
        {
            siblingSameSideChild = sibling.leftchild;
            siblingOppoSideChild = sibling.rightchild;
        }
        else
        {
            siblingSameSideChild = sibling.rightchild;
            siblingOppoSideChild = sibling.leftchild;
        }

        NodeColor siblingSameSideColor;
        if(siblingSameSideChild==null)
        {
            siblingSameSideColor = NodeColor.BALCK;//所有的空节点也是黑色
        }
        else
        {
            siblingSameSideColor = siblingSameSideChild.nodecolor;
        }
        NodeColor siblingOppoSideColor;
        if (siblingOppoSideChild == null)
        {
            siblingOppoSideColor = NodeColor.BALCK;//所有的空节点也是黑色
        }
        else
        {
            siblingOppoSideColor = siblingOppoSideChild.nodecolor;
        }

        //Case1:Double Black出现的节点有一个黑色的兄弟,并且兄弟节点的子节点至少有一个是红色
        if (sibling.nodecolor == NodeColor.BALCK && (siblingOppoSideColor == NodeColor.RED || siblingSameSideColor == NodeColor.RED))
        {
            //1.1 siblingsameside 是红色,将它变为1.2
            if (siblingSameSideChild.nodecolor == NodeColor.RED)
            {
                Rotate(siblingSameSideChild, sibling);
                siblingSameSideChild.nodecolor = NodeColor.BALCK;
                sibling.nodecolor = NodeColor.RED;
                SolveDoubleBlack(node);//这里取巧了,进行了一次重新调用
                return;
            }
            //1.2 siblingOppoSideColor 是红色
            Rotate(sibling, parent);
            sibling.nodecolor = parent.nodecolor;
            parent.nodecolor = NodeColor.BALCK;
            siblingOppoSideChild.nodecolor = NodeColor.BALCK;
        }
        //Case2:Double Black出现的节点有一个黑色的兄弟,并且兄弟节点的子节点两个都是黑色
        else if (sibling.nodecolor == NodeColor.BALCK && (siblingOppoSideColor == NodeColor.BALCK && siblingSameSideColor == NodeColor.BALCK))
        {
            sibling.nodecolor = NodeColor.RED;
            //2.1 parent 是红色
            if (parent.nodecolor == NodeColor.RED)
            {
                parent.nodecolor = NodeColor.BALCK;
                //这样处理之后不会存在两个连续红色的节点,因为CASE1中已经解决了
            }
            //2.2 parent 是黑色,需要递归解决
            else
            {
                SolveDoubleBlack(node);
            }
        }
        //CASE3:父节点为黑,兄弟节点为红色,变为CASE1或者CASE2的情况
        else
        {
            Rotate(parent, sibling);
            parent.nodecolor = NodeColor.RED;
            sibling.nodecolor = NodeColor.BALCK;
            SolveDoubleBlack(node);
        }

    }
    #endregion

    public bool Contains(IndexType index)
    {
        throw new NotImplementedException();
    }

    public int GetSize()
    {
        throw new NotImplementedException();
    }

    public bool IsEmpty()
    {
        throw new NotImplementedException();
    }


    private void Rotate(RBNode child,RBNode parent)
    {
        if(child.parent!=parent)
        {
            throw new ArgumentException("父子节点的关系好像错了~请检查一下程序");
        }

        if(child.IsLeftChild)//对应右旋的情况
        {
            //1.暂存一些关键数据
            RBNode grandparent = child.parent.parent;//方便之后转换
            bool parentSatus = parent.IsLeftChild;//是否为祖父节点左边的节点
            RBNode rightChildTmp = child.rightchild;
            //2.旋转整个树的结构
            if (grandparent != null)
            {
                if (parentSatus)
                    grandparent.leftchild = child;
                else
                    grandparent.rightchild = child;
            }
            else
            {
                root = child;
            }
            child.parent = grandparent;
            child.rightchild = parent;
            parent.parent = child;
            parent.leftchild = rightChildTmp;
            if(rightChildTmp!=null)
            {
                rightChildTmp.parent = parent;
            }
        }
        else//需要左旋的情况
        {
            //1.暂存一些关键数据
            RBNode grandparent = child.parent.parent;//方便之后转换
            bool parentSatus = parent.IsLeftChild;//是否为祖父节点左边的节点
            RBNode leftChildTmp = child.leftchild;
            //2.旋转整个树的结构
            if (grandparent != null)
            {
                if (parentSatus)
                    grandparent.leftchild = child;
                else
                    grandparent.rightchild = child;
            }
            else
            {
                root = child;
            }
            child.parent = grandparent;
            child.leftchild = parent;
            parent.parent = child;
            parent.rightchild = leftChildTmp;
            if(leftChildTmp!=null)
            {
                leftChildTmp.parent = parent;
            }
        }
    }

    private bool CheckInsertConflict(RBNode node)//检查红黑树的平衡是否产生冲突
    {
        //如果根节点是红色的,则需要调整,返回true
        if(node.parent==null && node.nodecolor == NodeColor.RED)
        {
            return true;
        }
        //标准的冲突,即连续两个节点都是红色
        if(node.parent.nodecolor==NodeColor.RED && node.nodecolor == NodeColor.RED)
        {
            return true;
        }
        return false;
    }

    private void SolveInsertConflict(RBNode node)//解决冲突的函数
    {
        //如果当前解决遍历到根节点了
        if(node.parent == null)//这种情况是在节点数小于3的情况下被触发的
        {
            node.nodecolor = NodeColor.BALCK;
            return;
        }
        else if(node.parent.parent == null)
        {
            node.parent.nodecolor = NodeColor.BALCK;
            return;
        }

        RBNode parent = node.parent;
        RBNode grandparent = node.parent.parent;
        //其余的标准情况
        RBNode siblingnode;
        if(parent.IsLeftChild)//得到父节点的兄弟节点
        {
            siblingnode = grandparent.rightchild;
        }
        else
        {
            siblingnode = grandparent.leftchild;
        }

        if(siblingnode==null || siblingnode.nodecolor == NodeColor.BALCK)//这里其实对应了CASE1的情况
        {
            //1.当前节点和父节点的兄弟节点在同一侧
            if (node.IsLeftChild != parent.IsLeftChild)
            {
                //先旋转一次当前节点和父节点
                Rotate(node, parent);
                //在旋转当前节点和组父节点(因为当前节点此时已经变成了父节点)
                Rotate(node, grandparent);
                //改变节点的颜色
                node.nodecolor = NodeColor.BALCK;
                grandparent.nodecolor = NodeColor.RED;
            }
            //2.当前节点和父节点的兄弟节点不在同一侧
            else
            {
                //这种情况旋转父节点和祖父节点
                Rotate(parent, grandparent);
                //改变父节点和组父节点的颜色
                parent.nodecolor = NodeColor.BALCK;
                grandparent.nodecolor = NodeColor.RED;
            }
        }
        //父节点和父节点的兄弟节点都是红色,这种情况需要递归处理
        else
        {
            //改变同层节点的颜色
            parent.nodecolor = NodeColor.BALCK;
            siblingnode.nodecolor = NodeColor.BALCK;
            //将祖父节点设置成红色
            grandparent.nodecolor = NodeColor.RED;
            //然后递归的检查是否有冲突
            if(CheckInsertConflict(grandparent))
            {
                SolveInsertConflict(grandparent);
            }
        }

    }

}

//每个节点或者是黑色,或者是红色
//根节点一定是黑色
//每个叶子节点(NULL)是黑色
//如果一个节点是红色的,则他的子节点必须是黑色的
//从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑色节点
//插入的节点一般为红色

测试脚本,随便挂个地方就行

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RedBlackTreeTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        TestRedBalckTreeAdd();
    }

    void TestRedBalckTreeAdd()
    {
        RedBlackTree<float, string> RBTree = new RedBlackTree<float, string>();
        RBTree.Add(10,"five");
        RBTree.Add(5, "one");
        RBTree.Add(15, "four");
        RBTree.Add(3, "three");
        RBTree.Add(7, "two");
        RBTree.Add(13, "thirdteen");
        RBTree.Add(17, "seventeen");
        RBTree.Add(6, "seventeen");
        RBTree.Add(6.5f, "seventeen");
        RBTree.Add(5.5f, "seventeen");
        printInLayer(RBTree.getRoot());
    }


    void printRBTreeInorder(RedBlackTree<float, string>.RBNode p)//尽量避免使用递归
    {
        Stack<RedBlackTree<float,string>.RBNode> sup = new Stack<RedBlackTree<float, string>.RBNode>();
        while(sup.Count!=0 || p!=null)
        {
            if(p!=null)
            {
                sup.Push(p);
                p = p.leftchild;
            }
            else
            {
                p = sup.Pop();
                Debug.Log(p.index + "-" + p.nodecolor + "  " );
                p = p.rightchild;
            }
        }
    }

    void printInorder(RedBlackTree<float, string>.RBNode p)//尽量避免使用递归
    {
        if(p==null)
        {
            return;
        }
        printInorder(p.leftchild);
        Debug.Log(p.index+"-"+p.nodecolor);
        printInorder(p.rightchild);
    }

    void printInLayer(RedBlackTree<float, string>.RBNode p)
    {
        Queue<RedBlackTree<float, string>.RBNode> sup = new Queue<RedBlackTree<float, string>.RBNode>();
        sup.Enqueue(p);
        while(sup.Count!=0)
        {
            p = sup.Dequeue();
            Debug.Log(p.index + "-" + p.nodecolor);
            if (p.leftchild != null)
            {
                sup.Enqueue(p.leftchild);
            }
            if(p.rightchild!=null)
            {
                sup.Enqueue(p.rightchild);
            }
        }
    }
}

PS:以上就是红黑树大致的全部内容了,为什么不做可视化呢?因为最近实在有点忙,而且红黑树也不算很难理解的算法,只是调整比较复杂而已,所以就没想着去做可视化了。
红黑树作为常用的二叉查找树结构还是十分重要的,特别是程序开发这一块,红黑树具有很好的查找时间复杂度(logn),所以算是必会的内容~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值