Skip to content

Conversation

@dimitrijjedich
Copy link
Collaborator

@dimitrijjedich dimitrijjedich commented Dec 13, 2024

Basics 🔴 ⚫ 🌳

A red–black tree is a self-balancing binary search tree data structure, which means that the differnce between its branches is minimal. The structure is used for fast storage and retrieval of ordered information. The nodes in a red-black tree have the addition of a color, which help with the balancing process. This happens after every node insertion and deletion and is accomplished using operations like rotation and recoloring.

The code written here follows along this article: geeksforgeeks - Intorduction to Red-Black-Tree

Rules 📝

The tree follows a number of rules which are:

  1. Each node is either red or black.
  2. The root of the tree is always black.
  3. Red nodes cannot have red children
  4. Every path from a node to its descendant null nodes (leaves) has the same number of black nodes.
  5. All leaves (NIL nodes) are black.

Operations ⚒️

The red-black-tree (rbt) consists of red-black-nodes (rbn) and should have the following operations:

  1. Insertion
  2. Rotation (needed for insertion)
  3. Deletion (optional)
  4. Traversal (conversion to Array)

The insertion will hapen on the rbt level, so will the traversal. The Rotation on the other hand will happen around a node and therefore will be implemented as method on the node.

Insertion

The insertion starts of with the basics of a binary tree. If the root is not set, declare the new node as the root and we are done. Since the second rule states, that the root has to be black, change the color and we are done for the rbt too.

Adding more elements to the 0️⃣1️⃣🌳

When the root is already set and new elements are added, we walk the tree node by node and decide if we travel to the left or the right by comparing the value of the current node with the value for the new node we want to insert.
Should the child we would travel to be a leaf (a nil node respectively a null RedBlackNode), we write the value to a new node as the child in the travel direction and stop the walk.

Edit

The rbt works with relationships to nodes higher up in the tree -> parent is needed in the node
The default color for a new node is Red, which is the reason we always need to change the color of the root.
We need to have a reference to the newly inserted node for the following steps

Error Correction (balancing) 🔧🛠️👨‍🔧

When a new Node is added we check against a list of criteria to validate, that the rules of the rbt are not violated. This happens in the following steps:

Happy paths 😊
  • The newly inserted node is now the root of the tree -> recolor to Black and we are done
  • The newly inserted node has a black parent -> done
Red uncel ⬆️⬆️⬇️🔴

If the uncle (sibling of parent) is red -> change color of parent and uncle to black -> change color of grandparent to red -> procede higher up in the tree

Black uncel ⬆️⬆️⬇️⚫

In the case of a black uncle thre are a few more subconditions that decide about the further logic. The conditions are that the node:

  1. is a right child of a left parent (or a left child of a right parent)
  2. is a right child of a right parent (or a left child of a left parent)

In both cases a rotation is needed which was implemented into the node class

The rotation can happen in two different way. The following showcases make the rotation around the node x and do not take into account, that the parents of x need to be updated to:

➡️ to the right
graph TD
 %% Define Styles
 classDef unchanged fill:grey;

 %% After Rotation
 subgraph "After Right Rotation"
     Y2["y"]
     A2["a"]
     X2["x"]
     B2["b"]
     C2["c"]
     Y2 --> A2
     Y2 --> X2
     X2 --> B2
     X2 --> C2
     class A2 unchanged
     class C2 unchanged
 end

 %% Before Rotation
 subgraph "Before Right Rotation"
     X["x"]
     Y["y"]
     A["a"]
     B["b"]
     C["c"]
     X --> Y
     X --> C
     Y --> A
     Y --> B
     class A unchanged
     class C unchanged
 end
     %% Arrow styles
     linkStyle 0 stroke:grey,stroke-width:2px;
     linkStyle 1 stroke:black,stroke-width:2px;
     linkStyle 2 stroke:black,stroke-width:2px;
     linkStyle 3 stroke:grey,stroke-width:2px;
     linkStyle 4 stroke:black,stroke-width:2px;
     linkStyle 5 stroke:grey,stroke-width:2px;
     linkStyle 6 stroke:grey,stroke-width:2px;
     linkStyle 7 stroke:black,stroke-width:2px;
Loading
⬅️ to the left
graph TD
 %% Define Styles
 classDef unchanged fill:grey;

 %% After Rotation
 subgraph "After Left Rotation"
     Y2["y"]
     X2["x"]
     A2["a"]
     B2["b"]
     C2["c"]
     Y2 --> X2
     Y2 --> C2
     X2 --> A2
     X2 --> B2
     class A2 unchanged
     class C2 unchanged
 end

 %% Before Rotation
 subgraph "Before Left Rotation"
     X["x"]
     Y["y"]
     A["a"]
     B["b"]
     C["c"]
     X --> A
     X --> Y
     Y --> B
     Y --> C
     class A unchanged
     class C unchanged
 end

     %% Arrow styles
     linkStyle 0 stroke:black,stroke-width:2px;
     linkStyle 1 stroke:black,stroke-width:2px;
     linkStyle 2 stroke:grey,stroke-width:2px;
     linkStyle 3 stroke:black,stroke-width:2px;
     linkStyle 4 stroke:grey,stroke-width:2px;
     linkStyle 5 stroke:black,stroke-width:2px;
     linkStyle 6 stroke:black,stroke-width:2px;
     linkStyle 7 stroke:grey,stroke-width:2px;
Loading
right child of a left child (or vice versa)

Depending on the condition the subtree is rotated agains the direction of the node around it's parent.

left child of a left child (or right of a right)

Depending on the direction of the new node, the rotations happens around the direction of the node around the grandparent befor fixing the coloring of the then rearanged subtree.

🏗️💪🔩 Refactoring

Refactoring is fun and makes the code much cleaner. This was made simpler by adding test for the functionality. After the test where added and some bug out the window, the main refactoring could happen:

  • Extracting the basic binary tree insert into a seperate method made the insert smaller and easier to read
  • Update for setter of $left and $right attributes where extended by an update of the parent and later on where extend to handle null as parameter which further reduced the complexity of the code
  • Because the rotateLeft and rotateRight methods both start of basicly the same, by checking if the parent of the center exists and update it's appropriate child and the parent of the corresponding child, the logic was also extracted and abstracted by providing it with the direction in which the child should be taken. Later on the syntax was slimmed down further by taking into account, that a cild which is neither the right nor the left sibling child, has no parent
  • Some syntactic ✨ sugar, more usage of the new setter functionality, some PHPDoc and a lot of fun later the insertion logic is complete
1️⃣2️⃣2️⃣ Enum

Since the idea of a color enum worked out fine and i was confident in the tests, i decided to go a step further and implement a direction emun.
The enum consists of left and right and provides them in uppercase as name and in lowercase as value. This enabled an additional refactoring, removin duplication for rotations, retrieval of sibling and dubplications for directional child checks.

Deletion

The deletion follows a similar strucutre. We first performe a basic binary tree deletion and then rebalance/fix the result.

Standart Binary Tree deletion

The standard deletion in a Binary tree has 3 scenarios which look like this:

  1. A node with no children is removed from the tree
  2. A node with one child is replaced by its child
  3. A node with two children is replaced by the most left child in the right subtree

@dimitrijjedich dimitrijjedich self-assigned this Dec 13, 2024

namespace Src\helper;

use Src\helper\enums\Color;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PSR4 Violation
should be

use Src\Helper\Enums\Color

Außerdem ist mir persönlich Color etwas zu generisch. Es sollte RedBlackTreeColor sein, damit es später keine Überschneidungen beim Enum Color gibt.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After renaming the enums folder PHPStan was complaining that the case was wrong. Was fixed by changing it to singular.

The Helper folder should be renamed in a seperate PR since it effect the code project wide. Open Issue for that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Color enum was kept generic for potential reuse later on since it could be extended without effecting the usage in the RedBlackNode/Tree implementation.

Gibt es hier ein Argument für die Spezifizierung? Allgemein wäre doch ein wiederverwendbares Color Enum sinnvoller als die Spezifizierung eine RedBlackTreeColor welches nur einen Zweck erfüllt oder?

@dimitrijjedich dimitrijjedich mentioned this pull request Dec 13, 2024
the problem arises, because the parent is set to left (null save) first and then the right is retrieved from the new parent. The previously unknown (not null save) parent is now null save but not recognised by PHPStan

The same goes for the rotate left in the other direction
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants