二分探索シミュレーター
二分探索は、整列済みの配列の真ん中を調べ、候補を毎回半分に減らしていく探索です。 low・mid・high の動きと、候補がどう減るかを1ステップずつ止めて確認できます。
動きを1ステップずつ確認する
- 調べる(M)
- 候補
- 除外
- 発見
L=low(下端)/M=mid(中央)/H=high(上端)
パソコンではキーボードでも操作できます:→ 次へ / ← 前へ / スペース 再生・一時停止
数値を自分で入力する
わざと並びを崩した配列も入力できます。その場合は注意が表示され、二分探索が失敗する様子を確認できます。
実行ログを見る(ここまでの処理)
ALGORITHM
二分探索とは何をしているか
二分探索(バイナリサーチ)は、小さい順に並んだ配列に対して、真ん中の要素と探したい値 target を比べ、
大小の関係から目的の値が存在しない側を丸ごと捨てる探索です。
辞書で単語を引くとき、真ん中あたりを開いて「もっと後ろだ」と判断して前half を見なくなるのと同じ考え方です。
使う変数は3つです。範囲の下端 low、範囲の上端 high、そしてその中央 mid。
シミュレーターではこの3つがマスの上に L・M・H として表示され、候補から外れたマスは薄い破線になります。
「次へ」を押すたびに、薄いマスが一気に増えていく様子が確認できます。
1回の比較で候補が半分になる
線形探索は1回の比較で候補が1個しか減りませんが、二分探索は半分減ります。 この差は要素数が増えるほど大きくなります。
| 要素数 | 線形探索(最悪) | 二分探索(最悪) |
|---|---|---|
| 8 個 | 8 回 | 4 回 |
| 1,000 個 | 1,000 回 | 約 10 回 |
| 100 万個 | 100 万回 | 約 20 回 |
100万個の中から探すのにたった20回で済みます。これが二分探索の強さで、 「整列してから探す」という手順に意味がある理由です。
mid の小数は切り捨てる
mid = (low + high) ÷ 2 で計算しますが、添字は整数でなければ配列を指せません。
そのため小数点以下は切り捨てます。たとえば low = 0、high = 5 なら
(0 + 5) ÷ 2 = 2.5 なので mid = 2 です。
シミュレーターでは、割り切れないときに「÷ は小数を切り捨てます」と表示されるので、実際の値と合わせて確認してください。
整列されていないと失敗する
二分探索が成り立つのは、「中央より大きいなら、目的の値は右側にしかない」と言い切れるからです。 並びが崩れていると、この判断が成り立ちません。捨てた側に目的の値が残っていても、二度と調べられないため見つけられません。
+1 と -1 を忘れると終わらない
範囲を更新するときは low = mid + 1、high = mid - 1 と書きます。
この +1 / -1 は、調べ終わった mid を範囲から必ず外すためにあります。
これを忘れて low = mid と書くと、範囲が狭まらないまま同じ mid を調べ続け、処理が終わらなくなります(無限ループ)。
CODE
3つの書き方で見る二分探索
擬似言語(DNCL風)・Python・JavaScript で並べました。変数名と処理の順番はすべて揃えてあります。
Nums = [2, 5, 8, 11, 14, 20] target = 14 low = 0 high = 要素数(Nums) - 1 pos = -1 low <= high の間繰り返す: |mid = (low + high) ÷ 2 |もし Nums[mid] == target ならば: ||pos = mid ||繰り返しを抜ける |そうでなくもし Nums[mid] < target ならば: ||low = mid + 1 |そうでなければ: ⎿⎿ high = mid - 1 表示する(pos)
nums = [2, 5, 8, 11, 14, 20]
target = 14
low = 0
high = len(nums) - 1
pos = -1
while low <= high:
mid = (low + high) // 2
if nums[mid] == target:
pos = mid
break
elif nums[mid] < target:
low = mid + 1
else:
high = mid - 1
print(pos)
const nums = [2, 5, 8, 11, 14, 20];
const target = 14;
let low = 0;
let high = nums.length - 1;
let pos = -1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (nums[mid] === target) {
pos = mid;
break;
} else if (nums[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
console.log(pos);
÷、Python の //、JavaScript の Math.floor() は、
いずれも小数を切り捨てて整数の添字にするための書き方です。
JavaScript の / は小数のまま残るため、Math.floor() が必要になります。
COST
比較回数と計算量
要素数を n とすると、候補は n → n/2 → n/4 → … と減り、1個になるまでの回数は約 log2(n) 回です。 シミュレーターで「中央」と「左端」を切り替えると、同じ配列でも比較回数が変わることが確認できます。
| 場合 | どんなとき | 比較回数 | 計算量 |
|---|---|---|---|
| 最良 | 最初の mid が target だった | 1 回 | O(1) |
| 最悪 | 端にある、または存在しない | 約 log2(n) + 1 回 | O(log n) |
| 前提 | 整列していない配列 | — | 使えない |
要素数が2倍になっても、比較回数は1回だけ増えます。線形探索が2倍になるのとは大きな違いです。 ただし、整列されていない配列を先に並べ替える場合は、その整列自体に手間(交換法や選択法なら n² に比例)がかかります。 1回だけ探すなら線形探索、何度も探すなら整列してから二分探索、という使い分けが問われることがあります。
EXAM POINT
共通テスト「情報Ⅰ」での出題ポイント
二分探索は、穴埋めとトレース(値の追跡)の両方でよく出ます。狙われやすいのは次の5点です。
- mid の式:
mid = (low + high) ÷ 2の空欄。切り捨てになることも合わせて確認されます。 - 範囲の更新:
low = mid + 1とhigh = mid - 1のどちらを使うか。「大きければ低い側を捨てる」の向きを間違えないことが要点です。 - 繰り返しの条件:
low <= highの<=部分。<にすると、候補が1個になった場面を調べ落とします。 - 前提条件:「このアルゴリズムを使うために配列が満たすべき条件は何か」という問い。答えは「小さい順(または大きい順)に整列していること」です。
- 比較回数:「最大で何回の比較が必要か」。要素数 8 なら 4 回、16 なら 5 回のように、半分にしていく回数を数えます。
MISTAKES
よくある間違い
| 間違い | 何が起きるか | 正しくは |
|---|---|---|
| 整列していない配列に使う | 配列の中にある値でも見つからない | 先に整列する(または線形探索を使う) |
| low = mid、high = mid と書く | 範囲が狭まらず、処理が終わらない | mid + 1 / mid - 1 にして mid を範囲から外す |
| 繰り返し条件を low < high にする | 候補が1個になった場面を調べずに終わる | low <= high にする |
| 大小の判断を逆にする | 目的の値がある側を捨ててしまう | 中央より大きいなら後半、小さいなら前半に絞る |
| mid の小数を切り上げる | 添字がずれて、範囲外を参照することがある | 切り捨てて整数の添字にする |
CHECK
理解度チェック
まず自分で考え、答えを開く前に上のシミュレーターで確かめてみてください。
Q1. Nums = [2, 5, 8, 11, 14, 20] で target = 8 のとき、最初の mid はいくつですか。
(0 + 5) ÷ 2 = 2.5、小数を切り捨てて 2 です。
Nums[2] は 8 なので、この場面では1回目の比較で見つかります(シミュレーターの「6個」+「中央」の場面)。