赤黒木で遊ぶ: 赤黒木を実装して理解する方法を教えます
導入
プログラミングを勉強したことがある人なら「赤黒ツリー」という言葉を聞いたことがあると思いますが、赤黒ツリーを理解する前に、それが二分木であることを理解しておく必要があります。赤黒の木?
- Javaのハッシュマップ。
- Linux システム用の CFS フェア スケジューリング アルゴリズム。
- マルチプレクサーのエポール。
- タイマー。
- nginxなど
これらはすべて、赤黒の木を使用した古典的なシナリオです。赤黒ツリーは非常に一般的に使用されるデータ構造であり、次の 2 つの用途があります:
(1) キーと値のペアとして検索に使用され、キーを介して値を検索します。Key は赤黒ツリー内に構築される二分木であり、例えば二分木にノードを挿入する場合、赤黒ツリーは Key を比較して挿入位置を決定します。
(2) 赤黒ツリーはバイナリ ソート ツリーであり、その順序トラバーサルはシーケンシャルであり、レンジ クエリに適したシーケンシャル実行として使用できます。
典型的な Key-Value データ構造。例: Github のトラフィック統計関数 (Traffic)。
これら 2 つのテーブルでは、サイト (どの Web サイトからジャンプしたか) をキーとして、ビュー (訪問数) を値として使用できます。同様に、コンテンツ (プロジェクト内のリソース) をキーとして、ビュー (訪問回数) を値として使用できます。価値。これは典型的な Key-Value 構造であり、このような構造に対して赤黒ツリーを構築して保存できます。同じリソースに再度アクセスするときは、赤黒ツリー内の対応するノードをクエリし、訪問数 (値) に 1 を追加して、統計的効果を得ることができます。
赤黒ツリーの一般的な使用法、epoll を例に挙げると、管理のために多数の IO が epoll に追加されるとき、データが到着したときに epoll はどの IO であるかをどのように認識するのでしょうか。これには、epoll 内の赤黒ツリーのキーと値の検索プロセスが含まれます。epoll は、赤黒ツリーを通じて対応するキーを検索し、対応する値を取得します。
Key-Value は強力な検索プロセスであり、主なデータ構造は次のとおりです。
- 赤黒の木。
- ハッシュ表。
- B/B+ ツリー。
- ジャンプ台。
もちろん、リンク リストなどの他のデータ構造を使用して強力な検索プロセスを実装することもできますが、リンク リストの各クエリを最初からたどる必要があり、時間の計算量が少ないため、パフォーマンスは比較的低くなります。高い。
1. 赤黒木の定義
1.1. 理論的知識
赤黒ツリーは本質的に二分木です。
赤黒ツリーは、二分木に基づいて次の特性を持ちます。
- 各ノードは赤または黒です。
- ルートノードは黒です。
- 各葉のノードは黒色です。
- ノードが赤の場合、その子は両方とも黒です。
- 各ノードについて、そのノードからその子孫へのすべてのパスには、同じ数の黒いノードが含まれます。
上記の特性を満たす二分木は赤黒木です。このうち 5 番目のプロパティは赤黒ツリーのバランスを決定するもので、AVL ツリーのように両側の部分木の高さの差が 1 であることを厳密に要求するわけではなく、黒ノードの高さが同じであることが要求されます。
4 番目と 5 番目の記事の特性から、数学的な結論を導き出すことができます。つまり、赤黒ツリーのルート ノードからリーフ ノードまでの最短パスと、ルート ノードからリーフ ノードまでの最長パスの比です。赤黒木は1 : ( 2 × N − 1 ) 1 : (2\times N-1)1:(2×N−1 )。
赤黒の木の性質を本当に理解しているかを確認するために、次の判断問題があります。どれが赤黒の木で、どれがそうでないかを判断してください。
- 黒ノードの高さから判断すると、14 (黒) –> 8 (赤) –> NIL (黒) となり、黒の高さは 2、14 (黒) –> 8 (赤) –> 10 (黒) NIL (黒)、黒の高さは 3 です。明らかに、各ノードがそのノードからその子孫までのすべてのパス上に同じ数の黒いノードを含むという赤黒ツリーの性質に準拠しません。これはすべて赤黒の木ではありません。
- ルート ノードは黒、黒の高さは 3、連続する赤いノードはありません。これは赤黒木の性質をすべて満たしており、赤黒木です。
- 黒ノードの高さから判断すると、14 (黒) –> 8 (赤) –> NIL (黒) となり、黒の高さは 2、14 (黒) –> 8 (赤) –> 10 (黒) NIL (黒)、黒の高さは 3 です。明らかに、各ノードがそのノードからその子孫までのすべてのパス上に同じ数の黒いノードを含むという赤黒ツリーの性質に準拠しません。これはすべて赤黒の木ではありません。
- ルート ノードは赤なので、赤黒ツリーではありません。
1.2. コードの実装
理論を理解したら、それをコードに実装する必要があります。赤黒ツリーノード構造の定義には次の内容が含まれます。
- 色の識別子を定義します。
- 左右のサブツリーを定義するポインター。
- 親ノードを実行するためのポインタを定義します。これは自然調整のニーズに対応します。
- キーと値を定義します。
typedef struct _rbtree_node {
int key;
void* value;
struct _rbtree_node *left;
struct _rbtree_node *right;
struct _rbtree_node *parent;
unsigned char color;
}rbtree_node;
色で定義された変数を構造体の最後に置くと、メモリを節約できます。
赤黒ツリーを定義するヘッド ノード構造には、次の内容が含まれます。
- 赤黒ツリーの先頭にあるルート ノード ルートを指します。
- 赤黒ツリーの性質によれば、すべてのリーフ ノードは黒であり、すべてのリーフ ノードは同じ点を指し、非表示にすることができます (つまり、NIL ノード)。
- 必要に応じて、効率を向上させるために最小値と最大値を指すノードを定義することもできます。
typedef struct _rbtree {
struct _rbtree_node *root;
struct _rbtree_node *nil
// 如果需要
//struct _rbtree_node *min;//指向value最小的节点
//struct _rbtree_node *max;//指向value最大的节点
}rbtree;
NULL の代わりにカスタム NIL ノードを使用する理由は、この NIL ノードが赤黒ツリーのすべてのプロパティを持たなければならないためです。赤黒ツリーの各種操作の判断は容易ですが、NULLを使用すると操作できなくなります。
このようにして、赤黒木の定義が完了する。プロジェクトのソースコードを読んだときに、色の定義、左ノード、右ノード、親ノードを含む構造があれば、それが赤黒ツリーであると高い確率で判断できます。
1.3. コードの最適化
上記の赤黒ツリーの定義に問題はありますか? 最大の問題は、この赤黒ツリーの定義が再利用できないことであり、業務と赤黒ツリーの実装がくっついており、移植性が低いことです。
汎用性と柔軟性を向上させるために、赤黒ツリーの定義をテンプレート化して赤黒のプロパティをカプセル化できます。
#define RBTREE_ENTRY(name,type) \
struct name {
\
struct type*left; \
struct type*right; \
struct type*parent; \
unsigned char color; \
}
typedef int KEY_TYPE;
typedef struct _rbtree_node {
// 业务相关
KEY_TYPE key;
void* value;
// 红黑树相关
RBTREE_ENTRY(,_rbtree_node);
}rbtree_node ;
typedef struct _rbtree {
struct _rbtree_node *root;
struct _rbtree_node *nil
// 如果需要
//struct _rbtree_node *min;//指向value最小的节点
//struct _rbtree_node *max;//指向value最大的节点
}rbtree;
たとえば、スレッドには、ready (準備完了)、wait (待機中)、sleep (スリープ)、exit (終了) などの状態があります。N 個のスレッドがあると仮定すると、それらの状態は異なり、各状態は赤と黒の色を使用して保存できます。 Tree の場合、次のように定義できます。
#define RBTREE_ENTRY(name,type) \
struct name {
\
struct type*left; \
struct type*right; \
struct type*parent; \
unsigned char color; \
}
typedef int KEY_TYPE;
typedef struct _thread_node {
// 业务相关
KEY_TYPE key;
void* value;
// 红黑树相关
RBTREE_ENTRY(,_thread_node) ready;
RBTREE_ENTRY(,_thread_node) wait;
RBTREE_ENTRY(,_thread_node) sleep;
RBTREE_ENTRY(,_thread_node) exit;
}thread_node ;
typedef struct _thread {
struct _thread_node *root;
struct _thread_node *nil
}_thread;
つまり、構造には複数の赤黒ツリーを含めることができます。
第二に、赤黒木の回転
赤黒木の性質が崩れた場合には、回転を発動して調整する必要があります。ローテーションは他の特性に影響を与えず、変色を良くするためです。
2.1. 理論的知識
回転には左巻きと右巻きの 2 種類があります。これら 2 つの回転は相互的なプロセスです。
ローテーションの目的: 赤と黒の木のバランスを維持するため。
左回転: 左回転操作は、ノードの右の子ノードをその親ノードに変更し、同時に右の子ノードの左の子ノードをノードの右の子ノードに変更します。
- 現在のノードの右側の子を新しいルート ノードにします。
- 新しいルート ノード (存在する場合) の左の子を、元のノードの右の子になるように移動します。元のノードを新しいルート ノードの左側の子にします。
- このようにして、左回転操作が完了すると、元のノードの右の子ノードが上昇して新しいルート ノードになり、元のノードが新しいルート ノードの左の子ノードになります。
右回転: 右回転操作は、ノードの左の子ノードをその親ノードに変更し、同時に左の子ノードの右の子ノードをノードの左の子ノードに変更することです。
- 現在のノードの左側の子を新しいルート ノードにします。
- 新しいルート ノード (存在する場合) の右側の子を、元のノードの左側の子に移動します。元のノードを新しいルート ノードの右側の子にします。
- 右回転操作により、元のノードの左の子ノードが新しいルート ノードに上昇し、元のノードが新しいルート ノードの右の子ノードになります。
左巻きと右巻きになる過程で何が変わったのでしょうか?左回転では 6 つのポインタの向きを 3 方向に変更する必要があります。上の図を例に挙げます。
- Xの右ポインタを変更します。
- Y の左ポインタを変更します。
- X親ノードのポインタを変更します。
これら 3 つのポインターは双方向であるため、ポインターは 6 つあります (たとえば、X の右ポインターは Y を指し、Y の親ポインターは X を指します)。つまり、X の右ポインタは Y の左ノードを指すように変更され、Y の左ポインタは X を指すように変更され、X の親ノード ポインタは Y を指すように変更されます。
右回転も左回転も同じであり、相互作用です。
ルートノードを例に挙げます。
概要:赤黒ツリーでノードを挿入または削除するには、ツリーの高さだけ回転する必要があります。
2.2. コードの実装
(1) 左利き。左手関数の実装にはどのような仮パラメータが必要ですか? 答えはヘッド ノードとローテーション ノードです。
- 赤黒ツリーの定義から、ヘッド ノードを渡す目的は、左側のサブツリーと右側のサブツリーがリーフ ノードであるかどうか、および回転ノードの親ノードがリーフ ノードであるかどうかを判断する必要があることがわかります。ヘッド ノードにはリーフ ノード nil とルート ノード root が格納されるため、ルート ノードになります。
- 変更するポインタ: x の右ポインタと y の左サブツリーの親ポインタを変更; y の親ポインタと x の親ノードの左サブツリーを変更; y の左ポインタと親ポインタを変更x方向の。
/**********************红黑树左旋 start***************************/
void rbtree_left_rotate(rbtree *T,rbtree_node *x)
{
rbtree_node *y = x->right;
// 1. 改变x的右指针指向和y左子树的父指针指向,这里需要判断y的左子树是否是叶子节点
x->right = y->left;
if (y->left != T->nil)
{
y->left->parent = x;
}
// 2. 改变y的父指针指向和x的父节点左子树的指向,这里需要判断x是不是根节点以及判断x节点是其父节点的左子树还是右子树
y->parent = x->parent;
if (x->parent == T->nil) // 根节点
T->root = y;
else if (x == x->parent->left) // 左子树
x->parent->left = y;
else
x->parent->right = y;
// 3. 改变y的左指针指向和x的父指针指向
y->left = x;
x->parent = y;
}
/**********************红黑树左旋 end***************************/
(2) 右折します。左利きと右利きは相互関係にあり、右利きと左利きのプロセスは同じですが、左利きを覚えれば右利きはさらに簡単です。
/**********************红黑树右旋 start***************************/
/*
* x改为y,y改为x,右改为左,左改为右
*
*****************************************************************/
void rbtree_right_rotate(rbtree *T, rbtree_node *y)
{
rbtree_node *x = y->left;
// 1
y->left = x->right;
if (x->right != T->nil)
{
x->right->parent = y;
}
// 2
x->parent = y->parent;
if (y->parent == T->nil)
T->root = x;
else if (y == y->parent->right)
y->parent->right = x;
else
y->parent->left = x;
// 3
x->right = y;
y->parent = x;
}
/**********************红黑树右旋 end***************************/
3. 赤黒ツリー挿入ノード
3.1. 理論的知識
赤黒ツリーは本質的に二分木であるため、その挿入プロセスは二分木のプロセスと似ています。ルート ノードから開始して、現在のノードより大きいものは右のサブツリーに移動し、現在のノードより小さいものは左のサブツリーに移動します。たとえば、次のようにノード 12 を挿入します
。
ノードを挿入するとき、次の状況が推測できます (たとえば、挿入されたノードが z である場合):
(1) z は赤でなければなりません;
(2) z の親ノードは赤でなければなりません;
(3) z の祖父母ノードは黒でなければなりません;
( 4) z の叔父ノードの色は不明です。
したがって、判定条件は主におじさんノードになります。最も単純な例は次のとおりです
。
より複雑な例として、親ノードが祖父母ノードの左側のサブツリーである場合を考えてみましょう (挿入されたノードが z であると仮定します): (1) 叔父ノードは赤です。このケースは最も単純で、重み
はこの状態のツリー自体はバランスが取れており、回転する必要はありません。親ノードと叔父ノードは直接黒になり、祖父母ノードは赤になります。
(2) 叔父ノードは黒で、現在のノードは右の子ノードです。祖父母ノードの左側にはノードが多く、右側にはノードが少ないことがわかります。現在のポインタによって保存されたノードを変更して親ノードを保存し、現在のノードから左手操作を実行して、現在のノードの左側のサブツリー。これは中間状態であり、バランスをとるには次のステップが必要です。
(3) 叔父ノードは黒で、現在のノードは左の子ノードです。祖父ノードの左側が多く、右側が少ないことがわかります。そのため、祖父ノードは右回転操作を実行して色を変更する必要があり、最終的にバランスが取れます。
ノードの挿入処理は主にこの 3 つの状態で行われますが、親ノードが祖父ノードの左側のサブツリーである場合、親ノードが祖父ノードの右側のサブツリーであるという状況を理解しやすくなります。
親ノードが祖父ノードの右側のサブツリーである場合は、親ノードが祖父ノードの左側のサブツリーである場合と似ているため、ここでは繰り返しません。
3.2. コードの実装
ステップを挿入します。
- 挿入されたノードは、最初に下部に挿入されますが、非表示のリーフ ノードの前に挿入されます。
- リーフ ノードの検索プロセスでは、キーが等しい場合、キーの破棄と微調整という 2 つの解決策を採用できます。たとえば、タイマーの赤黒ツリーはタイムスタンプをキーとしており、キーが同じ場合はキーのサイズを微調整して挿入できます。これは、平等の状況は赤黒ツリー自体ではなく、ビジネス シナリオに依存することを意味します。
- 挿入する前に、現在の赤黒ツリーが空かどうかを判断する必要があります。
- 赤黒ツリーがノードに挿入される前は、すでに赤黒ツリーになっています。したがって、黒の高さは変わらないので挿入したノードの色は赤となり、同時に赤いノードが連続してはいけないという判定条件も出ますので調整します。
/**********************红黑树插入 start***************************/
// 调整
void rbtree_insert_fixup(rbtree *T, rbtree_node *z)
{
// 红黑树特性之一:如果一个结点是红的,则它的两个儿子是黑的
while (z->parent->color == RED)
{
if (z->parent == z->parent->parent->left)
{
rbtree_node *y = z->parent->parent->right;
if (y->color == RED)//叔父结点为红色
{
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
// 保证 Z 永远是红色,才能调整
z = z->parent->parent;
}
else //y==black
{
if (z == z->parent->right)
{
z = z->parent;
rbtree_left_rotate(T, z);
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
//祖父结点旋转
rbtree_right_rotate(T, z->parent->parent);
}
}
else
{
rbtree_node *y = z->parent->parent->left;
if (y->color == RED)//叔父结点为红色
{
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
// 保证 Z 永远是红色,才能调整
z = z->parent->parent;
}
else {
if (z == z->parent->left) {
z = z->parent;
rbtree_right_rotate(T, z);
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
rbtree_left_rotate(T, z->parent->parent);
}
}
}
T->root->color = BLACK;
}
// 插入到底部
void rbtree_insert(rbtree *T, rbtree_node *z)
{
rbtree_node *y = T->nil;
rbtree_node *x = T->root;
while (x != T->nil)
{
y = x;
if (z->key < x->key)
x = x->left;
else if (z->key > x->key)
x = x->right;
else
return;
}
z->parent = y;
if (y == T->nil)
T->root = z;
else {
if (y->key > z->key)
y->left = z;
else
y->right = z;
}
z->left = z->right = T->nil;
z->color = RED;
rbtree_insert_fixup(T, z);
}
/**********************红黑树插入 end***************************/
4. 赤黒ツリー削除ノード
4.1. 理論的知識
赤黒ツリーの削除は以下の状況に分けられます。
(1) 左右のサブツリーが存在しない。次のように直接削除します。
(2) 左部分木または右部分木がある。現在のノードの左または右のサブツリーを指すように親ノードのサブツリーを変更し、現在のノードを削除します。たとえば
:
(3) 左部分木と右部分木がある場合、カバーノード、削除ノード、軸ノードを見つける必要がある。たとえば、次の例では、10 はカバー ノード、11 は削除ノード、12 は軸ノードです
。
(4) まず、現在のノードが親ノードの左側のサブツリーである場合について説明します。
1) 現在のノードの兄弟ノードは赤です。現在のノードを削除し、親ノードを赤に変更し、兄弟ノードを黒に変更して、右に調整します。
2) 現在のノードの兄弟ノードは黒で、兄弟ノードの 2 つの子ノードは両方とも黒です。現在のノードを削除し、兄弟ノードと叔父ノードを赤色に変更します。
3) 現在のノードの兄弟ノードは黒、兄弟ノードの左の子は赤、右の子は黒で、現在のノードは親ノードの左のサブツリーです。
4) 現在のノードの兄弟ノードは黒で、兄弟ノードの右側の子は赤です。
(5) 現在のノードは親ノードの右側のサブツリーです。この状況は、[現在のノードが親ノードの左側のサブツリーである]と同じです。
4.2、コードの実装
/**********************红黑树删除 start***************************/
rbtree_node *rbtree_mini(rbtree *T, rbtree_node *x) {
while (x->left != T->nil) {
x = x->left;
}
return x;
}
rbtree_node *rbtree_maxi(rbtree *T, rbtree_node *x) {
while (x->right != T->nil) {
x = x->right;
}
return x;
}
rbtree_node *rbtree_successor(rbtree *T, rbtree_node *x)
{
rbtree_node *y = x->parent;
if (x->right != T->nil)
{
return rbtree_mini(T, x->right);
}
while ((y != T->nil) && (x == y->right)) {
x = y;
y = y->parent;
}
return y;
}
//调整
void rbtree_delete_fixup(rbtree *T, rbtree_node *x) {
while ((x != T->root) && (x->color == BLACK)) {
if (x == x->parent->left) {
rbtree_node *w = x->parent->right;
if (w->color == RED) {
w->color = BLACK;
x->parent->color = RED;
rbtree_left_rotate(T, x->parent);
w = x->parent->right;
}
if ((w->left->color == BLACK) && (w->right->color == BLACK)) {
w->color = RED;
x = x->parent;
}
else {
if (w->right->color == BLACK) {
w->left->color = BLACK;
w->color = RED;
rbtree_right_rotate(T, w);
w = x->parent->right;
}
w->color = x->parent->color;
x->parent->color = BLACK;
w->right->color = BLACK;
rbtree_left_rotate(T, x->parent);
x = T->root;
}
}
else {
rbtree_node *w = x->parent->left;
if (w->color == RED) {
w->color = BLACK;
x->parent->color = RED;
rbtree_right_rotate(T, x->parent);
w = x->parent->left;
}
if ((w->left->color == BLACK) && (w->right->color == BLACK)) {
w->color = RED;
x = x->parent;
}
else {
if (w->left->color == BLACK) {
w->right->color = BLACK;
w->color = RED;
rbtree_left_rotate(T, w);
w = x->parent->left;
}
w->color = x->parent->color;
x->parent->color = BLACK;
w->left->color = BLACK;
rbtree_right_rotate(T, x->parent);
x = T->root;
}
}
}
x->color = BLACK;
}
rbtree_node *rbtree_delete(rbtree *T, rbtree_node *z)
{
rbtree_node *y = T->nil;
rbtree_node *x = T->nil;
if ((z->left == T->nil) || (z->right == T->nil))
{
y = z;
}
else
{
y=rbtree_successor(T, z);
}
if (y->left != T->nil)
x = y->left;
else if (y->right != T->nil)
x = y->right;
x->parent = y->parent;
if (y->parent == T->nil)
T->root = x;
else if (y == y->parent->left)
y->parent->left = x;
else
y->parent->right = x;
if (y != z)
{
z->key = y->key;
z->value = y->value;
}
// 调整
if (y->color == BLACK) {
rbtree_delete_fixup(T, x);
}
return y;
}
/**********************红黑树删除 end***************************/
5. 赤黒木探索
5.1. 理論的知識
赤黒ツリーは、まず第一に二分探索ツリーです。つまり、各ノードの左側のサブツリーの値はそれ自体の値より小さく、右側のサブツリーの値はその値より大きいため、サイズを比較して決定します。 徐々に検索を絞り込みます。
赤黒ツリーでの検索操作は、通常の二分探索ツリーと似ています。ルート ノードから開始して、検索中の値と現在のノードの値を再帰的に比較します。検索する値が現在のノードの値より小さい場合は、左側のサブツリーで検索を続けます。検索する値が現在のノードの値より大きい場合は、右側のサブツリーで検索を続けます。が等しい場合、ターゲット ノードが見つかりました。
5.2、コードの実装
/**********************红黑树查找 start***************************/
rbtree_node *rbtree_search(rbtree *T, KEY_TYPE key) {
rbtree_node *node = T->root;
while (node != T->nil) {
if (key < node->key) {
node = node->left;
}
else if (key > node->key) {
node = node->right;
}
else {
return node;
}
}
return T->nil;
}
/**********************红黑树查找 end***************************/
6、完全なコード
コードは github と gitee にアップロードされました。github:レッドブラックツリー
要約する
赤黒木が理解する必要がある難しさ: プロパティ、回転、挿入、削除。
- 赤黒ツリーは二分木であり、順序走査は絶対順序付けされます。赤黒木の性質が崩れた場合には、回転を発動して調整する必要があります。
- 回転には左巻きと右巻きの 2 種類があります。
- 赤黒木には次の特性があります。
- ノードは赤または黒のいずれかです。
- 各リーフ ノードは黒でなければなりません。
- ルート ノードは黒でなければなりません。
- ノードが赤の場合、その 2 つの息子は黒です。
- 各ノードについて、そのノードからその子孫へのすべてのパスには、同じ数の黒いノード、つまり黒い高さが含まれます。これにより、赤と黒の木のバランスが決まります。
赤と黒の数値のバランスは、主に黒の高さのバランスを取るためのものです。つまり、どのノードからその子リーフ ノードまでの黒いノードの数も同じになります。赤黒ツリーの挿入と削除は赤黒ツリーのプロパティに影響するため、調整する必要があります。
拡張補足、Linux でコードを記述するための環境:
- vmware+ubuntu。仮想システムを提供します。
- サンバ+SSH。Linux システムをローカルにマッピングします。
- gcc/g++ コンパイラ。
- vscode/sourceinsight/qtcreator エディター。