Menu

Liczby losowe w C: rand, srand i losowanie liczby z zakresu

Jak generować liczby losowe w C za pomocą rand() i RAND_MAX, dlaczego ziarno ustawia się przez srand(time(NULL)) dokładnie raz, jak zawęzić wynik do zakresu przez % i jakie drobne obciążenie to wprowadza, losowe liczby double i powtarzalne sekwencje.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Liczby losowe w C pochodzą z dwóch funkcji w <stdlib.h>: rand(), która zwraca kolejną wartość, i srand(), która ustawia punkt startowy. Nie są naprawdę losowe: to sekwencja pseudolosowa, wyliczana deterministycznie z ziarna. W kryptografii to ograniczenie, a w testowaniu zaleta.

rand() i RAND_MAX

rand() zwraca int gdzieś pomiędzy 0 a RAND_MAX włącznie. RAND_MAX to makro, które na pewno wynosi co najmniej 32767, a na Linuksie i macOS jest równe 2147483647.

Uruchom to dwa razy. Liczby za każdym razem są identyczne, i to nie jest błąd.

Ustawianie ziarna przez srand

Bez wywołania srand sekwencja zachowuje się tak, jakby wywołano srand(1). To samo ziarno, ta sama sekwencja, przy każdym uruchomieniu. Aby dostać inne liczby przy każdym uruchomieniu, ustaw ziarno na coś, co się zmienia, zwyczajowo na bieżący czas:

time(NULL) z <time.h> zwraca liczbę sekund od początku 1970 roku, więc każde uruchomienie dostaje inne ziarno. Rzutowanie na unsigned int wycisza ostrzeżenie o zawężaniu time_t.

Trzy zasady dotyczące ziarna, które wszyscy łamią:

Ustaw ziarno dokładnie raz, na początku main. Wywoływanie srand przed każdym rand() to klasyczny antywzorzec: w pętli, która kończy się w mniej niż sekundę, time(NULL) zwraca w każdej iteracji tę samą wartość, więc ziarno jest ciągle ustawiane na tę samą liczbę, a rand() za każdym razem zwraca tę samą pierwszą wartość. Wynik to kolumna identycznych „losowych” liczb.

Nie ustawiaj ziarna ponownie, żeby „poprawić” losowość. Jakość generatora bierze się z tego, że jego stan wewnętrzny posuwa się do przodu. Zresetowanie tego stanu wyrzuca sekwencję do kosza.

time(NULL) ma rozdzielczość jednej sekundy. Dwa programy uruchomione w tej samej sekundzie dostaną tę samą sekwencję. W grze to nie problem, ale jest to błąd wszędzie tam, gdzie liczy się niezależność.

Liczba z zakresu

Standardowy idiom korzysta z operatora reszty z dzielenia:

rand() % n            /* od 0 do n-1 */
rand() % n + min      /* od min do min+n-1 */

Aby dostać wartości od min do max włącznie, liczba możliwych wartości wynosi max - min + 1:

To w + 1 mieszkają błędy o jeden. rand() % 6 daje wartości od 0 do 5, więc rzut kostką to rand() % 6 + 1. Napisanie rand() % 7 + 1, żeby „uwzględnić 6”, daje kostkę siedmiościenną.

Uczciwa uwaga o obciążeniu modulo

rand() % n nie jest idealnie równomierne, chyba że n dzieli RAND_MAX + 1 bez reszty.

Pomyśl o tym na małych liczbach. Gdyby RAND_MAX wynosiło 9, czyli rand() zwracało od 0 do 9, dziesięć jednakowo prawdopodobnych wartości, to rand() % 3 zamienia 0, 3, 6, 9 na 0; 1, 4, 7 na 1; a 2, 5, 8 na 2. Wynik 0 pojawia się na cztery sposoby z dziesięciu, a wyniki 1 i 2 na trzy sposoby każdy. Zero jest o 33% bardziej prawdopodobne.

To samo odchylenie istnieje przy prawdziwym RAND_MAX, tylko jest znacznie mniejsze: nadmiarowe wartości to pierwsze (RAND_MAX + 1) % n wyniki, a każdy z nich zyskuje jedną dodatkową szansę na mniej więcej 2,1 miliarda. Przy rzucie kostką, tasowaniu talii czy symulacji jest to niemierzalne, więc używaj % i nie przejmuj się.

