データ構造を何年も研究されていますが、ツリーには何種類あるんですか?
データ構造には2分木、2分探索木、2-3分木、赤黒木など多くの木構造があります。この記事では、データ構造におけるいくつかの一般的な木の概念と使い方を、厳密には正確ではありませんが、簡単で理解しやすいようにまとめています。
- [1\. ]
- [1.1 バイナリツリーの定義]
- [1.2 バイナリツリーの例]
- [1.3 完全二分木と完全二分木]。
- [1.3.1 完全二分木]
- [1.3.2 完全二分木]
- [1.4 二分木の性質]
- [2\. 二分探索木]
- [3\. バランス2進木]
- [3.1 AVLツリーのバランスド・ルックアップ・ツリー]
- [3.2 赤黒平衡二分木]。
- [4\. B ]
- [4.1 B木とは]
- [4.2 B木の性質]
- [5\. B+Tree]
- [5.1 Bとは+ ]
- [5.2 B+ ]
- [6\. B*tree]
- [7\. R ]
- [8\. Trie ]
- [8.1 トライツリーとは]
- [8.2 トライ木の3つの基本的性質].
- [8.3 Tireツリーの応用]
バイナリツリー
バイナリ・ツリーは重要なデータ構造で、ツリー・テーブル・ファミリーの最も基本的な構造です。
バイナリツリーの定義
二分木の各ノードは最大2つの部分木を持ち、二分木の部分木は左と右で、順序を逆にすることはできません。
- バイナリツリーのi番目のレベルは、最大2i-1個のノードを持ちます;
- 深さkの2進木は、最大2k-1個のノードを持ちます;
- 任意の2分木Tについて、終端ノードの数をn_0n0、次数2のノードの数をn_2n2とすると、n_0=n_2+1n0=n2+1。
注:バイナリツリーノードのみ0、1、2の3つのケースの次数では、0の次数は、終端ノードです。バイナリツリーを構築するプロセスは、"成長 "ノードのプロセスの最初から元のノードです、初期状態は、元のノードが終端ノード、n0 = 1、n1 = 0、n2 = 0、いつでも元の終端ノードに "1度のノード "時間です。ただ端子の位置を少し下に移動し、n1 + +、n0とn2に影響を与えず、いつでも元の端子のノードに "2度ノード "に元の端子が消えた時、2つの端子を追加し、総効果はn0 + +、n2 + + +なので、n0とn2のバイナリツリーは常に同期増加、つまり、常に満たすn0 = n2 + +、n1 = 0、n2 = 0、n0 = 1、n1 = 0、n2 = 0。常にn0=n2+1を満たします。
バイナリツリーの例
![]
完全二分木と完全二分木
完全二分木
各レイヤーのノードは、子ノードを持たない最後のレイヤーを除いて、すべて2つの子ノードを持ちます。また、リーフノード以外のすべてのノードが2つの子ノードを持つように解釈することもできます。ノードの数は最大化され、すべてのリーフノードは同じレイヤー上になければなりません。
完全二分木の性質:
- ツリーは深さhと最大層数kを持ちます。深さは最大層数と同じで、k = hです。
- 葉の数は2h-1;
- 第k層のノード数は2k-1。
- 要約点の数は2k-1であり、ノードの総数は奇数でなければなりません。
完全二分木
二分木の深さがhで、すべての層(層hを除く)のノード数が最大数に達し、層hのすべてのノードが左端に連続して集中している場合、これは完全な二分木です。
注:完全二分木は非常に効率的なデータ構造であり、ヒープは完全二分木またはほぼ完全な二分木であるため、非常に効率的であり、非常に一般的なソートアルゴリズム、ダイクストラのアルゴリズム、プリムのアルゴリズムなどのように、それを最適化するためにヒープを使用する必要があり、二分木ソートツリーの効率は、完全二分木に基づいているバランシングの助けを借りて改善する必要があります。
![]
二分木の性質
- 空でない2分木では、レベルiのノードの総数は2i-1以下、i>=1;
- 深さ h のバイナリツリーは、最大 2h-1 個のノードと少なくとも h 個のノードを持ちます。
- 任意の2分木について、その葉ノードの数をN0、次数2のノードの総数をN2とすると、N0=N2+1;
- n個のノードを持つ完全な二分木の深さはlog_2 ⌊+1log_230n⌊⌋+1です。
- N個のノードを持つ完全な2分木のノードは、それらが順次に格納されている場合、互いに次のように関連しています:
![]
- h(N)個の異なる2分木を形成できるN個のノードが与えられたとき、h(N)はカートランド数のN番目の項であり、h(n)=C(2*n,n)/h(n)=C(2*n,n)/。
- i個の分岐点があり、Iはすべての分岐点の道の長さの合計、Jは葉の道の長さの合計J = I + 2iJ = I + 2i。
二分探索木
バイナリ・ルックアップ・ツリーの定義: バイナリ・ソート・ツリーやバイナリ・サーチ・ツリーとしても知られています。バイナリソート木は、空の木か、以下の性質を持つバイナリ木です:
- 左の部分木が空でない場合、左の部分木のすべてのノードの値は、そのルート・ノードの値より小さい;
- 右のサブツリーが空でない場合、右のサブツリーのすべてのノードは、そのルート・ノードの値以上の値を持ちます;
- 左と右のサブツリーもそれぞれバイナリソートされた木です;
- キー値が等しいノードはありません。
バイナリ・ルックアップ・ツリーの性質: バイナリ・ルックアップ・ツリーを中位で走査すると、順序付き系列が得られます。
バイナリ・ルックアップ・ツリーの時間複雑性: バイナリ・ルックアップと同じで、挿入とルックアップの両方の時間複雑性はO(logn)ですが、最悪の場合はO(n)の時間複雑性が残ります。(その理由は、要素を挿入したり削除したりするときに、木が「93」のバランスを維持せず、n回のルックアップ操作が必要になるからです)。ワーストケースを追求してもなお、より良い時間複雑性があり、これがバランスド・ルックアップ・ツリーの本来の設計です。
バイナリ・ルックアップ・ツリーの高さは、バイナリ・ルックアップ・ツリーのルックアップ効率を決定します。
バイナリー・ルックアップ・ツリーの挿入プロセスは以下の通り:
- 現在のバイナリ・ルックアップ・ツリーが空の場合、挿入される要素はルート・ノードになります。
- 挿入された要素の値がルート・ノードの値より小さい場合、その要素は左のサブツリーに挿入されます。
- 挿入された要素の値がルート・ノードの値より小さくない場合、その要素は右のサブツリーに挿入されます。
バイナリ・ルックアップ・ツリーの削除は3つのケースで処理されます:
- pがリーフ・ノードである場合、図aのように、そのノードを削除し、親ノードへのポインタを修正するのは簡単です。
- pは単一枝ノード。図bのように、pを削除すれば十分です。
- pの左部分木も右部分木も空ではありません。yは左サブツリーを持ってはならないので、yを削除してyの父ノードをyの右サブツリーの父ノードとし、pの値をyの値で置き換えることができます。図cのように
![]
![]
![]
バイナリツリー関連実装のソースコード:
挿入操作:
struct node
{
int val;
pnode lchild;
pnode rchild;
};
pnode BT = NULL;
//ノードを挿入する再帰的方法
pnode insert(pnode root, int x)
{
if(root == NULL){
pnode p = (pnode)malloc(LEN);
p->val = x;
p->lchild = NULL;
p->rchild = NULL;
root = p;
}
else if(x < root->val){
root->lchild = insert(root->lchild, x);
}
else{
root->rchild = insert(root->rchild, x);
}
return root;
}
//ノードを挿入するための非再帰的方法
void insert_BST(pnode q, int x)
{
pnode p = (pnode)malloc(LEN);
p->val = x;
p->lchild = NULL;
p->rchild = NULL;
if(q == NULL){
BT = p;
return ;
}
while(q->lchild != p && q->rchild != p){
if(x < q->val){
if(q->lchild){
q = q->lchild;
}
else{
q->lchild = p;
}
}
else{
if(q->rchild){
q = q->rchild;
}
else{
q->rchild = p;
}
}
}
return;
}
削除操作:
bool delete_BST(pnode p, int x) //削除された要素が見つかったかどうかを示すフラグを返す
{
bool find = false;
pnode q;
p = BT;
while(p && !find){ //削除された要素を見つける
if(x == p->val){ //削除された要素を見つける
find = true;
}
else if(x < p->val){ //左部分木に沿って探す
q = p;
p = p->lchild;
}
else{ //正しい部分木に沿って探す
q = p;
p = p->rchild;
}
}
if(p == NULL){ //見つからない。
cout << "見つからない "<< x << endl;
}
if(p->lchild == NULL && p->rchild == NULL){ //pリーフノードの場合
if(p == BT){ //pルート・ノード
BT = NULL;
}
else if(q->lchild == p){
q->lchild = NULL;
}
else{
q->rchild = NULL;
}
free(p); //ノードpを解放する
}
else if(p->lchild == NULL || p->rchild == NULL){ //p単一枝部分木の場合
if(p == BT){ //pルート・ノード
if(p->lchild == NULL){
BT = p->rchild;
}
else{
BT = p->lchild;
}
}
else{
if(q->lchild == p && p->lchild){ //pはqの左部分木であり、pは左部分木を持つ
q->lchild = p->lchild; //pの左部分木をqの左ポインタにリンクする
}
else if(q->lchild == p && p->rchild){
q->lchild = p->rchild;
}
else if(q->rchild == p && p->lchild){
q->rchild = p->lchild;
}
else{
q->rchild = p->rchild;
}
}
free(p);
}
else{ //pの左の部分木も右の部分木も、どちらも
pnode t = p;
pnode s = p->lchild; //pの左の子ノードから始める
while(s->rchild){ //pの前任者、すなわちpの左部分木の中で最大の値を持つノードを見つける
t = s;
s = s->rchild;
}
p->val = s->val; //ノードsの値をpに代入する
if(t == p){
p->lchild = s->lchild;
}
else{
t->rchild = s->lchild;
}
free(s);
}
return find;
}
操作の検索
pnode search_BST(pnode p, int x)
{
bool solve = false;
while(p && !solve){
if(x == p->val){
solve = true;
}
else if(x < p->val){
p = p->lchild;
}
else{
p = p->rchild;
}
}
if(p == NULL){
cout << "見つからない "<< x << endl;
}
return p;
}
バランス2進木
一般的な二項探索木の場合、その期待される高さはlog2nであり、その各操作の時間複雑度O(log2n)は同時にこれによって決まることが知られています。しかし、ある極端なケースでは、二項探索木は近似連鎖に縮退し、その演算の時間複雑度は線形、すなわちO(n)に縮退します。これは、二項探索木をランダム化によって構築することで可能な限り回避することができますが、多くの操作を行った後、削除するノードの後継は、削除するときに常に自分自身を置き換えるように選択されるため、木が左に沈む程度に常に右にあるノードの数が減少します。これは同時に木のバランスを崩し、操作の時間的複雑さを増加させます。こうして、以下に紹介する平衡二分木ができあがります。
釣り合い2分木の定義: 釣り合い2分木はAVL木とも呼ばれ、次のような性質を持ちます: 空木であるか、左と右の部分木の高さが絶対値1以上違わないこと、左と右の部分木が両方とも釣り合い2分木であること。平衡2分木のアルゴリズムとしてよく使われるのは、赤黒木、AVL木などです。平衡2分木では、その高さは一般にO(log2n)によく保たれ、操作の時間的複雑さを大幅に削減することがわかります。
最小2進バランス木のノードの公式は以下の通り:
![]
AVLツリーのバランスド・ルックアップ・ツリー
AVLツリーの定義: AVLツリーは、発明された最初の自己分散型バイナリルックアップツリーです。AVLツリーは、発明者であるG.M.Adelson-VelskyとE.M.Landisにちなんで命名されました。AVL木は、発明者であるG.M.Adelson-VelskyとE.M.Landisにちなんで命名されました。AVLでは、任意のノードの2つの子サブツリーの高さの最大差は1であるため、高バランス木とも呼ばれ、nノードのAVL木の最大深さは約1.44log2nです。 検索、挿入、削除は平均でも最悪の場合でもO(logn)です。追加と削除は、1つ以上の木の回転による木の再バランスを必要とする場合があります。
この方式は、バイナリ・ルックアップ・ツリーが連鎖リストに劣化する問題に対する良い解決策であり、挿入、ルックアップ、削除の時間複雑性を、最良の場合も最悪の場合もO(logN)に保ちます。しかし、頻繁なローテーションにより、挿入と削除はO(logN)程度犠牲になりますが、時間はバイナリ・ルックアップ・ツリーに比べてはるかに安定しています。
AVLツリーの自己バランス操作 - 回転:
AVLツリーの操作で最も重要で困難なステップは回転です。ローテーションは主に、ツリーに挿入と削除の操作が実行された後、AVLツリーをバランスに戻すための方法です。以下では、AVLツリーの回転に焦点を当てます。
均衡ノードでは、どのノードも最大2つの息子を持つので、このノードの2つのサブツリー間の高さの差が2になるとき、高さの不均衡が発生します:
![]
- 6ノードの左サブツリー3ノードの高さは右サブツリー7ノードより2大きく、左サブツリー3ノードの左サブツリー1ノードの高さは右サブツリー4ノードより大きいので、この場合は左レフトになります。
- 6ノードの左サブツリー2ノードの高さは右サブツリー7ノードの高さより2大きく、左サブツリー2ノードの左サブツリー1ノードの高さは右サブツリー4ノードの高さより小さく、この状態が左右になります。
- 右のサブツリー5ノードの左のサブツリー3ノードの高さが右のサブツリー6ノードより大きいので、このケースは右左になります。
- 右のサブツリー4ノードの左のサブツリー3ノードの高さは右のサブツリー6ノードより小さく、このケースは右-右となります。
赤黒平衡二分木
赤黒木の定義: 赤黒木は、コンピュータサイエンスで使用されるデータ構造で、連想配列を実装するために使用されます。1972年にルドルフ・ベルによって「対称バイナリBツリー」として発明され、1978年にレオ・J・ギバスとロバート・セジウィックによって発表された論文で現代的な名前になりました。複雑ですが、その演算はワーストケースの実行時間が長く、実際には効率的です。ルックアップ、挿入、削除をO(logn)時間で行うことができます。
赤黒木はAVL木と同様に、挿入時間、削除時間、ルックアップ時間について、可能な限り最良のワーストケース保証を提供します。これは、リアルタイムアプリケーションのような時間に敏感なアプリケーションで価値があるだけでなく、ワーストケース保証を提供する他のデータ構造の構成要素としても価値があります。さらに、赤黒木は2-3-4木と等価であり、赤黒木が2-3-4木を2進木として表現したものであることを除けば、同じ考え方です。
赤と黒の木の性質
赤黒木は、各ノードが赤か黒のいずれかの色属性を持つバイナリ・ルックアップ木です。バイナリルックアップツリーに課される一般的な要件に加えて、有効な赤黒木には以下の追加要件が課されます。
- Property 1. ノードは赤または黒。
- \. 根が黒い
- 自然。 葉はすべて黒。
- プロパティ 4。 各赤ノードには2つの黒子ノードが必要です。
- 特性5。 任意のノードからその葉の各々へのすべての単純パスは、同じ数の黒ノードを含みます。
以下は赤黒い木の具体的なイラスト:
![]
赤黒い木の例
これらの制約により、赤黒木の重要な特性である、根から葉への最長のパスは最短パスの2倍以下であることが保証されます。その結果、おおよそバランスのとれたツリーになります。挿入、削除、値の検索などの操作にかかる最悪の場合の時間は、木の高さに比例することが要求されるため、高さに対するこの理論的な上限によって、赤黒木は、通常のバイナリルックアップツリーとは異なり、最悪のシナリオでも効率的であることが可能になります。
なぜこれらの性質がこの結果を保証するのかを見るには、性質4が2つの連続した赤ノードを持つことができない経路を導くことに注意すれば十分です。可能な最短経路はすべて黒ノードで、可能な最長経路は赤ノードと黒ノードが交互に存在します。性質5により、すべての最長のパスは同じ数の黒ノードを持つので、どのパスも他のパスの2倍以上の長さにはならないことがわかります。
以下、[赤黒木のwiki wiki]より編集。
赤黒木の自己均衡演算:
すべての赤黒木は特殊化されたバイナリー・ルックアップ・ツリーでもあるため、赤黒木に対する読み取り専用操作は、通常のバイナリー・ルックアップ・ツリーに対する読み取り専用操作と同じです。しかし、赤黒木に対する挿入操作や削除操作は、赤黒木のプロパティと一致しなくなります。赤黒木の特性を復元するには、少数の色の変更と3回以上の木の回転が必要です。挿入と削除の複雑さにもかかわらず、操作時間はO(logn)回にとどまります。
まずバイナリ・ルックアップ・ツリー・メソッドでノードを追加し、赤い印をつけます。黒に設定すると、ルートからリーフまでの1つのパス上に余分な黒いノードができることになり、調整が難しくなります。しかし、赤いノードに設定すると、2つの連続した赤いノードが存在する競合が発生する可能性があります。その後に何が続くかは、隣接する他のノードの色に依存します。人間の家系図と同じように、ノードの親の兄弟ノードのことをおじノードと呼びます。注釈
- プロパティ1と3は常に維持されています。
- プロパティ4が脅かされるのは、赤いノードを追加したり、黒いノードを赤に再描画したり、回転させたりするときだけです。
- プロパティ5は、黒いノードを追加するとき、赤いノードを黒に再描画するとき、または回転を行うときにのみ脅かされます。
挿入操作:
挿入されるノードのラベルをN、Nの親ノードのラベルをP、Nの祖父ノードのラベルをG、Nの叔父ノードのラベルをUとします。
- シナリオ 1: ツリーが空で、ルート・ノードの位置を直接挿入し、プロパティ 1 に違反し、ノードの色を赤から黒に変更するだけ。
- シナリオ2:親Pが黒であるノードNの挿入は、どのプロパティにも違反せず、修正の必要もありません。このシナリオでは、ツリーはまだ有効です。新しいノードNが2つの黒い葉の子を持つとしても、特性5も脅かされません。しかし、新しいノードNは赤なので、それぞれの子を通るパスは、それが置き換わる黒い葉を通るパスと同じ数の黒いノードを持ち、特性はまだ満たされます。
注:シナリオ1は非常にシンプルで、シナリオ2ではPが黒ですべてがうまくいきますが、Pが赤の場合はそうではありません。
- シナリオ3:親ノードPと叔父ノードUが両方とも赤の場合、両方を黒に再描画し、祖父ノードGを赤に再描画します。親ノードPまたは叔父ノードUを通るパスは必ず祖父ノードGを通るので、これらのパス上の黒ノードの数は変わりません。しかし、赤の祖父ノードGが赤の親ノードを持つ可能性があり、これは性質4に違反します。この問題を解決するために、上記のシナリオのプロセス全体を祖父ノードGに対して再帰的に実行します。例えば、Gがルート・ノードであれば、Gを黒にするのは簡単です。Gがルート・ノードでなく、その親が黒であれば、すべての特性が満たされており、挿入するのは簡単です。Gがルート・ノードでなく、その親が赤であれば、再帰的に上記の処理を行います。
![]
- シナリオ4:親ノードPが赤で、おじノードUが黒、または欠けていて、新しいノードNがその親の左の子で、親ノードPがその親Gの左の子。この場合、祖父ノード G に対して右回転が 1 回実行されます。回転によって生成されるツリーでは、前の親ノード P が新しいノード N と前の祖父ノード G の親になります。前の祖父ノードGは黒であり、そうでなければ親ノードPは赤にはなりません。以前の親ノードPと祖父ノードGの色を入れ替えると、結果のツリーは特性4を満たします。以前は祖父ノードGを通過していた3つのノードのいずれかを通るすべてのパスは、現在は以前の親ノードPを通過するので、特性5も満たされたままです。
![]
- シナリオ5:親ノードPが赤で、祖父母ノードUが黒または欠損で、新しいノードNが親ノードPの右の子で、その親の左の子である場合。この場合、左回転が実行され、新しいノードとその親の役割が入れ替わります。次に、以前の親ノードPがケース4と同様に扱われ、プロパティ4が解決されますが、これはまだ失敗します。
![]
注:上記の呼び出しはすべて末尾再帰を使用しているため、挿入は実際にはインプレース・アルゴリズムです。
削除操作:
Bツリー
Bツリーもルックアップに使われるバランスツリーですが、バイナリーツリーではありません。
B木とは
推薦図書
[1] [データ構造とアルゴリズム|クイックソートは知っているが、その派生関数であるパーティション関数は知っているか].
[2] [データ構造とアルゴリズム|二分探索:ソード・オファー 53 並べ替えられた配列の中の数字を見つける].
![]
機械学習+インテリジェント制御





