二分探索木は左の子孫の値 < 親の値 ≦ 右の子孫の値」という制約を持つ二分木です。直接下の左の子だけでなく左部分木のすべてのノードの値が親よりも小さくなる特性があります。右部分木のすべてのノードの値も同様に親以上になる特性があります

二分探索木ではそれぞれのノードに対し、左の子<ノードの値≦右の子が成り立つので、データの探索をするときに全探索をしなくても少ない回数の比較で探索をすることができます。

二分探索木をC#で実装してみる

二分探索木をC#で実装してみることにします。

以下のようなNodeクラスを定義します。

挿入

ルートが存在しない場合は挿入しようとしているノードをルートノードとする。

ルートノードが存在する場合、ルートノードと目的の値を比較する。挿入しようとしている値のほうが着目しているノードより小さい場合は左の子、そうでないなら右の子が次の着目ノードとなる。

次の着目ノードが存在しなければその一にデータを挿入。存在する場合は次の着目ノードに移る・・・を繰り返す。

探索

二分探索木のなかに特定の値のデータがあるかどうかは以下の方法で調べることができます。

まずルートに着目する。
着目しているノードよりも目的の値が小さい場合は左の子、着目しているノードよりも目的の値が大きいなら右の子へ移る・・・を繰り返す。
着目しているノードと目的の値が等しい場合は「存在する」。着目ノードが存在しなければ「存在しない」として終了。

全データの列挙

通りがけ順探索をすることで全データをソートされた順序で列挙できる。

削除

探索、挿入に比べると、削除の処理はちょっと複雑です。削除しても探索ができ、全データの列挙をしたときにソートされた状態で結果が取得できる状態を維持できるようにしなければなりません。

まず削除するノードがあるか確認する。ある場合は以下の場合分けをする

(1) 削除するノードには子が存在しない。
(2) 削除するノードには左または右のみ子が存在する。
(3) 削除するノードには左右の子が存在する。

(1)の場合は簡単です。そのまま削除します。

(2)の場合は削除して削除したノードの親と子をつなぎます。

(3)の場合はさらに場合分けします。

(3a) 削除するノードの左の子に右の子が存在しない。
(3b) 削除するノードの左の子に右の子が存在する。

(3a)の場合は削除したノードの親と削除したノードの左の子をつなぎます。削除したノードの左の子の右の子を削除したノードの右の子にする。

長々とした変な文章ですが、図にするとこういうことです。

(3b) の場合

削除するノードの左の子から最大の値を探索する。これは右の子をたどり続けることで得られます。
これ(最大値ノード)を削除対象のノードと置き換える
最大値ノードの親の右の子を最大値ノードの左の子とする。

これも長々とした変な文章ですが、図にするとこういうことです。

実際に使ってみる

定義したBinarySearchTreeクラスを使って二分探索木にデータを追加、削除、探索ができるアプリケーションを作成してみることにします。