Dlaczego rand () + rand () generuje liczby ujemne?


304

Zauważyłem, że rand()funkcja biblioteki, gdy jest wywoływana tylko raz w pętli, prawie zawsze generuje liczby dodatnie.

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

Ale kiedy dodam dwa rand()połączenia, wygenerowane liczby mają teraz więcej liczb ujemnych.

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

Czy ktoś może wyjaśnić, dlaczego widzę liczby ujemne w drugim przypadku?

PS: Inicjuję ziarno przed pętlą jako srand(time(NULL)).


11
rand()nie może być negatywne ...
twentylemon

293
rand () + rand () może owerflow
maskacovnik

13
Co jest RAND_MAXdla twojego kompilatora? Zwykle można go znaleźć w stdlib.h. (Zabawne: sprawdzanie man 3 rand, zawiera opis w jednym wierszu „generator złych liczb losowych”.)
usr2564301,

6
rób to, co zrobiłby każdy rozsądny programista abs(rand()+rand()). Wolę mieć dodatni UB niż ujemny! ;)
Vinicius Kamakura

11
@hexa: nie ma to żadnego znaczenia dla UB, ponieważ występuje już dla dodania. Nie możesz sprawić, by UB stał się zdefiniowanym zachowaniem . Sane progrtammer by uniknąć UB jak cholera.
zbyt uczciwy jak na tę stronę

Odpowiedzi:


542

rand()jest zdefiniowany tak, aby zwracał liczbę całkowitą między 0i RAND_MAX.

rand() + rand()

może się przepełnić. To, co obserwujesz, jest prawdopodobnie wynikiem nieokreślonego zachowania spowodowanego przepełnieniem liczb całkowitych.


4
@JakubArnold: Jak to zachowanie przepełnienia jest określone przez każdy język inaczej? Na przykład Python nie ma go (cóż, do dostępnej pamięci), ponieważ int po prostu rośnie.
zbyt szczery dla tej strony

2
@Olaf To zależy od tego, jak język zdecyduje się reprezentować podpisane liczby całkowite. Java nie miała mechanizmu wykrywającego przepełnienie liczb całkowitych (aż do java 8) i zdefiniowała go do zawijania, a Go używa tylko reprezentacji uzupełnienia 2 i definiuje ją jako dozwoloną w przypadku przepełnienia liczby całkowitej ze znakiem. C oczywiście obsługuje więcej niż 2 uzupełnienia.
PP

2
@EvanCarslake Nie, to nie jest uniwersalne zachowanie. To, co mówisz, dotyczy reprezentacji uzupełnienia 2. Ale język C pozwala również na inne reprezentacje. Specyfikacja języka C mówi, że przepełnienie liczb całkowitych ze znakiem jest niezdefiniowane . Ogólnie rzecz biorąc, żaden program nie powinien polegać na takim zachowaniu i musi ostrożnie kodować, aby nie spowodować przepełnienia liczby całkowitej ze znakiem. Nie dotyczy to jednak liczb całkowitych bez znaku, ponieważ „zawiną się” w dobrze zdefiniowany sposób (moduł redukcji 2). [ciąg dalszy] ...
PP

12
Oto cytat ze standardu C związany z przepełnieniem liczby całkowitej ze znakiem: jeśli podczas oceny wyrażenia wystąpi wyjątkowy warunek (to znaczy, jeśli wynik nie jest zdefiniowany matematycznie lub nie mieści się w zakresie reprezentatywnych wartości dla jego typu), zachowanie jest niezdefiniowany.
PP

3
@EvanCarslake odchodząc nieco od pytania, kompilatory C używają standardu i dla liczb całkowitych ze znakiem mogą przyjąć, że a + b > ajeśli o tym wiedzą b > 0. Mogą również założyć, że jeśli później zostanie wykonana instrukcja, a + 5wówczas bieżąca wartość jest niższa niż INT_MAX - 5. Więc nawet na procesorze / interpreterze uzupełnień 2 bez pułapek program może nie zachowywać się tak, jakby intbył uzupełnieniem 2 bez pułapek.
Maciej Piechotka,

90

Problemem jest dodatek. rand()zwraca intwartość 0...RAND_MAX. Więc jeśli dodasz dwa z nich, wstaniesz RAND_MAX * 2. Jeśli to przekracza INT_MAX, wynik dodania przepełnia prawidłowy zakres, jaki intmoże utrzymać. Przepełnienie podpisanych wartości jest nieokreślonym zachowaniem i może prowadzić do tego, że klawiatura mówi do ciebie obcymi językami.

Ponieważ dodawanie dwóch losowych wyników nie przynosi korzyści, prostym pomysłem jest po prostu nie robienie tego. Alternatywnie możesz rzucić każdy wynik na unsigned intprzed dodaniem, jeśli może to pomieścić sumę. Lub użyj większego typu. Pamiętaj, że longniekoniecznie jest szerszy niż int, to samo dotyczy, long longjeśli intma co najmniej 64 bity!

Wniosek: po prostu unikaj dodawania. Nie zapewnia większej „losowości”. Jeśli potrzebujesz więcej bitów, możesz połączyć wartości sum = a + b * (RAND_MAX + 1), ale prawdopodobnie również wymaga to większego typu danych niż int.

Jako podany powód należy unikać wyniku zerowego: nie można tego uniknąć przez dodanie wyników dwóch rand()wywołań, ponieważ oba mogą być zerowe. Zamiast tego możesz po prostu zwiększyć. Jeśli RAND_MAX == INT_MAXnie można tego zrobić w int. Jednak (unsigned int)rand() + 1zrobi to bardzo, bardzo prawdopodobne. Prawdopodobnie (nie definitywnie), ponieważ wymaga UINT_MAX > INT_MAX, co jest prawdą we wszystkich implementacjach, o których wiem (co obejmuje całkiem sporo wbudowanych architektur, DSP i wszystkich platform stacjonarnych, mobilnych i serwerowych z ostatnich 30 lat).

Ostrzeżenie:

Mimo, że zostało już tutaj pokropionych komentarzami, należy pamiętać, że dodanie dwóch losowych wartości nie daje jednolitego rozkładu, ale rozkład trójkątny, taki jak rzucenie dwiema kościami: aby uzyskać 12(dwie kości) obie kości muszą się pokazać 6. ponieważ 11istnieją już dwa możliwe warianty: 6 + 5lub 5 + 6itd.

Tak więc dodanie jest również złe w tym aspekcie.

Zauważ też, że rand()generowane wyniki nie są od siebie niezależne, ponieważ są generowane przez generator liczb pseudolosowych . Należy również zauważyć, że standard nie określa jakości ani jednolitego rozkładu obliczonych wartości.


14
@badmad: A co, jeśli oba połączenia zwrócą 0?
zbyt uczciwy dla tej strony

3
@badmad: Zastanawiam się tylko, czy UINT_MAX > INT_MAX != falsejest to gwarantowane przez standard. (Brzmi prawdopodobnie, ale nie jest pewny, jeśli to konieczne). Jeśli tak, możesz po prostu rzucić pojedynczy wynik i przyrost (w tej kolejności!).
zbyt uczciwy dla tej strony

3
Korzyści z dodawania wielu liczb losowych, gdy chcesz uzyskać nierównomierną dystrybucję: stackoverflow.com/questions/30492259/...
Cœur

6
aby uniknąć 0, proste „gdy wynikiem jest 0, przerzucić”?
Olivier Dulac

2
Dodanie ich jest nie tylko złym sposobem na uniknięcie 0, ale także powoduje nierównomierny rozkład. Otrzymujesz rozkład podobny do wyników rzutu kostką: 7 jest 6 razy bardziej prawdopodobne niż 2 lub 12.
Barmar

36

To jest odpowiedź na wyjaśnienie pytania postawionego w komentarzu do tej odpowiedzi ,

powodem, dla którego dodawałem, było uniknięcie „0” jako liczby losowej w moim kodzie. rand () + rand () to szybkie, brudne rozwiązanie, które z łatwością przyszło mi do głowy.

Problem polegał na uniknięciu 0. Istnieją dwa (przynajmniej) dwa problemy z proponowanym rozwiązaniem. Jedna, jak wskazują inne odpowiedzi, rand()+rand()może wywoływać niezdefiniowane zachowanie. Najlepsza rada to nigdy nie powoływać się na niezdefiniowane zachowanie. Inną kwestią jest to, że nie ma gwarancji, że rand()nie da 0 dwa razy z rzędu.

Poniższe odrzuca zero, unika niezdefiniowanego zachowania, aw zdecydowanej większości przypadków będzie szybsze niż dwa wezwania do rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);

9
Co rand() + 1?
askvictor

3
@askvictor To może się przepełnić (choć jest mało prawdopodobne).
gerrit

3
@gerrit - zależy od MAX_INT i RAND_MAX
askvictor

3
@gerrit, chciałbym być zaskoczony, jeśli są nie takie same, ale przypuszczam, że jest to miejsce dla pedantów :)
askvictor

