回 2-6 道 2 データ構造とアルゴリズム

二分探索木は、どの節 (ふし、値を入れる箱) でも、左側には小さい値、右側には大きい値だけを置く木の形のデータ構造です。根から「小さければ左、大きければ右」と降りるので、形が整っていれば 1,000 件でも見る節は最大 10 個で済みます。ただし入れる順で形が変わり、小さい順に入れると 1 本の線に片寄って、1,000 件で最大 1,000 個の節を見ることになります。

入れる順で木の高さがどう変わるかを測り、片寄った二分探索木が遅くなる理由と、平衡木が要る場面を切り分けられるようになります。

1,000 件を入れた木で、なぜ 1,000 回も見ることがあるのか

二分探索木は、値を入れる順番によって、10 回で探せる木にも、1,000 回かかる木にもなります。

前の回 2-1 では、小さい順に並べた配列の真ん中を見て、候補を半分ずつ捨てる二分探索を学びました。1,000 件なら、真ん中を見る回数は最悪 10 回でした。

ただ、並べた配列は、新しい値を途中に入れると、その後ろの値を全部 1 つずつずらさなければなりません。

そこで、並びを配列の位置ではなく「つながり」で持つのが二分探索木 (にぶんたんさくぎ) です。入れるときにずらす物がなく、探すときは二分探索と同じように半分ずつ候補を減らせます。

この回では、木の言葉とたどり方を押さえたあと、入れる順を変えて木の高さを測り、小さい順に入れた木が 1 本の線になって遅くなる様子を確かめます。

二分探索木はどんな仕組みなのか

二分探索木は、どの節でも「左の子孫はすべて自分より小さく、右の子孫はすべて自分より大きい」という決まりを守る木です。この決まりのおかげで、根から比べて降りるだけで値を探せます。

根、葉、親子、高さという木の言葉

木は、節 (ノード) を上から下へ枝分かれにつないだ形です。言葉は次の通りです。

言葉意味
根 (ルート)いちばん上の節。木に 1 つだけ
親と子線でつながった上の節が親、下の節が子
葉子を 1 つも持たない節
部分木ある節とその下の全部をまとめた小さな木
高さ根から最も深い葉までの段の数

二分木 (バイナリツリー) は、どの節も子を 2 つまで (左の子と右の子) しか持たない木です。二分探索木は、その二分木に「左は小さく、右は大きい」という決まりを足した物です。

この頁では、高さを段の数で数え、根だけの木を高さ 1 とします。本によっては線の数で数えて根だけの木を高さ 0 とし、この頁の高さより 1 小さくなります。

子が 2 つまでの二分木に、左側は全部親より小さく、右側は全部親より大きいという決まりを足すと二分探索木になる図

探す値を、根から順に比べて降ります。手順は次の通りです。

  1. いま見ている節の値と同じなら、見つかったので止まる
  2. 探す値のほうが小さければ左の子へ、大きければ右の子へ降りる
  3. 降りる先が無ければ、その値は木に無い

入れるときも同じ道を降り、行き止まりの空いた所に新しい節を置きます。既にある節は動かしません。

この頁の木は、同じ値を 2 度入れない前提にします。下のコードの insert は同じ値が来ると右へ置くので、そのままでは「右は大きい」の決まりが崩れます。同じ値も扱うなら、節に個数を持たせ、同じ値が来たら数を 1 足す形にします。

探す手間は、見つけた節も含めた「見た節の数」で数えます。木にある値なら、見た節の数はその節が何段目にあるかと同じなので、最悪の回数は木の高さで決まります。

行きがけ、通りがけ、帰りがけのたどる順

木の節を全部 1 回ずつ訪ねる順には、よく使う形が 3 つあります。どれも左を右より先に訪ね、違いは親をいつ出すかだけです。

名前出す順主な使いどころ
行きがけ順親、左、右木を同じ形に写す
通りがけ順左、親、右小さい順に出す
帰りがけ順左、右、親子を先に片付けてから親を消す

二分探索木を通りがけ順でたどると、値が小さい順に出ます。左の部分木はすべて親より小さく、右はすべて大きいので、「左を全部、親、右を全部」と出せば、どの段でも小さい順になるからです。

