BST に値を挿入する

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

BST に値を挿入する

このレッスンで分かること

  • BST の鉄則は「左の子 << 右の子」
  • 通常の二分木は親子関係のみで、値の順序に制約はありません
  • この条件のおかげで、欲しい値を探すときも、新しい値を入れるときも、左 or 右 の二択で済みます

BST に値を挿入する とは

二分探索木 (BST) に新しい値を挿入し、挿入後の in-order 走査結果を配列で返す関数を実装する。

ここまでで普通の 二分木 を一通り扱えるようになりました。本章では、ノードに「順序」を入れた 二分探索木 (BST: Binary Search Tree) を実装していきます。BST は 左の子は親より小さい / 右の子は親より大きい というシンプルなルールを守るだけで、検索・挿入・削除が O(log n) で行える優れたデータ構造です。

本レッスンでは、配列 (BFS 順, 欠損は null) で表された BST に新しい値を挿入し、その結果を in-order 走査して 10 進数の配列で返す bstInsert 関数を作ります。挿入の感覚を体に覚え込ませると、後続の search delete の理解がぐっと楽になります。

BST の鉄則は「左の子 << 右の子」。挿入はこの不変条件を守りながら、末端に向かって枝を辿るだけ。

BST の不変条件

通常の二分木は親子関係のみで、値の順序に制約はありません。BST はそこに次の 不変条件 を足します。

  • 左部分木のすべての値は、ノードの値より 小さい
  • 右部分木のすべての値は、ノードの値より 大きい
  • 重複は扱わない (本レッスンでは無視)

この条件のおかげで、欲しい値を探すときも、新しい値を入れるときも、左 or 右 の二択で済みます。比較ごとに探索範囲が半分になるので、平均 O(log n) の計算量を得られます。

木の 高さh とすると、検索/挿入/削除のコストは全部 O(h)。平衡が崩れて棒のような木になると最悪 O(n)

配列での BST 表現

本コースでは木構造を BFS 順の配列で表現します。[10, 5, 15, 3, 7, null, 18] は次の図のようになります。

diagram (will load when visible)

図のポイント (テキスト併記)

  • インデックス i のノードの左の子は 2i + 1、右の子は 2i + 2 です

インデックス i のノードの左の子は 2i + 1、右の子は 2i + 2 です。null のところは穴を表します。配列表現は読み書きが楽な反面、深さが偏ると配列がスカスカに大きくなる欠点があります。

挿入アルゴリズム

挿入の手続きは次の通りです。

  1. ルートから出発する
  2. 新しい値が現在のノードより 小さい なら左へ、大きい なら右へ進む
  3. 進んだ先が null だったら、そこに新しい値を置く

再帰でも反復でも書けます。再帰は宣言的でわかりやすく、反復は呼び出しスタックを使わない分わずかに高速です。

Python での実装例

配列表現のままだと扱いにくいので、一度ノードオブジェクトに変換してから挿入し、最後に in-order 走査して配列に戻す方式が読みやすくなります。

Python

class Node: def __init__(self, val): self.val = val self.left = None self.right = None def bstInsert(arr, target): # arr (BFS 配列) -> Node ツリー -> 挿入 -> in-order 配列 root = buildTree(arr) root = insert(root, target) return inorder(root)

肝は insert 関数です。再帰で書くと「現在の node を返す」というシグネチャになり、None の場所に新しいノードを作って差し込めます。

Python

def insert(node, val): if node is None: return Node(val) if val < node.val: node.left = insert(node.left, val) elif val > node.val: node.right = insert(node.right, val) return node

JavaScript での実装例

JavaScript

function insert(node, val) { if (node === null) return { val, left: null, right: null }; if (val < node.val) node.left = insert(node.left, val); else if (val > node.val) node.right = insert(node.right, val); return node; }

ロジックは Python と同じです。null に当たったら新規ノードを返し、親側で left / right に繋ぎ直しています。

よくある間違い

まず多いのが、挿入 後に in-order 走査で結果を返すのを忘れるケースです。本問では戻り値が in-order 配列なので、ここを揃えないとテストが落ちます。次に多いのは、再帰で node.left = insert(...)代入 を書き忘れて、せっかく作った新ノードがツリーに繋がらないパターンです。return node も忘れないようにします。最後に、挿入対象が 重複 していた場合の扱いです。本問では「既に存在する値は無視する」ので、== なら何もしません。

挿入時に 重複 の扱いをどうするかは設計判断。本問では「無視」、別の問題では「カウンタを増やす」「複製を許す」など状況に応じて変える。

やってみよう

  • []5, 3, 8, 1 を順番に入れて、in-order が [1, 3, 5, 8] になるか確認する
  • 同じ値 5 を 2 回挿入しても、in-order に 5 は 1 回しか出ないことを確認する
  • 逆順 [10, 9, 8, 7] を空木に挿入すると、左に偏った棒のような木になることを観察する。これは 平衡 が崩れた状態

よくある質問

Q. BST はなぜ高速ですか?

A. 平衡している場合、検索・挿入・削除がいずれも O(log n) です。各ノードで「左<自分<右」を満たすため、半分ずつ探索範囲が絞れます。ただし偏ると O(n) に劣化するため、実用では赤黒木・AVL 木のような自動平衡化された実装を使います。

Q. in-order 走査で何が得られますか?

A. BST を in-order で巡回するとソート済みの順序で要素が得られます。これが BST と他のツリーの大きな違いで、ソート済みデータの逐次処理に向いています。範囲検索(lo 以上 hi 以下)も簡潔に書けます。

Q. BST に重複値を入れるとどうなりますか?

A. 実装次第ですが、通常は「等しい場合は右に入れる」「カウントを持って同じノードに記録する」のいずれかです。重複が多いユースケースなら TreeMap<Key, Integer> のように出現数を値に持たせる方が探索効率が上がります。

次のレッスン

次は BST から値を検索する で、BST の不変条件「左 < 親 < 右」を使って目的の値を効率よく探す方法を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. BST insert の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. BST insert とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. 配列 arr は BFS 順、欠損ノードは null (Python では None) で表される
  2. BST の不変条件 (左 < 親 < 右) を守って挿入する
  3. 戻り値は挿入後の in-order 走査結果の整数配列。重複は無視する

入出力例

test-cases.txt

bstInsert([], 5)[5] bstInsert([10,5,15], 7)[5,7,10,15] bstInsert([10,5,15], 20)[5,10,15,20] bstInsert([10,5,15,3,7,null,18], 1)[1,3,5,7,10,15,18] bstInsert([10,5,15], 10)[5,10,15] bstInsert([5,3,null,1], 4)[1,3,4,5]

ヒント

main.py
main.py
学習モード

メモ

BST に値を挿入する

⌘S で保存