Чому rand () + rand () видає від'ємні числа?


304

Я помітив, що rand()функція бібліотеки, коли вона викликається лише один раз у циклі, майже завжди видає додатні числа.

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

Але коли я додаю два rand()дзвінки, у генерованих номерів зараз більше негативних цифр.

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

Чи може хтось пояснити, чому я бачу негативні цифри у другому випадку?

PS: Я ініціалізую насіння перед циклом як srand(time(NULL)).


11
rand()не може бути негативним ...
twentylemon

293
rand () + rand () може відхилити потік
maskacovnik

13
Що RAND_MAXдля вашого компілятора? Зазвичай його можна знайти в stdlib.h. (Смішно: перевірка man 3 rand, вона містить однорядковий опис "генератор поганих випадкових чисел".)
usr2564301

6
робити те, що зробив би кожен здоровий програміст abs(rand()+rand()). Я вважаю за краще мати позитивний UB, ніж негативний! ;)
Вініцій Камакура

11
@hexa: це не зухвало для UB, як це відбувається для доповнення вже. Ви не можете змусити UB стати визначеною поведінкою . Розсудлива progrtammer б уникнути UB , як пекло.
занадто чесний для цього сайту

Відповіді:


542

rand()визначається для повернення цілого числа між 0та RAND_MAX.

rand() + rand()

може переповнитися. Те, що ви спостерігаєте, ймовірно, є наслідком невизначеної поведінки, викликаної цілим переповненням.


4
@JakubArnold: Як поведінка переповнення визначається кожною мовою по-різному? Наприклад, Python не має жодної програми (ну, до доступної пам'яті), оскільки int просто зростає.
занадто чесний для цього сайту

2
@Olaf Це залежить від того, як мова вирішує представляти підписані цілі числа. У Java не було механізму виявлення цілочисельного переповнення (до java 8) і визначив його, щоб обернутись, і Go використовує лише представлення доповнення 2 та визначає його законним для підписаних цілих переливів. C, очевидно, підтримує більш ніж 2 доповнення.
ПП

2
@EvanCarslake Ні, це не універсальна поведінка. Що ви говорите, це стосується представлення додатків 2. Але мова С допускає і інші уявлення. Специфікація мови C говорить, що підписане ціле число переповнення не визначено . Таким чином, загалом жодна програма не повинна покладатися на таку поведінку, і її потрібно ретельно кодувати, щоб не викликати переписане ціле число. Але це не застосовується для непідписаних цілих чисел, оскільки вони будуть "обертатися" чітко визначеним (модулем зменшення 2) способом. [продовження] ...
ПП

12
Це цитата зі стандарту C, пов’язана із переливанням підписаного цілого числа: Якщо під час оцінки виразу виникає виняткова умова (тобто якщо результат не визначений математично чи не знаходиться в діапазоні репрезентативних значень для його типу), поведінка не визначено.
ПП

3
@EvanCarslake трохи відійшов від питання, компілятори C використовують стандарт і для підписаних цілих чисел вони можуть вважати, що a + b > aякщо вони це знають b > 0. Вони також можуть вважати, що якщо є пізніший виконаний оператор, a + 5то поточне значення нижче INT_MAX - 5. Так що навіть на процесорі / перекладачі доповнення 2 програми 2 програма може не вести себе так, як ніби intдоповнення 2 було без пасток.
Maciej Piechotka

90

Проблема - додавання. rand()повертає intзначення 0...RAND_MAX. Отже, якщо ви додасте два з них, ви отримаєте рішення RAND_MAX * 2. Якщо це перевищує INT_MAX, результат додавання переповнює допустимий діапазон, який intможе містити. Переповнення підписаних значень є невизначеною поведінкою і може призвести до того, що ваша клавіатура розмовлятиме з вами іноземними мовами.

Оскільки тут немає додавання двох випадкових результатів, проста ідея - просто не робити цього. Можна також подати кожен результат до unsigned intдодавання, якщо це може призвести до суми. Або використовувати більший тип. Зауважте, що longце не обов'язково ширше, ніж intте саме стосується, long longякщо intмає принаймні 64 біт!

Висновок: Просто уникайте додавання. Це не забезпечує більшої "випадковості". Якщо вам потрібно більше бітів, ви можете об'єднати значення sum = a + b * (RAND_MAX + 1), але це також, ймовірно, вимагає більшого типу даних, ніж int.

Оскільки вказана причина полягає у тому, щоб уникнути нульового результату: цього не уникнути, додавши результати двох rand()дзвінків, оскільки обидва можуть бути нульовими. Натомість можна просто збільшувати. Якщо RAND_MAX == INT_MAXцього зробити не можна int. Однак (unsigned int)rand() + 1зробить дуже, дуже ймовірно. Ймовірно (не остаточно), тому що це вимагає UINT_MAX > INT_MAX, що справедливо для всіх реалізацій, про які я знаю (що охоплює досить багато вбудованих архітектур, DSP та всіх настільних, мобільних та серверних платформ за останні 30 років).

Увага:

Хоча вже посипають в коментарях тут, будь ласка , зверніть увагу , що при додаванні двох випадкових величин ніяк НЕ отримати рівномірний розподіл, а трикутний розподіл , як прокатки дві кістки: отримати 12(дві кістки) обидві кістки повинні показати 6. бо 11вже є два можливі варіанти: 6 + 5або 5 + 6і т.д.

Отже, додаток також поганий з цього аспекту.

Також зауважте, що результати rand()генерації не залежать один від одного, оскільки вони генеруються генератором псевдовипадкових чисел . Зауважимо також, що стандарт не визначає якість або рівномірний розподіл обчислених значень.


14
@badmad: Що робити, якщо обидва дзвінки повертають 0?
занадто чесний для цього сайту

3
@badmad: Мені просто цікаво, чи UINT_MAX > INT_MAX != falseгарантується цей стандарт. (Звучить вірогідно, але не впевнений, якщо потрібно). Якщо це так, ви можете просто навести один результат та приріст (у такому порядку!).
занадто чесний для цього сайту

3
Існує прибуток у додаванні декількох випадкових чисел, коли потрібно нерівномірний розподіл: stackoverflow.com/questions/30492259/…
Cœur

6
щоб уникнути 0, просте "в той час, як результат дорівнює 0, переверніть"?
Олів’є Дулак

2
Не тільки додавання їм поганого способу уникнути 0, але й призводить до нерівномірного розподілу. Ви отримаєте такий розподіл, як результати кочення кісток: 7 разів у 6 разів більше, ніж 2 або 12.
Бармар

36

Це відповідь на уточнення питання, зроблене в коментарі до цієї відповіді ,

Причиною, яку я додав, було уникати "0" як випадкового числа в моєму коді. rand () + rand () - це швидке брудне рішення, яке легко прийшло мені в голову.

Проблема полягала у тому, щоб уникнути 0. З запропонованим рішенням є (принаймні) дві проблеми. Один є, як свідчать інші відповіді, що rand()+rand()може викликати невизначене поведінку. Найкраща порада - ніколи не посилатися на невизначену поведінку. Інша проблема полягає в тому, що немає гарантії, що rand()не дасть 0 разів двічі поспіль.

Наступне відхиляє нуль, уникає невизначеної поведінки, і в переважній більшості випадків буде швидше, ніж два дзвінки на rand():

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

9
Про що rand() + 1?
запитувач

3
@askvictor Це може переповнитись (хоча це малоймовірно).
Герріт

3
@gerrit - залежить від MAX_INT та RAND_MAX
askvictor

3
@gerrit, я був би здивований, якщо вони не однакові, але я вважаю, що це місце для педантів :)
askvictor