このほか、上の段から 1 段ずつ左から右へ訪ねる幅優先の順もあります。段ごとに値を集める問題で使います。

in-order は BST を昇順に出す

並べた配列の二分探索と何が違うのか

並べた配列も二分探索木も、形が整っていれば探す手間は O(log n) (件数が 2 倍になっても 1 回増えるだけ) です。違いは、入れるときと消すときの手間と、形が崩れるかどうかです。

比べる所並べた配列二分探索木
探す真ん中を見て半分ずつ根から左右に降りる
入れる後ろの値を全部ずらす空いた所に置くだけ
消す後ろの値を全部詰めるつなぎ直すだけ
小さい順に出す前から読む通りがけ順でたどる
形が崩れるか崩れない入れる順で片寄る

Python の公式文書は、並べたリストに値を入れる bisect.insort について、O(log n) で探す手間は、O(n) かかる入れる手間に隠れてしまうと書いています。二分探索木は、このずらす手間をなくす代わりに、表の最後の行の「片寄り」を抱えます。

動かして確かめる

1 本目は、50、30、70、20、40、60、80 の 7 つを、この順に二分探索木に入れます。そのあと、3 つのたどる順で値を出し、高さを測り、4 つの値を探して、見つかったかと見た節の数を出します。

動かす 木を作って、たどって、探す

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

行き 50 30 20 40 70 60 80
通り 20 30 40 50 60 70 80
帰り 20 40 30 60 80 70 50
高さ 3
50 (True, 1)
60 (True, 3)
20 (True, 3)
55 (False, 3)

最初に入れた 50 が根、30 と 70 が 2 段目、残りの 4 つが 3 段目の葉になり、高さは 3 です。

通りがけ順の行は、並べ替えの処理を書かずに 20 から 80 まで小さい順に出ました。行きがけ順は根の 50 から始まり、帰りがけ順は根の 50 で終わります。

探した結果は、根の 50 が見た節 1 つ、3 段目の 60 と 20 が 3 つです。木に無い 55 は、50、70、60 と 3 つ見て、60 の左が空なので「無い」と分かりました。7 件の木なら、ある値でも無い値でも、見る節は最大 3 つです。

二分探索木はどこで崩れるのか

二分探索木は、入れる順で形が決まり、小さい順に入れると 1 本の線に片寄って、探す手間が件数と同じ O(n) まで落ちます。整った形のまま保つ仕組みは、普通の二分探索木には入っていません。

入れる順で、高さが 10 にも 1,000 にもなる

同じ 0 から 999 までの 1,000 個の値を、3 通りの順で入れて比べます。

  1. 真ん中から入れる。全体の真ん中の値を先に入れ、左右の半分でも同じことをくり返す
  2. 混ぜて入れる。乱数の代わりに決まった式で混ぜた順に入れる (誰が動かしても同じになる)
  3. 小さい順に入れる。0、1、2 … 999 の順

出力は左から、件数、入れる順、木の高さ、平均です。平均は、木にある値を 1 つずつ全部探したときの、見た節の数の平均です (各節が何段目にあるかの平均と同じです)。

動かす 入れる順で高さを測る

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

100 真ん中から 7 5.8
100 混ぜて 16 8.91
100 小さい順 100 50.5
1000 真ん中から 10 8.987
1000 混ぜて 22 12.336
1000 小さい順 1000 500.5

真ん中から入れた 1,000 件の木は高さ 10 で、2-1 の二分探索の最悪 10 回と同じです。100 件の高さ 7、平均 5.8 も、2-1 の数当ての最悪 7 回、平均 5.8 回と重なります。真ん中から入れた木は、二分探索と同じく、どの段でも候補を真ん中で半分に分ける形になるからです。

混ぜて入れた木は、高さ 22、平均 12.336 でした。真ん中からより高いものの、まだ速く探せます。

小さい順に入れた木は、高さが件数と同じ 1,000 になりました。新しい値がいつも前の値の右の子になり、枝分かれのない 1 本の線になったからです。999 を探すと 1,000 個の節を見ます。平均の 500.5 も、端から比べる線形探索と変わりません。

BST への挿入 入れる順番で形が変わる

図で入れる順を切り替えると、小さい順のときだけ右へ一直線に伸びます。通りがけ順の並びは変わらないので、出てくる値だけでは片寄りに気づけません。

