web-dev-qa-db-ja.com

二分探索で中間を計算する

私は、バイナリサーチ用の次のアルゴリズムを含むアルゴリズムの本を読んでいました。

_public class BinSearch {
  static int search ( int [ ] A, int K ) {
    int l = 0 ;
    int u = A. length −1;
    int m;
    while (l <= u ) {
      m = (l+u) /2;
      if (A[m] < K) {
        l = m + 1 ;
      } else if (A[m] == K) {
        return m;
        } else {
          u = m−1;
        }
       }
       return −1;
      }
 }
_

著者は、「エラーは割り当てm = (l+u)/2;にあり、オーバーフローを引き起こす可能性があるため、m = l + (u-l)/2で置き換える必要があります。」と述べています。

それがどのようにオーバーフローを引き起こすのか、私にはわかりません。いくつかの異なる入力に対してアルゴリズムを実行しているときに、midの値が配列のインデックスから出ていくのがわかりません。

それでは、どの場合にオーバーフローが発生しますか?

36
Bharat

この post は、この有名なバグを詳細にカバーしています。他の人が言ったように、それはオーバーフローの問題です。リンクで推奨されている修正は次のとおりです。

int mid = low + ((high - low) / 2);

// Alternatively
int mid = (low + high) >>> 1;

負のインデックスが許可されている場合、または検索されている配列でさえない場合(たとえば、ある条件を満たす整数範囲で値を検索している場合)、上記のコードも正しくない可能性があることにも言及する価値があるでしょう。 。この場合、醜いもの

(low < 0 && high > 0) ? (low + high) / 2 : low + (high - low) / 2

必要な場合があります。良い例の1つは 並べ替えられていない配列の中央値を変更するか、追加のスペースを使用せずに検索する 全体に対してバイナリ検索を実行するだけですInteger.MIN_VALUEInteger.MAX_VALUE 範囲。

53
Jeff Foster

問題はそれです (l+u)が最初に評価され、intをオーバーフローする可能性があるため、(l+u)/2は間違った値を返します。

6
murgatroid99

Jeffはこのバグについて読むために本当に良い post を提案しました、ここで簡単に概要を知りたい場合の概要を示します。

プログラミングの真珠で、ベントレーは類似の行が「mをlとuの平均に設定し、最も近い整数に切り捨てられる」と述べています。一見すると、このアサーションは正しいように見えるかもしれませんが、低および高のint変数の大きな値では失敗します。具体的には、lowとhighの合計が最大の正のint値(2 ^ 31-1)より大きい場合、失敗します。合計がオーバーフローして負の値になり、値を2で除算しても負のままです。Cでは、配列のインデックスが範囲外になり、予測できない結果が生じます。 Javaでは、ArrayIndexOutOfBoundsExceptionをスローします。

3
Vipin

簡単な答えは、追加の_l + u_はオーバーフローする可能性があり、一部の言語では未定義の動作があります。これは、 Javaバイナリ検索の実装用ライブラリ

一部の読者はそれが何であるかを理解していないかもしれません:

_l + (u - l) / 2
_

一部のコードでは、変数名が異なり、それは

_low + (high - low) / 2
_

答えは、200と210の2つの数値があり、「中間数」が必要な場合を考えてみましょう。そして、2つの数値を加算し、結果が255より大きい場合、オーバーフローする可能性があり、動作が定義されていない場合、どうすればよいでしょうか。単純な方法は、それらの差を半分に加えて小さい値に追加することです。200と210の差を見てください。10です(「差」または「長さ」と考えることができます) "、 それらの間の)。したがって、_10 / 2 = 5_を200に追加して205を取得するだけです。最初に200と210を加算する必要はありません。これが、計算に到達する方法です。_(u - l)_が違います。 _(u - l) / 2_はその半分です。それをlに追加すると、l + (u - l) / 2ができます。

これを歴史の観点に当てはめるために、ロバートセッジウィックは1946年に最初のバイナリ検索が言及されたと述べましたが、それは1964年まで正しくありませんでした。数時間かけて正しく書いてください。しかし、Jon Bentley自身でさえ、20年間オーバーフローのバグがありました。 1988年に発表された調査によると、バイナリ検索用の正確なコードは、20冊の教科書のうち5冊にしかありませんでした。 2006年に、Joshua Blochがmid値の計算に関するバグについてこのブログ投稿を書きました。したがって、このコードが正しくなるまでに60年かかりました。しかし、今度は就職の面接で、その20分以内に正しく書くことを忘れないでください。

2
nonopolarity

潜在的なオーバーフローはl+u追加自体。

これは実際にはJDKのバイナリ検索の 初期バージョンのバグ でした。

2
Nemo