応用情報技術者 2015年 秋期 午後 問03
2分探索木に関する次の記述を読んで、設問1~4に答えよ。
2分探索木とは、全てのノードNに対して、次の条件が成立している2分木のことである。
・Nの左部分木にある全てのノードのキー値は、Nのキー値よりも小さい。
・Nの右部分木にある全てのノードのキー値は、Nのキー値よりも大きい。
ここで、ノードのキー値は自然数で重複しないものとする。2分探索木の例を図1に示す。図中の数はキー値を表している。

2分探索木を実現するために、ノードを表す構造体Nodeを定義する。構造体Nodeの構成要素を表1に示す。

構造体の実体を生成するためには、次のように書く。
new Node(key)
生成した構造体への参照が戻り値となる。構造体の構成要素のうち、keyは引数keyの値で初期化され、leftとrightはnullで初期化される。
変数pが参照するノードをノードpという。ノードを参照する変数からそのノードドの構成要素へのアクセスには“.”を用いる。例えば、ノード p のキー値には、p.key でアクセスできる。
なお、変数 p の値が null の場合、木は空である。
〔2分探索木でのノードの探索〕
与えられたキー値をもつノードを探索する場合、親から子の方向へ、木を順次たどりながら探索を行う。
探索する 2 分探索木にノードがない場合は、目的のノードが見つからず、探索は失敗と判断して終了する。探索する 2 分探索木にノードがある場合は、与えられたキー値と木の根のキー値を比較し、等しければ、目的のノードが見つかったので探索は成功と判断して終了する。与えられたキー値の方が小さければ左部分木に、大きければ右部分木に移動する。移動先の部分木でも同様に探索を続ける。
この手順によって探索を行う関数 search のプログラムを図2に示す。このプログラムでは、探索が成功した場合は見つかったノードへの参照を返し、失敗した場合は null を返す。

〔2 分探索木へのノードの挿入〕
2 分探索木にノードを挿入する場合、探索と同様に、親から子の方向へ、木を順次たどりながら、適切な位置にノードを挿入する。
挿入する 2 分探索木にノードがない場合は、挿入するキー値のノードを作成する。挿入する2分探索木にノードがある場合は、挿入するキー値と木の根のキー値を比較し、挿入するキー値の方が小さければ左部分木に、大きければ右部分木に移動する。移動先の部分木でも同様の処理を続ける。
この手順によって挿入を行う関数 addNode のプログラムを図3に示す。このプログラムでは、挿入の結果として得られた2分探索木の根のノードへの参照を返す。ただし、このプログラムは、挿入するキー値と同じキー値をもつノードが2分探索木に既に存在するときは何もしない。

〔2分探索木からのノードの削除〕
2分探索木から、あるキー値をもつノードを削除する場合、次の(1)~(3)の手順を行う。
(1)2分探索木にノードがない場合は、何もしないで処理を終了する。
(2)削除するキー値と木の根のキー値を比較し、削除するキー値の方が小さければ左部分木に、大きければ右部分木に移動する。移動先の部分木でも同様の処理を続ける。
(3)削除するキー値と木の根のキー値が等しい場合、削除するキー値をもつノードを削除するため、次の(3-1)~(3-3)を実行する。
(3-1)削除するノードが子ノードをもたない場合、そのノードを削除する。
(3-2)削除するノードがノードを一つだけもつ場合、削除するノードの位置にその子ノードを置く。
(3-3)削除するノードが左右両方に子ノードをもつ場合、削除するノードの左部分木の中で最大のキー値をもつノードを左部分木から取り除き、削除するノードの位置に置く。
この手順を使って2分探索木からノードの削除を行う関数removeNodeのプログラムを図4に示す。このプログラムでは、削除した後の2分探索木の根のノードへの参照を返す。ただし、このプログラムは、削除するキー値をもつノードが2分探索木に存在しないときは何もしない。
図4中の関数extractMaxNodeは、引数で指定されたノードを根とする2分探索木の中で最大のキー値をもつノードを木から削除し、削除されたノードへの参照を大域変数extractedNodeに設定した上で、削除した後の2分探索木の根のノードへの参照を返す。関数extractMaxNodeのプログラムを図5に示す。


