C/C++ Programmierung: Zufallszahlen

„Ohne Zufallszahlen geht in einem Computerspiel gar nichts!“

Mit der rand()-Funktion erfügt der C/C++-Programmierer über eine komfortable Möglichkeit für die Erzeugung von Zufallszahlen. Genaugenommen handelt es sich um so genannte Pseudo-Zufallszahlen, da diese auf mathematischem Wege berechnet werden, also nicht wirklich zufällig sind sondern nur so erscheinen. Die Illusion von „ein wenig mehr Zufall“ lässt sich erzeugen, wenn man den Zufallsgenerator vor dem ersten Aufruf der rand()-Funktion neu initialisiert. Hierfür dient die srand()-Funktion, der sie als Parameter beispielsweise die aktuelle Systemzeit übergeben können:

srand(GetTickCount());


Hinweis:
Durch den srand()-Funktionsaufruf wird der Startwert verändert, den die rand()-Funktion für die Berechnung der ersten Zufallszahl verwendet. Daraus resultiert eine veränderte Zufallszahl. Da der Wert einer Zufallszahl stets in die Berechnung der nachfolgenden Zufallszahl mit einfließt, ändern sich die Werte aller weiteren Zufallszahlen ebenfalls. Um die rand()- bzw. srand()-Funktion nutzen zu können, müssen sie zunächst die Header-Datei stdlib.h in ihr Programm einbinden.

Wie lassen sich nun Zufallszahlen in einem genau definierten Wertebereich erzeugen (z.B. von 2 bis 10 oder von -1.0f bis 1.0f)?
Ganz einfach, indem sie die beiden nachfolgenden Funktionen verwenden – frnd() für die Generierung von Fließkommazahlen und lrnd() für die Erzeugung von Ganzzahlen. Beiden Funktionen müssen sie als Parameter sowohl die kleinste als auch die größte zu erzeugende Zufallszahl übergeben.

Hinweis:
Da ganzzahlige Zufallszahlen häufig dafür genutzt werden, um auf ein zufälliges Arrayelement zuzugreifen, müssen wir sicherstellen, dass man bei einem Aufruf von lrnd(0, NumArrayElements) nicht zufällig als Ergebnis den Wert NumArrayElements erhält. Der Versuch, mit diesem Wert auf ein Arrayelement zuzugreifen, führt im schlimmsten Fall zu einem Programmabsturz, da das Array bereits mit Element Array[NumArrayElements-1] endet. Aus diesem Grund liefert die lrnd()-Funktion anstelle der größtmöglichen Zufallszahl einfach die kleinstmögliche Zufallszahl als Ergebnis zurück.


float INV_RAND_MAX = 1.0f/RAND_MAX;

inline float frnd(float low, float high)
{
    return low + ( high - low ) * ( (float)rand() ) * INV_RAND_MAX;
}

inline long lrnd(long low, long high)
{
    long tempLrnd = low + ( high - low ) * ( (long)rand() ) / RAND_MAX;

    if(tempLrnd < high)
        return tempLrnd;
    else
        return low;
}

Aus Performancegründen bietet sich mitunter auch die Erzeugung von Zufallszahlen mithilfe einer Look-Up-Tabelle an. Dabei ersetzt die Look-Up-Tabelle die Aufrufe der rand()-Funktion durch im Voraus berechnete Werte. Gespeichert werden diese im nachfolgenden RandomNumberList-Array:

#define NumRandomNumberListSize 100000
float RandomNumberList[NumRandomNumberListSize];

// für den Zugriff auf ein RandomNumberList-Arrayelement:
long RandomNumberListCounter = 0;

srand(0);

for(long i = 0; i < NumRandomNumberListSize; i++)
    RandomNumberList[i] = (float)rand();

Hinweis:
Alternativ lassen sich auch die im Voraus berechneten Werte aus einer Datei laden.

Hier nun die beiden modifizierten Funktionen für die Berechnung der Zufallszahlen:

inline float frnd_LookUp(float low, float high)
{
    if(RandomNumberListCounter == NumRandomNumberListSize)
        RandomNumberListCounter = 0;

    return low + (high - low)*
           RandomNumberList[RandomNumberListCounter++]*INV_RAND_MAX;
}

inline long lrnd_LookUp(long low, long high)
{
    if(RandomNumberListCounter == NumRandomNumberListSize)
        RandomNumberListCounter = 0;

    long tempVal = low + (high - low)*
                  ((long)RandomNumberList[RandomNumberListCounter++])/
                  RAND_MAX;

    if(tempVal < high)
        return tempVal;
    else
        return low;
}

Hinweis:
Müssen Zufallszahlen in mehreren Threads parallel erzeugt werden, dann sollten Sie für jeden Thread separate frnd_LookUp() und lrnd_LookUp()-Funktionen verwenden (bsp. lrnd_LookUp_Thread1(), lrnd_LookUp_Thread2() usw.). Innerhalb dieser Funktionen erfolgt der Zugriff auf das RandomNumberList-Array dann mittels thread-spezifischer Zugriffsvariablen (bsp. RandomNumberListCounter_Thread1, RandomNumberListCounter_Thread2 usw.).

Soll die Erzeugung der Zufallszahlen wieder von vorn beginnen – also mit dem ersten RandomNumberList-Arrayeintrag – dann bietet sich die Verwendung der Reset_RandomNumbers_LookUp()-Funktion an. Zusätzlich dazu kann man auch mithilfe der Reinitialize_RandomNumbers_LookUp()-Funktion einen benutzerdefinierten Startwert vorgeben – gewissermaßen als Alternative zur srand()-Funktion.

inline void Reset_RandomNumbers_LookUp(void)
{
    RandomNumberListCounter = 0;
}


inline void Reinitialize_RandomNumbers_LookUp(long value)
{
    if(value >= NumRandomNumberListSize)
        value = 0;
    if(value < 0)
        value = 0;

    RandomNumberListCounter = value;
}