10
Jeśli RAND_MAX == MAX_INT, rand () + 1 przepełni się z dokładnie takim samym prawdopodobieństwem jak wartość rand () wynosząca 0, co czyni to rozwiązanie całkowicie bezcelowym. Jeśli chcesz zaryzykować i zignorować możliwość przepełnienia, możesz równie dobrze użyć rand (), jak to możliwe i zignorować możliwość powrotu 0.
Emil Jeřábek

3

Zasadniczo rand()twórz liczby pomiędzy 0i RAND_MAX, 2 RAND_MAX > INT_MAXw twoim przypadku.

Możesz moduł z maksymalną wartością typu danych, aby zapobiec przepełnieniu. To oczywiście zakłóci rozkład liczb losowych, ale randjest tylko sposobem na uzyskanie szybkich liczb losowych.

#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;
}

2

Być może możesz spróbować raczej podchwytliwego podejścia, upewniając się, że wartość zwrócona przez sumę 2 rand () nigdy nie przekracza wartości RAND_MAX. Możliwym podejściem może być sum = rand () / 2 + rand () / 2; Zapewniłoby to, że dla 16-bitowego kompilatora o wartości RAND_MAX 32767, nawet jeśli oba randy zwrócą 32767, nawet wtedy (32767/2 = 16383) 16383 + 16383 = 32766, a zatem nie spowodowałoby sumy ujemnej.


1
PO chciał wykluczyć 0 z wyników. Dodanie nie zapewnia również jednolitego rozkładu wartości losowych.
zbyt uczciwy jak na tę stronę

@Olaf: Nie ma gwarancji, że dwa kolejne wywołania, aby rand()oba nie przyniosły zera, więc chęć uniknięcia zera nie jest dobrym powodem do dodania dwóch wartości. Z drugiej strony, chęć posiadania nierównomiernego rozkładu byłaby dobrym powodem do dodania dwóch losowych wartości, jeśli jedna zapewnia, że ​​nie nastąpi przepełnienie.
supercat

1

powodem, dla którego dodawałem, było uniknięcie „0” jako liczby losowej w moim kodzie. rand () + rand () to szybkie, brudne rozwiązanie, które z łatwością przyszło mi do głowy.

Proste rozwiązanie (ok, nazwij to „hack”), które nigdy nie daje wyniku zerowego i nigdy się nie przepełni:

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

Ograniczy to twoją maksymalną wartość, ale jeśli nie przejmujesz się tym, to powinno działać dobrze.


1
Sidenote: Ostrożnie z odpowiednimi przesunięciami podpisanych zmiennych. Jest dobrze zdefiniowany tylko dla wartości nieujemnych, dla negatywnych, jest implementowany. (Na szczęście rand()zawsze zwraca wartość nieujemną). Jednak pozostawiłbym optymalizację kompilatorowi tutaj.
zbyt uczciwy dla tej strony,

@Olaf: Ogólnie podpisany podział na dwa będzie mniej skuteczny niż zmiana. O ile pisarz kompilatora nie zainwestuje wysiłku w poinformowanie kompilatora, że randbędzie nieujemny, przesunięcie będzie bardziej wydajne niż dzielenie przez podpisaną liczbę całkowitą 2. Dzielenie według 2umoże działać, ale jeśli xjest, intmoże to powodować ostrzeżenia o niejawnej konwersji z niepodpisanego do podpisania.
supercat

@ supercat: Proszę ponownie przeczytać mój komentarz car3efully. Powinieneś bardzo dobrze wiedzieć, że każdy rozsądny kompilator i tak użyje zmiany / 2(widziałem to nawet w przypadku czegoś takiego -O0, tj. Bez wyraźnej prośby o optymalizację). Jest to prawdopodobnie najbardziej trywialna i najbardziej ugruntowana optymalizacja kodu C. Punkt to podział, który jest dobrze zdefiniowany przez standard dla całego zakresu liczb całkowitych, nie tylko wartości nieujemnych. Znowu: pozostaw optymalizacje kompilatorowi, w pierwszej kolejności napisz poprawny i czytelny kod. Jest to jeszcze ważniejsze dla początkujących.
zbyt uczciwy dla tej strony

@Olaf: Każdy kompilator, który przetestowałem, generuje bardziej wydajny kod, gdy przesuwasz się rand()o jeden lub dzieląc przez, 2uniż gdy dzielisz przez 2, nawet podczas używania -O3. Można zasadnie powiedzieć, że taka optymalizacja raczej nie ma znaczenia, ale powiedzenie „pozostaw takie optymalizacje kompilatorowi” oznaczałoby, że kompilatory prawdopodobnie by je wykonały. Czy znasz jakieś kompilatory, które faktycznie będą?
supercat