10
Якщо RAND_MAX == MAX_INT, rand () + 1 переллється з точно такою ж ймовірністю, що і значення rand (), що дорівнює 0, що робить це рішення абсолютно безглуздим. Якщо ви готові ризикувати і ігнорувати можливість переповнення, ви можете також використовувати rand (), як є, і ігнорувати можливість його повернення 0.
Emil Jeřábek

3

В основному rand()виробляє число між 0і RAND_MAX, і 2 RAND_MAX > INT_MAXв вашому випадку.

Ви можете модулювати з максимальним значенням вашого типу даних, щоб запобігти переповненню. Цей звичайно порушить розподіл випадкових чисел, але randце лише спосіб отримати швидкі випадкові числа.

#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

Можливо, ви можете спробувати досить складний підхід, гарантуючи, що значення, повернене сумою 2 rand (), ніколи не перевищує значення RAND_MAX. Можливий підхід може бути sum = rand () / 2 + rand () / 2; Це забезпечило б, що для 16-бітового компілятора зі значенням RAND_MAX 32767, навіть якщо обидва rand повертаються 32767, навіть тоді (32767/2 = 16383) 16383 + 16383 = 32766, таким чином, не призведе до негативної суми.


1
ОП хотіла виключити 0 з результатів. Додавання також не забезпечує рівномірного розподілу випадкових величин.
занадто чесний для цього сайту

@Olaf: Немає гарантії, що два поспіль виклики rand()не принесуть обом нуль, тому бажання уникнути нуля не є вагомою причиною для додавання двох значень. З іншого боку, бажання мати неоднорідний розподіл було б вагомою причиною додати два випадкових значення, якщо одне гарантує, що переповнення не відбудеться.
supercat

1

Причиною, яку я додав, було уникати "0" як випадкового числа в моєму коді. rand () + rand () - це швидке брудне рішення, яке легко прийшло мені в голову.

Просте рішення (гаразд, називайте це "Злом"), яке ніколи не дає нульового результату і ніколи не переповнює:

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

Це обмежить ваше максимальне значення, але якщо ви не переймаєтесь цим, то це повинно працювати для вас.


1
Sidenote: обережно з правильним зрушенням підписаних змінних. Він лише добре визначений для негативних значень, для негативів, це визначено реалізацією. (На щастя, rand()завжди повертається негативне значення). Однак я б залишив тут оптимізацію компілятору.
занадто чесний для цього сайту

