Warum erzeugt rand () + rand () negative Zahlen?

304

Ich habe festgestellt, dass rand()Bibliotheksfunktionen, wenn sie nur einmal innerhalb einer Schleife aufgerufen werden, fast immer positive Zahlen erzeugen.

for (i = 0; i < 100; i++) {
    printf("%d\n", rand());
}

Wenn ich jedoch zwei rand()Anrufe hinzufüge , haben die generierten Nummern jetzt mehr negative Nummern.

for (i = 0; i < 100; i++) {
    printf("%d = %d\n", rand(), (rand() + rand()));
}

Kann jemand erklären, warum ich im zweiten Fall negative Zahlen sehe?

PS: Ich initialisiere den Startwert vor der Schleife als srand(time(NULL)).

böse verrückt
quelle
11
rand()kann nicht negativ sein ...
Twentylemon
293
rand () + rand () kann fließen
maskacovnik
13
Was ist RAND_MAXfür Ihren Compiler? Sie können es normalerweise in finden stdlib.h. (Witzig: Überprüfung man 3 rand, es trägt die einzeilige Beschreibung "Generator für schlechte Zufallszahlen".)
usr2564301
6
tun, was jeder vernünftige Programmierer tun würde abs(rand()+rand()). Ich hätte lieber eine positive als eine negative UB! ;)
Vinicius Kamakura
11
@hexa: das ist keine sotution für die UB, da die für die Addition bereits auftritt. Sie können UB nicht zu einem definierten Verhalten machen . Ein vernünftiger Programmierer würde UB höllisch aus dem Weg gehen.
zu ehrlich für diese Seite

Antworten:

542

rand()ist definiert, um eine Ganzzahl zwischen 0und zurückzugeben RAND_MAX.

rand() + rand()

könnte überlaufen. Was Sie beobachten, ist wahrscheinlich ein Ergebnis eines undefinierten Verhaltens, das durch einen Ganzzahlüberlauf verursacht wird.

PP
quelle
4
@JakubArnold: Wie wird das als Überlaufverhalten von jeder Sprache unterschiedlich spezifiziert? Python zum Beispiel hat keine (bis zum verfügbaren Speicher), da int nur wächst.
zu ehrlich für diese Seite
2
@Olaf Es hängt davon ab, wie eine Sprache vorzeichenbehaftete Ganzzahlen darstellt. Java hatte keinen Mechanismus zum Erkennen eines Ganzzahlüberlaufs (bis Java 8) und definierte ihn zum Umschließen. Go verwendet nur die Komplementdarstellung von 2 und definiert ihn als legal für vorzeichenbehaftete Ganzzahlüberläufe. C unterstützt offensichtlich mehr als das 2er-Komplement.
PP
2
@EvanCarslake Nein, das ist kein universelles Verhalten. Was Sie sagen, handelt von der Komplementdarstellung von 2. Die C-Sprache erlaubt aber auch andere Darstellungen. Die C-Sprachspezifikation besagt, dass ein vorzeichenbehafteter Ganzzahlüberlauf undefiniert ist . Im Allgemeinen sollte sich kein Programm auf ein solches Verhalten verlassen und sorgfältig codieren müssen, um keinen vorzeichenbehafteten Ganzzahlüberlauf zu verursachen. Dies gilt jedoch nicht für vorzeichenlose Ganzzahlen, da diese auf genau definierte Weise (Reduktionsmodulo 2) "umlaufen" würden. [Fortsetzung] ...
PP
12
Dies ist das Zitat aus dem C-Standard in Bezug auf einen vorzeichenbehafteten Ganzzahlüberlauf: Wenn während der Auswertung eines Ausdrucks eine Ausnahmebedingung auftritt (dh wenn das Ergebnis nicht mathematisch definiert ist oder nicht im Bereich der darstellbaren Werte für seinen Typ liegt), das Verhalten ist nicht definiert.
PP
3
@EvanCarslake entfernt sich ein wenig von der Frage, dass die C-Compiler den Standard verwenden, und für vorzeichenbehaftete Ganzzahlen können sie davon ausgehen, dass a + b > asie das wissen, wenn sie das wissen b > 0. Sie können auch davon ausgehen, dass bei einer später ausgeführten Anweisung der a + 5aktuelle Wert niedriger ist INT_MAX - 5. Selbst auf dem 2er-Komplement-Prozessor / Interpreter ohne Traps verhält sich das Programm möglicherweise nicht so, als ob ints das 2er-Komplement ohne Traps wäre.
Maciej Piechotka
90