〔2分探素木の計算量〕
2分探素木における計算量は、木の高さに依存する。図2の関数searchを使ってn個のノードから成る2分探索木を探索する場合、想定される最大の計算量は、O(ク)である。木構造が完全2分木であれば、その計算量は最大でもO(ケ)である。
設問1:
図1中のアに入れる適切な数を答えよ。
模範解答
ア:11
解説
解答の論理構成
- まず 2 分探索木の定義より、
「Nの左部分木にある全てのノードのキー値は、Nのキー値よりも小さい。」
「Nの右部分木にある全てのノードのキー値は、Nのキー値よりも大きい。」
と【問題文】に明記されています。 - 図1では、根のキー値が「15」、その左部分木に「8」、さらに「10」「12」が右方向に並び、その子として[ア]が配置されています。
- 左部分木に属する時点で、[ア]は「15」より小さくなければなりません。
- 「10」の右部分木に位置するため、「10」より大きい必要があります。
- 「12」の左子(左部分木)であるため、「12」より小さくなければなりません。
- 以上より成り立つ不等式は
10 < [ア] < 12
です。キー値は自然数で重複しない条件から取り得る値は唯一「11」となります。
したがって
ア:11
ア:11
誤りやすいポイント
- 「左右」を取り違え、[ア]を「12」の右子と勘違いすると 13 や 14 を選びやすいです。
- ルート「15」より小さいという制約を見落とし、25 未満なら何でも良いと誤解するケースがあります。
- 自然数で重複しないという前提を忘れ、既に木に存在する値を候補に挙げてしまうミスが散見されます。
FAQ
Q: もし「12」の右側にもう一つノードを追加するなら、どの値が入りますか?
A: 右部分木なので「12」より大きく、かつ「15」より小さい値のうち未使用のもの、例えば 13 や 14 が適切です。
A: 右部分木なので「12」より大きく、かつ「15」より小さい値のうち未使用のもの、例えば 13 や 14 が適切です。
Q: ルートが「15」でなかったら[ア]は変わりますか?
A: 左部分木か右部分木かで制約が変わるため、ルート値が変わると[ア]に許される範囲も変化します。
A: 左部分木か右部分木かで制約が変わるため、ルート値が変わると[ア]に許される範囲も変化します。
Q: 同じキー値が入るケースを考えなくてよいのはなぜですか?
A: 【問題文】で「ノードのキー値は自然数で重複しないものとする」と明示されており、重複キーは発生しません。
A: 【問題文】で「ノードのキー値は自然数で重複しないものとする」と明示されており、重複キーは発生しません。
関連キーワード: 2分探索木、探索アルゴリズム、部分木、ノードキー、木構造
設問2:
図2〜4中のイ〜キに入れる適切な字句を答えよ。
模範解答
イ:kがp.keyより小さい
ウ:new Node(k)
エ:returnp
オ:p.left
カ:p.right
キ:p ← r
解説
解答の論理構成
-
イ
【問題文】には「与えられたキー値の方が小さければ左部分木に、大きければ右部分木に移動する。」とあります。
左部分木へ進む条件ですから、search/addNode いずれも
「k が p.key より小さい」が成立する場合に p.left へ進みます。
よって イ は
kがp.keyより小さい。 -
ウ
【問題文】には「構造体の実体を生成するためには、次のように書く。 new Node(key)」と明記されています。
空木に対して最初のノードを作成する行なので、 new Node(k) が入ります。 -
エ
addNode は「挿入の結果として得られた2分探索木の根のノードへの参照を返す」と説明されています。
挿入処理後、局所変数 p がその根を指していますから
return p が正しい返却になります。 -
オ と カ
removeNode で「削除するノードがノードを一つだけもつ場合」の処理です。- 右部分木を残すケースは「左部分木が null」なので p.left が null
- 左部分木を残すケースは「右部分木が null」なので p.right が null
したがって
オ:p.left
カ:p.right
-
キ
子ノードを二つもつ場合、【問題文】の(3-3)に従い「左部分木の中で最大のキー値をもつノードを…削除するノードの位置に置く」とあります。
extractMaxNode で取り除いたノードを r に保持し、左右ポインタを設定した後、p をその r に置き換えれば完成です。
よって p ← r。
誤りやすいポイント
- 左右判定の条件を「≦」「≧」と書いてしまう
2分探索木は「重複しない」ので、等しい場合は探索成功・挿入しない。 - オ・カ を逆に書く
“片方が null なら残っている方を昇格させる”という基本を取り違えやすい。 - キ を r ← p と逆にしてしまう
置き換えるのは削除対象 p であり、抽出した r ではない点に注意。
FAQ
Q: 条件式を「k < p.key」と「k が p.key より小さい」のどちらで書いても良いですか?
A: 本試験では日本語による擬似コード表記に合わせ「kがp.keyより小さい」と書くのが無難です。
A: 本試験では日本語による擬似コード表記に合わせ「kがp.keyより小さい」と書くのが無難です。
Q: new Node(k) で生成したノードの left/right は初期化が必要ですか?
A: 【問題文】に「leftとrightはnullで初期化される」とあるので追加の代入は不要です。
A: 【問題文】に「leftとrightはnullで初期化される」とあるので追加の代入は不要です。
Q: 削除で左右両方に子があるとき、なぜ“左部分木の最大値”を使うのでしょう?
A: 最大値は削除ノードより小さく、かつ右部分木のキーよりは小さいため、置き換えても2分探索木の順序が保てるからです。
A: 最大値は削除ノードより小さく、かつ右部分木のキーよりは小さいため、置き換えても2分探索木の順序が保てるからです。
関連キーワード: 2分探索木、再帰処理、ノード削除、擬似コード、ポインタ参照
設問3:
本文中のク、ケに入れる適切な字句を答えよ。
模範解答
ク:n
ケ:logn
解説
解答の論理構成
-
問題文では「2分探素木における計算量は、木の高さに依存する」と明言されています。
つまり、探索の比較回数 ≒ 木を下る回数 ≒ 木の高さです。 -
さらに「図2の関数searchを使ってn個のノードから成る2分探索木を探索する場合、想定される最大の計算量は、O(ク)である」とあります。
・最悪の場合とは、木が片側にのみ伸びて“ひと続きの鎖”になるケースです。
・このとき高さは n − 1、比較回数は n オーダーなので 。
よって ク には n が入ります。 -
次に「木構造が完全2分木であれば、その計算量は最大でもO(ケ)である」とあります。
・完全2分木では高さが 程度まで抑えられます。
・したがって比較回数は 。
よって ケ には logn が入ります。 -
以上より
ク:n
ケ:logn
誤りやすいポイント
- 「完全2分木」と「平衡木」を混同し、平均計算量を考えてしまう。問題文は“最大の計算量”です。
- 対数の底を意識し過ぎて log₂n や log₁₀n と書きたくなるが、計算量記法では底は省略可。解答欄には「logn」と素直に書くのが安全です。
- 最悪計算量を と誤記するケース。探索では一度に1本のパスしか通らないため、二重ループのような にはなりません。
FAQ
Q: “鎖状態”の木は具体的にどのような挿入順で発生しますか?
A: 昇順または降順にキーを連続挿入すると、常に右(または左)子に追加され続け、1本の鎖になります。
A: 昇順または降順にキーを連続挿入すると、常に右(または左)子に追加され続け、1本の鎖になります。
Q: 「完全2分木」と「平衡2分探索木」は同義ですか?
A: 厳密には異なりますが、計算量評価の観点ではどちらも高さが に抑えられる点が共通しています。
A: 厳密には異なりますが、計算量評価の観点ではどちらも高さが に抑えられる点が共通しています。
Q: 底が違う対数同士を比較するときはどうすればよいですか?
A: なので、定数倍が付くだけでオーダーは変わりません。したがって とまとめて表記します。
A: なので、定数倍が付くだけでオーダーは変わりません。したがって とまとめて表記します。
関連キーワード: BinarySearchTree, 時間計算量、最悪計算量、完全2分木、木の高さ
設問4:
次の順でキー値の挿入と削除を行った後でノードqを根とする2分探索木を答えよ。2分探索木は、図1の例に倣って表現すること。

