Tracks
/
Julia
Julia
/
Übungen
/
Kapitänslogbuch
Kapitänslogbuch

Kapitänslogbuch

Lernübung

Einführung

Viele Programme brauchen (scheinbar) zufällige Werte, um Ereignisse aus der realen Welt zu simulieren.

Häufige und bekannte Beispiele sind:

  • Ein Münzwurf: ein zufälliger Wert aus ('H', 'T').
  • Der Wurf eines Würfels: eine zufällige Ganzzahl von 1 bis 6.
  • Das Mischen eines Kartendecks: eine zufällige Reihenfolge einer Kartenliste.

Wirklich zufällige Werte mit einem Computer zu erzeugen, ist eine überraschend schwierige technische Herausforderung. Deshalb werden diese Ergebnisse manchmal als „pseudozufällig“ bezeichnet.

Wichtig: Dieses Konzept behandelt nicht kryptografisch sichere Zufallszahlen, die eine noch viel schwierigere Herausforderung sind.

Gut entworfene Bibliotheken wie das Modul Random in der Julia-Standardbibliothek sind jedoch schnell, flexibel und liefern Ergebnisse, die für die meisten Anwendungen in Modellierung, Simulation und Spielen mehr als gut genug sind.

Julia verteilt die Zufallsfunktionalität auf mehrere Orte:

  • Nur ein paar grundlegende, aber sehr vielseitige Funktionen in Base, die immer verfügbar sind.
  • Eine größere Auswahl an Möglichkeiten im Modul Random.
  • Speziellere Funktionalität in Paketen, die vor der Verwendung installiert werden müssen (und in Exercism nicht verfügbar sind).

Random ist Teil der Standardbibliothek und wahrscheinlich schon vorinstalliert. Du musst aber using Random am Anfang deines Programms einfügen, um die Inhalte in den Namensraum zu holen.

Die Funktion rand()

Was diese Funktion macht, hängt von den Argumenten ab, die du ihr gibst. Es gibt viele Möglichkeiten.

Ohne Argumente erzeugt sie eine Gleitkommazahl zwischen 0 (einschließlich) und 1. Das ist eine uniform-Verteilung, bei der alle Werte gleich wahrscheinlich sind, wie weiter unten im Abschnitt „Arbeiten mit Verteilungen“ beschrieben.

Ein einzelnes Ganzzahlargument erzeugt einen Vektor dieser Länge.

julia> rand()
0.10261774967264703

julia> rand(5)
5-element Vector{Float64}:
 0.24134501977563894
 0.5664193284851202
 0.9804412082089355
 0.6229551330613335
 0.47589221741904664

Für einen anderen Bereich verschiebst und skalierst du das Ergebnis einfach passend.

Das folgende Beispiel nutzt Broadcasting für die Subtraktion, das im Konzept „Vektoroperationen“ behandelt wird. Das .- wendet diese Arithmetik einfach auf jedes Vektorelement an.

# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
 -0.5303906759076336
  0.9635682226775855
 -0.048823697086981754
  0.465842804648374
  0.9880834344780736

Wenn ein Typ das einzige Argument ist, verwendet rand typemin und typemax als Grenzen. Das ist wahrscheinlich nicht das, was du willst!

Für zufällige Ganzzahlen können wir einen Bereich angeben, und optional, wie viele Werte erzeugt werden sollen.

julia> rand(Int64)
-9159538335234594326 # not very useful

julia> rand(1:10, 5)
5-element Vector{Int64}:
 1
 1
 1
 4
 7

Im obigen Beispiel rand(1:10, 5) fällt auf, dass Werte (zufällig) mehrfach vorkommen, weil jede Auswahl unabhängig ist. Das ist „Ziehen mit Zurücklegen“, worauf wir weiter unten genauer eingehen.

Für Gleitkommawerte in einem Bereich musst du normalerweise eine Schrittweite angeben. Sonst ist die Schrittweite standardmäßig 1.0, was selten nützlich ist.

julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
 3.19
 2.53
 3.14
 3.13

Alternativ gibst du ein Array oder ein Tupel an, und rand gibt einen zufälligen Eintrag zurück:

julia> rand([4, 9, 16, 25])
16

# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
 1
  "name"

Ziehen mit oder ohne Zurücklegen

Stell dir vor, wir haben einen Beutel mit 3 roten und 4 grünen Kugeln und ziehen zufällig eine Kugel heraus. Für eine zweite Kugel gibt es zwei Möglichkeiten:

  1. Du legst die erste Kugel zurück in den Beutel und schüttelst alles gut durch, bevor du eine weitere ziehst. Die Anzahl der Kugeln ist jetzt dieselbe wie vorher (7), und das Verhältnis von Rot zu Grün ist ebenfalls gleich.
  2. Du legst die erste Kugel auf den Tisch, bevor du eine zweite ziehst. Jetzt sind nur noch 6 Kugeln im Beutel, und das Verhältnis Rot:Grün hängt von der Farbe der ersten Kugel ab.

Szenario 1 ist mit Zurücklegen, Szenario 2 ohne, und sie liefern unterschiedliche Ergebnisse.

