Menu

מספרים אקראיים ב-C: rand, srand ומספר בטווח

איך מייצרים מספרים אקראיים ב-C עם rand() ו-RAND_MAX, למה מאתחלים עם srand(time(NULL)) פעם אחת בדיוק, מיפוי לטווח עם % וההטיה הקטנה שזה יוצר, מספרי double אקראיים ורצפים שחוזרים על עצמם.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

המספרים האקראיים של C מגיעים משתי פונקציות ב-<stdlib.h>: rand(), שמפיקה את הערך הבא, ו-srand(), שקובעת את נקודת ההתחלה. הם לא באמת אקראיים: זה רצף פסאודו־אקראי שמחושב באופן דטרמיניסטי מתוך seed. זו מגבלה בקריפטוגרפיה ויתרון בבדיקות.

rand() ו-RAND_MAX

rand() מחזירה int כלשהו בין 0 ל-RAND_MAX, כולל. RAND_MAX הוא מאקרו שמובטח שיהיה לפחות 32767, וב-Linux וב-macOS הוא 2147483647.

הריצו את זה פעמיים. המספרים זהים בשתי הפעמים, וזה לא באג.

אתחול עם srand

בלי קריאה ל-srand, הרצף מתנהג כאילו קראתם ל-srand(1). אותו seed, אותו רצף, בכל הרצה. כדי לקבל מספרים שונים בכל הרצה, אתחלו עם משהו שמשתנה, ולפי המוסכמה עם השעה הנוכחית:

time(NULL) מ-<time.h> מחזירה את מספר השניות מתחילת 1970, כך שכל הרצה מקבלת seed שונה. ה-cast ל-unsigned int משתיק אזהרה על צמצום של time_t.

שלושה כללים לגבי אתחול, ואנשים טועים בכולם:

אתחלו פעם אחת בדיוק, בתחילת main. קריאה ל-srand לפני כל rand() היא ה-anti-pattern הקלאסי: בתוך לולאה שמסתיימת בפחות משנייה, time(NULL) מחזירה את אותו ערך בכל איטרציה, כך שאתם מאתחלים שוב עם אותו מספר, ו-rand() מחזירה בכל פעם את אותו ערך ראשון. הפלט הוא עמודה של מספרים "אקראיים" זהים.

אל תאתחלו מחדש כדי "לשפר" את האקראיות. האיכות של המחולל נובעת מהמצב הפנימי שלו שמתקדם, ואיפוס המצב הזה זורק את הרצף.

ל-time(NULL) יש רזולוציה של שנייה אחת. שתי תוכניות שהופעלו באותה שנייה מקבלות את אותו רצף. זה בסדר למשחק, ושגוי לכל דבר שבו חשובה עצמאות.

מספר בטווח

הניב הסטנדרטי משתמש באופרטור השארית:

rand() % n            /* 0 עד n-1 */
rand() % n + min      /* min עד min+n-1 */

כדי לקבל ערכים מ-min עד max כולל, מספר הערכים האפשריים הוא max - min + 1:

ה-+ 1 הוא המקום שבו גרות שגיאות off-by-one. rand() % 6 נותן 0 עד 5, ולכן הטלת קובייה היא rand() % 6 + 1. כתיבת rand() % 7 + 1 כדי "לכלול את 6" נותנת לכם קובייה עם שבע פאות.

הערה כנה על modulo bias

rand() % n אינו אחיד לחלוטין, אלא אם n מחלק את RAND_MAX + 1 בדיוק.

חשבו על זה עם מספרים קטנים. אם RAND_MAX היה 9, כלומר rand() מחזירה 0 עד 9, עשרה ערכים בסיכוי שווה, אז rand() % 3 ממפה את 0,3,6,9 ל-0, את 1,4,7 ל-1, ואת 2,5,8 ל-2. התוצאה 0 מתקבלת בארבע דרכים מתוך עשר, והתוצאות 1 ו-2 בשלוש דרכים כל אחת. לאפס יש סיכוי גבוה ב-33%.

אותה הטיה קיימת גם עם ה-RAND_MAX האמיתי, רק קטנה בהרבה: הערכים העודפים הם (RAND_MAX + 1) % n התוצאות הראשונות, וכל אחת מהן מקבלת הזדמנות נוספת אחת מתוך כ-2.1 מיליארד. להטלת קובייה, לחפיסת קלפים מעורבבת או לסימולציה, זה בלתי מדיד: השתמשו ב-% והמשיכו הלאה.

כשזה כן חשוב, בעבודה סטטיסטית או בכל דבר שקשור לאבטחה, דחו את הערכים העודפים במקום לקפל אותם פנימה:

