JavaScript/Tutorials/Zufallszahlen

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Der folgende Artikel basiert auf Beiträgen von Gunnar Bittersmann und Jürgen Berkemeier im SELFHTML Forum. Vielen Dank an die beiden.

Vorbemerkung zur mathematischen Schreibweise

Um Zahlenbereiche zu bezeichnen, verwendet man in der Alltagssprache »von 5 bis 10« oder »zwischen 5 und 10«. Allerdings ist nicht immer eindeutig, was damit gemeint ist und ob die Zahlen am Anfang und Ende noch dazugehören. Daher verwendet dieser Artikel eine gängige mathematische Schreibweise für Zahlenbereiche, sogenannte Intervalle. Damit lassen sich die Sachverhalte rund um die Erzeugung von Zufallszahlen kompakter beschreiben:

  • [0;5] Abgeschlossenes Intervall. Bezeichnet alle Zahlen, die größer oder gleich 0 sind und kleiner oder gleich 5 sind. Eine Zahl x ist Teil des Intervalls, wenn für sie gilt 0 ≤ x ≤ 5. Die Zahl 3.5 ist im Intervall, die Zahl 6 nicht mehr.
  • [0;5[ Rechtsoffenes Intervall. Bezeichnet alle Zahlen, die größer oder gleich 0 sind, aber kleiner als 5 sind. Eine Zahl x ist Teil des Intervalls, wenn für sie gilt 0 ≤ x < 5. Die Zahl 0.5 ist im Intervall, die Zahl 5 nicht mehr.

Dieser Unterschied wird uns noch mehrfach beschäftigen, daher sollten Sie darauf achten, in welche Richtung die letzte Klammer zeigt. Diese bestimmt, ob die Zahl, die die Obergrenze bildet, noch zum Intervall dazugehört oder nicht.


Erste Schritte mit Math.random()

Math.random() liefert gemäß dem ECMAScript-Standard eine Pseudo-Zufallszahl aus dem Intervall [0;1[, das heißt größer gleich 0 und kleiner als 1. Es handelt sich um eine rationale Zahl, zum Beispiel0.06631505941813465.

Nun will man in JavaScript äußerst selten eine Zufallszahl aus dem Intervall [0;1[ , sondern aus [x;y] . x und y stehen für beliebige Werte wie zum Beispiel 5 und 10. Es gilt also, die von Math.random() gelieferten Werte auf eine anderes Intervall abzubilden.

Der erste Versuch könnte so aussehen:

Beispiel
var min = 5;
var max = 10;
var x = (Math.random() * (max - min)) + min;


Dies liefert eine rationale Zahl x aus dem Intervall [5;10[.

Dies erfüllt zwei übliche Wünsche nicht:

  • Man will die maximale Zahl (hier 10) nicht aussparen. Man will eine zufällige Zahl aus dem Intervall [5;10], nicht aus dem Intervall [5;10[.
  • Man will eine natürliche Zahl. Die Zahl soll aus der Zahlenmenge { 5, 6, 7, 8, 9, 10 } stammen.

Ganzzahlige Zufallswerte mit Hilfe von Math.round()

Die übliche Lösung, die diese beiden Anforderungen zunächst erfüllt, sieht so aus:

Beispiel
var x = Math.round(Math.random() * (max - min)) + min;

Math.round() rundet kaufmännisch, das heißt, 5.2 wird 5, 7.2 wird 7 und 9.8 wird 10. Damit bekommen wir eine natürliche Zahl aus dem Intervall [5;10].

Diese Lösung findet sich zwar überall im Web, aber sie ist äußerst problematisch und fehlerhaft.

Will man eine zufällige Zahl aus der Menge { 5, 6, 7, 8, 9, 10 } ziehen, so soll jede Zahl mit gleicher Wahrscheinlichkeit gezogen werden. Der Wahrscheinlichkeitswert einer jeden Zahl aus dieser Menge mit sechs Zahlen wäre 1/6 (ein Sechstel). Das kennen wir von einem sechsseitigen Würfel.

Die obige Lösung mit Math.round() sorgt aber nicht für eine gleichmäßige Wahrscheinlichkeit. Die Zahl 5 wird nicht gleich häufig gezogen wie die Zahl 6.

Der Teilterm Math.random() * (10 - 5) berechnet zunächst eine zufällige rationale Zahl aus dem Intervall [0;5[. Diese Verteilung ist noch gleichmäßig. Das heißt, jede Zahl in diesem Intervall wird mit gleicher Wahrscheinlichkeit gezogen.

Bei der anschließenden Rundung muss nun bedacht werden, welcher Teilintervall zu welchem ganzzahligen Wert führt. Diese Rundung von Zahlen und die anschließende Addition des Minimalwertes soll folgende Tabelle veranschaulichen.

a = Math.random() * (10 - 5) [0; 0.5[ [0.5; 1[ [1; 1.5[ [1.5; 2[ [2; 2.5[ [2.5; 3[ [3; 3.5[ [3.5; 4[ [4; 4.5[ [4.5; 5[
b = Math.round(a) 0 1 2 3 4 5
c = b + 5 5 6 7 8 9 10
Wahrscheinlichkeit 1/10 1/5 1/5 1/5 1/5 1/10

Die Tabelle ist so zu lesen: Der besagte Teilterm liefert eine Zufallszahl a aus dem Intervall [0;5[. Dieser ist in 0.5-er Teilintervallen in Spalten aufgeteilt.

Der springende Punkt ist nun, dass die Rundung die Zahlen aus dem Intervall [0; 0.5[ auf 0 abrundet. Alle Zahlen aus dem Intervall [0.5; 1.5[ werden hingegen auf 1 auf- bzw. abgerundet. Die Menge der Zufallszahlen, die gerundet 1 ergeben, ist also doppelt so groß wie die Menge der Zahlen, die gerundet 0 ergeben. Dasselbe Problem liegt am Ende vor: Lediglich die Zahlen aus dem Intervall [4.5; 5[ ergeben gerundet 5.

Der Effekt dieser Rundung ist eine ungleiche Verteilung der Wahrscheinlichkeiten. Die Zahlen 6, 7, 8 und 9 werden jeweils mit doppelter Wahrscheinlichkeit gezogen als die Zahlen 5 und 10. Die beiden Zahlen am Anfang und am Ende der Zahlenmenge, aus der zufällig gezogen werden soll, sind also benachteiligt in der Ziehung.


Ganzzahlige Zufallswerte mit Hilfe von Math.floor()

Lange Rede, kurzer Sinn: Ein anderes Verfahren ist nötig, damit jede Zahl mit gleicher Wahrscheinlichkeit gezogen wird. Eine Rundung ist zwar weiterhin nötig, aber eine, die immer ein Intervall der Länge 1 auf eine ganze Zahl abbildet.

Diese Aufgabe erfüllt Math.floor(). Math.floor() rundet jede Zahl auf die nächstniedrigere ganze Zahl ab. Die Berechnung der Zufallszahl wird entsprechend geändert:

Beispiel
var min = 5;
var max = 10;
var x = Math.floor(Math.random() * (max - min)) + min;

Dies gibt uns eine zufällige natürliche Zahl aus dem Intervall [5;10[. Dies fällt hinter die Lösung mit Math.round() in dem Punkt zurück, dass die maximale Zahl 10 wieder ausgespart wird. Daher wird der Term folgendermaßen ergänzt:

Beispiel
var x = Math.floor(Math.random() * (max - min + 1)) + min;

Die korrekte Funktionsweise wird deutlich, wenn wir die gleiche Tabelle mit der verbesserten Lösung aufstellen:

a = Math.random() * (10 - 5+1) [0; 1[ [1; 2[ [2; 3[ [3, 4[ [4; 5[ [5; 6[
b = Math.floor(a) 0 1 2 3 4 5
c = b + 5 5 6 7 8 9 10
Wahrscheinlichkeit 1/6 1/6 1/6 1/6 1/6 1/6


Anmerkung: Die Benutzung von Math.round() bei der Erzeugung von zufälligen Ganzzahlen ist kein Übel an sich. Die Lösung mit Math.round() lässt sich abändern, sodass ebenfalls ein Intervall der Länge 1 auf eine ganze Zahl abgebildet wird und die Gleichverteilung gewährleistet ist. Dazu verschiebt man die Zahlen vor der Rundung um 0.5 nach links, also substrahiert 0.5:

Beispiel
var x = Math.round(Math.random() * (max - min + 1) - 0.5) + min;

Dies sei hier nur der Vollständigkeit erwähnt. Um Verwirrung bei verschiedenen Lösungen mit Math.round() zu vermeiden, wird hier die Umsetzung mit Math.floor() favorisiert.


Helferfunktion zur Berechnung ganzzahliger, gleichverteilter Zufallszahlen

Eine allgemeine Helferfunktion, die eine Zufallszahl aus dem Intervall [min;max] zurückgibt, könnte so aussehen:

allgemeine Helferfunktion zur Berechnung ganzzahliger Zufallszahlen ansehen …
  var min = 5,
      max = 10;

 function rand (min, max) {
	return Math.floor(Math.random() * (max - min + 1)) + min;
}

Diese Funktion verhält sich wie rand() in der Programmiersprache PHP. Um eine zufällige ganze Zahl größer gleich 5 und kleiner gleich 10, also aus der Zahlenmenge { 5, 6, 7, 8, 9, 10 } zu ziehen, rufen wir rand(5, 10) auf.


Demonstration

Bei 120 000 Ziehungen sollte jede Zahl aus der Menge { 5, 6, 7, 8, 9, 10 } im statistischen Mittel 20 000 mal gezogen werden, denn der Wahrscheinlichkeitswert liegt bei 1/6, also 0.16̅ (Periode 6, das heißt 0,16666... usw.). Klicken Sie auf »Berechnung starten«, um die beiden Lösungen zu vergleichen. Die angezeigten Wahrscheinlichkeiten sind gerundet. Sie können den Test auch mehrfach durchführen, um genauere Resultate zu bekommen. Sie werden das Ungleichgewicht bei den Zahlen 5 und 10 auf der Seite der Lösung mit Math.round() feststellen.

Beispiel ansehen …
var test = {
	results : {
		mathfloor : [],
		mathround : []
	},
	min : 5,
	max : 10,
	iterations : 120000,
	drawings : 0,
	randFloor : function () {
		return Math.floor(Math.random() * (test.max - test.min + 1)) + test.min;
	},
	randRound : function () {
		return Math.round(Math.random() * (test.max - test.min)) + test.min;
	},
	start : function (button) {
		for (var i = 0; i < test.iterations; i++) {
			test.save("mathfloor", test.randFloor());
			test.save("mathround", test.randRound());
		}
		test.drawings += test.iterations;
		test.report();
	},
	save : function (type, n) {
		var resultArray = test.results[type];
		if (typeof resultArray[n] == "number") {
			resultArray[n]++;
		} else {
			resultArray[n] = 1;
		}
	},
	report : function () {
		var tbody = document.getElementById("ausgabe");
		var child = tbody.firstChild;
		for (var i = tbody.childNodes.length - 1, child; i >= 0; i--) {
			tbody.removeChild(tbody.childNodes[i]);
		}
		var html = "";
		for (var i = test.min; i <= test.max; i++) {
			var nRound = test.results.mathround[i];
			var pRound = (nRound / test.drawings).toString().substr(0, 6);
			var nFloor = test.results.mathfloor[i];
			var pFloor = (nFloor / test.drawings).toString().substr(0, 6);
			var row = tbody.insertRow(-1);
			(row.insertCell(-1)).appendChild(document.createTextNode(i));
			(row.insertCell(-1)).appendChild(document.createTextNode(nRound));
			(row.insertCell(-1)).appendChild(document.createTextNode(pRound));
			(row.insertCell(-1)).appendChild(document.createTextNode(nFloor));
			(row.insertCell(-1)).appendChild(document.createTextNode(pFloor));
		}
	}
  };