Das Problem ist die Hinzufügung. rand()gibt einen intWert von zurück 0...RAND_MAX. Wenn Sie also zwei davon hinzufügen, werden Sie aufstehen RAND_MAX * 2. Wenn dies überschritten wird INT_MAX, überschreitet das Ergebnis der Addition den gültigen Bereich, den ein inthalten kann. Ein Überlauf von vorzeichenbehafteten Werten ist ein undefiniertes Verhalten und kann dazu führen, dass Ihre Tastatur in Fremdsprachen mit Ihnen spricht.

Da das Hinzufügen von zwei zufälligen Ergebnissen hier keinen Vorteil bringt, besteht die einfache Idee darin, dies einfach nicht zu tun. Alternativ können Sie jedes Ergebnis unsigned intvor der Addition umwandeln, wenn dies die Summe enthalten kann. Oder verwenden Sie einen größeren Typ. Beachten Sie, dass longnicht notwendigerweise breiter ist als intdas gleiche gilt , long longwenn intmindestens 64 Bit!

Fazit: Vermeiden Sie einfach den Zusatz. Es bietet keine "Zufälligkeit" mehr. Wenn Sie mehr Bits benötigen, können Sie die Werte verketten sum = a + b * (RAND_MAX + 1), dies erfordert jedoch wahrscheinlich auch einen größeren Datentyp als int.

Als Ihr angegebener Grund ist es, ein Null-Ergebnis zu vermeiden: Dies kann nicht vermieden werden, indem die Ergebnisse von zwei rand()Aufrufen addiert werden , da beide Null sein können. Stattdessen können Sie einfach inkrementieren. Wenn RAND_MAX == INT_MAXdies nicht möglich ist int. Allerdings (unsigned int)rand() + 1tut sehr, sehr wahrscheinlich. Wahrscheinlich (nicht definitiv), weil es erforderlich ist UINT_MAX > INT_MAX, was für alle mir bekannten Implementierungen gilt (die einige eingebettete Architekturen, DSPs und alle Desktop-, Mobil- und Serverplattformen der letzten 30 Jahre abdecken).

Warnung:

Obwohl bereits hier in den Kommentaren bestreut, beachten Sie bitte , dass das Hinzufügen von zwei Zufallswerten nicht nicht erhält eine gleichmäßige Verteilung, sondern eine Dreiecksverteilung mit zwei Würfel wie walzen zu bekommen 12(zwei Würfel) beiden Würfel zeigen müssen 6. denn 11es gibt bereits zwei mögliche Varianten: 6 + 5oder 5 + 6usw.

Daher ist der Zusatz auch in dieser Hinsicht schlecht.

Beachten Sie auch, dass die rand()generierten Ergebnisse nicht unabhängig voneinander sind, da sie von einem Pseudozufallszahlengenerator generiert werden . Beachten Sie auch, dass der Standard nicht die Qualität oder gleichmäßige Verteilung der berechneten Werte festlegt.

zu ehrlich für diese Seite
quelle
14
@badmad: Was ist, wenn beide Aufrufe 0 zurückgeben?
zu ehrlich für diese Seite
3
@badmad: Ich frage mich nur, ob UINT_MAX > INT_MAX != falseder Standard dies garantiert. (Klingt wahrscheinlich, ist sich aber bei Bedarf nicht sicher). In diesem Fall können Sie nur ein einzelnes Ergebnis umwandeln und inkrementieren (in dieser Reihenfolge!).
zu ehrlich für diese Seite
3
Das Hinzufügen mehrerer Zufallszahlen ist vorteilhaft,
Cœur
6
um 0 zu vermeiden, ein einfaches "solange das Ergebnis 0 ist, erneut würfeln"?
Olivier Dulac
2
Das Hinzufügen ist nicht nur ein schlechter Weg, um 0 zu vermeiden, sondern führt auch zu einer ungleichmäßigen Verteilung. Sie erhalten eine Verteilung wie die Ergebnisse des
Würfelns
36

