Red-Black Trees

Briefly discuss how the color rules help with balancing the tree (write a bit about 2-4 trees). Compare the performance of this red black tree to a random binary search tree or treap.

The four color rules of the red-black tree help the tree maintain a structure that is balanced. To understand more on red-black trees, it can be think of as first a 2-4 tree structure with colors and more rules. When we try to insert or remove a node, we would change the entire structure of the tree immediately. In 2-4 trees, by adding a leaf we would split w into w and w’, meaning we would get another whole structure. And that’s just the leaf. Removing also has its fix ups. Now, for a red-black tree, we have the same idea – we change the form of the tree by adding or removing. Every after insertion and removal of nodes, we perform a fix-up method for both functions and its implementation is quite complicated. However, the benefit of this is having a good time complexity with no operations running longer than O(log n).

Comparing the performance of red-black trees to a random binary search tree or a treap, I would say that we are more sure that the operations of red-black trees would perform better. This is because as we are adding or removing nodes, we are dynamically changing the shape of the tree, hence balancing the tree. If I were to rank by performance from worst to best, I would say, the random BSTs, treaps then red-black trees. Treaps do have rotations and structure changes when adding and removing values but they don’t have rules like red-black trees do. With this, we would still have the possibility of a worse time complexity of O(n), but it’s at minimum, because of the randomness of the priority. It’s unlike red-black trees, wherein it’s really sure that it has at most, O(log n). However, the cost for this good time complexity for red-black trees is the complicated implementation of it.

Code Used:

Red = 'Red'
Black = 'Black'

# sample RB tree of test_case 1 
# 
#                                           quick, black
#                                   /                        \
#                        fox, red                           the, black
#                 /                   \
#         brown, black             lazy, black
#           /      \                /        \
#               dog, red     jumps, red     over, red
# 
# 

class RedBlackNode:
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None
        self.parent = None
        self.color = Red 

class RedBlackTree:
    def __init__(self):
        NIL = RedBlackNode(0) # We use this NIL node to be used over again when we will attempt to insert a node
        NIL.color = Black
        self.NIL = NIL
        self.root = self.NIL # Our starting root node will be an empty NIL node and its color will always be black
        self.fixUpsCount = 0 # counter for the number of fix ups done from addFixUp()
        self.comparisonsCount = 0 # counter for the number of the number of searches done by find()

    def add(self, u):
        y = self.NIL # variable for the parent of the added node
        temp = self.root #  

        while temp != self.NIL: # goes down the tree until at leaf nodes that are NIL nodes
            y = temp
            if u.data < temp.data:
                temp = temp.left
            else:
                temp = temp.right
            if y.data == u.data:
                break

        u.parent = y

        if y == self.NIL:
            self.root = u
            
        elif u.data == y.data: # checks if data is already existing, then returns
            return

        elif u.data < y.data: # if data of child is less than its parent,
            y.left = u
        else:
            y.right = u
        
        # adds a value of NIL (None) values that are still nodes for our leaf nodes in the tree
        u.right = self.NIL 
        u.left = self.NIL

        self.addFixup(u) # we apply the fix up, everytime we call up addFixup

    def addFixup(self, u):
        while u.parent.color == Red:
            if u.parent == u.parent.parent.left: # u.parent is the left child

                y = u.parent.parent.right #uncle of u

                if y.color == Red: #case 1
                    u.parent.color = Black
                    y.color = Black
                    u.parent.parent.color = Red
                    u = u.parent.parent
                    self.fixUpsCount += 1
                    
                else: # case 2 or case 3
                    if u == u.parent.right: # case 2
                        u = u.parent # marked u.parent as new u
                        self.rotate_left(u)
                        
                    # case 3
                    u.parent.color = Black # made parent black
                    u.parent.parent.color = Red # made parent red
                    self.rotate_right(u.parent.parent)
                    self.fixUpsCount += 1
                
                self.fixUpsCount += 1

            else: # u.parent is the right child
                y = u.parent.parent.left #uncle of u

                if y.color == Red:
                    u.parent.color = Black
                    y.color = Black
                    u.parent.parent.color = Red
                    u = u.parent.parent
                    self.fixUpsCount += 1
                    
                else:
                    if u == u.parent.left:
                        u = u.parent # marked u.parent as new u
                        self.rotate_right(u)
                        
                    u.parent.color = Black # made parent black
                    u.parent.parent.color = Red # made parent red
                    self.rotate_left(u.parent.parent)
                    self.fixUpsCount += 1

                self.fixUpsCount += 1
            
        
        self.root.color = Black

    def rotate_left(self, x): 
        y = x.right
        x.right = y.left

        if y.left != self.NIL:
            y.left.parent = x
        y.parent = x.parent

        if x.parent == self.NIL: # x is root
            self.root = y

        elif x == x.parent.left: # x is left child
            x.parent.left = y

        else: #x is right child
            x.parent.right = y

        y.left = x
        x.parent = y

    def rotate_right(self, x):
        y = x.left
        x.left = y.right

        if y.right != self.NIL:
            y.right.parent = x
        y.parent = x.parent

        if x.parent == self.NIL: # x is root
            self.root = y

        elif x == x.parent.right: # x is right child
            x.parent.right = y

        else: # x is left child
            x.parent.left = y

        y.right = x
        x.parent = y
    
    def sizeOfTree(self, node): # recursively gets the number of all nodes except the NIL 
        if node == None or node == self.NIL:
            return 0
        else:
            return 1 + self.sizeOfTree(node.left) + self.sizeOfTree(node.right)

    def blackHeight(self, node):
        if node == None:
            return 0
        if node.color == Black: # gets only the height for black colored nodes
            return 1 + max(self.blackHeight(node.left),self.blackHeight(node.right))
        else:
            return 0 + max(self.blackHeight(node.left),self.blackHeight(node.right))
    
    def find(self, search_node): 
        cur_node = self.root # starts off at the root node
        comparisons = 0 # counter for the number of comparison made

        while cur_node is not None:
            if search_node < cur_node.data: # if node is at the left side of the tree
                cur_node = cur_node.left
                comparisons += 1

            elif search_node > cur_node.data: # if node is at the right side of the tree
                cur_node = cur_node.right
                comparisons += 1

            else:
                comparisons += 1
                return comparisons

        return comparisons


input_dat = []

# reading from input.dat
with open('Week 9\input.dat', 'r') as input:
    for line in input:
        input_dat.append(line.split())
        
    input_dat = input_dat[1:]

# print(input_dat)
# print()

for _input in input_dat:
    words = _input[1:-1]
    search_word = _input[-1]

    RBTree = RedBlackTree()
    for word in words:
        RBTree.add(RedBlackNode(word))
        # print(word)
        
    print(RBTree.sizeOfTree(RBTree.root), end=' ')
    print(RBTree.fixUpsCount, end=' ')
    print(RBTree.blackHeight(RBTree.root), end=' ')
    print(RBTree.find(search_word), end=' ')
    # print(search_word)
    print()


# I can't really get the same sample output for the number of fixups. 

# For reference, I used this website
# https://www.codesdope.com/course/data-structures-red-black-trees-insertion/?fbclid=IwAR1POX8pduiRS1NiREzm5m7TqwacuX4WxLSvXjDYUYRm2Iemg9oKneVByFM

Sample Input

3
9 the quick brown fox jumps over the lazy dog over
14 I would rather make a new one line long haiku than this sentence now haiku
18 somebody once told me the world was gonna roll me I ain't the sharpest tool in the shed world

Sample Output

8 10 3 4 
14 24 4 5 
15 20 4 4

Leave a comment

Design a site like this with WordPress.com
Get started