[learning summary] use C# to write a red black tree in Unity

As a basic binary search data structure, red black tree is probably something that the game client must know. So you don't have to do it twice. Just write one by hand.

Reference articles or videos:

Characteristics of red black tree

The first thing to know is the characteristics of red black tree:

  1. Each node of the red black tree must be red or black
  2. The root node of a red black tree must be black
  3. Each leaf node (referred to here as NULL nodes) is black
  4. If a node is red, its child nodes must be black, that is, there are no two consecutive red nodes
  5. All paths from a node to its descendants contain the same number of black nodes

    Like AVL tree, red black tree is also a balanced binary tree, so it naturally needs rotation to help balance the whole tree. Therefore, two basic operations of balanced binary tree, left rotation and right rotation, are introduced here.

Parent child node changes of left and right rotation (no different from AVL):

(in the code part at the end of the text, you can find the Rotate algorithm in RBTree and compare it with it.)

After knowing the basic principle of rotation, you will correspond to two more important operations that will produce red black tree balance conflict, adding nodes and deleting nodes.

Add node to red black tree

The general steps for adding nodes are as follows:

  1. First, the red black tree is treated as a binary lookup tree, and the nodes are inserted into the tree
  2. Set the inserted node color to red (setting to red can effectively reduce the number of rotations)
  3. By solving the conflict, that is, by a series of operations of rotating and transforming shading, the red black tree is changed back to the standard red black tree

According to the conflict caused by the inserted node, it can be divided into three cases:

  1. The inserted node is the root node. In this case, you can directly turn the node into black
  2. The parent node of the inserted node is black. Because the current inserted node is red, the black height (i.e. feature 5) will not be affected and will not be processed
  3. The parent node of the inserted node is red, and two consecutive red nodes are generated. At this time, the conflict needs to be resolved

The types of conflicts can be summarized into three types:
CASE1: when the parent node is red, and the parent node's sibling node is also red
Solution:
1. Set the parent node and the sibling node of the parent node to black
2. Set the grandfather node to red
3. Set the grandfather node as the node that needs to resolve the conflict, and recurse upward

CASE2: when the parent node is red, the parent node's sibling node is black, and the parent node's sibling node and the current node are on the opposite side (one is the left node and the other is the right node)
1. Change the parent node to black
2. Change the grandfather node to red
3. Rotate the parent node and grandfather node

CASE3: when the parent node is red, the parent node's sibling node is black, and the parent node's sibling node and the current node are on the same side (both are right nodes or left nodes)

  1. First, rotate the current node and parent node without changing any color (this will become CASE2)
  2. Carry out further rotation according to CASE2
    PS: the processing method of CASE3 is different in the cited video and article. I use the solution in the video here, because the solution in the article needs further recursion, which looks awkward.

Red black tree delete node

To delete a node:

  1. First, delete it in binary order (directly delete the leaf node or find a replaceable child node)
  2. Re shade and rotate

All deletion operations can be summarized into the following situations

  1. A red node is deleted, and the nature of the whole red black tree does not change
  2. The deleted node is a black node, but it is the root node. The nature of the whole red black tree does not change
  3. The deleted node is a black node and is not the root node
    For the third case, it is the most complex case and the most common case. Because a black node is missing from the corresponding path, feature 5 cannot be maintained, that is, all nodes have the same black height. This situation is generally called DOUBLE BLACK. Generally, it can also be divided into the following three situations:

CASE1: the sibling node of the node generated by Double Black has at least one red sub node
CASE1.1: the red child node is on the opposite side of the DoubleBlack node:

  1. Rotate parent and sibling nodes
  2. Exchange the colors of the original parent node and brother node (the parent is black, the brother remains unchanged; the parent is red, the brother becomes red)
  3. Changes the color of the red child node to black

CASE1.2: the red child node is on the same side of the doubleback node

  1. Rotate the red child and sibling nodes
  2. Swap the colors of two nodes
  3. At this time, it is converted to case 1.1 for further processing

CASE2: the two child nodes of the sibling node of the node generated by double black are black
CASE2.1: the parent node is red

  1. The parent node turns black
  2. The sibling node turns red

CASE2.2: parent node is black

  1. Replace sibling nodes with red
  2. The parent node is marked doubleback and recurses further up

CASE3: the sibling node of the node generated by DoubleBlack is red

  1. Rotate parent and sibling nodes
  2. Swap the colors of the two nodes
  3. At this time, the subtree with A as the root node is converted to CASE1/CASE2

The core principle of maintaining balance of red and black trees

To sum up, it is not difficult to see from the idea and algorithm of insertion and deletion that the core principle of red black tree adjustment balance is to maintain feature 5, that is, the black height of each node should be balanced.
All adjustments exist to keep the same number of black child nodes on each path

Code part

There are three files in total:
Red black tree interface file:

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();
}

