klips/cpp/algorithms/trees/redblack/redblack.cpp

789 lines
29 KiB
C++

/*#############################################################################
## Author: Shaun Reed ##
## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ##
## About: An example of a red-black tree implementation ##
## The algorithms in this example are seen in MIT Intro to Algorithms ##
## ##
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################
*/
#include "redblack.h"
/*******************************************************************************
* Constructors, Destructors, Operators
*******************************************************************************/
// Use a static member for nil
// + All RBTs will share this value to represent values Not In the List
RedBlackTree::RedBlackNode *RedBlackTree::nil = new RedBlackTree::RedBlackNode();
/* RedBlackNode Copy Constructor
*
* Runs in O( n ) time, since we visit each node in the RBT once
* + Where n is the total number of nodes within the RBT
*
* @param rhs An existing RBT to initialize this node (and children) with
*/
RedBlackTree::RedBlackNode::RedBlackNode(const RedBlackNode &toCopy)
{
// Base case, breaks recursion when we hit a null node
// + Returns to the previous call in the stack
if (&toCopy == nil) return;
// Set the element of this RedBlackNode to the value in toCopy->element
element = toCopy.element;
// If there is a left / right node, copy it using recursion
// + If there is no left / right node, set them to nullptr
if (toCopy.left != nil) {
left = new RedBlackNode(*toCopy.left);
left->parent = this;
}
else left = nil;
if (toCopy.right != nil) {
right = new RedBlackNode(*toCopy.right);
right->parent = this;
}
else right = nil;
if (toCopy.parent == nil) parent = nil;
color = toCopy.color;
}
/** RedBlackTree Copy Assignment Operator
* @brief Empty the calling object's root RedBlackNode, swap with the rhs data
* + Utilizes the copy-swap-idiom
*
* Runs in O( n ) time, since we visit each node in the RBT once
* + Where n is the total number of nodes within the RBT
*
* makeEmpty() and clone() are both O( n ), and we call each sequentially
* + This would appear to be O( 2n ), but we drop the constant of 2
*
* @param rhs The RBT to copy, beginning from its root RedBlackNode
* @return RedBlackTree The copied RedBlackTree object
*/
RedBlackTree& RedBlackTree::operator=(RedBlackTree rhs)
{
// If the objects are already equal, do nothing
if (this == &rhs) return *this;
// Empty this->root
makeEmpty(root);
// Copy rhs to this->root
std::swap(root, rhs.root);
return *this;
}
RedBlackTree::RedBlackTree(const RedBlackTree &rhs) {
root = clone(rhs.getRoot());
}
/*******************************************************************************
* Public Member Functions
*******************************************************************************/
/** contains
* @brief Determines if value exists within a RedBlackNode and its children
*
* Runs in O( height ) time, given the height of the current RBT
* + In the worst case, we search for a node at the bottom of the RBT
*
* @param value The value to search for within the RBT
* @param start The root RedBlackNode to begin the search
* @return true If the value is found within the root node or its children
* @return false If the value is not found within the root node or its children
*/
bool RedBlackTree::contains(const int &value, RedBlackNode *start) const
{
// If tree is empty
if (start == nil) return false;
// If x is smaller than our current value
else if (value < start->element) return contains(value, start->left);
// If x is larger than our current value, check the right node
else if (value > start->element) return contains(value, start->right);
else return true;
}
/** rotateLeft
* @brief Rotates a node and it's children counter-clockwise around a pivotNode
*
* Runs in O( 1 ) constant time, only immediately available values are used
*
* @param pivotNode The node to begin rotation from
*/
void RedBlackTree::rotateLeft(RedBlackNode *pivotNode)
{
// To rotateLeft, we must relocate the rightChild node
RedBlackNode *rightChild = pivotNode->right;
pivotNode->right = rightChild->left;
// If the rightChild->left node exists, update it's parent to the pivotNode
if (rightChild->left != nil) rightChild->left->parent = pivotNode;
// After we rotateLeft, the rightChild will be in the position of pivotNode
// + So we update rightChild->parent to reflect this
rightChild->parent = pivotNode->parent;
// The following conditions relocate the rightChild to its new position
// Case 1: pivotNode has no parent, so it must be the rootNode
// Case 2: pivotNode is the left child of its parent node
// Case 2: pivotNode is the right child of its parent node
if (pivotNode->parent == nil) root = rightChild;
else if (pivotNode == pivotNode->parent->left) {
pivotNode->parent->left = rightChild;
}
else pivotNode->parent->right = rightChild;
// The rightChild is now the parent of pivotNode
// + Since we rotated left, set rightChild->left to point to the pivotNode
// + Update the pivotNode->parent to reflect this
rightChild->left = pivotNode;
pivotNode->parent = rightChild;
}
/** rotateRight
* @brief Rotates a node and it's children clockwise around a pivotNode
*
* Runs in O( 1 ) constant time, only immediately available values are used
*
* @param pivotNode The node to begin rotation from
*/
void RedBlackTree::rotateRight(RedBlackTree::RedBlackNode *pivotNode)
{
// To rotateRight, we must relocate the leftChild node
RedBlackNode *leftChild = pivotNode->left;
pivotNode->left = leftChild->right;
// If the leftChild->left node exists, update it's parent to the pivotNode
if (leftChild->left != nil) leftChild->right->parent = pivotNode;
// After we rotateRight, the leftChild will be in the position of pivotNode
// + So we update leftChild->parent to reflect this
leftChild->parent = pivotNode->parent;
// The following conditions relocate the leftChild to its new position
// Case 1: pivotNode has no parent, so it must be the rootNode
// Case 2: pivotNode is the left child of its parent node
// Case 2: pivotNode is the right child of its parent node
if (pivotNode->parent == nil) root = leftChild;
else if (pivotNode == pivotNode->parent->left) {
pivotNode->parent->left = leftChild;
}
else pivotNode->parent->right = leftChild;
// The leftChild is now the parent of pivotNode
// + Since we rotated right, set leftChild->right to point to the pivotNode
// + Update the pivotNode->parent to reflect this
leftChild->right = pivotNode;
pivotNode->parent = leftChild;
}
/** insertFixup
* @brief Corrects RBT properties to enforce proper node colors after insertion
*
* Runs in O( lg(n) ) time, where n is the number of nodes in the RBT
*
* @param start The node to begin the fixup operation from
*/
void RedBlackTree::insertFixup(RedBlackTree::RedBlackNode *start)
{
// While the parent of start node is valid and colored red
// + Check for the two red nodes in a row, and update their colors
while (start->parent != nil && start->parent->color == Red) {
// If the parent of start node is a *left* child
if (start->parent == start->parent->parent->left) {
// Check the *right* uncle node to enforce RBT properties
// + Since the start->parent->right would be the start node's sibling
// ++ The start->parent->parent->left would be the start node's uncle
RedBlackNode *uncleNode = start->parent->parent->right;
// If the uncleNode is red, we are in case 1
// + In case 1, start->parent->color and uncleNode->color are both Red
if (uncleNode->color == Red) {
start->parent->color = Black;
uncleNode->color = Black;
start->parent->parent->color = Red;
// Advance start to it's grandparent node
start = start->parent->parent;
}
else { // Otherwise, if case 1 is not violated...
// If the start node is a right child, we are in case 2
// + In case 2, uncleNode is Black, and start node is a right child
if (start == start->parent->right) {
// Advance start up the tree to it's parent node, then *rotateLeft*
start = start->parent;
rotateLeft(start);
// After this rotation, we have forced ourselves into case 3
// + So, whether this case (2) executes or not, we end up in case 3
}
// No need for an if statement here, since the start node must be either
// + a right or left child. It cannot be neither always run these steps.
// ++ The only time these steps are skipped, is when we are in case 1
// Start node is a left child, we are in case 3
// + In case 3, uncleNode is Black and start node is a left child
start->parent->color = Black;
start->parent->parent->color = Red;
// Rotate around the grandparent node
rotateRight(start->parent->parent);
}
}
else { // If the parent of start node is a *right* child
// We follow the same 3 cases as above..
// + but with all left / right rotations and references swapped
// Check the *left* uncle node to enforce RBT properties
RedBlackNode *uncleNode = start->parent->parent->left;
// If the uncleNode is red, we are in case 1
if (uncleNode != nil && uncleNode->color == Red) {
start->parent->color = Black;
uncleNode->color = Black;
start->parent->parent->color = Red;
// Advance start to it's grandparent node
start = start->parent->parent;
}
else { // Otherwise, if case 1 is not violated...
// If the start node is a left child, we are in case 2
if (start == start->parent->left) {
// Advance start up the tree to it's parent node, then *rotateRight*
start = start->parent;
rotateRight(start);
// After this rotation, we have forced ourselves into case 3
// + So, whether this case (2) executes or not, we end up in case 3
}
// The only time these steps are skipped, is when we are in case 1
// + Always perform these steps when not in case 1
// Start node is a *right* child, we are in case 3
start->parent->color = Black;
start->parent->parent->color = Red;
// Rotate around the grandparent node
rotateLeft(start->parent->parent);
}
}
// The while() loop will always terminate after case 2 and (or) 3 is ran
}
// The root of the RBT should always be black
root->color = Black;
}
/** deleteFixup
* @brief Corrects RBT properties to enforce proper node colors after removal
*
* Runs in O( lg(n) ) time, where n is the number of nodes in the RBT
*
* @param start The node to begin the fixup operation from
*/
void RedBlackTree::deleteFixup(RedBlackTree::RedBlackNode *start)
{
// Until we reach the root of the RBT, move the extra black node up the tree
while (start != root && start->color == Black) {
// If the start node is a *left* child
if (start == start->parent->left) {
RedBlackNode *siblingNode = start->parent->right;
// If start node's siblingNode is colored Red, we are in case 1
// + In case 1, the only requirement is siblingNode is Red
if (siblingNode->color == Red) {
// Color the siblingNode Black, and the start->parent node Red
siblingNode->color = Black;
start->parent->color = Red;
// rotateLeft around the parent node, and update the siblingNode
// + siblingNode now represents the new sibling
// + start->parent is now a Red node with two Black siblings
// ++ This is ideal, since Red nodes can only have Black children nodes
rotateLeft(start->parent);
siblingNode = start->parent->right;
}
// If the siblingNode is Black, we are in case 2
// + In case 2, siblingNode is Black and has two Black children nodes
if (siblingNode->left->color == Black && siblingNode->right->color == Black) {
// Color the siblingNode Red and advance start node up the tree
siblingNode->color = Red;
start = start->parent;
}
else { // If either one of the siblingNode's children are Red...
// If the siblingNode->right child node is Black, we are in case 3
// + In case 3, the start node's siblingNode is black
// ++ And the siblingNode's right child is Black
if (siblingNode->right->color == Black) {
// Color the left child of siblingNode Black, and siblingNode to Red
siblingNode->left->color = Black;
siblingNode->color = Red;
// rotateRight(siblingNode) places the Red siblingNode as the right
// + child of the previous siblingNode->left
rotateRight(siblingNode);
// Update the siblingNode of start node to reflect the new sibling
siblingNode = start->parent->right;
// After rotateRight(siblingNode), we have put ourselves into case 4
}
// siblingNode->left is colored Black, we are in case 4
// + In case 4, the start node's sibling is Black
// ++ And the siblingNode's right child is Red
siblingNode->color = start->parent->color;
start->parent->color = Black;
siblingNode->right->color = Black;
// rotateLeft around the start->parent node, placing the siblingNode
// + In the previous position of the start->parent node
rotateLeft(start->parent);
// The previous start->parent node is now siblingNode's left child
// Setting the start node to root node ensures the while() terminates
start = root;
}
}
else { // If the start node is a right child
// We follow the same 3 cases as above..
// + but with all left / right rotations and references swapped
RedBlackNode *siblingNode = start->parent->left;
// If start node's siblingNode is colored Red, we are in case 1
if (siblingNode->color == Red) {
// Color the siblingNode Black, and the start->parent node Red
siblingNode->color = Black;
start->parent->color = Red;
// *rotateRight* around the parent node, and update the siblingNode
rotateRight(start->parent);
siblingNode = start->parent->left;
}
// If the siblingNode is Black, we are in case 2
if (siblingNode->left->color == Black && siblingNode->right->color == Black) {
siblingNode->color = Red;
start = start->parent;
}
else { // If either one of the siblingNode's children are Red...
// If the siblingNode->left child node is Black, we are in case 3
if (siblingNode->left->color == Black) {
// Color the *left* child of siblingNode Black, and siblingNode to Red
siblingNode->right->color = Black;
siblingNode->color = Red;
// rotateLeft(siblingNode) places the Red siblingNode as the *left*
// + child of the previous siblingNode->right
rotateLeft(siblingNode);
// Update the siblingNode of start node to reflect the new sibling
siblingNode = start->parent->left;
// After rotateRight(siblingNode), we have put ourselves into case 4
}
// siblingNode->left is colored Black, we are in case 4
siblingNode->color = start->parent->color;
start->parent->color = Black;
siblingNode->left->color = Black;
// rotateRight around the start->parent node, placing the siblingNode
// + In the previous position of the start->parent node
rotateRight(start->parent);
// Setting the start node to root node ensures the while() terminates
start = root;
}
}
}
// TODO: I could only get this working by using nil->parent
// to store the parent of the last replacementNode within removeNode()
// Is this expected, or have I missed something and hacked this?
// Update nil->parent to nullptr if it is any other value
// + When we transplant(), we temporarily store the parent of the relocatedNode
// + relocatedNode is seen passed to transplant() within remove()
if (nil->parent != nullptr) nil->parent = nullptr;
}
/** makeEmpty
* @brief Recursively delete the given root RedBlackNode and all of its children
*
* Runs in O( n ) time, since we need to visit each node in the tree once
* + Where n is the total number of nodes in the tree
*
* @param tree The root RedBlackNode to delete, along with all child nodes
*/
void RedBlackTree::makeEmpty(RedBlackTree::RedBlackNode * & tree)
{
// Base case: When all nodes have been deleted, tree is a nullptr
// + Breaks from recursion
if (tree != nil) {
makeEmpty(tree->left);
makeEmpty(tree->right);
delete tree;
// Set each deleted node = nil, overwriting unused children with nullptr
tree = nil;
}
}
/** isEmpty
* @brief Determine whether or not the calling RBT object is empty
*
* Runs in constant time, O( 1 )
*
* @return true If this->root node points to an empty tree (nullptr)
* @return false If this->root node points to a constructed RedBlackNode
*/
bool RedBlackTree::isEmpty() const
{
return root == nil;
}
/** insert
* @brief Insert a value into the tree starting at a given RedBlackNode
* + Uses recursion
*
* Runs in O( lg(n) ) time, since the height of RBTs are <= lg(n)
* + Where n is the number of nodes in the RBT
* + The *sequential* call to insertFixup is also O( lg(n) )..
* ++ Appears to be O( 2lg(n) ), but we drop the constant 2; No extra overhead
*
* @param newValue The value to be inserted
* @param start The RedBlackNode to begin insertion
* @param prevNode The last checked RedBlackNode
* + prevNode is used to initialize new node's parent
*/
void RedBlackTree::insert(const int &newValue,
RedBlackNode *&start, RedBlackNode *prevNode)
{
// Base case: We found a valid position which is empty for the newValue
if (start == nil) {
// Build a new node, place it at the current position
// + Breaks out of recursion after this code block finishes
// TODO: Valgrind thinks there is a memory leak here
// It seems to me that calling `delete start` would delete the static nil?
start = new RedBlackNode(newValue, Red, nil, nil, prevNode);
// Enforce RBT properties on the entire subtree of start node
// + By default, start is the root node of the RBT unless specified
// + This is done with a call to the inlined insert(int) function
insertFixup(start);
}
else if (newValue < start->element) insert(newValue, start->left, start);
else if (newValue > start->element) insert(newValue, start->right, start);
else return;
}
/** remove
* @brief Removes a value from the RBT of the given RedBlackNode
*
* Runs in O( lg(n) ) time, since the height of RBTs are <= lg(n)
* + Where n is the number of nodes in the RBT
* + The *sequential* call to removeFixup is also O( lg(n) )..
* ++ Appears to be O( 2lg(n) ), but we drop the constant 2; No extra overhead
*
* @param removeNode The RedBlackNode to remove from the RBT
*/
void RedBlackTree::remove(RedBlackNode *removeNode)
{
RedBlackNode *replacementNode = removeNode;
Color originalColor = removeNode->color;
// This node will be passed to removeFixup() later to enforce RBT properties
RedBlackNode *fixupNode;
if (removeNode->left == nil) {
// removeNode has no left node; Replace removeNode with removeNode->right
// + It doesn't matter if removeNode->right is nullptr or a valid node
// + Since there is no left node, this is the only possible valid transplant
// Transplant the right node and its subtree over this node
fixupNode = removeNode->right;
transplant(removeNode, removeNode->right);
}
else if (removeNode->right == nil) {
// removeNode has no right node; Replace removeNode with removeNode->right
// + removeNode->left exists, in this case
fixupNode = removeNode->left;
transplant(removeNode, removeNode->left);
}
else {
// The node to remove has both left and right child nodes
// + We should find a replacement within the child with a larger subtree
// Compare the depth of subtrees for both children of removeNode...
// + Red child node indicates the equal or longer subtree
// + Black child node indicates the depth of the child's subtree is smaller
if (removeNode->left->color) {
// + findMax(removeNode->right) returns the next smallest value in the RBT
replacementNode = findMax(removeNode->left);
originalColor = replacementNode->color;
fixupNode = replacementNode->right;
if (replacementNode->parent == removeNode) {
fixupNode->parent = replacementNode;
}
else {
transplant(replacementNode, replacementNode->right);
replacementNode->right = removeNode->right;
replacementNode->right->parent = replacementNode;
}
transplant(removeNode, replacementNode);
replacementNode->left = removeNode->left;
replacementNode->left->parent = replacementNode;
replacementNode->color = removeNode->color;
}
else {
// If the removeNode->left child is black, use the right subtree
// + findMin(removeNode->right) returns the next largest value in the RBT
replacementNode = findMin(removeNode->right);
originalColor = replacementNode->color;
fixupNode = replacementNode->right;
if (replacementNode->parent == removeNode) {
fixupNode->parent = replacementNode;
}
else {
transplant(replacementNode, replacementNode->right);
replacementNode->right = removeNode->right;
replacementNode->right->parent = replacementNode;
}
transplant(removeNode, replacementNode);
replacementNode->left = removeNode->left;
replacementNode->left->parent = replacementNode;
replacementNode->color = removeNode->color;
}
}
// If originalColor of the replacementNode is black, enforce RBT properties
if (!originalColor) deleteFixup(fixupNode);
}
/** printInOrder
* @brief Uses recursion to output left subtree, root node, then right subtrees
*
* Runs in O( n ) time, since we need to visit each node in the tree once
* + Where n is the total number of nodes within the RBT
*
* @param start The root RedBlackNode to begin the 'In Order' output
*/
void RedBlackTree::printInOrder(RedBlackNode *start) const
{
if(start != nil) {
printInOrder(start->left);
std::cout << start->element << " ";
printInOrder(start->right);
}
}
/** printPostOrder
* @brief Uses recursion to output left subtree, right subtree, then the root
*
* Runs in O( n ) time, since we need to visit each node in the tree once
* + Where n is the total number of nodes within the RBT
*
* @param start The root RedBlackNode to begin the 'Post Order' output
*/
void RedBlackTree::printPostOrder(RedBlackNode *start) const
{
if (start != nil) {
printPostOrder(start->left);
printPostOrder(start->right);
std::cout << start->element << " ";
}
}
/** printPreOrder
* @brief Uses recursion to output the root, then left subtree, right subtrees
*
* Runs in O( n ) time, since we need to visit each node in the tree once
* + Where n is the total number of nodes within the RBT
*
* @param start The root RedBlackNode to begin the 'Pre Order' output
*/
void RedBlackTree::printPreOrder(RedBlackNode *start) const
{
if (start != nil) {
std::cout << start->element << " ";
printPreOrder(start->left);
printPreOrder(start->right);
}
}
/** search
* @brief Search for a given value within a tree or subtree using recursion
*
* Runs in O( height ) time
* + In the worst case, we are searching for a node at the bottom of the RBT
*
* @param value The value to search for
* @param start The node to start the search from; Can be a subtree
* @return A pointer to the RedBlackNode containing the value within the RBT
* + Returns nullptr if the node was not found
*/
RedBlackTree::RedBlackNode *RedBlackTree::search(
const int &value, RedBlackNode *start) const
{
// Base case: If RBT is empty, or holds the value we are searching for
// + Breaks out of recursion
if (start == nil || start->element == value) return start;
else if (start->element < value) return search(value, start->right);
else if (start->element > value) return search(value, start->left);
else return nullptr;
}
/** findMin
* @brief Find the minimum value within the RBT of the given RedBlackNode
* + This example uses a while loop; findMax uses recursion
*
* Runs in O( height ) time
* + In the worst case, we traverse to to the left-most bottom of the RBT
*
* @param start The root RedBlackNode to begin checking values
* @return A pointer to the RedBlackNode which contains the smallest value
* + Returns nullptr if RBT is empty
*/
RedBlackTree::RedBlackNode * RedBlackTree::findMin(RedBlackNode *start) const
{
// If our tree is empty
if (start == nil) return nullptr;
while (start->left != nil) start = start->left;
// If current node has no smaller children, it is min
return start;
}
/** findMax
* @brief Find the maximum value within the RBT of the given RedBlackNode
* + This example uses recursion; findMin uses a while loop
* ++ Both functions can be implemented using a loop or recursion
*
* Runs in O( height ) time
* + In the worst case, we traverse to to the right-most bottom of the RBT
*
* @param start The root RedBlackNode to begin checking values
* @return A pointer to the RedBlackNode which contains the largest value
* + returns nullptr if RBT is empty
*/
RedBlackTree::RedBlackNode * RedBlackTree::findMax(RedBlackNode *start) const
{
// If our tree is empty
if (start == nil) return nullptr;
// Base case: If current node has no larger children, it is max; Break recursion
if (start->right == nil) return start;
// Move down the right side of our tree and check again
return findMax(start->right);
}
/** predecessor
* @brief Finds the previous value in-order from the value at a given startNode
*
* Runs in O( height ) time
* + In the worst case we traverse to the bottom of the RBT
*
* @param startNode The node containing the value to find predecessor of
* @return The node which is the predecessor of startNode
*/
RedBlackTree::RedBlackNode * RedBlackTree::predecessor(RedBlackNode *startNode) const
{
if (startNode->left != nil) return findMax(startNode->left);
// If startNode has a parent, walk up the tree until we reach the top
// + If startNode has no parent, we set it to nullptr and return
RedBlackNode *temp = startNode->parent;
while (temp != nil && temp->left == startNode) {
startNode = temp;
temp = temp->parent;
}
return temp;
}
/** successor
* @brief Finds the next value in-order from the value at a given startNode
*
* Runs in O( height ) time
* + In the worst case we traverse to the bottom of the RBT
*
* @param startNode The node containing the value to find successor of
* @return The node which is the successor of startNode
*/
RedBlackTree::RedBlackNode * RedBlackTree::successor(RedBlackNode *startNode) const
{
// If there is a right subtree, next value in-order is findMin(rightSubtree)
if (startNode->right != nil) return findMin(startNode->right);
// If startNode has a parent, walk up the tree until we reach the top
// + If startNode has no parent, we set it to nullptr and return
RedBlackNode *temp = startNode->parent;
while (temp != nil && temp->right == startNode) {
startNode = temp;
temp = temp->parent;
}
return temp;
}
/*******************************************************************************
* Private Member Functions
*******************************************************************************/
/** clone
* @brief Clone a RBT node and all its children using recursion
*
* Runs in O( n ) time, since each node must be copied individually
*
* @param start The node to begin cloning from
* @return A pointer to the RedBlackNode which is root node of the copied tree
*/
RedBlackTree::RedBlackNode * RedBlackTree::clone(RedBlackNode *start)
{
if (root == nil) root = new RedBlackNode();
// Base case: There is nothing to copy, break from recursion
if (start == nil) return nil;
// Construct all child nodes through recursion, return root node
return new RedBlackNode(*start);
}
/** transplant
* @brief Replaces, or overwrites, a node and with a new node
* + The subtree attaches to oldNode is replace with that of newNode
*
* Runs in constant O( 1 ) time
* + We only need to check and update values immediately available
*
* @param oldNode The node to overwrite with newNode
* @param newNode The new node to take the place of oldNode
*/
void RedBlackTree::transplant(RedBlackNode *oldNode, RedBlackNode *newNode)
{
// case 1: If oldNode is the root node at this->root
// + 2: if the oldNode is the left child of it's parent
// + 3: case if the oldNode is the right child of it's parent
if (oldNode->parent == nil) root = newNode;
else if (oldNode == oldNode->parent->left) {
// Update the parent of oldNode to reflect the transplant
oldNode->parent->left = newNode;
}
else if (oldNode == oldNode->parent->right) {
// Update the parent of oldNode to reflect the transplant
oldNode->parent->right = newNode;
}
// If we did not replace oldNode with a nullptr, update newNode's parent
newNode->parent = oldNode->parent;
}