94 Stimmen

Berechnung der Mitte bei der binären Suche

Ich habe ein Buch über Algorithmen gelesen, das den folgenden Algorithmus für die binäre Suche enthielt:

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 = m1;
        }
       }
       return 1;
      }
 }

Der Autor sagt: "Der Fehler liegt in der Zuordnung m = (l+u)/2; kann es zu einem Überlaufen kommen und sollte ersetzt werden durch m = l + (u-l)/2 ."

Ich kann mir nicht vorstellen, wie das zu einem Überlauf führen soll. Wenn ich den Algorithmus in meinem Kopf für ein paar verschiedene Eingaben laufen lasse, sehe ich nicht, dass der Wert der Mitte aus dem Array-Index herausgeht.

In welchen Fällen würde der Überlauf also auftreten?

135voto

Jeff Foster Punkte 41930

Diese Beitrag behandelt diesen berühmten Fehler sehr detailliert. Wie andere bereits gesagt haben, handelt es sich um ein Überlaufproblem. Die in dem Link empfohlene Lösung lautet wie folgt:

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

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

Es ist wahrscheinlich auch erwähnenswert, dass für den Fall, dass negative Indizes erlaubt sind, oder vielleicht ist es nicht einmal ein Array, das gesucht wird (z. B. die Suche nach einem Wert in einem ganzzahligen Bereich, der eine Bedingung erfüllt), der obige Code möglicherweise auch nicht korrekt ist. In diesem Fall kann etwas so hässliches wie

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

erforderlich sein kann. Ein gutes Beispiel ist Suche nach dem Median in einem unsortierten Feld, ohne es zu verändern oder zusätzlichen Platz zu verbrauchen indem Sie einfach eine binäre Suche über die gesamte Integer.MIN_VALUE - Integer.MAX_VALUE Bereich.

55voto

nonopolarity Punkte 138211

Das folgende C++-Programm kann Ihnen zeigen, wie ein Überlauf bei einer 32-Bit-Ganzzahl ohne Vorzeichen entstehen kann:

#include <iostream>
using namespace std;

int main ()
{
  unsigned int  low = 33,  
                high = 4294967290, 
                mid;

  cout << "The value of low is " << low << endl;
  cout << "The value of high is " << high << endl;

  mid = (low + high) / 2;

  cout << "The value of mid is " << mid << endl;

  return 0;
}

Wenn Sie es auf einem Mac ausführen:

$ g++ try.cpp && ./a.out
The value of low is 33
The value of high is 4294967290
The value of mid is 13

Der Wert von mid könnte man erwarten, dass 2147483661 aber low + high überlaufen, weil eine 32-Bit-Ganzzahl ohne Vorzeichen nicht den richtigen Wert enthalten kann, und geben zurück 27 , und so mid wird 13 .

Wenn die Berechnung von mid wird geändert in

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

Dann wird angezeigt

The value of mid is 2147483661

Die einfache Antwort ist, dass der Zusatz l + u kann überlaufen und hat in einigen Sprachen ein undefiniertes Verhalten, wie in ein Blogbeitrag von Joshua Bloch über einen Fehler in der Java-Bibliothek für die Implementierung der Binärsuche .

Einige Leser verstehen vielleicht nicht, worum es geht:

l + (u - l) / 2

Beachten Sie, dass in einigen Codes die Variablennamen anders lauten, und es ist

low + (high - low) / 2