Dies ist eine Antwort auf eine Klarstellung der Frage, die im Kommentar zu dieser Antwort gestellt wurde.

Der Grund, den ich hinzufügte, war, '0' als Zufallszahl in meinem Code zu vermeiden. rand () + rand () war die schnelle, schmutzige Lösung, die mir sofort in den Sinn kam.

Das Problem bestand darin, 0 zu vermeiden. Bei der vorgeschlagenen Lösung gibt es (mindestens) zwei Probleme. Eine ist, wie die anderen Antworten zeigen, rand()+rand()die undefiniertes Verhalten hervorrufen kann. Der beste Rat ist, niemals undefiniertes Verhalten aufzurufen. Ein weiteres Problem ist, dass es keine Garantie gibt, dass rand()nicht zweimal hintereinander 0 erzeugt wird.

Das Folgende lehnt Null ab, vermeidet undefiniertes Verhalten und ist in den allermeisten Fällen schneller als zwei Aufrufe an rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);
David Hammen
quelle
9
Was ist mit rand() + 1?
Askvictor
3
@askvictor Das könnte überlaufen (obwohl es unwahrscheinlich ist).
Gerrit
3
@gerrit - hängt von MAX_INT und RAND_MAX ab
askvictor
3
@gerrit, ich wäre überrascht, wenn sie nicht gleich sind, aber ich nehme an, dies ist ein Ort für Pedanten :)
askvictor
10
Wenn RAND_MAX == MAX_INT, läuft rand () + 1 mit genau der gleichen Wahrscheinlichkeit über, mit der der Wert von rand () 0 ist, was diese Lösung völlig sinnlos macht. Wenn Sie bereit sind, es zu riskieren und die Möglichkeit eines Überlaufs zu ignorieren, können Sie ebenso rand () wie es ist verwenden und die Möglichkeit ignorieren, dass es 0
Emil Jeřábek
3

rand()Produzieren Sie grundsätzlich Zahlen zwischen 0und RAND_MAXund 2 RAND_MAX > INT_MAXin Ihrem Fall.

Sie können mit dem Maximalwert Ihres Datentyps modulieren, um einen Überlauf zu verhindern. Dies stört natürlich die Verteilung der Zufallszahlen, randist jedoch nur ein Weg, um schnelle Zufallszahlen zu erhalten.

#include <stdio.h>
#include <limits.h>

int main(void)
{
    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;
}
Khaled.K
quelle
2

Möglicherweise können Sie einen kniffligen Ansatz ausprobieren, indem Sie sicherstellen, dass der durch die Summe von 2 rand () zurückgegebene Wert niemals den Wert von RAND_MAX überschreitet. Ein möglicher Ansatz könnte sum = rand () / 2 + rand () / 2 sein; Dies würde sicherstellen, dass für einen 16-Bit-Compiler mit einem RAND_MAX-Wert von 32767, selbst wenn beide Rand 32767 zurückgeben, selbst dann (32767/2 = 16383) 16383 + 16383 = 32766 keine negative Summe resultiert.

Jibin Mathew
quelle
1
Das OP wollte 0 von den Ergebnissen ausschließen. Die Addition liefert auch keine gleichmäßige Verteilung von Zufallswerten.
zu ehrlich für diese Seite
@Olaf: Es gibt keine Garantie dafür, dass zwei aufeinanderfolgende Aufrufe von rand()nicht beide Null ergeben. Daher ist der Wunsch, Null zu vermeiden, kein guter Grund, zwei Werte hinzuzufügen. Andererseits wäre der Wunsch nach einer ungleichmäßigen Verteilung ein guter Grund, zwei zufällige Werte hinzuzufügen, wenn einer sicherstellt, dass kein Überlauf auftritt.
Supercat
1