הלולאה זורקת את הטווח הקטן של ערכים שהיה גורם להטיה, ומגרילה שוב. היא מסתיימת מהר, כי הפרוסה הנדחית היא חלק זעיר מהשלם.

לאקראיות שבאמת רגישה לאבטחה, rand() הוא הכלי הלא נכון בכל רמת זהירות: השתמשו ב-arc4random_buf ב-macOS וב-BSD, ב-getrandom() ב-Linux, או ב-BCryptGenRandom ב-Windows.

מספרי double אקראיים

חלקו ב-RAND_MAX כדי לנחות בטווח [0.0, 1.0], ואז שנו את קנה המידה:

ה-cast ב-(double) rand() הוא הכרחי. בלעדיו, rand() / RAND_MAX הוא חילוק של מספרים שלמים ומחושב ל-0 כמעט תמיד, ול-1 בסיכוי של אחד לשני מיליארד לפגוע במקסימום. זה באג שנראה כמו "כל ה-double האקראיים שלי הם אפס". ראו המרת טיפוסים כדי להבין למה.

רצפים שחוזרים על עצמם

seed קבוע נותן את אותו רצף בדיוק בכל הרצה, וזה בדיוק מה שרוצים בבדיקה, בסשן דיבוג או במשחק עם קודי שלבים שאפשר לשתף:

seed 42 מפיק את אותם חמישה מספרים בכל פעם שמשתמשים בו, בהרצה הזו ובכל הרצה אחרת עם אותה ספרייה. בזכות היכולת הזו לשחזר, סימולציה צריכה לאפשר לבחור את ה-seed: הריצו עם השעון בדרך כלל, והעבירו seed קבוע כשמשחזרים באג.

הסתייגות אחת: הרצף של seed מסוים אינו נייד. ספריות C שונות משתמשות במחוללים שונים, כך ש-seed 42 ב-glibc ו-seed 42 ב-Windows נותנים מספרים שונים. ניתן לשחזור על מכונה אחת, לא בין מכונות.

משחק קוביות

הכול ביחד: אתחול פעם אחת, פונקציית עזר לטווח, ומערך שסופר את התוצאות:

ההיסטוגרמה צריכה להגיע לשיא ב-7 ולרדת לכיוון 2 ו-12: יש שש דרכים לקבל 7 ודרך אחת בלבד לקבל 2 או 12. מחולל שהיה מפיק כאן התפלגות שטוחה היה שבור.

שני עמודים קשורים: עמוד הספרייה הסטנדרטית ממפה את שאר <stdlib.h>, ועמוד פונקציות המתמטיקה עוסק ב-<math.h>, שתצטרכו ברגע שערכים אקראיים ייכנסו לחישובים אמיתיים.

שאלות נפוצות

איך מייצרים מספר אקראי ב-C?

כוללים את <stdlib.h>, מאתחלים פעם אחת בתחילת main עם srand((unsigned) time(NULL)) (שדורש את <time.h>), ואז קוראים ל-rand() עבור כל ערך. rand() מחזירה int בין 0 ל-RAND_MAX, כולל.

איך מקבלים מספר אקראי בין שני ערכים ב-C?

משתמשים ב-rand() % (max - min + 1) + min. להטלת קובייה בין 1 ל-6 זה rand() % 6 + 1. ה-% n ממפה את התוצאה לטווח 0..n-1, והוספת min מזיזה את החלון. רק ודאו שהספירה כוללת את שני הקצוות, וזה בדיוק מה ש-+ 1 עושה.

למה תוכנית ה-C שלי מדפיסה את אותם מספרים אקראיים בכל פעם?

כי מעולם לא קראתם ל-srand. בלי seed, הפונקציה rand() מתנהגת כאילו אותחלה עם 1, ולכן כל הרצה מפיקה בדיוק את אותו רצף. קראו ל-srand((unsigned) time(NULL)) פעם אחת בתחילת התוכנית. פעם אחת, ולא לפני כל קריאה ל-rand(), מה שרק היה מחמיר את המצב.

מה זה modulo bias ביצירת מספרים אקראיים?

rand() % n אחיד לחלוטין רק כש-n מחלק את RAND_MAX + 1 בלי שארית. אחרת, כמה הערכים הראשונים מופיעים פעם אחת נוספת על פני כל הטווח, ולכן הסיכוי שלהם גבוה מעט מאוד. כש-RAND_MAX הוא 2147483647 ו-n קטן, ההטיה רחוקה מאוד ממה שמשחק או סימולציה יבחינו בו, אבל לקריפטוגרפיה או לסטטיסטיקה השתמשו בלולאת דחייה או במחולל ראוי.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל