コンテンツにスキップ

第9回:ツリー(2)平衡二分探索木

前回まで:二分探索木なら、左 < 自分 < 右 のルールで、検索も追加もたどる線 = 高さ分(バランス時 O(log n))。でも、入れる順が悪いと木が偏り、最悪 O(n) に逆戻りする。

今日の問い: 入れた順は選べないことが多い。入れた順が悪くても、自分で直して速さを保てないか?


順番に振り回される問題

前回の演習で、同じ 7 単語を abc 順で入れていくと、新しく入れる単語は毎回いまある全部より大きいので、ずっと右の子になる。結果、木が一方向に長く伸びる:

graph TD
    apple --> banana
    banana --> cherry
    cherry --> grape
    grape --> lemon
    lemon --> melon
    melon --> peach

このとき、高さは n - 1(= 6)。検索も追加もたどる線が長くなり、O(n)。連結リストと同じ速さに落ちる。入れた順がそのまま速さに直結してしまう。


どこをつまんで持ち上げる?

一方向に伸びてしまった木を、「どこか 1 語を選んで天辺に持ち上げる」と想像してみる。ソートされた語の列のうち、真ん中を天辺にすれば、それより小さい語が左、大きい語が右に、ほぼ半分ずつ分かれる。

7 単語なら abc 順の真ん中= grape を選ぶと、左右が 3 語ずつになる:

graph TD
    grape --> S["小さい 3 語"]
    grape --> L["大きい 3 語"]

高さは一気に下がる。

でも、「真ん中を一気に天辺へ持ち上げる」という直接の操作は無い。やれるのは、支点の周りの数個だけを組み替える局所操作 = 回転だけ。


回転

片側に伸びすぎた所で、真ん中を支点にして持ち上げ、高さを下げる組み替えを回転と呼ぶ。動かすのは支点の周りの数個だけ。そして 左 < 自分 < 右 のルールは絶対に崩さない

例:apple → banana → cherry の一直線。

graph TD
    apple --> banana
    banana --> cherry

ここで banana を支点にして持ち上げると:

graph TD
    banana --> apple
    banana --> cherry

apple < banana < cherry の順序関係はそのまま。でも高さは 2 から 1 に下がった。これが回転の一回ぶん。

回転のキモ

動かすのは支点とその周り数個だけの局所操作。木全体を作り直す必要は無い。順序関係(左 < 自分 < 右)は崩れない。


でも「いつ」回す?

毎回木全体を測って「偏っているか」を判定するのは無駄。そこで、各節に 1 つだけ情報を持たせる

  • 各節は 「右の高さ − 左の高さ」(差)を覚えておく
  • 差が ±1 までならほぼ水平とみなして許す
  • 差が ±2 になった瞬間、その節で回転する

この「差 ±2 で回す」方式の代表が、Adelson-Velskii and Landis' tree、略して AVL 木

差で見張る例

さっきの apple → banana → cherry の一直線を、各節の差で見ると:

graph TD
    a["apple<br/>(差 +2)"] --> b["banana<br/>(差 +1)"]
    b --> c["cherry<br/>(差 0)"]

apple の差が ±2 に達した → ここで回転。回転後は:

graph TD
    b2["banana<br/>(差 0)"] --> a2["apple<br/>(差 0)"]
    b2 --> c2["cherry<br/>(差 0)"]

どの節も差 0 で水平に戻った。回転前後で順序関係(左 < 自分 < 右)は崩れていないことも確認できる。


演習9-1

7 単語(apple, banana, cherry, grape, lemon, melon, peach)を二分探索木に入れる。

  • (a) 高さが最小になる入れ方を 1 つ考え、入れる順番と、出来上がる木を描け。
  • (b) 高さが最大になる入れ方を 1 つ考え、入れる順番と、出来上がる木を描け。

考えるヒント

新しく追加した節は必ず葉になる。葉になった節がどこに付くかは、たどっていく途中の比較結果で決まる。どんな順に並べると左右が分かれやすく、どんな順に並べると片側に寄っていくか?


平衡二分探索木

差 ±2 で回転する仕組みを組み込んだ二分探索木を、平衡二分探索木(balanced BST) と呼ぶ。やることは普段の BST と同じだが、追加のたびに整える工程が入る。

