レッスン
理論 — 二分探索:四つの選択、十六通りのループ
二分探索は、整列済み配列から値xを探す方法です。xが存在するなら必ず含まれる添字の範囲をloからhiまでに保ち、各回で中央の要素a[mid]を読み、xがある側の半分だけを残します。hiが範囲内に残る最後の添字なら閉区間です(lo <= hi、hiの初期値はn − 1)。hiが範囲の直後を指すなら半開区間です(lo < hi、hiの初期値はn)。
各記号の意味
lo, hi- まだ探索すべき範囲の両端
mid- ⌊(lo + hi)/2⌋、今回読む要素の添字
n- 要素数。添字は0からn − 1まで
N- 判定欄で調べる最長の配列。0からNまでの各nと全探索値を合わせて(N + 1)²ケース
- 前提
- 配列中の値は互いに異なり、a[mid]がxと等しくなれば直ちに返すループを考えます。また、実行欄と判定欄では上限のない整数を仮定します。オーバーフロー表だけは最後の仮定を外し、Javaと同じ32ビット演算を行います。
- 成り立たない場合
- 値に重複があっても、二つの正しいループはxが置かれた添字を返します。ただし、最初の位置とは限りません。整列済み索引の範囲検索などで最初の位置が必要なら、等しい場合にも止まらないループを使います。半開区間とし、a[mid] ≥ xなら常に
hi = midと更新すれば、終了時のloが答えです。
全プロセスの詳細解説
-
最後まで残るループが二つだけなのはなぜか 4 ステップ
lo = mid で停止を押してみよう。判定器が調べられるのは 256 要素までで、その先は試せない。任意の n について、
lo = mid + 1を使う半開区間型のループが必ず終了すること、同じループをlo = midに変えると hi = lo + 1 かつ a[lo] < x のとき停止することを示し、この議論から閉区間型のループに必要な更新を導こう。-
半開区間型では lo < hi の間だけ本体を実行する。このとき、切り捨てて求めた mid は lo 以上であり、hi より必ず小さい。
-
したがって、
hi = midなら hi は少なくとも一つ下がり、lo = mid + 1なら lo は少なくとも一つ上がる。どちらの場合も、初めは n だった hi − lo が反復ごとに少なくとも一つ減る。しかも負にはならないので、ループは n 回以内に終了する。 -
次に
lo = midとする。hi = lo + 1 なら、mid は ⌊(2lo + 1)/2⌋ = lo である。ここで a[lo] < x なら、更新しても lo は動かず、次もまったく同じ反復になる。用意された例では lo = 6、hi = 7 に達したとき、a[6] = 13、x = 14 となる。 -
閉区間型のループは lo <= hi の間動くので、lo = hi も許される。このとき mid = lo = hi であり、mid までしか動かさない更新では何も変わらない。したがって、どちらの更新も mid を越えなければならない。a[mid] はすでに読んでいるので、それで候補を失うことはない。一方、半開区間では hi は初めから範囲外にある。そこで
hi = midとすれば、a[mid] > x によって候補でないと分かった、今読んだ要素とそれより上を除外できる。hi = mid − 1では、x かもしれない a[mid − 1] まで除外してしまう。
解答
各反復で探索範囲を必ず狭め、x があり得ないインデックスだけを捨て、初期範囲には配列全体を含める必要がある。
lo <= hiなら hi = n − 1、hi = mid − 1、lo = mid + 1。lo < hiなら hi = n、hi = mid、lo = mid + 1となる。判定器でも、この二つだけが 256 要素まで生き残る。しかし今の議論は任意の n に通用する。有限のテストでは得られない保証である。32ビット演算では、さらに lo + hi が表現範囲に収まるという前提が要る。その前提を外した結果が、三つ目の発見につながる。 -
学習の道すじ
アルゴリズムが正しい理由
参考文献 (2)
- The overflow in the JDK's binary search, the nine years it went unnoticed, and Bentley's proved-correct version carrying the same line; also the source of the 1946 and 1962 dates the first insight quotes from Programming Pearls: Joshua Bloch, "Extra, Extra - Read All About It: Nearly All Binary Searches and Mergesorts are Broken." Google Research Blog, 2 June 2006.
- The bug report the page reproduces: java.util.Arrays.binarySearch on a 1,100,000,000-byte array, searching for its last element, throwing ArrayIndexOutOfBoundsException: -1064671149: OpenJDK bug JDK-5045582, "(coll) binarySearch() fails for size larger than 1<<30." Filed 11 May 2004 against Java 5.0, resolved (fixed in 6) 28 April 2006.