BST から値を削除する

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

BST から値を削除する

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

  • 削除は O(h) です
  • 挿入・検索・min/max と比べて、BST の 削除 は段違いに複雑です
  • 削除アルゴリズムは、削除対象のノードの 子の数 によって 3 つのケースに分かれます

BST から値を削除する とは

BST から指定の値を削除し、削除後の in-order 走査結果を配列で返す関数を実装する。本レッスンでは、BST から値を削除する の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

挿入・検索・min/max と比べて、BST の 削除 は段違いに複雑です。理由はシンプルで、ノードを取り除いた 穴埋め を上手にやらないと、BST の不変条件が崩れてしまうからです。本レッスンでは bstDelete(arr, target) を実装し、削除後の in-order 走査結果を返します。

削除アルゴリズムは、削除対象のノードの 子の数 によって 3 つのケースに分かれます。1 つずつ丁寧に見ていきましょう。

削除は BST 操作の中で唯一、面接で時間切れになりやすい問題。3 ケースの分岐をスラスラ書けるかが勝負。

ケース 1: 葉ノード (子なし)

もっとも簡単。削除対象に子がないなら、親から見て ただ取り外す だけです。

diagram (will load when visible)

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

  • 3 を削除すれば、5 の左の子を null にして完了です

3 を削除すれば、5 の左の子を null にして完了です。

ケース 2: 子が 1 つだけ

削除対象に子が 1 つだけある場合、その子を削除対象の 位置にそのまま昇格 させます。BST の順序関係はそのまま保たれます。

例えば下の木で 15 を削除する場合、右の子 20 をそのまま 15 の位置に持ち上げれば OK。

diagram (will load when visible)

ケース 3: 子が 2 つ

ここからが本番です。削除対象に左右両方の子があるとき、そのまま外す ことができません。代わりに、in-order での後継 または in-order での先行 で値を置き換えてから、元の場所を削除します。

in-order 後継とは、右部分木の中で最小のノード のことです。これを successor と呼びます。手順は次の通り。

  1. 右部分木の最小ノード (左端) を見つける = successor
  2. 削除対象のノードの値を successor.val に書き換える
  3. 右部分木に対して successor.val削除 する (これは ケース 1 か 2 に帰着)

このトリックで、BST の不変条件を保ったままノードを取り除けます。

in-order 後継は「ぴったり次に大きい値」なので、置き換えても順序関係が崩れない。先行 (左部分木の最大) でも同じ理屈で OK。

Python での実装例

Python

def deleteNode(node, val): if node is None: return None if val < node.val: node.left = deleteNode(node.left, val) elif val > node.val: node.right = deleteNode(node.right, val) else: # 削除対象が見つかった if node.left is None: return node.right if node.right is None: return node.left # 子が 2 つ: 右部分木の最小を見つける succ = node.right while succ.left is not None: succ = succ.left node.val = succ.val node.right = deleteNode(node.right, succ.val) return node

コメントを取り除けば 15 行ほどの短さですが、3 ケースの分岐を完璧にカバーしています。再帰の node.left = deleteNode(...) というパターンは、挿入のときと同じく 親側で子の参照を更新する 鉄則です。

JavaScript での実装例

JavaScript

function deleteNode(node, val) { if (node === null) return null; if (val < node.val) { node.left = deleteNode(node.left, val); } else if (val > node.val) { node.right = deleteNode(node.right, val); } else { if (node.left === null) return node.right; if (node.right === null) return node.left; let succ = node.right; while (succ.left !== null) succ = succ.left; node.val = succ.val; node.right = deleteNode(node.right, succ.val); } return node; }

Python と完全に同じロジックを JS に書き換えただけ。BST 操作は言語にほぼ依存せず、再帰null チェック だけで書けます。

よくある間違い

第 1 に、削除対象が存在しない (target が木にない) 場合の扱いを忘れること。本問では「何もせず in-order をそのまま返す」とします。再帰版なら自然と「null まで到達して何もしない」挙動になります。第 2 に、子 2 つのケースで succ.val で置き換えた後、その succ削除し忘れる こと。これをやらないと木に同じ値が 2 つ存在し、BST が壊れます。第 3 に、ルートを削除するケースを忘れること。再帰の戻り値で root を更新する形にすれば自然と対応できます。

計算量

削除は O(h) です。子 2 つの場合に successor 探しで h 段降り、その後の再削除でさらに h 段降りますが、合計しても O(h)。平衡 BST なら O(log n)、最悪 O(n)

削除を繰り返すと BST が偏る可能性があるため、本番では 自己平衡木 (AVL赤黒木) の出番。

やってみよう

  • [10, 5, 15, 3, 7, 13, 20] から 15 (子 2 つ) を削除し、in-order が [3, 5, 7, 10, 13, 20] になるか確認する
  • 3 の削除、ルート 10 の削除、子 1 つのノードの削除 (例 [10, 5, 15, 3, 7, 20] のように 15 の左の子を省いた木で 15 を削除) を一通り試す
  • 存在しない値 999 を削除しても in-order が変わらないことを確認する

よくある質問

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> のように出現数を値に持たせる方が探索効率が上がります。

次のレッスン

次は 第 3 章 まとめクイズ で、BST から指定の値を削除し、削除後の in-order 走査結果を配列で返す関数を実装する を学びます。

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

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. 削除対象が葉 / 子 1 つ / 子 2 つ の 3 ケースを正しく扱う
  2. 子 2 つの場合は in-order 後継 (右部分木の最小) で置き換える
  3. 存在しない値の削除は何もせず in-order を返す。空木は空配列を返す

入出力例

test-cases.txt

bstDelete([], 5)[] bstDelete([10], 10)[] bstDelete([10,5,15], 5)[10,15] bstDelete([10,5,15,3,7,13,20], 15)[3,5,7,10,13,20] bstDelete([10,5,15,3,7,13,20], 10)[3,5,7,13,15,20] bstDelete([10,5,15,null,7], 5)[7,10,15] bstDelete([10,5,15], 999)[5,10,15]

ヒント

main.py
main.py
学習モード

メモ

BST から値を削除する

⌘S で保存