小さい順に入れる場面は、思ったより多い

会員番号や注文番号、日時のように増えていく値を鍵 (探すときの目印の値) にして順に入れると、小さい順に入れたのと同じになります。外から来る値の順に入れる作りなら、相手が並べた順で送るだけで、こちらの木は片寄ります。

並べた配列と、入れる手間を比べる

1,000 個の値を入れる手間を比べます。木では入れる場所を探して見た節の数、並べた配列では bisect で位置を探したあと後ろへずらした値の数を、それぞれ合計します。

動かす 入れる手間を比べる

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

混ぜて
 木で見た節 11336
 配列でずらす 246632
小さい順
 木で見た節 499500
 配列でずらす 0

混ぜた順で入れると、木で見た節は合計 11336、配列でずらした値は合計 246632 でした。木のほうが、20 分の 1 より少ない手間で済んでいます。

小さい順で入れると逆転します。木は 1 本の線になり、入れるたびに端まで降りるので、見た節は合計 499500 (0 から 999 までを足した数) に膨らみました。配列は新しい値がいつも最後に入るので、ずらす値は 0 です。

普通の二分探索木が得なのは、入れる順がばらけているときだけです。

片寄りを直す木がある

片寄りを防ぐには、入れ消しのたびに木の形を少し組み替えて高さを抑えます。この組み替えを回転と呼び、こうした木を平衡二分探索木と呼びます。代表は AVL 木と赤黒木 (あかくろぎ) です。

Linux カーネルの公式文書は、赤黒木を「自分で釣り合いを取る二分探索木」と説明し、入れるときは最大 2 回、消すときは最大 3 回の回転で釣り合いが取れると書いています。回転の中身はこの回では扱いません。入れる順に関係なく高さを抑える仕組みがある、と知っておけば十分です。

二分探索木は仕事でどう確かめるのか

仕事では、言語に入っている平衡木を使い、自分で書いた木なら通りがけ順と高さを試験で確かめます。

  1. 通りがけ順で出した値が小さい順に並んでいれば、二分探索木の決まりを守っている。下のコードのように隣どうしを比べる
  2. 高さを測る。件数 n に対して高さが n に近いなら片寄っている。上の depths で最大と平均を出す
  3. 入れる値の順を確かめる。番号や日時のように増えていく値を鍵にするなら、普通の二分探索木は使わない
  4. 言語の道具を使う。Java なら赤黒木の TreeMap、Python で値がめったに増えないなら bisect で並べたリストで足りる

通りがけ順で決まりを確かめる

def is_bst(root):
    out = ino(root, [])
    ok = all(out[i] < out[i + 1]
             for i in range(
                 len(out) - 1))
    return ok

関わる実例

Linux カーネルは、赤黒木で並んだデータを持つ (2007年1月18日の文書)

Linux カーネルの公式文書「Red-black Trees (rbtree) in Linux」(2007年1月18日、Rob Landley) は、赤黒木を、並べられる鍵と値の組を持つための、自分で釣り合いを取る二分探索木だと説明しています。rb_first と rb_next で小さい順にたどれる、とも書いています。

文書は Linux Weekly News を引いて、入出力の順番を決める仕組みが要求を、高い精度の時計の仕組みがタイマーを赤黒木で持つと紹介しています。書かれた当時の使い方で、今のカーネルで全部が同じとは限りません。

守られている前提は「入れる順は、こちらが選べない」です。要求やタイマーが来る順はカーネルの側で決められないので、普通の二分探索木では片寄った順で来たときに O(n) に落ちます。赤黒木は回転で高さを抑え、入れる順に関係なく O(log n) で探せます。

出典は Red-black Trees (rbtree) in Linux (The Linux Kernel documentation) です。

Java の TreeMap は、赤黒木で log(n) を保証する

Java の java.util.TreeMap は、Java SE 21 の公式文書で「赤黒木をもとにした実装」と説明され、containsKey、get、put、remove に log(n) の手間を保証すると書かれています。版の欄には 1.2 から入ったとあります。

会員番号のように増えていく鍵を順に put しても、木は 1 本の線になりません。「入れる順がばらける」という前提を、ライブラリの側で要らなくしています。