@ superupat: Powinieneś wtedy użyć bardziej nowoczesnych kompilatorów. gcc właśnie wygenerował dobry kod, kiedy ostatnio sprawdzałem wygenerowany asembler. Niemniej jednak, jak bardzo lubię mieć groopie, wolałbym nie być nękany w stopniu, w jakim prezentujesz się ostatnim razem. Te posty mają lat, moje komentarze są całkowicie poprawne. Dziękuję Ci.
zbyt szczery dla tej strony

1

Aby uniknąć 0, spróbuj tego:

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

Musisz dołączyć limits.h.


4
To podwoi prawdopodobieństwo uzyskania 1. Jest to w zasadzie to samo (ale możliwe, że wolniej) jak warunkowo dodanie 1, jeśli rand()daje 0.
zbyt uczciwe jak na tę stronę

Tak, masz rację Olaf. Jeśli rand () = 0 lub INT_MAX -1 rnumb będzie 1.
Doni

Co gorsza, kiedy o tym myślę. W rzeczywistości podwoi on predyspozycje 1i 2(wszystkie zakładane RAND_MAX == INT_MAX). Zapomniałem o - 1.
zbyt uczciwy jak na tę stronę

1
-1Służy tu żadnej wartości. rand()%INT_MAX+1; nadal generowałby wartości tylko w zakresie [1 ... INT_MAX].
chux - Przywróć Monikę

-2

Podczas gdy to, co wszyscy inni powiedzieli o prawdopodobnym przepełnieniu, może równie dobrze być przyczyną negatywności, nawet jeśli używasz liczb całkowitych bez znaku. Prawdziwym problemem jest fakt, że funkcja nasion / daty jest używana jako ziarno. Jeśli naprawdę zapoznałeś się z tą funkcjonalnością, będziesz wiedział dokładnie, dlaczego to mówię. To, co tak naprawdę robi, to podanie odległości (czasu, który upłynął) od określonej daty / godziny. Chociaż użycie funkcji daty / godziny jako materiału źródłowego dla rand () jest bardzo powszechną praktyką, tak naprawdę nie jest najlepszą opcją. Powinieneś szukać lepszych alternatyw, ponieważ istnieje wiele teorii na ten temat, a ja nie mógłbym przejść do wszystkich z nich. Dodajesz do tego równania możliwość przepełnienia, a to podejście było skazane od samego początku.

Ci, którzy opublikowali rand () + 1, korzystają z rozwiązania, z którego najczęściej korzystają, aby zagwarantować, że nie otrzymają liczby ujemnej. Ale to podejście nie jest tak naprawdę najlepsze.

Najlepszą rzeczą, jaką możesz zrobić, to poświęcić dodatkowy czas na napisanie i użycie odpowiedniej obsługi wyjątków i dodawanie tylko do liczby rand (), jeśli i / lub kiedy skończy się wynikiem zero. I, aby poprawnie radzić sobie z liczbami ujemnymi. Funkcja rand () nie jest doskonała, dlatego należy jej używać w połączeniu z obsługą wyjątków, aby uzyskać pożądany wynik.

Poświęcenie dodatkowego czasu i wysiłku na zbadanie, przestudiowanie i prawidłowe wdrożenie funkcji rand () jest warte czasu i wysiłku. Tylko moje dwa centy. Powodzenia w twoich staraniach ...


2
rand()nie określa, jakiego materiału źródłowego należy użyć. Średnia robi określić go użyć generatora pseudolosowego, a nie stosunku do każdej chwili. Nie mówi też o jakości generatora. Rzeczywistym problemem jest oczywiście przepełnienie. Uwaga, której rand()+1używa się, aby uniknąć 0; rand()nie zwraca wartości ujemnej. Przepraszam, ale nie trafiłeś tutaj w sedno. Nie chodzi o jakość PRNG. ...
zbyt uczciwy jak na tę stronę

... Dobra praktyka pod GNU / Linuksem polega na tym, aby /dev/randomnajpierw wysyłać i używać dobrego PRNG (nie jestem pewien co do jakości rand()z glibc) lub kontynuować korzystanie z urządzenia - ryzykując zablokowanie aplikacji, jeśli nie jest dostępna wystarczająca entropia. Próba uzyskania entropii w aplikacji może być bardzo podatna na ataki, ponieważ prawdopodobnie łatwiej ją zaatakować. A teraz chodzi o hartowanie - nie tutaj
zbyt uczciwe jak na tę stronę
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.