Red black tree ontology implementation file

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//Red black tree node
    {
        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)//If the root node is empty
        {
            this.root = new RBNode(index, data);
            this.root.nodecolor = NodeColor.BALCK;//The root node must be black

            return; //No more downward insertion
        }

        RBNode newNode = new RBNode(index, data);//Recreate a node
        RBNode currentNode = this.root;//Traverse the insert from the root node
        while(true)
        {
            int res = newNode.index.CompareTo(currentNode.index);
            if(res==0)
            {
                throw new ArgumentException("The index already exit");
            }
            else if(res>0)//The insertion value is greater than the current node value
            {
                if(currentNode.rightchild==null)
                {
                    //Simple put
                    currentNode.rightchild = newNode;
                    newNode.parent = currentNode;

                    //The red black tree structure needs to be adjusted
                    if (CheckInsertConflict(newNode))
                    {
                        SolveInsertConflict(newNode);
                    }


                    this.size++;//Increase the number of nodes
                    return;
                }
                else
                {
                    currentNode = currentNode.rightchild;//Continue to iterate down recursively
                }

            }
            else//The insertion value is smaller than the current node
            {
                if(currentNode.leftchild==null)
                {
                    currentNode.leftchild = newNode;
                    newNode.parent = currentNode;
                    //The red black tree structure needs to be adjusted
                    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. Delete using traditional binary sort tree 2. Resolve conflicts
    {
        RBNode currentNode = root;
        while(true)
        {
            int res = index.CompareTo(currentNode.index);
            if (res == 0)//Description the current node was found
            {
                RemoveHelper(currentNode);
                return;
            }
            else if (res > 0)
            {
                if (currentNode.rightchild == null)
                {
                    throw new ArgumentException("The node to be deleted was not found");
                }
                else
                {
                    currentNode = currentNode.rightchild;
                }
            }
            else
            {
                if (currentNode.leftchild == null)
                {
                    throw new ArgumentException("The node to be deleted was not found");
                }
                else
                {
                    currentNode = currentNode.leftchild;
                }
            }
        }
    }

    private void RemoveHelper(RBNode node)
    {
        //1. If the current node is a leaf node
        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;
            }
            //The GC collection mechanism is then automatically triggered

            return;
        }
        //2. If there are two child nodes
        if(node.leftchild!=null && node.rightchild != null)
        {
            //The method used here is to find the leftmost node in the right subtree and replace it
            RBNode successor = node.rightchild;
            while(successor.leftchild!=null)
            {
                successor = successor.leftchild;
            }

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

            RemoveHelper(successor);
        }
        //3. There is only one child node, so the child writing will be more concise
        else
        {
            //Get the unique child node first
            RBNode child;
            if (node.leftchild != null)
            {
                child = node.leftchild;
            }
            else
            {
                child = node.rightchild;
            }

            //Delete the original node
            if(node.IsLeftChild)
            {
                node.parent.leftchild = child;
            }
            else
            {
                node.parent.rightchild = child;
            }
            child.parent = node.parent;

            //3.1 if the colors are the same
            if(node.nodecolor == child.nodecolor)
            {
                child.nodecolor = NodeColor.BALCK;
            }
            else//If not the same color
            {
                SolveDoubleBlack(node);
            }
        }
    }
    private void SolveDoubleBlack(RBNode node)
    {
        //Case0:Double Black appears on the root node
        if (node.parent == null)
        {
            return;
        }
        //In other cases, pre store some data first
        bool currentNodeIsLeft = node.IsLeftChild;
        RBNode parent = node.parent;
        RBNode sibling;//According to the red black tree principle, the sibling node here cannot be an empty node
        if (currentNodeIsLeft)//The corresponding sibling nodes are obtained according to the left and right nodes
            sibling = node.rightchild;
        else
            sibling = node.leftchild;

        RBNode siblingSameSideChild;//The two nodes here may be 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;//All empty nodes are also black
        }
        else
        {
            siblingSameSideColor = siblingSameSideChild.nodecolor;
        }
        NodeColor siblingOppoSideColor;
        if (siblingOppoSideChild == null)
        {
            siblingOppoSideColor = NodeColor.BALCK;//All empty nodes are also black
        }
        else
        {
            siblingOppoSideColor = siblingOppoSideChild.nodecolor;
        }

        //Case1: the node with double black has a black brother, and at least one child node of the brother node is red
        if (sibling.nodecolor == NodeColor.BALCK && (siblingOppoSideColor == NodeColor.RED || siblingSameSideColor == NodeColor.RED))
        {
            //1.1 siblingsameside is red, change it to 1.2
            if (siblingSameSideChild.nodecolor == NodeColor.RED)
            {
                Rotate(siblingSameSideChild, sibling);
                siblingSameSideChild.nodecolor = NodeColor.BALCK;
                sibling.nodecolor = NodeColor.RED;
                SolveDoubleBlack(node);//It's a coincidence here. A re call is made
                return;
            }
            //1.2 siblingOppoSideColor is red
            Rotate(sibling, parent);
            sibling.nodecolor = parent.nodecolor;
            parent.nodecolor = NodeColor.BALCK;
            siblingOppoSideChild.nodecolor = NodeColor.BALCK;
        }
        //Case2: the node with double black has a black brother, and the child nodes of the brother node are both black
        else if (sibling.nodecolor == NodeColor.BALCK && (siblingOppoSideColor == NodeColor.BALCK && siblingSameSideColor == NodeColor.BALCK))
        {
            sibling.nodecolor = NodeColor.RED;
            //2.1 parent is red
            if (parent.nodecolor == NodeColor.RED)
            {
                parent.nodecolor = NodeColor.BALCK;
                //After this processing, there will be no two consecutive red nodes, because it has been solved in CASE1
            }
            //2.2 the parent is black and needs to be solved recursively
            else
            {
                SolveDoubleBlack(node);
            }
        }
        //CASE3: when the parent node is black and the brother node is red, it becomes CASE1 or 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("The relationship between parent and child nodes seems to be wrong~Please check the procedure");
        }

        if(child.IsLeftChild)//Corresponding to right rotation
        {
            //1. Temporarily store some key data
            RBNode grandparent = child.parent.parent;//Convert after convenience
            bool parentSatus = parent.IsLeftChild;//Is it the node to the left of the grandfather node
            RBNode rightChildTmp = child.rightchild;
            //2. Rotate the structure of the whole tree
            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//Left hand rotation is required
        {
            //1. Temporarily store some key data
            RBNode grandparent = child.parent.parent;//Convert after convenience
            bool parentSatus = parent.IsLeftChild;//Is it the node to the left of the grandfather node
            RBNode leftChildTmp = child.leftchild;
            //2. Rotate the structure of the whole tree
            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)//Check whether the balance of red black tree conflicts
    {
        //If the root node is red, it needs to be adjusted and returns true
        if(node.parent==null && node.nodecolor == NodeColor.RED)
        {
            return true;
        }
        //Standard conflict, that is, two consecutive nodes are red
        if(node.parent.nodecolor==NodeColor.RED && node.nodecolor == NodeColor.RED)
        {
            return true;
        }
        return false;
    }

    private void SolveInsertConflict(RBNode node)//Conflict resolution function
    {
        //If the current solution traverses to the root node
        if(node.parent == null)//This situation is triggered when the number of nodes is less than 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;
        //Other standard conditions
        RBNode siblingnode;
        if(parent.IsLeftChild)//Get the sibling node of the parent node
        {
            siblingnode = grandparent.rightchild;
        }
        else
        {
            siblingnode = grandparent.leftchild;
        }

        if(siblingnode==null || siblingnode.nodecolor == NodeColor.BALCK)//This actually corresponds to CASE1
        {
            //1. The sibling nodes of the current node and the parent node are on the same side
            if (node.IsLeftChild != parent.IsLeftChild)
            {
                //Rotate the current node and parent node once first
                Rotate(node, parent);
                //When rotating the current node and the group parent node (because the current node has become the parent node at this time)
                Rotate(node, grandparent);
                //Change the color of nodes
                node.nodecolor = NodeColor.BALCK;
                grandparent.nodecolor = NodeColor.RED;
            }
            //2. The sibling nodes of the current node and the parent node are not on the same side
            else
            {
                //In this case, the parent and grandfather nodes are rotated
                Rotate(parent, grandparent);
                //Change the color of parent nodes and group parent nodes
                parent.nodecolor = NodeColor.BALCK;
                grandparent.nodecolor = NodeColor.RED;
            }
        }
        //Both the parent node and the sibling node of the parent node are red, which requires recursive processing
        else
        {
            //Change the color of nodes in the same layer
            parent.nodecolor = NodeColor.BALCK;
            siblingnode.nodecolor = NodeColor.BALCK;
            //Set the grandfather node to red
            grandparent.nodecolor = NodeColor.RED;
            //Then recursively check whether there is a conflict
            if(CheckInsertConflict(grandparent))
            {
                SolveInsertConflict(grandparent);
            }
        }

    }

}

//Each node is either black or red
//The root node must be black
//Each leaf node (NULL) is black
//If a node is red, its child nodes must be black
//All paths from a node to its descendants contain the same number of black nodes
//The inserted node is generally red

Test script, just hang it anywhere

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)//Try to avoid recursion
    {
        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)//Try to avoid recursion
    {
        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: the above is the general content of red black tree. Why not do visualization? Because I'm a little busy recently, and the red black tree is not a difficult algorithm to understand, but the adjustment is more complex, so I don't want to do visualization.
As a common binary search tree structure, red black tree is still very important, especially in program development. Red black tree has good search time complexity (logn), so it is a necessary content~

Tags: C# Unity Algorithm data structure

Posted on Sun, 19 Sep 2021 03:00:05 -0400 by suzzane2020