出典は TreeMap (Java SE 21 API documentation) です。

よくある問い

二分探索木から値を消すにはどうするのですか

消す節の子の数で 3 通りに分けます。葉ならそのまま外し、子が 1 つならその子を親につなぎ直します。子が 2 つなら、右の部分木でいちばん小さい値 (通りがけ順で次の値) を消す節へ写し、写した元の節を消します。

二分探索木は配列で表せますか

表せますが、片寄ると無駄が大きくなります。根を 0 番、i 番の節の左の子を 2i + 1 番、右の子を 2i + 2 番に置くと、1 本の線の木はほとんどの箱が空いた大きな配列になります。そのため、節ごとに左右の子への参照 (つながり) を持たせて作るのが普通です。

二分探索木の計算量は、平均と最悪でどう違いますか

探す、入れる、消すの手間は、どれも木の高さに比例します。形が整っていれば O(log n)、1 本の線に片寄れば O(n) です。上の測りでも、1,000 件で高さ 10 の木と 1,000 の木ができました。

AVL 木や赤黒木は、普通の二分探索木と何が違いますか

どちらも二分探索木の決まりに、入れ消しのたびに回転して高さを抑える仕組みを足した物で、入れる順に関係なく O(log n) で済みます。Linux カーネルの文書によると、赤黒木は AVL 木より入れ消しの最悪が速く、探すのはわずかに遅いとされています。

確認クイズ

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。3 問すべてに正しく答えると、この回は修了で、石が緑に変わります (この端末のブラウザに残ります)。間違えた問いは、あとから解き直せます。

1 問目

このコードを動かすと何が出ますか。insert は二分探索木に値を入れ、ino は通りがけ順で値を集めます。

通りがけ順で出す

root = None
for k in [8, 3, 10, 1, 6]:
    root = insert(root, k)
print(*ino(root, []))

2 問目

0 から 999 までの 1,000 個の値を、0、1、2 … 999 と小さい順に普通の二分探索木へ入れました。999 を探すとき、見る節はいくつですか。

3 問目

1,000 個の値を、0、1、2 … 999 と小さい順に入れていきます。上の「入れる手間を比べる」のコードで、入れる手間の合計が少なかったのはどれですか。

説明課題

同じ 1,000 個の値を入れた二分探索木でも、真ん中から入れると高さが 10、小さい順に入れると高さが 1,000 になります。小さい順に入れるとなぜ遅くなるのかを、「右の子」「高さ」「見た節の数」という言葉を使って、3 文以内で説明してください。書いたら、下の模範解答を開いて比べてください。

模範解答

自分の説明を書いてから開いて、比べてください。

この回の先の一歩

データベースの索引や並んだ順で引く辞書の裏には木があり、入れる順で遅くなる理由を知っていると、遅い検索の原因を絞り込めます。いまの自分が転職までの道のどの段にいるかは、転職診断で確かめられます。

もっと深く

この回の続きを、ゆめさくのコースの章や道具で深められます。頁の中の確認クイズに答えたあと、手を動かす量を増やしたいときに使ってください。

一次資料

どれも 2026年10月26日に開いて、題と中身を確かめました。

次へ

道 2 データ構造とアルゴリズムの回

  1. 2-0アルゴリズムとは順次、分岐、反復の 3 つで書く手順頁で読む
  2. 2-1線形探索と二分探索100 万件でも 20 回で見つかる頁で読む
  3. 2-2バブルソートと挿入・選択ソート遅いソート 3 つ、2 倍で約 4 倍頁で読む
  4. 2-3クイックソートとマージソート速いソート 2 つ、平均と最悪頁で読む
  5. 2-4スタックとキュー積む順と並ぶ順頁で読む
  6. 2-5再帰自分を呼んで小さくする頁で読む
  7. 2-6木と二分探索木たどる順、片寄りこの回
  8. 2-7ハッシュテーブル鍵から一気に引く、衝突頁で読む
  9. 2-8ダイクストラ法と最短経路点と線、幅優先、重みのある道頁で読む
  10. 2-9動的計画法同じ計算を 2 度しない頁で読む
  11. 2-10試験と面接で確かめる基本情報 科目B、道場 10 問、面接 20 問頁で読む

道 2 の頁へ戻るほかの道を見る