@Olaf: Загалом, підписаний поділ на два буде менш ефективним, ніж зміна. Якщо автор-компілятор не вкладе зусиль у те, щоб сказати компілятору, що randбуде негативним, зсув буде ефективнішим, ніж ділення за підписаним цілим числом 2. Розділення 2uможе працювати, але якщо xце intможе призвести до попереджень про неявну конвертацію з непідписаного підписати.
supercat

@supercat: Будь ласка, прочитайте мій коментар car3efully ще раз. Вам слід дуже добре знати, що будь-який розумний компілятор буде використовувати зрушення для / 2будь-якого (я бачив це навіть для чогось подібного -O0, тобто без оптимізацій, явно запитуваних). Це, можливо, найбільш тривіальна і найвідоміша оптимізація коду С. Точка поділу добре визначена стандартом для цілого цілого діапазону, не тільки негативних значень. Знову ж таки: залиште оптимізацію компілятору, в першу чергу напишіть правильний і чіткий код. Це навіть важливіше для новачків.
занадто чесний для цього сайту

@Olaf: Кожен тестований компілятор генерує більш ефективний код при зміщенні rand()вправо на один або при діленні на, 2uніж при діленні на 2, навіть при використанні -O3. Можна обґрунтовано сказати, що така оптимізація навряд чи має значення, але сказати "залишити такі оптимізації компілятору" означатиме, що компілятори, ймовірно, будуть їх виконувати. Чи знаєте ви будь-які компілятори, які насправді будуть?
supercat

@supercat: Тоді слід використовувати більш сучасні компілятори. gcc щойно створив тонкий код, коли я останній раз перевіряв створений Асемблер. Тим не менш, наскільки я вдячний за групову групу, я вважаю за краще не зазнавати тяжких труднощів, коли ви представили останній раз. Цим публікаціям років, мої коментарі цілком справедливі. Дякую.
занадто чесний для цього сайту

1

Щоб уникнути 0, спробуйте:

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

Вам потрібно включити limits.h.


4
Це вдвічі збільшить ймовірність отримати 1. Це в основному те саме (але можливо повільніше), як умовно додавання 1, якщо rand()врожай 0.
занадто чесний для цього сайту

Так, ти прав, Олафе. Якщо rand () = 0 або INT_MAX -1, то джгут буде 1.
Доні

Ще гірше, коли я придумую це думати. Це фактично вдвічі збільшить масштабність для ( 1і 2всі RAND_MAX == INT_MAX). Я забув про своє - 1.
занадто чесний для цього сайту

1
-1Тут не служить ніякої цінності. rand()%INT_MAX+1; все ще створюватиме значення лише в діапазоні [1 ... INT_MAX].
chux

-2

Хоча те, що всі інші говорили про ймовірне переповнення, цілком може стати причиною негативу, навіть якщо ви використовуєте непідписані цілі числа. Справжньою проблемою є фактично використання функції часу / дати як основного. Якщо ви справді ознайомилися з цією функціональністю, ви точно будете знати, чому я це кажу. Оскільки це насправді - це дати відстань (минулий час) від заданої дати / часу. Хоча використання функції дати / часу в якості початкового для ранду () є дуже поширеною практикою, це дійсно не найкращий варіант. Вам слід шукати кращі альтернативи, оскільки існує багато теорій на цю тему, і я не міг би зайнятися всіма ними. Ви додаєте в це рівняння можливість переповнення, і такий підхід був приречений з самого початку.

Ті, хто опублікував rand () + 1, використовують рішення, яке більшість використовують, щоб гарантувати, що вони не отримають від’ємне число. Але такий підхід насправді не найкращий спосіб.

Найкраще, що ви можете зробити, це витратити додатковий час для написання та використання правильної обробки винятків, і додати лише номер rand (), якщо та / або коли ви отримаєте нульовий результат. І правильно поводитися з негативними числами. Функціональність rand () не є ідеальною, тому її потрібно використовувати разом з обробкою винятків, щоб забезпечити бажаний результат.

Додатковий час та зусилля, щоб дослідити, вивчити та належним чином реалізувати функціональність rand (), варто витратити час та зусилля. Всього два мої копійки. Успіхів у ваших починаннях ...


2
rand()не вказує, яке насіння використовувати. Стандарт робить вказівку його використовувати псевдовипадковий генератор, а не ставлення до будь-якого часу. Він також не говорить про якість генератора. Актуальною проблемою явно є перелив. Зверніть увагу, що rand()+1використовується для уникнення 0; rand()не повертає негативного значення. Вибачте, але ви пропустили суть тут. Йдеться не про якість PRNG. ...
занадто чесний для цього сайту

... Належна практика GNU / Linux - це виводити з неї /dev/randomта використовувати гарний PRNG згодом (не впевнений у якості rand()від glibc) або продовжувати користуватися пристроєм - ризикуючи заблокувати вашу програму, якщо недостатньо ентропії. Спроба отримати свою ентропію в додатку може бути дуже вразливою, оскільки це можливо простіше атакувати. І тепер справа до загартовування - не тут
занадто чесна для цього сайту
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.