تأتي الأعداد العشوائية في C من دالّتين في <stdlib.h>: rand() التي تُنتج القيمة التالية، وsrand() التي تضبط نقطة البداية. وهي ليست عشوائية حقًا - بل متتالية شبه عشوائية، تُحسَب حتميًا من بذرة - وهذا قيد للتعمية وميزة للاختبار.
rand() وRAND_MAX
تُرجِع rand() قيمة int في مكان ما بين 0 وRAND_MAX متضمّنَين. وRAND_MAX ماكرو مضمون أنه 32767 على الأقل؛ وعلى لينكس وmacOS يساوي 2147483647.
شغّل ذلك مرتين. الأعداد متطابقة في المرتين - وهذا ليس خطأً.
البذر بـ srand
بلا استدعاء لـ srand، تتصرّف المتتالية كما لو كنت استدعيت srand(1). البذرة نفسها، والمتتالية نفسها، في كل تشغيل. وللحصول على أعداد مختلفة لكل تشغيل، ابذر بشيء يتغيّر - وعرفًا هو الوقت الحالي:
تُرجِع time(NULL) من <time.h> الثواني منذ بداية 1970، فيحصل كل تشغيل على بذرة مختلفة. والتحويل إلى unsigned int يُسكِت تحذيرًا عن تضييق time_t.
وثلاث قواعد عن البذر، يخطئ الناس فيها كلها:
ابذر مرة واحدة بالضبط، في بداية main. فاستدعاء srand قبل كل rand() هو النمط المضادّ الكلاسيكي - إذ تُرجِع time(NULL) داخل حلقة تنتهي في أقلّ من ثانية القيمةَ نفسها في كل تكرار، فتعيد البذر بالرقم نفسه وتُرجِع rand() القيمة الأولى نفسها في كل مرة. فيكون الخرج عمودًا من أعداد "عشوائية" متطابقة.
لا تعد البذر لـ "تحسين" العشوائية. فجودة المولّد تأتي من تقدّم حالته الداخلية؛ وإعادة ضبط تلك الحالة ترمي المتتالية.
دقّة time(NULL) ثانية واحدة. فبرنامجان أُطلقا في الثانية نفسها يحصلان على المتتالية نفسها. وهذا مقبول للعبة وخاطئ لأي شيء يهمّ فيه الاستقلال.
عدد ضمن مجال
يستخدم الاصطلاح المعياري عامل باقي القسمة:
rand() % n /* 0 to n-1 */
rand() % n + min /* min to min+n-1 */
وللحصول على min حتى max متضمّنَين، يكون عدد القيم الممكنة max - min + 1:
و+ 1 هي حيث تسكن أخطاء الانزياح بمقدار واحد. فـ rand() % 6 تعطي 0 حتى 5، فرمية النرد هي rand() % 6 + 1. وكتابة rand() % 7 + 1 لـ "تضمين 6" تعطيك نردًا بسبعة أوجه.
الملاحظة الصريحة عن انحياز باقي القسمة
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() على لينكس، وBCryptGenRandom على ويندوز.
الأعداد العشرية العشوائية
اقسم على RAND_MAX لتقع في [0.0, 1.0]، ثم قِس:
والتحويل في (double) rand() جوهري. فبدونه تكون rand() / RAND_MAX قسمةً صحيحة وتساوي 0 في كل الأحوال تقريبًا، و1 في فرصة واحدة من مليارين عند بلوغ القيمة العظمى - وهو خطأ يبدو كـ "أعدادي العشرية العشوائية كلها أصفار". انظر تحويل الأنواع للسبب.
المتتاليات القابلة للتكرار
البذرة الثابتة تعطي المتتالية نفسها في كل تشغيل، وهذا بالضبط ما تريده لاختبار أو جلسة تنقيح أو لعبة برموز مراحل قابلة للمشاركة:
تُنتج البذرة 42 الأعدادَ الخمسة نفسها في كل مرة تُستخدم فيها، في هذا التشغيل وفي أي تشغيل آخر على المكتبة نفسها. وتلك القابلية للتكرار هي سبب وجوب السماح باختيار البذرة في محاكاة: شغّل بالساعة عادةً، ومرّر بذرة ثابتة عند إعادة إنتاج خطأ.
وتحفّظ واحد: المتتالية لبذرة معيّنة ليست قابلة للنقل. فمكتبات C المختلفة تستخدم مولّدات مختلفة، فالبذرة 42 على glibc والبذرة 42 على ويندوز تعطيان أعدادًا مختلفة. قابلة للتكرار على جهاز واحد، لا عبر الأجهزة.
لعبة نرد
كل شيء معًا - البذر مرة واحدة، ودالة مساعِدة للمجال، ومصفوفة تحصي النتائج:
ينبغي أن يبلغ المدرّج ذروته عند 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 قط. فبدون بذرة تتصرّف rand() كما لو بُذرت بـ 1، فينتج كل تشغيل المتتالية نفسها. استدعِ srand((unsigned) time(NULL)) مرة واحدة عند بدء البرنامج - مرة واحدة، لا قبل كل استدعاء لـ rand()، فذلك سيزيد الأمر سوءًا.
ما انحياز باقي القسمة في توليد الأعداد العشوائية؟
rand() % n منتظمة تمامًا فقط حين تقسم n القيمةَ RAND_MAX + 1 قسمة تامة. وإلا فإن القيم القليلة الأولى تظهر مرة إضافية عبر المدى كاملًا، فتصبح أرجح قليلًا جدًا. ومع RAND_MAX عند 2147483647 وn صغيرة يكون الميل أدنى بكثير من أي شيء تلاحظه لعبة أو محاكاة، لكن للتعمية أو الإحصاء استخدم حلقة رفض أو مولّدًا مناسبًا.