C# | for-schleife als Rekursiv?
for (int i = 0; i < ziehungen; i++)
{
for (int j = 0; j < wieviel; j++)
{
/* Neue Zahlen werden hier zufällig generiert */
int neuezahlen = generator.Next(1, von + 1);
/* Überprüfen ob eine Zahl bereits vorhanden ist -> Wenn ja wird eine neue gezogen */
while (zahlen.Contains((neuezahlen)))
{
neuezahlen = generator.Next(1, von + 1);
}
zahlen[j] = neuezahlen;
}
Hat jemand eine Idee, wie ich den bisherigen Code zu einem rekursiven Ablauf umschreiben kann? Ich würde das gerne ohne die for-Schleifen machen, habe aber noch nicht so viel Erfahrung, was die Rekursion betrifft.
2 Antworten
Ich würde das stückweise lösen, also erstmal nur eine Ziehung in eine Rekursion umwandeln. Im zweiten Schritt kann dann das mehrfache Ziehen erfolgen.
Da ich keine Möglichkeit zum Testen habe, kann ich nur den Ansatz skizzieren.
void Ziehung(int iAnz, zahlen[])
{
if(zahlen.length()<iAnz) // Anker: Wenn iAnz erreicht, soll er nicht tiefer in ie Rekursion einsteigen.
{
int zufallszahl = generator.Next(1, von + 1);
if(!zahlen.contains(zufallszahl);
zahlen.add(zufallszahl);
Ziehung(iAnz, zahlen);
}
}
Die einzelnen Befehlsausprägungen müsstest du noch umsetzen und eventuell mit der Referenzierung des letzten Elements etwas jonglieren.
"zahlen.add" kannst du zum Beispiel in eine separate Funktion auslagern, wenn es das so noch nicht fertig gibt.
Wie gesagt 1:1 wird das nicht funktionieren. Ich hoffe, dir mit dem Ansatz trotzdem geholfen zu haben.
Ganz ehrlich: Lass es bleiben
Rekursion mag zwar cool klingen, tatsächlich ist es aber so, dass man nach Möglichkeit versucht, sie zu vermeiden. Der Grund ist, dass Du damit sehr viele aufeinander folgende Methoden-Aufrufe hast, die in jeder Hinsicht deine Arbeit und die im Hintergrund des Programms verkomplizieren.
Bei sehr vielen Durchläufen, kann das auf die Performance schlagen, theoretisch könnte das sogar zu einer StackOverflowException führen (hatte ich schon, ist ziemlich nervig). Aber auch wenn alles gut geht, hast Du Freude, wenn Du einen Fehler suchen willst und im CallStack zig tausende Aufrufe von ein und der selben Methode hast; oder wenn Du debuggen willst und keinen klaren Ablauf hast. Da sind deine Schleifen sehr viel einfacher :)
Daneben ist die Lesbarkeit aber der wichtigste Punkt. Wenn Du rein intuitiv for-Schleifen geschrieben hast und jetzt nach einer rekursiven Version suchst, dann zeigt das schon ganz eindeutig, dass deine jetzige Variante lesbarer und besser wartbar ist, als die Rekursion je sein könnte ;)
Wenn ich mir die Lösung von Suboptimierer anschaue, sehe ich meine Behauptung bestätigt. Der Code mag zwar auf den ersten Blick kürzer sein, aber wenn ein Fremder den Code liest, hätte er damit mehr Probleme, als mit deinen Schleifen.
Ich persönlich finde die for-Schleifen auch sehr gut lesbar. Ich würde die Variablen zwar auf englisch benennen, aber ansonsten sieht der Code recht simpel und leicht verständlich aus - genau so, wie es sein sollte.
Allerdings erschließt sich mir der Sinn der ersten Schleifen nicht :D
Berufserfahrung? So..So..
Ich schreibe den folgenden Text unter einer Annahme und zwar der dass C genauso wie Haskell (ich arbeite nicht mit C) auch über laziness verfügt - wie Haskell.
Du hast Recht. Rekursion ist einfach langsamer als eine induktive Lösung.
Aber ;D hier kommt ein kleiner Witz unseres wunderschönen Kompliers:
Der Kompiler rechnet Rekursionen um zu einer induktiven Lösung :P.
Damit ist das Endprodukt gleich schnell.
(zumindest in Haskell)
Abgesehen davon hilft Rekursion beim Verständnis von Zusammenhängen und ermöglicht es sehr kurze Lösungen zu schreiben.
Ja, Berufserfarung, das steht da nicht nur zum Spaß
Und ja, ich hab auch gelesen, dass manche Compiler das tun. Der C#-Compiler tut das aber nicht, zumindest hat er bei meinem Minimal-Test die Rekursion nicht aufgelöst.
Abgesehen davon hilft Rekursion beim Verständnis von Zusammenhängen und ermöglicht es sehr kurze Lösungen zu schreiben.
Nur, wenn die Rekursion zu den Zusammenhängen passt. Daher habe ich geschrieben: Wenn man bewusst nach einer rekursiven Lösung suchen muss, obwohl man intuitiv eine iterative Lösung geschrieben hat, dann ist die rekursive Lösung sehr wahrscheinlich nicht besser für das Verständnis.
Ein Gegenbeispiel wäre ein Such-Algorithmus in z.B. einer Ordner-Struktur, das schreit förmlich nach einer rekursiven Lösung. Hier finde ich die rekursive Lösung besser lesbar, die anderen genannten Nachteile bleiben natürlich trotzdem.
Nur ist das ziemlich blöd..
Weil unser Lehrer in der Berufsschule meinte wir sollen diesen Code in Rekursiv nachschreiben :/
Berufsschul-Lehrer ... Meiner Erfahrung nach sind deren Aufgaben meistens Quatsch :D Ein sehr, sehr viel besseres Beispiel wäre z.B. ein kleiner Such-Algorithmus um in Dateien zu suchen, in so einem Fall macht Rekursion nämlich Sinn.
Aber jetzt kannst Du ihm ja erklären, warum das Bullshit ist ;)
Ich kann nicht erklären, wie ich es lösen würde, ohne es dabei selber zu lösen. Daher kann ich es auch gleich ganz machen:
static void V1_ZieheZahlen(int anzahlZiehungen, int anzahlZahlen)
{
for (int i = 0; i < anzahlZiehungen; i++)
for (int j = 0; j < anzahlZahlen; j++)
ZieheZahl(i, j);
}
static void V2_ZieheZahlen(int anzahlZiehungen, int anzahlZahlen)
{
V2_ZieheZahlen(0, anzahlZahlen, anzahlZahlen);
}
static void V2_ZieheZahlen(int ziehungNr, int anzahlZiehungen, int anzahlZahlen)
{
if (ziehungNr >= anzahlZiehungen)
return;
V2_ZieheZahl(ziehungNr, 0, anzahlZahlen);
V2_ZieheZahlen(ziehungNr + 1, anzahlZiehungen, anzahlZahlen);
}
static void V2_ZieheZahl(int ziehungNr, int zahlNr, int anzahlZahlen)
{
if (zahlNr >= anzahlZahlen)
return;
ZieheZahl(ziehungNr, zahlNr);
V2_ZieheZahl(ziehungNr, zahlNr + 1, anzahlZahlen);
}
static void V3_ZieheZahlen(int ziehungNr, int anzahlZiehungen, int zahlNr, int anzahlZahlen)
{
if (ziehungNr >= anzahlZiehungen)
return;
if (zahlNr < anzahlZahlen)
{
ZieheZahl(ziehungNr, zahlNr);
V3_ZieheZahlen(ziehungNr, anzahlZiehungen, zahlNr + 1, anzahlZahlen);
}
else
V3_ZieheZahlen(ziehungNr + 1, anzahlZiehungen, 0, anzahlZahlen);
}
static void ZieheZahl(int ziehungNr, int zahlNr)
{
// Für mich zum testen:
Console.WriteLine($"{ziehungNr}-{zahlNr}");
#region Dein Code
///* Neue Zahlen werden hier zufällig generiert */
//int neuezahlen = generator.Next(1, von + 1);
///* Überprüfen ob eine Zahl bereits vorhanden ist -> Wenn ja wird eine neue gezogen */
//while (zahlen.Contains((neuezahlen)))
//{
// neuezahlen = generator.Next(1, von + 1);
//}
//zahlen[j] = neuezahlen;
#endregion
}
static void Main(string[] args)
{
var ziehungen = 3;
var wieviel = 5;
V1_ZieheZahlen(ziehungen, wieviel);
Console.WriteLine("=====");
V2_ZieheZahlen(0, ziehungen, wieviel);
Console.WriteLine("=====");
V3_ZieheZahlen(0, ziehungen, 0, wieviel);
Console.ReadKey();
}
Ich hab das Ziehen in eine eigene "ZieheZahl"-Methode ausgelagert, damit es einheitlich bleibt.
V1 - Mit for-Schleifen
V2 - Zwei Rekursionen, je eine pro Schleife
V3 - Eine Rekursion, die beide Schleifen abdeckt
Du siehst, V1 ohne Rekursion ist die mit Abstand einfachste Lösung.
Ich lege dir nahe, dir den Code im Detail anzuschauen und auch zu debuggen. Du wirst fest stellen, V1 ist besonders beim Debuggen sehr viel einfacher bzw. übersichtlicher.
Ach ja: Ich würde kein Array nutzen.
Das lernt man zwar als erstes in der Schule, ist aber für die meisten Fälle einfach zu unflexibel. .NET hat dafür die List<T>. Die verwaltet intern selber ein Array, kümmert sich aber darum, dass Du Items hinzufügen oder entfernen kannst, ohne dich in irgendeiner Form um die Größe des Arrays kümmern zu müssen.
Allerdings erschließt sich mir der Sinn der ersten Schleifen nicht :D
Das ist ein Programm was Zahlen generiert.
Die erste Schleife fragt nur ab wie oft man Generieren möchte.
Der Output von dem Programm sieht folgendermaßen aus:
Ziehungen = 5
Wie viel = 3
Von = 4
Output:
2 1 4
3 2 1
1 3 2
4 2 3
3 1 2
Dann hast Du da aber einen massiven Fehler drin :P
Denn mit jeder neuen Ziehung überschreibst Du die Ergebnisse der vorherigen Ziehung - zumindest wenn Du nichts vom Code entfernt hast.
Wieso`?
Ich bekomme normal den Output wie es sein sollte mit dem Code aus der Frage.
Nur soll ich eben aufgrund meines Lehrers den Code Rekursiv schreiben.
Der Code, den ich sehe, überschreibt mit jeder Ziehung (erste for-Schleife) das Ergebnis der einzelnen tatsächlichen Ziehungen (zweite for-Schleife).
Du nutzt nirgendwo den i-Index, nur den j-Index. Bei jeder Ziehung nutzt Du im Array die selben j-Indices wie bei den vorherigen Ziehungen, Du überschreibst das Ergebnis im Array.
Es fehlt der Part, der nach jeder Ziehung das Ergebnis anzeigt - oder irgendwo weg archiviert um dann alle Ziehungen auf einen Schlag anzuzeigen. Ob das nun bewusst von dir entfernt wurde, oder tatsächlich fehlt, kann ich natürlich nicht sagen.
for (int i = 0; i < ziehungen; i++)
{
for (int j = 0; j < wieviel; j++)
{
/* Neue Zahlen werden hier Zufällig generiert */
int neuezahlen = generator.Next(1, von + 1);
/* Überprüfen ob eine Zahl bereits vorhanden ist -> Wenn ja wird eine neue gezogen */
while (zahlen.Contains((neuezahlen)))
{
neuezahlen = generator.Next(1, von + 1);
}
zahlen[j] = neuezahlen;
}
Console.WriteLine((string.Join(" ", zahlen) + " | -> " + zaehlerEigenesSpiel));
Das ist der ganze Code
Sieh an, da ist auch der fehlende Teil, der die Zahlen ausgibt ;)
Tipp:
In Visual Studio den Code markieren, Shift gedrückt halten und so lange Tab drücken, bis der Code ganz links ausgerichtet ist. Dann brauchst Du hier nichts neu schreiben, sondern kannst einfach rüber kopieren, ohne dass Du die hässlichen Einrückungen hast.
Danke :)
Werde ich gleich ausprobieren ^^