模範解答
(図を参照)

解説
解答の論理構成
-
初期状態
変数 q は null なので木は空です(q ← null)。 -
挿入操作
すべての挿入は、【問題文】の
「挿入するキー値の方が小さければ左部分木に、大きければ右部分木に移動する。」
という 2 分探索木の性質に従います。- q ← addNode(5, q) → 根が 5
- q ← addNode(2, q) → 5 の左に 2
- q ← addNode(7, q) → 5 の右に 7
- q ← addNode(1, q) → 2 の左に 1
- q ← addNode(8, q) → 7 の右に 8
- q ← addNode(4, q) → 2 の右に 4
- q ← addNode(3, q) → 4 の左に 3
- q ← addNode(12, q) → 8 の右に 12
-
削除操作 (1) ― q ← removeNode(5, q)
キー 5 は左右に子を持つため、【問題文】(3-3)
「削除するノードが左右両方に子ノードをもつ場合、…左部分木の中で最大のキー値をもつノードを…位置に置く。」
を適用します。
左部分木(根 2)で最大のキーは 4。-
extractMaxNode により 4 を取り出し、4 の左に 2(右子は 3)、右に 7 を接続。
-
p ← r(図4 の キ)で根が 4 になります。
木は4 /
2 7 / \
1 3 8
12
-
-
削除操作 (2) ― q ← removeNode(7, q)
キー 7 は右に 1 つだけ子を持つので、【問題文】(3-2)
「削除するノードがノードを一つだけもつ場合…その子ノードを置く。」
を適用します。コードでは
elseif (オ と null が等しい) → p ← p.right
に該当し、7 の位置に 8 を置きます。 -
最終形
4
/
2 8 / \
1 3 12これは模範解答図と一致します。
誤りやすいポイント
- 最大キー・最小キーの取り違え
削除時 (3-3) では「左部分木の中で最大」なので 4 を選びます。右部分木の最小を選ぶと誤答になります。 - extractMaxNode 実行後の部分木更新忘れ
4 を抜いた左部分木の根が 3 に変わる点を見落とすと構造が崩れます。 - 子が一つだけの削除処理
条件分岐 オ と カ を逆に読んで左右を取り違えるミスが頻発します。 - 挿入順の書き出しミス
addNode は既存キーがあれば「何もしない」ことを忘れ、重複を誤って追加してしまうケース。
FAQ
Q: 削除時に右部分木の最小キーを使っても良いですか?
A: アルゴリズムとしては左右どちらか一方を決め打ちにすれば成立しますが、本問題は【問題文】で「左部分木の中で最大のキー値をもつノード」と明示しているため、それに従う必要があります。
A: アルゴリズムとしては左右どちらか一方を決め打ちにすれば成立しますが、本問題は【問題文】で「左部分木の中で最大のキー値をもつノード」と明示しているため、それに従う必要があります。
Q: extractMaxNode で取り除いたノードに左子がある場合はどうなりますか?
A: コードにある p ← p.left により、その左子が元の位置を引き継ぎます。今回 4 の左に 3 があったため、3 が 2 の右子になります。
A: コードにある p ← p.left により、その左子が元の位置を引き継ぎます。今回 4 の左に 3 があったため、3 が 2 の右子になります。
Q: 子が 1 つだけのノード削除で左右を判定する簡単なコツは?
A: 「null でない方を残す」と覚えると良いです。実装では p.left が null なら p ← p.right、逆なら p ← p.left です。
A: 「null でない方を残す」と覚えると良いです。実装では p.left が null なら p ← p.right、逆なら p ← p.left です。
関連キーワード: 2分探索木、挿入操作、削除操作、ノード置換、再帰呼び出し