Der Grund, den ich hinzufügte, war, '0' als Zufallszahl in meinem Code zu vermeiden. rand () + rand () war die schnelle, schmutzige Lösung, die mir sofort in den Sinn kam.

Eine einfache Lösung (okay, nennen Sie es einen "Hack"), die niemals ein Null-Ergebnis erzeugt und niemals überläuft, ist:

x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

Dies begrenzt Ihren Maximalwert, aber wenn Sie sich nicht darum kümmern, sollte dies für Sie gut funktionieren.

Kevin Fegan
quelle
1
Nebenbemerkung: Vorsicht bei der Verschiebung der vorzeichenbehafteten Variablen nach rechts. Es ist nur für nichtnegative Werte gut definiert, für Negative ist es implementierungsdefiniert. (Gibt zum Glück rand()immer einen nicht negativen Wert zurück). Allerdings würde ich die Optimierung hier dem Compiler überlassen.
zu ehrlich für diese Seite
@Olaf: Im Allgemeinen ist die signierte Division durch zwei weniger effizient als eine Schicht. Sofern ein Compiler-Writer keine Anstrengungen unternommen hat, um dem Compiler mitzuteilen, randdass er nicht negativ ist, ist die Verschiebung effizienter als die Division durch eine vorzeichenbehaftete Ganzzahl 2. Die Division durch 2ukönnte funktionieren, aber wenn dies der Fall xist, intkann dies zu Warnungen vor impliziter Konvertierung von vorzeichenlos führen zu unterschreiben.
Supercat
@supercat: Bitte lies meinen Kommentar noch einmal sorgfältig durch. Sie sollten sehr gut wissen, dass jeder vernünftige Compiler / 2sowieso eine Verschiebung verwenden wird (ich habe dies sogar für so etwas gesehen -O0, dh ohne ausdrücklich angeforderte Optimierungen). Dies ist möglicherweise die trivialste und etablierteste Optimierung von C-Code. Punkt ist, dass die Division durch den Standard für den gesamten ganzzahligen Bereich gut definiert ist, nicht nur für nicht negative Werte. Nochmals: Überlassen Sie die Optimierung dem Compiler, schreiben Sie zunächst den richtigen und klaren Code. Dies ist für Anfänger noch wichtiger.
zu ehrlich für diese Seite
@Olaf: Jeder Compiler, den ich getestet habe, generiert effizienteren Code, wenn er rand()durch eins nach rechts verschoben oder durch geteilt wird, 2uals wenn er durch 2 geteilt wird, selbst wenn er verwendet wird -O3. Man könnte vernünftigerweise sagen, dass eine solche Optimierung wahrscheinlich keine Rolle spielt, aber wenn man sagt, dass "solche Optimierungen dem Compiler überlassen", würde dies bedeuten, dass Compiler sie wahrscheinlich ausführen würden. Kennen Sie irgendwelche Compiler , dass eigentlich will?
Supercat
@supercat: Dann sollten Sie modernere Compiler verwenden. gcc hat gerade feinen Code generiert, als ich den generierten Assembler das letzte Mal überprüft habe. Trotzdem würde ich es vorziehen, nicht in dem Maße belästigt zu werden, wie Sie es das letzte Mal präsentieren, so sehr ich mich für einen Groopie beklage. Diese Beiträge sind Jahre alt, meine Kommentare sind vollkommen gültig. Danke dir.
zu ehrlich für diese Seite
1

Versuchen Sie Folgendes, um 0 zu vermeiden:

int rnumb = rand()%(INT_MAX-1)+1;

Sie müssen einschließen limits.h.

