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.
Das Sieb des Eratosthenes ist ein Klassiker unter den algorithmischen Problemen: Du willst alle Primzahlen bis zu einer Zahl n finden.
Bekannt ist es, weil es einen einfachen, aber sehr cleveren Trick zeigt: Statt jede Zahl aufwändig zu prüfen, markierst du systematisch alle Nicht-Primzahlen.
In diesem Artikel lernst du das Prinzip an einem einsteigerfreundlichen Beispiel und setzt es in Java um.
Der Fokus liegt auf Arrays und einer sauberen Markierungsstrategie, damit du dabei auch gleich gute Grundlagen fürs Programmieren trainierst.
Problemstellung
Finde alle Primzahlen im Bereich von 2 bis einschließlich n.
- Eingabe: eine ganze Zahl
n(z.B.30) - Ausgabe: alle Primzahlen bis
n(z.B.2, 3, 5, 7, 11, 13, 17, 19, 23, 29) - Regeln: Eine Primzahl ist größer als
1und nur durch1und sich selbst teilbar.
Mini-Beispiel für n = 10:
- Starte mit den Zahlen
2..10. - Markiere Vielfache von
2:4, 6, 8, 10. - Nächste unmarkierte Zahl ist
3, markiere Vielfache von3:6, 9. - Übrig bleiben
2, 3, 5, 7als Primzahlen.
Die Idee dahinter
Du legst ein Array an, das für jede Zahl speichert: ist diese Zahl zusammengesetzt (also keine Primzahl)?
Dann gehst du die Zahlen von 2 aufwärts durch. Wenn eine Zahl noch nicht markiert ist, ist sie eine Primzahl. Anschliessend markierst du alle ihre Vielfachen als "nicht prim".
falsebedeutet: noch nicht als zusammengesetzt markiert (kann prim sein)truebedeutet: markiert (ist keine Primzahl)
Kurze Visualisierung für n = 12 (M = markiert):
2(prim) - markiere4, 6, 8, 10, 123(prim) - markiere6, 9, 124(M)5(prim) - markiere106(M),7(prim),8(M),9(M),10(M),11(prim),12(M)
Schritt für Schritt zur Lösung
- Array vorbereiten: Erzeuge ein
boolean[]der Längen + 1, damit du direkt per Index auf Zahlen zugreifen kannst. - Grenzen setzen: Wenn
n < 2, gibt es keine Primzahlen. - Durchlaufen: Gehe von
2bisndurch und prüfe: istmarked[i]nochfalse? - Primzahl gefunden: Wenn nicht markiert, ist
ieine Primzahl. - Vielfache markieren: Markiere alle Vielfachen von
ialstrue, damit sie später übersprungen werden. - Ergebnis sammeln: Sammle alle Zahlen, die am Ende nicht markiert sind.
Java-Implementierung (Variante A: straightforward)
import java.util.ArrayList;
import java.util.List;
public class SieveStraightforward {
public static void main(String[] args) {
int n = 30;
List<Integer> primes = sieve(n);
System.out.println("n = " + n);
System.out.println("primes = " + primes);
}
public static List<Integer> sieve(int n) {
List<Integer> primes = new ArrayList<>();
if (n < 2) {
return primes;
}
boolean[] marked = new boolean[n + 1];
for (int i = 2; i <= n; i++) {
if (!marked[i]) {
primes.add(i);
markMultiples(marked, i, n);
}
}
return primes;
}
private static void markMultiples(boolean[] marked, int base, int n) {
for (int multiple = base * 2; multiple <= n; multiple += base) {
marked[multiple] = true;
}
}
}
Diese Variante ist bewusst direkt: Wenn du eine Primzahl findest, markierst du ab base * 2 alle Vielfachen. Das ist leicht nachzuvollziehen und gut zum Lernen der Grundidee. Durch die Hilfsmethode bleibt der Code übersichtlich. Performance ist okay für kleine bis mittlere n, aber es gibt unnötige Markierungen.
Java-Implementierung (Variante B: Alternative oder Verbesserung)
import java.util.ArrayList;
import java.util.List;
public class SieveImproved {
public static void main(String[] args) {
int n = 30;
List<Integer> primes = sieve(n);
System.out.println("n = " + n);
System.out.println("primes = " + primes);
}
public static List<Integer> sieve(int n) {
List<Integer> primes = new ArrayList<>();
if (n < 2) {
return primes;
}
boolean[] composite = new boolean[n + 1];
int limit = (int) Math.sqrt(n);
for (int i = 2; i <= limit; i++) {
if (!composite[i]) {
markFromSquare(composite, i, n);
}
}
for (int i = 2; i <= n; i++) {
if (!composite[i]) {
primes.add(i);
}
}
return primes;
}
private static void markFromSquare(boolean[] composite, int p, int n) {
long start = (long) p * p;
if (start > n) {
return;
}
for (long multiple = start; multiple <= n; multiple += p) {
composite[(int) multiple] = true;
}
}
}
Hier startest du das Markieren bei p * p. Alles darunter hat bereits einen kleineren Teiler und wurde daher früher markiert. Ausserdem läuft die Markierungs-Schleife nur bis zur Wurzel von n, was die Arbeit deutlich reduziert. Durch long beim Start vermeidest du Überlauf, wenn p * p größer wird.
Beispiel: Eingabe und Ausgabe
- Eingabe:
n = 10Ausgabe:[2, 3, 5, 7] - Eingabe:
n = 2Ausgabe:[2] - Eingabe:
n = 1Ausgabe:[]
Typische Fehler
- Off-by-one beim Array:
boolean[] marked = new boolean[n]ist falsch, weil du Indexnauch brauchst. Richtig istn + 1. - Falscher Start beim Markieren: In Variante A
base * 2vergessen oder mitbasestarten würde die Primzahl selbst markieren. - Abbruchbedingung zu groß/zu klein: In der verbesserten Variante muss die Markierungsbasis nur bis
sqrt(n)laufen. Zu weit laufen ist langsamer, zu kurz liefert falsche Ergebnisse. - *Überlauf bei `p p
:** Bei größerennkannintüberlaufen. Nutze für den Startlong` und caste erst beim Index. - Falsche Bedeutung von
true/false: Wenn dutrueals "prim" interpretierst, verdrehst du schnell die Logik. Benenne das Array passend, z.B.composite. - Grenzfälle ignorieren: Bei
n < 2muss die Ausgabe leer sein, sonst bekommst du merkwürdige Ergebnisse oder Schleifenfehler.
Fazit
Mit dem Sieb des Eratosthenes hast du gelernt, wie man Primzahlen effizient über ein Markierungs-Array findet. Das Problem ist ein gutes Training für Arrays, Schleifen und sauberes Zerlegen in Hilfsmethoden. Du hast ausserdem gesehen, wie eine kleine Änderung der Markierungsstrategie (p * p statt 2 * p) viel Arbeit sparen kann.
