In der Serie "Named Problems" stelle ich dir klassische, namentlich bekannte Algorithmus-Probleme vor, die seit Jahrzehnten in Ausbildung, Studium und Interviews verwendet werden. Anhand verständlicher Problemstellungen wie FizzBuzz, Türme von Hanoi oder dem Sieb des Eratosthenes lernst du hier, algorithmisch zu denken, Lösungsstrategien zu entwickeln und diese sauber in Java umzusetzen.
Der Euklidische Algorithmus ist ein klassisches Problem aus der Mathematik: Wir berechnen den grüßten gemeinsamen Teiler (ggT) von zwei ganzen Zahlen.
Warum ist das bekannt? Weil es eine extrem einfache Idee hat, aber in vielen Bereichen wieder auftaucht: Brüche kürzen, Kryptografie-Grundlagen, Zahlentheorie, und als Übung für Iteration und Rekursion.
In diesem Artikel lernst du, was der Algorithmus macht, warum er funktioniert (ohne Beweis-Overkill) und wie du ihn in Java 17+ sauber implementierst: einmal straightforward und einmal als Alternative.
Problemstellung
Gegeben sind zwei ganze Zahlen a und b. Gesucht ist ihr ggT, also die größte positive Zahl, die beide ohne Rest teilt.
- Die Eingaben sind ganze Zahlen (typisch:
intoderlong). - Negative Werte sollen sinnvoll behandelt werden (der ggT ist trotzdem positiv).
- Falls eine Zahl
0ist, gilt:ggT(a, 0) = |a|. - Falls beide
0sind, ist der ggT nicht sinnvoll definiert (das sollten wir abfangen).
Mini-Beispiel:
a = 48,b = 18- Der ggT ist
6, weil48 % 6 == 0und18 % 6 == 0.
Die Idee dahinter
Kernkonzept: Mathematische Iteration/Rekursion über den Rest.
Die zentrale Beobachtung ist: Der ggT von a und b ist derselbe wie der ggT von b und a % b. Denn alles, was a und b gemeinsam teilt, teilt auch den Rest a % b.
Das wiederholst du, bis der Rest 0 ist. Dann ist der aktuelle Divisor der ggT.
- Starte mit
(a, b) - Ersetze durch
(b, a % b) - Stoppe, wenn
b == 0
Schritt für Schritt zur Lösung
- Normalisiere die Eingaben: arbeite mit
abs(a)undabs(b), damit der ggT positiv ist. - Prüfe Sonderfall: Wenn beide
0sind, wirf eine Exception (sonst bekommst du keinen sinnvollen ggT). - Solange
b != 0, berechne den Restr = a % b. - Schiebe weiter: Setze
a = bundb = r. - Wenn die Schleife endet, ist
ader ggT (Rückgabewert).
Java-Implementierung (Variante A: straightforward)
public class EuclidStraightforward {
public static int gcdStraightforward(int a, int b) {
a = Math.abs(a);
b = Math.abs(b);
if (a == 0 && b == 0) {
throw new IllegalArgumentException("gcd for 0 and 0 is undefined");
}
while (b != 0) {
int remainder = a % b;
a = b;
b = remainder;
}
return a;
}
public static void main(String[] args) {
System.out.println(gcdStraightforward(48, 18)); // 6
System.out.println(gcdStraightforward(21, 14)); // 7
System.out.println(gcdStraightforward(-42, 56)); // 14
System.out.println(gcdStraightforward(25, 0)); // 25
}
}
Diese Variante ist bewusst direkt: eine while-Schleife und ein Zwischenschritt remainder. Durch Math.abs bekommst du einen positiven Rückgabewert, auch wenn Eingaben negativ sind. Der Sonderfall (0, 0) wird klar abgefangen, statt stillschweigend 0 zu liefern.
Java-Implementierung (Variante B: Alternative oder Verbesserung)
public class EuclidAlternative {
public static int gcdRecursive(int a, int b) {
a = Math.abs(a);
b = Math.abs(b);
if (a == 0 && b == 0) {
throw new IllegalArgumentException("gcd for 0 and 0 is undefined");
}
return gcdRecursiveNormalized(a, b);
}
private static int gcdRecursiveNormalized(int a, int b) {
if (b == 0) {
return a; // clear base case
}
return gcdRecursiveNormalized(b, a % b);
}
public static int gcdIterativeSwap(int a, int b) {
a = Math.abs(a);
b = Math.abs(b);
if (a == 0 && b == 0) {
throw new IllegalArgumentException("gcd for 0 and 0 is undefined");
}
for (; b != 0; ) {
int remainder = a % b;
a = b;
b = remainder;
}
return a;
}
public static void main(String[] args) {
System.out.println(gcdRecursive(48, 18)); // 6
System.out.println(gcdRecursive(21, 14)); // 7
System.out.println(gcdIterativeSwap(25, 0)); // 25
}
}
Hier siehst du die Idee als Rekursion: Die Abbruchbedingung ist sehr deutlich if (b == 0) return a;. Das macht das Prinzip gcd(a, b) = gcd(b, a % b) praktisch 1:1 sichtbar. Zusätzlich ist eine zweite iterative Variante enthalten, die das gleiche tut, aber mit einer kompakteren Schleifenform.
Beispiel: Eingabe und Ausgabe
- Eingabe:
a=48,b=18Output:6 - Eingabe:
a=21,b=14Output:7 - Eingabe:
a=-42,b=56Output:14
Typische Fehler
- Falsche Abbruchbedingung: Bei Rekursion
if (a == 0)stattif (b == 0)kann zu falschen Ergebnissen oder Endlosschleifen führen. - Vergessen, negative Werte zu normalisieren: Ohne
Math.abskann der Rückgabewert negativ sein oder das Verhalten wirkt "komisch". - Sonderfall (0, 0) ignorieren: Dann lieferst du oft stillschweigend
0, obwohl der ggT nicht definiert ist. - Vertauschen der Variablen in der Schleife: Wenn du
aundbin falscher Reihenfolge überschreibst, verlierst du den alten Wert und bekommst Mist. - Modulo mit 0: Wenn du
a % bberechnest, obwohlb == 0, bekommst du eine Exception. Darum immer erst die Bedingungb != 0sichern. - Zu große Datentypen falsch eingeschätzt: Bei sehr grossen Zahlen kann
intüberlaufen. Dann lieberlongverwenden (Prinzip bleibt gleich).
Fazit
Du hast gesehen, wie der Euklidische Algorithmus den ggT über wiederholte Restbildung findet. Dabei eignet er sich perfekt, um Iteration und Rekursion sauber zu vergleichen: gleiche Idee, anderer Stil.