Doni
quelle
4
Das wird die Wahrscheinlichkeit verdoppeln, 1 zu erhalten. Es ist im Grunde das gleiche (aber möglicherweise langsamer) wie das bedingte Hinzufügen von 1, wenn es rand()0 ergibt.
zu ehrlich für diese Seite
Ja, du hast recht, Olaf. Wenn rand () = 0 oder INT_MAX -1 ist, ist rnumb 1.
Doni
Noch schlimmer, wenn ich darüber nachdenke. Es wird tatsächlich die Propabilität für 1und 2(alle angenommen RAND_MAX == INT_MAX) verdoppeln . Ich habe das vergessen - 1.
zu ehrlich für diese Seite
1
Das -1hier dient keinem Wert. rand()%INT_MAX+1; würde immer noch nur Werte im Bereich [1 ... INT_MAX] erzeugen.
chux
-2

Während das, was alle anderen über den wahrscheinlichen Überlauf gesagt haben, sehr wohl die Ursache für das Negative sein kann, selbst wenn Sie vorzeichenlose Ganzzahlen verwenden. Das eigentliche Problem besteht darin, die Zeit- / Datumsfunktionalität als Startwert zu verwenden. Wenn Sie sich mit dieser Funktionalität wirklich vertraut gemacht haben, wissen Sie genau, warum ich das sage. Was es wirklich tut, ist eine Entfernung (verstrichene Zeit) seit einem bestimmten Datum / einer bestimmten Uhrzeit anzugeben. Die Verwendung der Datums- / Zeitfunktion als Startwert für einen Rand () ist zwar eine weit verbreitete Praxis, aber nicht die beste Option. Sie sollten nach besseren Alternativen suchen, da es viele Theorien zu diesem Thema gibt und ich unmöglich auf alle eingehen könnte. Sie fügen dieser Gleichung die Möglichkeit eines Überlaufs hinzu, und dieser Ansatz war von Anfang an zum Scheitern verurteilt.

Diejenigen, die rand () + 1 gepostet haben, verwenden die am häufigsten verwendete Lösung, um sicherzustellen, dass sie keine negative Zahl erhalten. Aber dieser Ansatz ist auch nicht der beste Weg.

Das Beste, was Sie tun können, ist, sich die zusätzliche Zeit zu nehmen, um die richtige Ausnahmebehandlung zu schreiben und zu verwenden, und die rand () - Nummer nur dann zu addieren, wenn und / oder wenn Sie am Ende ein Ergebnis von Null erhalten. Und um mit negativen Zahlen richtig umzugehen. Die rand () - Funktionalität ist nicht perfekt und muss daher in Verbindung mit der Ausnahmebehandlung verwendet werden, um sicherzustellen, dass Sie das gewünschte Ergebnis erzielen.

Es lohnt sich, sich die zusätzliche Zeit und Mühe zu nehmen, um die rand () - Funktionalität zu untersuchen, zu untersuchen und ordnungsgemäß zu implementieren. Nur meine zwei Cent. Viel Glück bei Ihren Bemühungen ...

Mark Krug
quelle
2
rand()gibt nicht an, welcher Startwert verwendet werden soll. Der Standard legt fest , dass ein Pseudozufallsgenerator verwendet wird, nicht eine Beziehung zu irgendeiner Zeit. Es gibt auch keinen Hinweis auf die Qualität des Generators. Das eigentliche Problem ist eindeutig der Überlauf. Beachten Sie, dass rand()+1verwendet wird, um zu vermeiden 0; rand()gibt keinen negativen Wert zurück. Entschuldigung, aber Sie haben den Punkt hier verpasst. Es geht nicht um die Qualität des PRNG. ...
zu ehrlich für diese Seite
... Gute Praxis unter GNU / Linux ist es, /dev/randomein gutes PRNG zu verwenden und anschließend zu verwenden (nicht sicher über die Qualität rand()von glibc) oder das Gerät weiter zu verwenden - und zu riskieren, dass Ihre Anwendung blockiert, wenn nicht genügend Entropie verfügbar ist. Der Versuch, Ihre Entropie in die Anwendung zu bringen, ist möglicherweise eine Sicherheitslücke, da diese möglicherweise leichter anzugreifen ist. Und jetzt kommt es zum Härten - nicht hier
zu ehrlich für diese Seite