はじめての C

二分木 続き 7

AVL とは、この algorithm を 生みだした 2人の 数学者 Adelson-Velsky と Landies の 2人を 意味しています。

基本となる 考え方は、それぞれの ノードの 左の分肢の 高さと、右の分肢の 高さを 比べて、その差が 必ず 1 以内に 収まることで、木の つりあいを とる、ということです。

この定義だと、(本来の) 最適な つりあいという 結果には なってませんが、実行上では 意外なことに なるんです。

AVL木は、それぞれの ノードが -1、0 または +1 であるという つりあい要因 - balance factor - によって、保たれています。

新しいノードが 挿入されると、挿入によって 右の分肢が +1 分 伸びるか、または 左の分肢が -1 分 小さくなり、そのため つりあい要因を 1つ もつことになり、木の 再均衡が 必要に なります。

ここで 2つのケースが 生じます (それぞれの ケースでは、左に 反映されています)(Knuth 3, 6.2.3)。

Case 1) 新しいノードが、B の 右の分肢の 高さを h から h+1 へと 伸ばす ▽


  A
 /  |
h   B
   /  |
  h  h+1

ここで 求められるのは、つりあいを 再均衡させるため、左への single 回転を 実行させることです。 注意するのは、左への回転により 高さが h+1 に なるので、他の h の 高さの 分肢を、前よりも ノード 1つ分 高い h+1 と なるよう 適応させていることです ▽


     B
   /   |
  A   h+1
 /  |
h   h

Case 2) 新しいノードが、B の 左の分肢の 高さを h から h+1 へと 伸ばす*1


      A
    /   |
   h    B
       /  |
      X   h
    /  |
  h-1   h

これは より難しいケースで、はじめに B の右、次に X の左という、double 回転が 要求されています ▽


      X
    /   |
  A      B
 / |    / |
h   h  h   h

AVL木での 挿入の 実際の algorithm は、いくつかの step に 分割する - be broken up into - ことが できます。

1) Search (検索)
-a Compare (比較)
-b Move left or move right (移動)
2) Insert (挿入)
-a Adjust balance factors (調整)
3) Replacement (取り換え)
-a Single or double routine (回転)

当然ですが、(実際の) step では 上のリストに 比べると 相当 複雑になります。

ここでは AVL Algorithm の 実行に、再帰を 使います (おそらく その効果が 認められないと 文句が でるでしょうけどね)。

いいわけを しておくと、再帰での 入力の方が、反復による挿入 - iterative insertion - よりも シンプルで 短くなるからです。

反復による 削除では、どこかに 明示的に stack を 求められます。それで、代わりに 再帰を 使えば、操作のためのコードが 簡素化されるのです。

さて、tutorial へと 戻りましょう。 この AVL 挿入ルーティンでは、次のような 疑似コードが 用いられます ▽


# Returns 1 if the changes, 0 otherwise
sub avl_insert(tree, item)
  if empty tree
    insert node
    return 1
  endif
  if tree > item
    # Move left
    if tree.left != null
      if avl_insert(tree.left, item) != 0
        if tree.balance < 1
          # Still balanced
          return tree.balance += 1
        elif tree.left.balance > 0
          # Case 1
          rotate_right(tree)
          tree.balance = 0
          tree.right.balance = 0
        else
          # Case 2
          rotate_left(tree.left)
          rotate_right(tree)
          # Adjust balance factors
          if tree.balance == 1
            tree.left.balance = -1
            tree.right.balance = 0
          elif tree.balance == 0
            tree.left.balance = 0
            tree.right.balance = 0
          else
            tree.left.balance = 0
            tree.right.balance = 1
          endif
          tree.balance = 0
        endif
      else
        return 0
      endif
    else
      tree.left = item
      return tree.balance += 1
    endif
  elif tree < item
    # Move right
    if tree.right != null
      if avl_insert(tree.right, item) != 0
        if tree.balance < 1
          # Still balanced
          return tree.balance += 1
        elif tree.left.balance > 0
          # Case 1
          rotate_left(tree)
          tree.balance = 0
          tree.left.balance = 0
        else
          # Case 2
          rotate_right(tree.right)
          rotate_left(tree)
          # Adjust balance factors
          if tree.balance == 1
            tree.left.balance = -1
            tree.right.balance = 0
          elif tree.balance == 0
            tree.left.balance = 0
            tree.right.balance = 0
          else
            tree.left.balance = 0
            tree.right.balance = 1
          endif
          tree.balance = 0
        endif
      else
        return 0
      endif
    else
      tree.left = item
      return tree.balance += 1
    endif
  endif
  return 0
end sub

このコードは 見てのとおり、helper 関数で wrap した*2 回転のコードと 同じくらい、かなり 複雑です。

その関数の 多くが、つりあい要因の 点検と 交換とに 費されています。

それで、多分 すべての点において まごつくでしょうけど、実際には 見た目よりも ずっと 簡単なんです。

先へ 進む前に、それが 納得できるまで、ロジックを 紙の上に 書きだしてみることを 勧めます。



終わりました ? OK、では 疑似コードを C に 変換する前に、役にたつものが いくつか 必要になります。

最初に、メンバとしての つりあい要因を もっている、新しい 構造体が 入用です ▽

struct Avl
{
struct Avl *left;
struct Avl *right;
int balance;
int data;
};

comparison 関数も 役にたってくれます。production code では、多分 comparison 関数を avl_insert 関数に 渡すのに ポインタが 使われるでしょう。 でも この tutorial では、関数の ポインタに 詳しくない 場合 (を考えて)、直接 呼び出すことにします ▽

int compare(int a, int b)
{
if (a < b)
return -1;
else if (a > b)
return +1;
else
return 0;
}

どちらも シンプルですので、説明の必要は ないでしょう。 もしも 説明が 必要だとすると、この tutorial を 読んでないんですね :p

次の rotation 関数には、ここでは 読みやすくするためと、Avl 構造体に 適応させるために、ほんのわずか 変更が 加えられています ▽

void rotate_left(struct Avl **subtree)
{
struct Avl *save;
struct Avl *node;

node = *subtree;
save = node->right;
node->right = save->left;
save->left = node;
*subtree = save;
}

void rotate_right(struct Avl **subtree)
{
struct Avl *save;
struct Avl *node;

node = *subtree;
save = node->left;
node->left = save->right;
save->right = node;
*subtree = save;
}

では 最後に、これ以上 めんどうなことは ありませんので、avl_insert ルーティンを C の コードへと つくり換えてみましょう ▽

*1:h-1 に 小さくするではなくて

*2:??