追加の手順

  1. いつも通り、左 < 自分 < 右 をたどって葉のところに新しい節を置く
  2. 葉から根まで戻りながら、各節で「右の高さ − 左の高さ」を確認する
  3. 差が ±2 になった節があれば、その場で回転して高さを下げる
  4. 高い方の子を支点として持ち上げる

入れる」と「整える」をセットでやる木。結果として、どんな順で入れても高さは常に \(\log n\) あたりに保たれる。


演習9-2

次の順で 1 単語ずつ平衡二分探索木に入れていく。

apple → banana → cherry → grape → lemon → melon → peach
  1. 単語を 1 つ入れたら、葉から根へ戻って各節の「右の高さ − 左の高さ」を確認する
  2. 最初に差が ±2 になった節を見つける
  3. その節で、高い方の子を持ち上げて回転する

各ステップで、

  • 入れた語
  • ±2 になった節(あれば)
  • 上げた子(あれば)

を順に書き出していけ。

進め方のヒント

最初の数語ではまだ差が ±1 までしか開かない。差が ±2 に到達した瞬間がその節で初めて回転が必要になるタイミング。たどってきた道を葉から根へ戻りながらチェックするのが速い。


順番に振り回されない

素の二分探索木 平衡二分探索木
バランスのとき O(log n) O(log n)
偏ったとき O(n)(一直線) O(log n)(回転で直す)
入れる順 結果が左右される 結果が変わらない

素の BST は平均 O(log n)・最悪 O(n)。平衡 BST は最悪でも O(log n)。前回の「順番ガチャ」が消える。


計算量のまとめ

検索 追加
ソート済み配列 O(log n) O(n)
連結リスト O(n) O(1)(先頭に入れるだけ)
二分探索木(素) バランス時 O(log n) / 偏ると O(n) バランス時 O(log n) / 偏ると O(n)
平衡二分探索木(AVL) O(log n) O(log n)

平衡 BST だけが、検索も追加も両方 O(log n) を最悪でも守る。


演習9-3

好きな 11 語を選び、平衡二分探索木に入れていく。回転が一度も起きない入れ順を 1 つ作って、その順番を書け。

考えるヒント

どんな順番で入れたら、どの節で見ても「右の高さ − 左の高さ」が ±1 を越えずに済むか? 平らを保つには、入れる順にどんな規則性があればよいか考えよ。


演習9-4

次の 3 場面で、ソート済み配列 / 連結リスト / 二分探索木(素) / 平衡二分探索木 のうち、どれを使うのが適切か。理由とともに答えよ。

  • (a) これから入ってくる単語はランダム順だとわかっている。検索も追加もそこそこ速ければよい。
  • (b) どんな順で単語が入ってくるか全く読めない。とにかく検索と追加の最悪を抑えたい。
  • (c) 最初にまとめてデータを作ったら、もう追加は一切しない。検索だけ速くしたい。

考えるヒント

検索の速さ・追加の速さ・最悪ケースのリスク、3 つの軸でそれぞれの入れ物を見比べる。追加頻度ゼロのケースと、追加が頻繁に起きるケースで、何を重視すべきか変わるはず。


まとめ

  • 回転: 傾いた所で真ん中を支点に持ち上げ、高さを下げる局所操作。左 < 自分 < 右 のルールは絶対に崩さない
  • 平衡二分探索木(AVL 木): 追加のたびに左右の高さの差を見張り、±2 になったらその場で回転。どんな順で入れても 最悪 O(log n)
  • 回転で左右をほぼ半分に保つから、第8回の「順番ガチャ」が消える = 入れる順に振り回されない
  • どこまで平らを保つか」は、検索の速さ ⇄ 追加の手間 のトレードオフ

次回予告

今日まで:平衡二分探索木なら、どんな順で入れても検索も追加も O(log n) を守れる。でも、欲しいのが「いちばん小さい 1 個を取り出す」だけのとき、高さ \(\log n\) のたどりすら毎回やる必要はあるのか?

いちばん小さい 1 個を最速で取り出す」ことだけに特化した入れ物があれば……。

次回:ツリー(3)ヒープ。順序は気にしない。でも最小だけは一瞬で取り出せる入れ物は、どう作る?