Um das Ziehen ohne Zurücklegen in Julia zu simulieren, gibt es ein paar Möglichkeiten.

Am einfachsten (und innerhalb von Exercism die einzige Möglichkeit) verwendest du Random.shuffle(), um die Einträge in eine zufällige Reihenfolge zu bringen, und nimmst dann die ersten n Elemente. Das ist für kleine Probleme in Ordnung, skaliert aber bei großen Sammlungen möglicherweise schlecht: shuffle muss das gesamte Array erzeugen, auch wenn du nur einen kleinen Teil davon willst.

Um das Ziehen mit Zurücklegen „richtig“ zu machen, installierst du das Paket StatsBase.jl. Das bietet die Funktion sample() mit einer breiten Palette an Optionen.

Wir können begründet hoffen, dass ähnliche Funktionalität in einer künftigen Version in Random aufgenommen wird, um sie zu einem Teil der Standardbibliothek zu machen (die Codebeispiele in diesem Dokument wurden mit Julia 1.11 getestet).

Arbeiten mit Verteilungen

Bisher haben wir uns auf Fälle konzentriert, in denen alle Ergebnisse gleich wahrscheinlich sind. Zum Beispiel gibt rand(1:100) mit gleicher Wahrscheinlichkeit jede Ganzzahl von 1 bis 100 zurück.

Viele Situationen aus der realen Welt sind weit weniger einfach. Deshalb haben Statistiker eine Vielzahl von distributions entwickelt, um „reale“ Ergebnisse mathematisch zu beschreiben.

Gleichverteilungen

Die oben beschriebene Funktion rand() wird verwendet, wenn alle Wahrscheinlichkeiten gleich sind. Das nennt man eine uniform-Verteilung.

Gauß-Verteilung

Sie wird auch „Normalverteilung“ oder „Glockenkurve“ genannt und ist eine sehr verbreitete Art, Ungenauigkeiten bei gemessenen Werten zu beschreiben.

Angenommen, die Fabrik, in der du arbeitest, hat gerade 10.000 Schrauben gekauft, die eigentlich identisch sein sollten. Du willst den Fabrikroboter so einrichten, dass er sie handhabt, wiegst also eine Stichprobe von 100 Stück und stellst fest, dass sie ein durchschnittliches (oder mean) Gewicht von 4,731g haben. Es ist extrem unwahrscheinlich, dass das bedeutet, dass sie alle genau 4,731g wiegen. Vielleicht stellst du fest, dass die Werte von 4,627 bis 4,794g reichen, sich aber um 4,731g häufen.

Das ist die Gaussian distribution, bei der die Wahrscheinlichkeiten beim Mittelwert am höchsten sind und zu beiden Seiten symmetrisch abfallen (daher „glockenförmig“). Um das in Software zu simulieren, brauchen wir eine Möglichkeit, die Breite der Kurve anzugeben (typischerweise häufen sich teure Schrauben enger um den Mittelwert als billige!).

Üblicherweise geschieht das mit der standard deviation: kleine Werte für eine scharfe, schmale Kurve, große für eine flache, breite Kurve. Mathematiker lieben griechische Buchstaben, also verwenden wir μ („mu“), um den Mittelwert darzustellen, und σ („sigma“) für die Standardabweichung. Wenn du also liest, dass „95 % der Werte innerhalb von 2σ um μ liegen“ oder „das Higgs-Boson wurde mit 5-Sigma-Konfidenz nachgewiesen“, beziehen sich solche Aussagen auf die Standardabweichung.

Mehr dazu gibt es im Konzept Statistics.

Die Funktion randn()

Die Abkürzung steht für „random normal“. Sie ähnelt der Gleitkomma-Variante von rand(), nur dass die Werte gaußverteilt mit Mittelwert 0 und Standardabweichung 1 sind.

Auch hier willst du die Rohausgabe von randn vielleicht für die Standardabweichung skalieren und für den Mittelwert verschieben. Das folgende Beispiel wandelt sie in einen Mittelwert von 30 und eine Standardabweichung von 5 um.

julia> raw = randn(5)
5-element Vector{Float64}:
  3.0762588867281475
  1.5101100620253902
 -0.5914858221637778
  0.684175554069735
 -0.8416433926114673

julia> raw * 5 .+ 30
5-element Vector{Float64}:
 45.38129443364074
 37.55055031012695
 27.04257088918111
 33.420877770348675
 25.791783036942665

An der Ausgabe ist schwer zu erkennen, dass die Rohwerte näher bei null liegen als bei einer Gleichverteilung. Wenn du es bezweifelst, erzeuge 1000 oder mehr und zeichne sie auf, damit es deutlicher wird.

Das Modul Random

Dieses Modul enthält die nächste Stufe der Funktionalität, die aus Base weggelassen wurde, um die Größe von Julias Standardkonfiguration gering zu halten.

Random ergänzt rand und randn in Base um mutierende Varianten, rand! und randn!.

Eine nützliche Ergänzung ist randstring, das einen String mit gegebener Länge erzeugt. Standardmäßig verwendet es Groß- und Kleinbuchstaben sowie die Ziffern 0 bis 9, aber auch andere Zeichen können angegeben werden.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