Die Antwort ist: Sagen wir, Sie haben zwei Zahlen: 200 und 210, und du willst nun die "mittlere Zahl". Und wenn Sie zwei Zahlen addieren und das Ergebnis größer als 255 ist, kann es zu einem Überlauf kommen und das Verhalten ist undefiniert, was können Sie dann tun? Eine einfache Möglichkeit besteht darin, die Differenz zwischen den beiden Zahlen, aber nur die Hälfte davon, zum kleineren Wert zu addieren: Schauen Sie sich an, was die Differenz zwischen 200 und 210 ist. Sie beträgt 10. (Man kann sie als "Differenz" oder "Länge" zwischen den beiden Werten betrachten). Sie müssen also nur Folgendes addieren 10 / 2 = 5 auf 200 und erhalten 205. Man muss nicht erst 200 und 210 zusammenzählen - und so kommen wir zur Berechnung: (u - l) ist der Unterschied. (u - l) / 2 ist die Hälfte davon. Addieren Sie das zu l und wir haben l + (u - l) / 2 .

Wenn wir zwei Bäume betrachten, von denen einer 200 Fuß hoch ist und der andere 210 Fuß hoch, was ist dann der "Mittelpunkt" oder der "Mittelwert"? Wir müssen sie nicht erst zusammenzählen. Wir können einfach sagen, dass der Unterschied 10 Fuß beträgt, und wir können die Hälfte davon, also 5, zu 200 addieren, und wir wissen, dass es 205 Fuß sind.

Um dies in eine historische Perspektive zu rücken, erwähnte Robert Sedgewick, dass die erste binäre Suche im Jahr 1946 angegeben wurde und erst 1964 korrekt war. Jon Bentley beschrieb 1988 in seinem Buch Programming Pearls, dass mehr als 90 % der professionellen Programmierer sie in ein paar Stunden nicht korrekt schreiben könnten. Aber sogar Jon Bentley selbst hatte diesen Überlauffehler 20 Jahre lang. Eine 1988 veröffentlichte Studie zeigte, dass nur in 5 von 20 Lehrbüchern korrekter Code für die Binärsuche zu finden war. Im Jahr 2006 schrieb Joshua Bloch diesen Blogbeitrag über den Fehler bei der Berechnung der mid Wert. Es hat also 60 Jahre gedauert, bis dieser Code korrekt war. Aber denken Sie beim nächsten Vorstellungsgespräch daran, dass Sie den Code in diesen 5 Minuten richtig schreiben müssen.

11voto

murgatroid99 Punkte 16865

Das Problem ist, dass (l+u) wird zuerst ausgewertet und könnte int überlaufen lassen, so dass (l+u)/2 würde den falschen Wert zurückgeben.

5voto

Vipin Punkte 4403

Jeff schlug wirklich gute Beitrag um über diesen Fehler zu lesen, hier ist eine Zusammenfassung, wenn Sie einen schnellen Überblick wünschen.

In Programming Pearls sagt Bentley, dass die analoge Zeile "m auf den Durchschnitt von l und u, abgeschnitten auf die nächste ganze Zahl, setzt". Auf den ersten Blick mag diese Behauptung richtig erscheinen, aber scheitert er bei großen Werten der int-Variablen low und high. Insbesondere schlägt er fehl, wenn die Summe von low und high größer ist als der maximale positive int-Wert (2^31 - 1). Die Summe läuft zu einem negativen Wert über, und der Wert bleibt negativ, wenn er durch zwei geteilt wird. In C führt dies zu einem Array-Index außerhalb der Grenzen mit unvorhersehbaren Ergebnissen. In Java löst es ArrayIndexOutOfBoundsException aus.

5voto

Sambhav Khare Punkte 51

Hier ein Beispiel: Angenommen, Sie haben ein sehr großes Array der Größe 2,000,000,000 y 10 (10^9 + 10) und die linke index war bei 2,000,000,000 und das Recht index war bei 2,000,000,000 + 1 .

Durch die Verwendung von lo + hi wird sich summieren zu 2,000,000,000 + 2,000,000,001 = 4,000,000,001 . Da der Maximalwert einer integer です 2,147,483,647 . Sie werden also nicht 4,000,000,000 + 1 erhalten Sie eine integer overflow .

Mais low + ((high - low) / 2) wird funktionieren. 2,000,000,000 + ((2,000,000,001 - 2,000,000,000) / 2) = 2,000,000,000

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X