A red-black tree is a self-balancing binary search tree. Each node stores an additional color field ("RED" or "BLACK") to ensure the tree remains balanced during insertions and deletions.
The red-black tree is a variant of a 4th-order B-tree (2-3-4 tree).[^gilbas1978]
Properties
A valid red-black tree must satisfy the following four properties:
Nodes are either red or black.
NIL nodes (empty leaf nodes) are black.
The children of a red node are black.
Every path from the root node to any NIL node contains the same number of black nodes.
The following diagram shows a valid red-black tree:
Note
Some references include a fifth property: the root node must be black. This property requires that if the root becomes red after an insertion, it must be recolored black. However, since recoloring the root black can also be deferred until deletion operations, this property is not strictly necessary (the code implementation provided in this article satisfies it). For rigor, the Wikipedia excerpt is quoted here:
Some authors, e.g. Cormen & al.,[^cite_note-cormen2009-18]claim "the root is black" as fifth requirement; but not Mehlhorn & Sanders[^cite_note-mehlhorn2008-17]or Sedgewick & Wayne.[^cite_note-algs4-16]Since the root can always be changed from red to black, this rule has little effect on analysis. This article also omits it, because it slightly disturbs the recursive algorithms and proofs.
/** * An RBTree-based set implementation * * @tparam key_t key type * @tparam compare_t compare function */template<typenamekey_t,typenamecompare_t=std::less<key_t>>structrb_tree{/** * Tree node */structnode_t{node_t*fa;// == nullptr if root of the tree, otherwise parentnode_t*ch[2];// == nullptr if child is empty// ch[0]: left child, ch[1]: right childkey_tdata;size_tsz;// Size of subtreeboolred;// == true if node is red, otherwise black/** * Get child direction of current non-null node * * @return true if this node is right child of its parent, otherwise false */autochild_dir()const->bool{returnthis==fa->ch[1];}};usingpointer=node_t*;usingconst_pointer=constnode_t*;usingpointer_const=node_t*const;constcompare_tcompare;pointerroot;rb_tree():compare{},root{nullptr}{}// ...};
Note
In the storage of red-black tree nodes, using an array to store child node pointers can improve code reusability.
Operations
Note
There are multiple ways to implement insertion/deletion in red-black trees. This article adopts the implementation from Introduction to Algorithms, dividing post-insertion balance maintenance into 3 cases and post-deletion balance maintenance into 4 cases.
Traversal, finding minimum/maximum, searching for an element, finding the rank of an element, retrieving an element by rank, finding predecessor/successor, and other operations are identical to those in a Binary Search Tree and will not be repeated here.
Additionally, in the code comments for insertion/deletion balance maintenance below, we adopt the following conventions:
p indicates node p is black;
[p] indicates node p is red;
{p} indicates node p is either red or black;
|p| indicates node p is a NIL node or is black.
Rotation
Rotation is the key operation that allows most balanced trees to maintain balance. It can change the depth of local nodes without altering the in-order traversal result of a valid BST.
Implementation
1 2 3 4 5 6 7 8 91011121314151617181920
/** * @param p root of subtree (may be same as {@code root}) * @param dir direction. 0: left rotate; 1: right rotate * @return new root of subtree */autorotate(pointerp,booldir)->pointer{autog=p->fa;autos=p->ch[!dir];// new root of subtreeassert(s);// pointer to true node requireds->sz=p->sz,p->sz=size(p->ch[dir])+size(s->ch[dir])+1;autoc=s->ch[dir];if(c)c->fa=p;p->ch[!dir]=c,s->ch[dir]=p;p->fa=s,s->fa=g;if(g)g->ch[p==g->ch[1]]=s;elseroot=s;returns;}
Insertion
The insertion operation in a red-black tree is similar to that in a standard BST. For a red-black tree, a newly inserted node is initially red. After insertion, adjustments are made based on the states of the inserted node and related nodes to satisfy the four properties mentioned above.
/** * @return nullptr if insert failed, otherwise pointer of inserted node */autoinsert(constkey_t&data)->const_pointer{pointern=newnode_t;n->fa=n->ch[0]=n->ch[1]=nullptr;n->data=data,n->sz=1;pointernow=root,p=nullptr;booldir=0;while(now){p=now;dir=compare(now->data,data);now=now->ch[dir];}insert_fixup_leaf(p,n,dir);returnn;}/** * Insert leaf node {@code n} to {@code p} * * @param p parent of node which will be inserted * @param n leaf node which will be inserted * @param dir direction of n, 0: left; 1: right */voidinsert_leaf(pointer_constp,pointer_constn,booldir){if(!p){root=n;return;}p->ch[dir]=n,n->fa=p;autonow=p;while(now)now->sz++,now=now->fa;}/** * Insert leaf node {@code n} to {@code p}, then fixup * * @param p parent of node which will be inserted * @param n node which will be inserted * @param dir direction of n, 0: left; 1: right */voidinsert_fixup_leaf(pointerp,pointern,booldir){n->red=p;insert_leaf(p,n,dir);// Fix double red// ...// Post process: color root blackroot->red=false;}
Post-Insertion Balance Maintenance
Note
To deepen understanding, readers are encouraged to verify whether property 4 holds after balance maintenance.
Since the inserted node is red if it is not the root, insertion may violate property 3, requiring balance maintenance.
Let the inserted node be \(n\), its parent be \(p\), its grandparent be \(g\), and its uncle be \(u\). From property 3, \(g\) must be black.
We recursively maintain balance upward from the insertion point. If \(p\) is black, we can stop. Otherwise, there are 3 cases.
\(p\) and \(u\) are both red. In this case, we only need to recolor.
Implementation
1 2 3 4 5 6 7 8 9101112
// Case 1: both p and u are red// g [g]// / \ / \ // [p] [u] ==> p u// / /// [n] [n]if(is_red(u)){p->red=u->red=false;g->red=true;n=g;continue;}
Insert case 2
\(p\) is red, \(u\) is black, and the direction of \(p\) relative to its parent differs from the direction of \(n\) relative to \(p\).
In this case, we need to rotate node \(p\) to convert it into case 3.
Implementation
12345678
// p is red and u is black// Case 2: dir of n is different with dir of p// g g// / \ / \ // [p] u ==> [n] u// \ /// [n] [p]if(n->child_dir()!=p_dir)rotate(p,p_dir),std::swap(n,p);
Insert case 3
\(p\) is red, \(u\) is black, and the direction of \(p\) relative to its parent is the same as the direction of \(n\) relative to \(p\).
In this case, we need to rotate node \(g\) to make \(p\) the root of the subtree, then swap the colors of \(p\) and \(g\).
Implementation
12345678
// Case 3: p is red, u is black and dir of n is same as dir of p// g p// / \ / \ // [p] u ==> [n] [g]// / \ // [n] up->red=false,g->red=true;rotate(g,!p_dir);
Deletion
The deletion operation in a red-black tree involves more steps compared to a standard BST. Specifically:
If the node \(n\) to be deleted has two children, swap the data of \(n\) with the maximum node \(s\) in the left subtree, and set \(n\) to \(s\). Now \(n\) cannot have two children.
If the node \(n\) to be deleted has one child \(s\). From property 4, \(s\) must be red, and from property 3, \(n\) must be black. Therefore, simply replace the pointer in the parent node \(p\) corresponding to \(n\) with the address of \(s\), replace the parent pointer of \(s\) with the address of \(p\), and then recolor \(s\) black.
If the node \(n\) to be deleted has no children. If \(n\) is the root or \(n\) is red, it can be deleted directly. Otherwise, direct deletion would violate property 4, requiring balance maintenance.
Implementation
```cpp
/*
* @return succeed or not
/
auto erase(const key_t &key) -> bool {
auto p = lower_bound(key);
if (!p || p->data != key) return false;
erase(p);
return true;
}
/*
* @return {@code next(p)}
/
auto erase(pointer p) -> const_pointer {
if (!p) return nullptr;
pointer result;
if (p->ch[0] && p->ch[1]) {
auto s = leftmost(p->ch[1]);
std::swap(s->data, p->data);
result = p, p = s;
} else
result = next(p);
erase_fixup_branch_or_leaf(p);
delete p;
return result;
}
/*
* Erase node {@code n}
*
* @param n node which will be deleted, must have no more than 2 child
/
void erase_branch_or_leaf(pointer_const n) {
auto p = n->fa, s = n->ch[0] ? n->ch[0] : n->ch[1];
if (s) s->fa = p;
if (!p) {
root = s;
return;
}
p->ch[n->child_dir()] = s;
auto now = p;
while (now) now->sz--, now = now->fa;
}
/*
* Erase node {@code n}, then fixup
*
* @param n node which will be deleted, must have no more than 2 child
/
void erase_fixup_branch_or_leaf(pointer n) {
bool n_dir = n == root ? false : n->child_dir();
erase_branch_or_leaf(n);
auto p = n->fa;
if (!p) { // n is root
if (root) root->red = false;
return;
} else {
auto s = p->ch[n_dir];
if (s) { // n has 1 child
// n must be black and s must be red, so we need to color s black
s->red = false;
return;
}
}
// n is not root but leaf with black color, need to be fixup
// ...
// Post process: see case 2 & case 4
n->red = false;
}
1
```
Post-Deletion Balance Maintenance
Note
To deepen understanding, readers are encouraged to verify whether property 4 holds after balance maintenance.
From the above discussion, \(n\) is a black leaf node and not the root. Let \(n\)'s parent be \(p\), sibling be \(s\), and nephews be \(c\) and \(d\).
Deletion maintenance also proceeds recursively upward from \(n\). If \(n\) is the root or \(n\) is red, we can stop. Otherwise, there are 4 cases.
1 2 3 4 5 6 7 8 910111213
while(p&&!n->red){autos=p->ch[!n_dir];// Delete case 1// ...// Other cases// s must be blackautoc=s->ch[n_dir],d=s->ch[!n_dir];// ...end_erase_fixup:p=n->fa;if(!p)break;n_dir=n->child_dir();}
Delete case 1
\(s\) is red.
In this case, we rotate \(p\) to make \(s\) the root of the subtree, then swap the colors of \(s\) and \(p\) to convert to one of the other three cases.
Implementation
1 2 3 4 5 6 7 8 91011
// Case 1: s is red// p s// / \ / \ // |n| [s] ==> [p] d// / \ / \ // c d |n| cif(is_red(s)){s->red=false,p->red=true;rotate(p,n_dir);s=p->ch[!n_dir];}
Delete case 2
The color of \(p\) is uncertain; \(s\), \(c\), and \(d\) are all black.
In this case, simply recolor \(s\) to red.
Note that if \(p\) is red, property 3 would be violated. However, if \(p\) is red, the loop will exit directly, so we recolor it black at the end.
Implementation
1 2 3 4 5 6 7 8 9101112
// Case 2: both c and d are black// {p} {p}// / \ / \ // |n| s ==> |n| [s]// / \ / \ // c d c d// p will be colored black in the endif(!is_red(c)&&!is_red(d)){s->red=true;n=p;gotoend_erase_fixup;}
Delete case 3
The color of \(p\) is uncertain; \(s\) and \(d\) are black, \(c\) is red.
In this case, rotate \(s\) to make \(c\) the root of the subtree originally rooted at \(s\), and swap the colors of \(s\) and \(c\) to convert to case 4.
Implementation
1 2 3 4 5 6 7 8 910111213
// Case 3: c is red and d is black// {p} {p}// / \ / \ // |n| s ==> |n| c// / \ \ // [c] d [s]// \ // dif(!is_red(d)){c->red=false,s->red=true;rotate(s,!n_dir);s=p->ch[!n_dir],c=s->ch[n_dir],d=s->ch[!n_dir];}
Delete case 4
The colors of \(p\) and \(c\) are uncertain; \(s\) is black, \(d\) is red.
In this case, rotate \(p\) to make \(s\) the root of the subtree, swap the colors of \(s\) and \(p\), and recolor \(d\) black to terminate balance maintenance.
Implementation
12345678
// Case 4: d is red// {p} {s}// / \ / \ // |n| s ==> p d// / \ / \ // {c} [d] |n| {c}s->red=p->red,p->red=d->red=false;rotate(p,n_dir),n=root;
Reference Code
The following code implements a set using a red-black tree:
/** * @file rbtree.hpp * @brief An RBTree-based set implementation * @details The set is sorted according to the {@code compare_t} function * provided; This implementation provides find, insert, remove, find order, find * key by order in O(log(n)) time. * @author [Tiphereth-A](https://github.com/Tiphereth-A) */#ifndef RBTREE_HPP#define RBTREE_HPP#include<cassert>#include<cstdint>#include<functional>usingstd::size_t;/** * An RBTree-based set implementation * * @tparam key_t key type * @tparam compare_t compare function */template<typenamekey_t,typenamecompare_t=std::less<key_t>>structrb_tree{/** * Tree node */structnode_t{node_t*fa;// == nullptr if root of the tree, otherwise parentnode_t*ch[2];// == nullptr if child is empty// ch[0]: left child, ch[1]: right childkey_tdata;size_tsz;// Size of subtreeboolred;// == true if node is red, otherwise black/** * Get child direction of current non-null node * * @return true if this node is right child of its parent, otherwise false */autochild_dir()const->bool{returnthis==fa->ch[1];}};usingpointer=node_t*;usingconst_pointer=constnode_t*;usingpointer_const=node_t*const;constcompare_tcompare;pointerroot;rb_tree():compare{},root{nullptr}{}~rb_tree(){post_order([](autoit){deleteit;});}autosize()const->size_t{returnsize(root);}template<typenameF>voidpre_order(Fcallback){autof=[&](auto&&f,pointerp){if(!p)return;callback(p),f(f,p->ch[0]),f(f,p->ch[1]);};f(f,root);}template<typenameF>voidin_order(Fcallback){autof=[&](auto&&f,pointerp){if(!p)return;f(f,p->ch[0]),callback(p),f(f,p->ch[1]);};f(f,root);}template<typenameF>voidpost_order(Fcallback){autof=[&](auto&&f,pointerp){if(!p)return;f(f,p->ch[0]),f(f,p->ch[1]),callback(p);};f(f,root);}autoleftmost(const_pointerp)const{returnmost(p,0);}autorightmost(const_pointerp)const{returnmost(p,1);}autoprev(const_pointerp)const{returnneighbour(p,0);}autonext(const_pointerp)const{returnneighbour(p,1);}autolower_bound(constkey_t&key)const->pointer{const_pointernow=root,ans=nullptr;while(now){if(!compare(now->data,key))ans=now,now=now->ch[0];elsenow=now->ch[1];}return(pointer)ans;}autoupper_bound(constkey_t&key)const->pointer{const_pointernow=root,ans=nullptr;while(now){if(compare(key,now->data))ans=now,now=now->ch[0];elsenow=now->ch[1];}return(pointer)ans;}// Order start from 0autoorder_of_key(constkey_t&key)const->size_t{size_tans=0;autonow=root;while(now){if(!compare(now->data,key))now=now->ch[0];elseans+=size(now->ch[0])+1,now=now->ch[1];}returnans;}// Order start from 0autofind_by_order(size_torder)const->const_pointer{const_pointernow=root,ans=nullptr;while(now&&now->sz>=order){autolsize=size(now->ch[0]);if(order<lsize)now=now->ch[0];else{ans=now;if(order==lsize)break;now=now->ch[1],order-=lsize+1;}}returnans;}/** * @return nullptr if insert failed, otherwise pointer of inserted node */autoinsert(constkey_t&data)->const_pointer{pointern=newnode_t;n->fa=n->ch[0]=n->ch[1]=nullptr;n->data=data,n->sz=1;pointernow=root,p=nullptr;booldir=0;while(now){p=now;dir=compare(now->data,data);now=now->ch[dir];}insert_fixup_leaf(p,n,dir);returnn;}/** * @return succeed or not */autoerase(constkey_t&key)->bool{autop=lower_bound(key);if(!p||p->data!=key)returnfalse;erase(p);returntrue;}/** * @return {@code next(p)} */autoerase(pointerp)->const_pointer{if(!p)returnnullptr;pointerresult;if(p->ch[0]&&p->ch[1]){autos=leftmost(p->ch[1]);std::swap(s->data,p->data);result=p,p=s;}elseresult=next(p);erase_fixup_branch_or_leaf(p);deletep;returnresult;}private:staticautosize(const_pointerp)->size_t{returnp?p->sz:0;}staticautois_red(const_pointerp)->bool{returnp?p->red:false;}/** * @param dir 0: leftmost, 1: rightmost */automost(const_pointerp,booldir)const->pointer{if(!p)returnnullptr;while(p->ch[dir])p=p->ch[dir];return(pointer)p;}/** * @param dir 0: prev, 1: next */autoneighbour(const_pointerp,booldir)const->pointer{if(!p)returnnullptr;if(p->ch[dir])returnmost(p->ch[dir],!dir);if(p==root)returnnullptr;while(p&&p->fa&&p->child_dir()==dir)p=p->fa;returnp?p->fa:nullptr;}/** * Insert leaf node {@code n} to {@code p} * * @param p parent of node which will be inserted * @param n leaf node which will be inserted * @param dir direction of n, 0: left; 1: right */voidinsert_leaf(pointer_constp,pointer_constn,booldir){if(!p){root=n;return;}p->ch[dir]=n,n->fa=p;autonow=p;while(now)now->sz++,now=now->fa;}/** * Erase node {@code n} * * @param n node which will be deleted, must have no more than 2 child */voiderase_branch_or_leaf(pointer_constn){autop=n->fa,s=n->ch[0]?n->ch[0]:n->ch[1];if(s)s->fa=p;if(!p){root=s;return;}p->ch[n->child_dir()]=s;autonow=p;while(now)now->sz--,now=now->fa;}/** * @param p root of subtree (may be same as {@code root}) * @param dir direction. 0: left rotate; 1: right rotate * @return new root of subtree */autorotate(pointerp,booldir)->pointer{autog=p->fa;autos=p->ch[!dir];// new root of subtreeassert(s);// pointer to true node requireds->sz=p->sz,p->sz=size(p->ch[dir])+size(s->ch[dir])+1;autoc=s->ch[dir];if(c)c->fa=p;p->ch[!dir]=c,s->ch[dir]=p;p->fa=s,s->fa=g;if(g)g->ch[p==g->ch[1]]=s;elseroot=s;returns;}#pragma GCC diagnostic ignored "-Wcomment"/** * Insert leaf node {@code n} to {@code p}, then fixup * * @param p parent of node which will be inserted * @param n node which will be inserted * @param dir direction of n, 0: left; 1: right */voidinsert_fixup_leaf(pointerp,pointern,booldir){n->red=p;insert_leaf(p,n,dir);// Fix double redwhile(is_red(p=n->fa)){boolp_dir=p->child_dir();autog=p->fa,u=g->ch[!p_dir];// Case 1: both p and u are red// g [g]// / \ / \ // [p] [u] ==> p u// / /// [n] [n]if(is_red(u)){p->red=u->red=false;g->red=true;n=g;continue;}// p is red and u is black// Case 2: dir of n is different with dir of p// g g// / \ / \ // [p] u ==> [n] u// \ /// [n] [p]if(n->child_dir()!=p_dir)rotate(p,p_dir),std::swap(n,p);// Case 3: p is red, u is black and dir of n is same as dir of p// g p// / \ / \ // [p] u ==> [n] [g]// / \ // [n] up->red=false,g->red=true;rotate(g,!p_dir);}// Post process: color root blackroot->red=false;}/** * Erase node {@code n}, then fixup * * @param n node which will be deleted, must have no more than 2 child */voiderase_fixup_branch_or_leaf(pointern){booln_dir=n==root?false:n->child_dir();erase_branch_or_leaf(n);autop=n->fa;if(!p){// n is rootif(root)root->red=false;return;}else{autos=p->ch[n_dir];if(s){// n has 1 child// n must be black and s must be red, so we need to color s blacks->red=false;return;}}// n is not root but leaf with black color, need to be fixupwhile(p&&!n->red){autos=p->ch[!n_dir];// Case 1: s is red// p s// / \ / \ // |n| [s] ==> [p] d// / \ / \ // c d |n| cif(is_red(s)){s->red=false,p->red=true;rotate(p,n_dir);s=p->ch[!n_dir];}// s must be blackautoc=s->ch[n_dir],d=s->ch[!n_dir];// Case 2: both c and d are black// {p} {p}// / \ / \ // |n| s ==> |n| [s]// / \ / \ // c d c d// p will be colored black in the endif(!is_red(c)&&!is_red(d)){s->red=true;n=p;gotoend_erase_fixup;}// Case 3: c is red and d is black// {p} {p}// / \ / \ // |n| s ==> |n| c// / \ \ // [c] d [s]// \ // dif(!is_red(d)){c->red=false,s->red=true;rotate(s,!n_dir);s=p->ch[!n_dir],c=s->ch[n_dir],d=s->ch[!n_dir];}// Case 4: d is red// {p} {s}// / \ / \ // |n| s ==> p d// / \ / \ // {c} [d] |n| {c}s->red=p->red,p->red=d->red=false;rotate(p,n_dir),n=root;end_erase_fixup:p=n->fa;if(!p)break;n_dir=n->child_dir();}// Post process: see case 2 & case 4n->red=false;}};#pragma GCC diagnostic warning "-Wcomment"#endif // RBTREE_HPP
#include<cassert>#include<cstdint>#include<functional>usingstd::size_t;/** * An RBTree-based set implementation * * @tparam key_t key type * @tparam compare_t compare function */template<typenamekey_t,typenamecompare_t=std::less<key_t>>structrb_tree{/** * Tree node */structnode_t{node_t*fa;// == nullptr if root of the tree, otherwise parentnode_t*ch[2];// == nullptr if child is empty// ch[0]: left child, ch[1]: right childkey_tdata;size_tsz;// Size of subtreeboolred;// == true if node is red, otherwise black/** * Get child direction of current non-null node * * @return true if this node is right child of its parent, otherwise false */autochild_dir()const->bool{returnthis==fa->ch[1];}};usingpointer=node_t*;usingconst_pointer=constnode_t*;usingpointer_const=node_t*const;constcompare_tcompare;pointerroot;rb_tree():compare{},root{nullptr}{}~rb_tree(){post_order([](autoit){deleteit;});}autosize()const->size_t{returnsize(root);}template<typenameF>voidpre_order(Fcallback){autof=[&](auto&&f,pointerp){if(!p)return;callback(p),f(f,p->ch[0]),f(f,p->ch[1]);};f(f,root);}template<typenameF>voidin_order(Fcallback){autof=[&](auto&&f,pointerp){if(!p)return;f(f,p->ch[0]),callback(p),f(f,p->ch[1]);};f(f,root);}template<typenameF>voidpost_order(Fcallback){autof=[&](auto&&f,pointerp){if(!p)return;f(f,p->ch[0]),f(f,p->ch[1]),callback(p);};f(f,root);}autoleftmost(const_pointerp)const{returnmost(p,0);}autorightmost(const_pointerp)const{returnmost(p,1);}autoprev(const_pointerp)const{returnneighbour(p,0);}autonext(const_pointerp)const{returnneighbour(p,1);}autolower_bound(constkey_t&key)const->pointer{const_pointernow=root,ans=nullptr;while(now){if(!compare(now->data,key))ans=now,now=now->ch[0];elsenow=now->ch[1];}return(pointer)ans;}autoupper_bound(constkey_t&key)const->pointer{const_pointernow=root,ans=nullptr;while(now){if(compare(key,now->data))ans=now,now=now->ch[0];elsenow=now->ch[1];}return(pointer)ans;}// Order start from 0autoorder_of_key(constkey_t&key)const->size_t{size_tans=0;autonow=root;while(now){if(!compare(now->data,key))now=now->ch[0];elseans+=size(now->ch[0])+1,now=now->ch[1];}returnans;}// Order start from 0autofind_by_order(size_torder)const->const_pointer{const_pointernow=root,ans=nullptr;while(now&&now->sz>=order){autolsize=size(now->ch[0]);if(order<lsize)now=now->ch[0];else{ans=now;if(order==lsize)break;now=now->ch[1],order-=lsize+1;}}returnans;}/** * @return nullptr if insert failed, otherwise pointer of inserted node */autoinsert(constkey_t&data)->const_pointer{pointern=newnode_t;n->fa=n->ch[0]=n->ch[1]=nullptr;n->data=data,n->sz=1;pointernow=root,p=nullptr;booldir=0;while(now){p=now;dir=compare(now->data,data);now=now->ch[dir];}insert_fixup_leaf(p,n,dir);returnn;}/** * @return succeed or not */autoerase(constkey_t&key)->bool{autop=lower_bound(key);if(!p||p->data!=key)returnfalse;erase(p);returntrue;}/** * @return {@code next(p)} */autoerase(pointerp)->const_pointer{if(!p)returnnullptr;pointerresult;if(p->ch[0]&&p->ch[1]){autos=leftmost(p->ch[1]);std::swap(s->data,p->data);result=p,p=s;}elseresult=next(p);erase_fixup_branch_or_leaf(p);deletep;returnresult;}private:staticautosize(const_pointerp)->size_t{returnp?p->sz:0;}staticautois_red(const_pointerp)->bool{returnp?p->red:false;}/** * @param dir 0: leftmost, 1: rightmost */automost(const_pointerp,booldir)const->pointer{if(!p)returnnullptr;while(p->ch[dir])p=p->ch[dir];return(pointer)p;}/** * @param dir 0: prev, 1: next */autoneighbour(const_pointerp,booldir)const->pointer{if(!p)returnnullptr;if(p->ch[dir])returnmost(p->ch[dir],!dir);if(p==root)returnnullptr;while(p&&p->fa&&p->child_dir()==dir)p=p->fa;returnp?p->fa:nullptr;}/** * Insert leaf node {@code n} to {@code p} * * @param p parent of node which will be inserted * @param n leaf node which will be inserted * @param dir direction of n, 0: left; 1: right */voidinsert_leaf(pointer_constp,pointer_constn,booldir){if(!p){root=n;return;}p->ch[dir]=n,n->fa=p;autonow=p;while(now)now->sz++,now=now->fa;}/** * Erase node {@code n} * * @param n node which will be deleted, must have no more than 2 child */voiderase_branch_or_leaf(pointer_constn){autop=n->fa,s=n->ch[0]?n->ch[0]:n->ch[1];if(s)s->fa=p;if(!p){root=s;return;}p->ch[n->child_dir()]=s;autonow=p;while(now)now->sz--,now=now->fa;}/** * @param p root of subtree (may be same as {@code root}) * @param dir direction. 0: left rotate; 1: right rotate * @return new root of subtree */autorotate(pointerp,booldir)->pointer{autog=p->fa;autos=p->ch[!dir];// new root of subtreeassert(s);// pointer to true node requireds->sz=p->sz,p->sz=size(p->ch[dir])+size(s->ch[dir])+1;autoc=s->ch[dir];if(c)c->fa=p;p->ch[!dir]=c,s->ch[dir]=p;p->fa=s,s->fa=g;if(g)g->ch[p==g->ch[1]]=s;elseroot=s;returns;}#pragma GCC diagnostic ignored "-Wcomment"/** * Insert leaf node {@code n} to {@code p}, then fixup * * @param p parent of node which will be inserted * @param n node which will be inserted * @param dir direction of n, 0: left; 1: right */voidinsert_fixup_leaf(pointerp,pointern,booldir){n->red=p;insert_leaf(p,n,dir);// Fix double redwhile(is_red(p=n->fa)){boolp_dir=p->child_dir();autog=p->fa,u=g->ch[!p_dir];// Case 1: both p and u are red// g [g]// / \ / \ // [p] [u] ==> p u// / /// [n] [n]if(is_red(u)){p->red=u->red=false;g->red=true;n=g;continue;}// p is red and u is black// Case 2: dir of n is different with dir of p// g g// / \ / \ // [p] u ==> [n] u// \ /// [n] [p]if(n->child_dir()!=p_dir)rotate(p,p_dir),std::swap(n,p);// Case 3: p is red, u is black and dir of n is same as dir of p// g p// / \ / \ // [p] u ==> [n] [g]// / \ // [n] up->red=false,g->red=true;rotate(g,!p_dir);}// Post process: color root blackroot->red=false;}/** * Erase node {@code n}, then fixup * * @param n node which will be deleted, must have no more than 2 child */voiderase_fixup_branch_or_leaf(pointern){booln_dir=n==root?false:n->child_dir();erase_branch_or_leaf(n);autop=n->fa;if(!p){// n is rootif(root)root->red=false;return;}else{autos=p->ch[n_dir];if(s){// n has 1 child// n must be black and s must be red, so we need to color s blacks->red=false;return;}}// n is not root but leaf with black color, need to be fixupwhile(p&&!n->red){autos=p->ch[!n_dir];// Case 1: s is red// p s// / \ / \ // |n| [s] ==> [p] d// / \ / \ // c d |n| cif(is_red(s)){s->red=false,p->red=true;rotate(p,n_dir);s=p->ch[!n_dir];}// s must be blackautoc=s->ch[n_dir],d=s->ch[!n_dir];// Case 2: both c and d are black// {p} {p}// / \ / \ // |n| s ==> |n| [s]// / \ / \ // c d c d// p will be colored black in the endif(!is_red(c)&&!is_red(d)){s->red=true;n=p;gotoend_erase_fixup;}// Case 3: c is red and d is black// {p} {p}// / \ / \ // |n| s ==> |n| c// / \ \ // [c] d [s]// \ // dif(!is_red(d)){c->red=false,s->red=true;rotate(s,!n_dir);s=p->ch[!n_dir],c=s->ch[n_dir],d=s->ch[!n_dir];}// Case 4: d is red// {p} {s}// / \ / \ // |n| s ==> p d// / \ / \ // {c} [d] |n| {c}s->red=p->red,p->red=d->red=false;rotate(p,n_dir),n=root;end_erase_fixup:p=n->fa;if(!p)break;n_dir=n->child_dir();}// Post process: see case 2 & case 4n->red=false;}};#pragma GCC diagnostic warning "-Wcomment"#include<iostream>#include<numeric>#include<vector>template<boolP6136>// == false: P3369, == true: P6136structIO_luogu_P3369_P6136{intlast_ans=0;std::size_tn,m;std::vector<int>a;std::vector<int>ans_list;voidinit(){std::cin>>n;if(!P6136){m=n;return;}std::cin>>m;a.resize(n);for(auto&i:a)std::cin>>i;}intopt(){intx;std::cin>>x;returnx;}intx(){intx;std::cin>>x;returnP6136?x^last_ans:x;}voidprint(intans){ans_list.push_back(last_ans=ans);}voidprint_total_ans(){if(P6136)std::cout<<std::accumulate(ans_list.begin(),ans_list.end(),0,std::bit_xor<int>{})<<'\n';elsefor(auto&&i:ans_list)std::cout<<i<<'\n';}};// Change the template parameter here to false for the code of "Luogu P3369【Template】Ordinary Balanced Tree"IO_luogu_P3369_P6136<true>io;intmain(){std::cin.tie(nullptr)->sync_with_stdio(false);io.init();std::size_tn=io.n,m=io.m;rb_tree<std::pair<int,int>>bt;intcnt=0;for(autoi:io.a)bt.insert(std::make_pair(i,cnt++));for(inti=0,opt,x;(std::size_t)i<m;++i){opt=io.opt(),x=io.x();switch(opt){case1:bt.insert({x,cnt++});break;case2:bt.erase(bt.lower_bound({x,0}));break;case3:io.print((int)bt.order_of_key({x,0})+1);break;case4:io.print(bt.find_by_order((uint32_t)x-1)->data.first);break;case6:io.print(bt.upper_bound({x,n+m})->data.first);break;case5:autoit=bt.lower_bound({x,0});io.print((it?bt.prev(it):bt.rightmost(bt.root))->data.first);break;}}io.print_total_ans();return0;}
Relationship with 2-3-4 Trees
A 2-3-4 tree is a 4th-order B-tree. Like general B-trees, 2-3-4 trees can perform search, insertion, and deletion operations in \(O(\log n)\) time. Nodes in a 2-3-4 tree are of three types: 2-nodes, 3-nodes, and 4-nodes, containing one, two, or three data elements, respectively. All leaf nodes are at the same depth (the bottom level), and all data is stored in sorted order.
2-3-4 trees and red-black trees are isomorphic; any red-black tree uniquely corresponds to a 2-3-4 tree. Insertion and deletion operations on a 2-3-4 tree cause node expansion, splitting, and merging, which correspond to recoloring and rotations in a red-black tree. The diagram below shows the red-black tree nodes corresponding to 2-nodes, 3-nodes, and 4-nodes in a 2-3-4 tree. Note that a 3-node in a 2-3-4 tree corresponds to two cases in a red-black tree: a red node with a left child or a red node with a right child. Therefore, one red-black tree may correspond to multiple 2-3-4 trees.
The diagram below shows a red-black tree and its corresponding 2-3-4 tree. By moving the red nodes in the red-black tree up to the left and right sides of their parent nodes to form a B-tree node, we obtain the corresponding 2-3-4 tree. It can be observed that the number of nodes in the red-black tree equals the number of nodes in the 2-3-4 tree.
The insertion and deletion operations of red-black trees can be understood by comparing them with 2-3-4 trees.[^234-vs-rbt]
Usage in Real-World Projects
Since red-black trees are currently the most efficient memory-based balanced trees in mainstream industry, they are widely used in practical projects. Here are a few real-world use cases with corresponding source code links for readers to compare and learn.
All operations in Linux's red-black tree are implemented using iterative loops, ensuring efficiency while adding extensive comments for readability. It is highly recommended for readers to study. Red-black trees are used extensively in the Linux kernel; here are only a few classic examples.
After the stable kernel version 2.6.24, Linux adopted a new scheduler called CFS. All non-real-time runnable processes are maintained in a red-black tree keyed by virtual runtime to achieve fairer and more efficient scheduling of all tasks. CFS abandoned active/expired arrays and dynamic priority calculation, no longer tracking task sleep times or distinguishing interactive tasks. Instead, it uses a red-black tree based on time-calculated keys to select the next task, determining task priority based on the CPU time usage of all tasks.
epoll, short for event poll, is an implementation of I/O multiplexing in the Linux kernel, an improvement over the original poll/select. The Linux implementation of epoll chooses to use a red-black tree to store file descriptors.
The user-space timer in nginx is implemented using a red-black tree. In nginx, all timer nodes are maintained by a red-black tree. In each loop of the worker process, the function ngx_process_events_and_timers is called, which in turn calls the timer processing function ngx_event_expire_timers. This function continuously retrieves the node with the smallest time value from the red-black tree, checks if it has timed out, and executes its function until the retrieved node's time has not expired.
There are many public resources analyzing the red-black tree source code in nginx; readers can search for them.
Additionally, libstdc++ provides __gnu_cxx::rb_tree in <ext/rb_tree>, which inherits from std::_Rb_tree and can be considered a type alias for external use. Note that this header file is not part of the C++ standard, so its use is not recommended unless necessary.