第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 と同じだが、追加のたびに整える工程が入る。
追加の手順¶
- いつも通り、左 < 自分 < 右 をたどって葉のところに新しい節を置く
- 葉から根まで戻りながら、各節で「右の高さ − 左の高さ」を確認する
- 差が ±2 になった節があれば、その場で回転して高さを下げる
- 高い方の子を支点として持ち上げる
「入れる」と「整える」をセットでやる木。結果として、どんな順で入れても高さは常に \(\log n\) あたりに保たれる。
演習9-2¶
次の順で 1 単語ずつ平衡二分探索木に入れていく。
- 単語を 1 つ入れたら、葉から根へ戻って各節の「右の高さ − 左の高さ」を確認する
- 最初に差が ±2 になった節を見つける
- その節で、高い方の子を持ち上げて回転する
各ステップで、
- 入れた語
- ±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)ヒープ。順序は気にしない。でも最小だけは一瞬で取り出せる入れ物は、どう作る?