Außerdem gibt es die Funktion bitrand, die ein zufälliges BitArray mit der angegebenen Länge erzeugt.

julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
 1
 1
 0
 0
 1

Mischen und Permutationen

Um Einträge in einem Vector zufällig zu mischen, gibt es shuffle; außerdem shuffle!, um den Eingabevektor direkt an Ort und Stelle zu verändern.

julia> v = ['A', '1', '2', 'J', 'Q', 'K'];

julia> shuffle(v)
6-element Vector{Char}:
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)

# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)

Manchmal ist es nützlich, stattdessen die gemischten Indizes zu haben. Dafür verwendest du randperm(n), wobei n die Länge der Sequenz ist.

julia> randperm(6)
6-element Vector{Int64}:
 6
 2
 4
 1
 3
 5

Tatsächlich liefert das obige Beispiel dieselben Ergebnisse wie shuffle(1:6).

Anleitung

Mary ist ein großer Fan der Fernsehserie Star Trek: The Next Generation. Sie spielt oft Pen-and-Paper-Rollenspiele, in denen sie und ihre Freunde in die Rolle der Crew des Raumschiffs Enterprise schlüpfen. Marys Charakter ist Captain Picard, was bedeutet, dass sie das Logbuch des Captains führen muss. Sie liebt den kreativen Teil des Spiels, mag es aber nicht, Zufallsdaten spontan zu erzeugen.

Hilf Mary, indem du Zufallsgeneratoren für Daten erstellst, die häufig im Logbuch des Captains vorkommen.

1. Generiere einen zufälligen Planeten

Das Raumschiff Enterprise trifft auf seinen Reisen auf viele Planeten. Planeten im Star-Trek-Universum werden anhand ihrer Eigenschaften in Kategorien eingeteilt. Zum Beispiel ist die Erde ein Planet der Klasse M. Alle möglichen Planetenklassen sind: D, H, J, K, L, M, N, R, T und Y.

Implementiere die Funktion random_planet. Sie soll eine der Planetenklassen zufällig zurückgeben.

julia> random_planet_class()
'K'

2. Generiere eine zufällige Raumschiffregisternummer

Die Enterprise (Registernummer NCC-1701) ist nicht das einzige Raumschiff, das herumfliegt! Wenn sie sich mit einem anderen Raumschiff trifft, muss Mary die Registernummer dieses Raumschiffs ins Logbuch eintragen.

Registernummern beginnen mit dem Präfix „NCC-“ und verwenden dann eine Zahl von 1000 bis 9999 (einschließlich).

Implementiere die Funktion random_ship_registry_number, die eine zufällige Raumschiffregisternummer zurückgibt.

julia> random_ship_registry_number()
"NCC-1947"

3. Generiere eine zufällige Sternzeit

Was nützt ein Logbuch, wenn es keine Datumsangaben enthält?

Eine Sternzeit ist eine Gleitkommazahl. Die Abenteuer des Raumschiffs Enterprise aus der ersten Staffel von The Next Generation spielen zwischen den Sternzeiten 41000.0 und 42000.0. Die „4“ steht für das 24. Jahrhundert, die „1“ für die erste Staffel.

Implementiere die Funktion random_stardate, die eine Gleitkommazahl zwischen 41000.0 (einschließlich) und 42000.0 zurückgibt.

julia> random_stardate()
41458.15721310934

4. Generiere eine gerundete Sternzeit

Es gab Beschwerden über zu viele Dezimalstellen in Sternzeiten, also beschließt Mary, die Logbucheinträge auf eine Dezimalstelle zu runden.

Obwohl man dies durch Runden der Werte von random_stardate() erreichen könnte, stellst du fest, dass es einen viel einfacheren Ansatz gibt.

Implementiere die Funktion random_stardate_v2, die eine Gleitkommazahl zwischen 41000.0 und 42000.0 (einschließlich) mit einer einzigen Dezimalstelle zurückgibt.

julia> random_stardate_v2()
41732.6

5. Wähle ein paar zufällige Raumschiffe aus einer Liste aus.

Eine Version des Spiels beinhaltet die Interaktion mit anderen Raumschiffen, und Mary braucht eine Auswahl.

Es gibt eine vollständige Liste möglicher Raumschiffe, aber in jedem Spiel braucht man nur wenige davon, und sie sollten zufällig ausgewählt werden.

Implementiere die Funktion pick_starships(starships, number_needed), die einen Vektor eindeutiger Raumschiffregisternummern entgegennimmt und einen Vektor der Länge number_needed zurückgibt. Die Ergebniswerte sollten aus starships stammen und keine Duplikate enthalten.

julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
 "NCC-1228"
 "NCC-5011"

5a. Denk optional über andere Argumenttypen nach.

Wir haben das Argument starships auf einen Vektor beschränkt. Wie würdest du pick_starships() implementieren, damit es mit anderen Iterables wie Tupeln oder Mengen funktioniert?

Für diesen Teil gibt es keine Tests.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
Julia Exercism

Bereit, mit Kapitänslogbuch zu starten?

Melde dich bei Exercism an, um Julia mit 35 Konzepte128 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.