Gdy to jednak ma znaczenie, czyli w pracy statystycznej i wszystkim, co dotyczy bezpieczeństwa, odrzucaj nadmiarowe wartości, zamiast je zawijać:

Pętla odrzuca niewielki zakres wartości, który powodowałby odchylenie, i losuje ponownie. Kończy się szybko, bo odrzucany wycinek to znikomy ułamek całości.

Do losowości naprawdę wrażliwej pod względem bezpieczeństwa rand() nie nadaje się niezależnie od staranności: użyj arc4random_buf na macOS i BSD, getrandom() na Linuksie albo BCryptGenRandom na Windows.

Losowe liczby double

Podziel przez RAND_MAX, żeby trafić w przedział [0.0, 1.0], a potem przeskaluj:

Rzutowanie w (double) rand() jest niezbędne. Bez niego rand() / RAND_MAX to dzielenie całkowite, które prawie zawsze daje 0, a 1 tylko przy szansie jeden na dwa miliardy trafienia w maksimum. To błąd, który wygląda jak „wszystkie moje losowe double to zera”. Dlaczego tak jest, wyjaśnia strona o rzutowaniu typów.

Powtarzalne sekwencje

Stałe ziarno daje identyczną sekwencję przy każdym uruchomieniu, a to dokładnie to, czego potrzebujesz w teście, podczas debugowania albo w grze z kodami poziomów do udostępniania:

Ziarno 42 daje te same pięć liczb za każdym razem, w tym uruchomieniu i w każdym innym z tą samą biblioteką. Dzięki tej powtarzalności symulacja powinna pozwalać na wybór ziarna: normalnie uruchamiasz ją z zegarem, a przy odtwarzaniu błędu podajesz stałe ziarno.

Jedno zastrzeżenie: sekwencja dla danego ziarna nie jest przenośna. Różne biblioteki C używają różnych generatorów, więc ziarno 42 w glibc i ziarno 42 na Windows dają różne liczby. Powtarzalne na jednej maszynie, ale nie między maszynami.

Gra w kości

Wszystko razem: jednorazowe ustawienie ziarna, funkcja pomocnicza do zakresu i tablica zliczająca wyniki:

Histogram powinien mieć szczyt przy 7 i opadać w stronę 2 i 12: sumę 7 można uzyskać na sześć sposobów, a 2 lub 12 tylko na jeden. Generator, który dawałby tu płaski rozkład, byłby zepsuty.

Dwie powiązane strony: biblioteka standardowa opisuje resztę <stdlib.h>, a funkcje matematyczne omawiają <math.h>, który przyda ci się, gdy tylko losowe wartości trafią do prawdziwych obliczeń.

Najczęściej zadawane pytania

Jak wylosować liczbę w C?

Dołącz <stdlib.h>, ustaw ziarno raz na początku main przez srand((unsigned) time(NULL)) (potrzebuje <time.h>), a potem wywołuj rand() dla każdej wartości. rand() zwraca int od 0 do RAND_MAX włącznie.

Jak wylosować liczbę z przedziału między dwiema wartościami w C?

Użyj rand() % (max - min + 1) + min. Dla rzutu kostką od 1 do 6 to rand() % 6 + 1. % n sprowadza wynik do zakresu 0..n-1, a dodanie min przesuwa okno. Pilnuj tylko, żeby liczba wartości obejmowała oba końce, i właśnie to robi + 1.

Dlaczego mój program w C za każdym razem losuje te same liczby?

Bo nie wywołujesz srand. Bez ziarna rand() zachowuje się tak, jakby ziarnem było 1, więc każde uruchomienie daje identyczną sekwencję. Wywołaj srand((unsigned) time(NULL)) raz na starcie programu. Raz, a nie przed każdym wywołaniem rand(), bo to tylko pogorszyłoby sprawę.

Czym jest obciążenie modulo przy losowaniu liczb?

rand() % n jest idealnie równomierne tylko wtedy, gdy n dzieli RAND_MAX + 1 bez reszty. W przeciwnym razie kilka pierwszych wartości pojawia się w całym zakresie o jeden raz więcej, więc są minimalnie bardziej prawdopodobne. Przy RAND_MAX równym 2147483647 i małym n to odchylenie jest daleko poniżej tego, co zauważy gra czy symulacja, ale w kryptografii lub statystyce użyj pętli odrzucającej albo porządnego generatora.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