はじめての C

二分木 続き 6

splay木では、root への 挿入の 変更 - modification - を 利用することで、どちらの 最悪の ケースとなる 可能性も 除くように 考えられています。

必要となる 変更は 最小限ですが、一番 効果的に なるよう、挿入と 検索ルーティンの 両方で、同様の 操作を行う 必要が あります。

結果的に、splay木による 良好な 自己最適化 - self-organizing - を 促すことで、検索パターンが 頻繁に small set of keys と なる場合では とても 役にたちます。

splay の 操作では、ノードを root へと 移動する simple 回転の かわりに、ノードを root へと 移動し 他のノードを root に 近い 途中へ 移動するという double 回転を 使っています。

簡単に いえば、実行する 回転では、root から 途中 出会う 他のノードに つながる 通路を、半分だけ 切り離すのです。

注意するのは、この実装では、すべての 操作で その平均的 cost が 保証されていることです。

単一の 検索 または 挿入では linear time が かかるのですが、実装が 全体的に 集中することで、代表的な アプリケーションでは、他の バランスのとれた木と 同様になります。

splay によって どのように 変質した木を、わずかな 検索で、つりあいよく (ノードを) 移動させるのでしょう ?

tutorial用に 書いた テストケースから、0 から 6 間での 数のある 検証用の木を 1つ 示してみます ▽


      6
     5
    4
   3
  2
 1
0

value を 3 として、splay 検索を 実行すると、木は このように 変化します ▽


  3
 2 5
  4 6

つりあいが 大きく 変化したとは 思いませんか ? spray 検索の コードは 次のように なります ▽

int splay_search(struct Tree **tree, int key)
{
int rc;

if ((*tree)->data == key)
rc = 1;
else if (key < (*tree)->data) {
if ((*tree)->left->data == key)
rc = 1;
else if (key < (*tree)->left->data) {
rc = splay_insert(&(*tree)->left->left, key);
rotate_right(tree);
}
else if (key > (*tree)->left->data) {
rc = splay_insert(&(*tree)->left->right, key);
rotate_left(&(*tree)->left);
}
else
rc = 0;
rotate_right(tree);
}
else if (key > (*tree)->data) {
if ((*tree)->right->data == key)
rc = 1;
else if (key < (*tree)->right->data) {
rc = splay_insert(&(*tree)->right->right, key);
rotate_left(tree);
}
else if (key > (*tree)->right->data) {
rc = splay_insert(&(*tree)->right->left, key);
rotate_right(&(*tree)->right); }
else
rc = 0;
rotate_left(tree);
}
else
rc = 0;

return rc;
}

ここでは、次の 4つの 移動ケースの あることに 注意してください。

left-left, left-right, right-left, right-right

それぞれの ケースでは、分肢の 構造体によって 異なった タイプの 回転が 求められます。

これと root_insert 関数とで、たった 1つ 異なる点は (項目を 挿入するかわりに、単に flag を 返してるところは 別にすると)、bottom からの single 回転よりも、top からの double 回転を 使ってるところです。

splay を 用いた 挿入の コードも まったく 同様で、ただ 検索の ところが 挿入に 変更されています。

これは、基本的には、item == key の ケースが item == NULL に 取り換えられ、new_node を 呼び出している、ということを 意味しています ▽

int splay_insert(struct Tree **tree, int key)
{
int rc;

if (*tree == NULL) {
*tree = new_node(key);
rc = (*tree == NULL) ? 0 : 1;
}
else if (key < (*tree)->data) {
if ((*tree)->left == NULL) {
(*tree)->left = new_node(key);
rc = ((*tree)->left == NULL) ? 0 : 1;
}
else if (key < (*tree)->left->data) {
rc = splay_insert(&(*tree)->left->left, key);
rotate_right(tree);
}
else if (key > (*tree)->left->data) {
rc = splay_insert(&(*tree)->left->right, key);
rotate_left(&(*tree)->left);
}
else
rc = 0;
rotate_right(tree);
}
else if (key > (*tree)->data) {
if ((*tree)->right == NULL) {
(*tree)->right = new_node(key);
rc = ((*tree)->right == NULL) ? 0 : 1;
}
else if (key < (*tree)->right->data) {
rc = splay_insert(&(*tree)->right->right, key);
rotate_left(tree);
}
else if (key > (*tree)->right->data) {
rc = splay_insert(&(*tree)->right->left, key);
rotate_right(&(*tree)->right);
}
else
rc = 0;
rotate_left(tree);
}
else
rc = 0;

return rc;
}

次は、それぞれの 分肢の 高さの つりあいを 見てまわることで つりあいを 保つという、より 複雑な algorithm を 調べてみましょう。

そうは 思えないかも しれませんけど、この algorithm は 二分木の つりあいを 保つための、一番 効果のある 方法の 1つなんです。

運の悪いことに、これもまた とっても 不可解なんですよ ...

最後まで 訳せるんだろうか ... (;_;)
(追記) ちょっと 訂正。