Block 02 · Algorithmen & Programmierung – Lösungen

Lösungssatz zu 02_Algorithmen.html · Pseudocode im IHK-Stil, dazu lauffähige Java-Dateien im Ordner java\02_Algorithmen\ (Start mit java Aufgabe_ALG_NN_k.java)
Hinweise zur Bewertung Wie in den IHK-Lösungshinweisen gilt: Andere sach- und fachgerechte Lösungen (andere Schleifenart, Java- oder C#-Syntax, Struktogramm) werden mit voller Punktzahl bewertet. Syntaxfehler kosten in der Regel keine Punkte, Logikfehler schon. Die Java-Ausgaben unten stammen aus einem echten Programmlauf (JDK 21).

Lösungen

ALG-01 · Algorithmus in Pseudocode schreiben

Aufgabe ALG-01.1 · 6 Punkte

a) Lücken (5 P)

LückeInhalt
(1)0
(2)raeder.size() - 1
(3)r.getAkkuStand() < 25 und nicht r.isDefekt()
(4)anzahl := anzahl + 1
(5)anzahl
funktion zaehleLadebedarf(raeder : List<Fahrrad>) : integer
  anzahl : integer := 0
  zähle i von 0 bis raeder.size() - 1
    r : Fahrrad := raeder.get(i)
    wenn r.getAkkuStand() < 25 und nicht r.isDefekt() dann
      anzahl := anzahl + 1
    Ende wenn
  Ende zähle
  rückgabe anzahl
Ende funktion

b) Ergebnis (1 P)

Die Funktion liefert 3 (Räder 102 mit 15 %, 104 mit 22 % und 107 mit 8 %). Rad 110 hat 28 % (nicht unter 25), Rad 103 ist defekt, wird aber schon wegen 60 % nicht gezählt.

Java · Aufgabe_ALG_01_1.java (Auszug)

// Lösung ALG-01.1: zählt einsatzfähige (nicht defekte) Räder mit Akkustand unter 25 %
static int zaehleLadebedarf(List<Fahrrad> raeder) {
    int anzahl = 0;                                       // Lücke 1
    for (int i = 0; i <= raeder.size() - 1; i++) {        // Lücke 2
        Fahrrad r = raeder.get(i);
        if (r.getAkkuStand() < 25 && !r.isDefekt()) {     // Lücke 3
            anzahl = anzahl + 1;                          // Lücke 4
        }
    }
    return anzahl;                                        // Lücke 5
}

Ausgabe

zaehleLadebedarf(Testdaten) = 3
Betroffene Räder: 102 104 107
Leere Liste -> 0
Bewertung: je Lücke 1 P; Lücke (3) nur mit beiden Teilbedingungen. Gleichwertig: r.isDefekt() = false, Java-Schreibweise anzahl++, i < raeder.size() bei Java-Schleifen. b) 1 P für Wert und Radnummern.

Erklärung: Klassisches Zählmuster. „unter 25 %“ heißt <, nicht <=. Die Schleife läuft bis size() - 1, weil der letzte gültige Index eins kleiner als die Anzahl ist.

Aufgabe ALG-01.2 · 10 Punkte

a) einsatzbereit (5 P)

funktion einsatzbereit(raeder : List<Fahrrad>, typ : string,
                       minAkku : integer) : List<Fahrrad>
  ergebnis : List<Fahrrad> := new List<Fahrrad>()
  zähle i von 0 bis raeder.size() - 1
    r : Fahrrad := raeder.get(i)
    wenn r.getTyp() = typ und nicht r.isDefekt() und r.getAkkuStand() >= minAkku dann
      ergebnis.add(r)
    Ende wenn
  Ende zähle
  rückgabe ergebnis
Ende funktion

b) durchschnittsStrecke (5 P)

funktion durchschnittsStrecke(ausleihen : List<Ausleihe>, stationNr : integer) : double
  summe : double := 0.0
  anzahl : integer := 0
  zähle i von 0 bis ausleihen.size() - 1
    a : Ausleihe := ausleihen.get(i)
    wenn a.getStartStation() = stationNr dann
      summe := summe + a.getStrecke()
      anzahl := anzahl + 1
    Ende wenn
  Ende zähle
  wenn anzahl = 0 dann
    rückgabe 0.0
  Ende wenn
  rückgabe summe / anzahl
Ende funktion

Java · Aufgabe_ALG_01_2.java (Auszug)

// a) alle einsatzbereiten Räder eines Typs mit Mindest-Akkustand in eine neue Liste
static List<Fahrrad> einsatzbereit(List<Fahrrad> raeder, String typ, int minAkku) {
    List<Fahrrad> ergebnis = new ArrayList<>();
    for (int i = 0; i < raeder.size(); i++) {
        Fahrrad r = raeder.get(i);
        if (r.getTyp().equals(typ) && !r.isDefekt() && r.getAkkuStand() >= minAkku) {
            ergebnis.add(r);
        }
    }
    return ergebnis;
}

// b) durchschnittliche Strecke aller Ausleihen, die an einer Station beginnen
static double durchschnittsStrecke(List<Ausleihe> ausleihen, int stationNr) {
    double summe = 0.0;
    int anzahl = 0;
    for (int i = 0; i < ausleihen.size(); i++) {
        Ausleihe a = ausleihen.get(i);
        if (a.getStartStation() == stationNr) {
            summe = summe + a.getStrecke();
            anzahl = anzahl + 1;
        }
    }
    if (anzahl == 0) {
        return 0.0;                 // keine Division durch 0
    }
    return summe / anzahl;
}

Ausgabe

a) einsatzbereit(raeder, "E-Bike", 30):
   Rad 101 (E-Bike, Akku 85 %)
   Rad 105 (E-Bike, Akku 95 %)
   Rad 109 (E-Bike, Akku 55 %)
   Rad 111 (E-Bike, Akku 90 %)
   Rad 112 (E-Bike, Akku 33 %)
   einsatzbereit(raeder, "Lastenrad", 30): [Rad 106 (Lastenrad, Akku 40 %)]
b) durchschnittsStrecke je Startstation:
   Station 1: 4,6000 km
   Station 2: 2,1000 km
   Station 3: 6,9667 km
   Station 4: 5,0000 km
   Station 5: 10,7500 km
   Station 6: 7,9000 km
   Station 7: 0,0000 km
Bewertung: a) neue Ergebnisliste anlegen 1 P, Schleife über alle Räder 1 P, vollständige Bedingung (Typ, nicht defekt, >= minAkku) 2 P, add und Rückgabe 1 P. b) Summe und Zähler initialisiert 1 P, Schleife 1 P, Bedingung mit Aufsummieren und Zählen 1 P, Behandlung „keine Ausleihe“ 1 P, Rückgabe des Durchschnitts 1 P. In Java ist der Stringvergleich mit equals korrekt; im Pseudocode wird = akzeptiert (Anlage 5).

Erklärung: a) ist das Filtermuster – die Originalliste bleibt unverändert, weil nur gelesen und in eine neue Liste geschrieben wird. b) kombiniert Summe und Zähler; ohne die Abfrage anzahl = 0 würde für eine Station ohne Ausleihen durch 0 geteilt (in Java mit double ergibt das NaN, also ein sinnloses Ergebnis ohne Fehlermeldung).

Aufgabe ALG-01.3 · 30 Punkte

a) langeFahrtenJeStation (6 P)

funktion langeFahrtenJeStation(ausleihen : List<Ausleihe>,
                               anzahlStationen : integer) : integer[]
  ergebnis : integer[] := new integer[anzahlStationen]     // alle Elemente sind 0
  zähle i von 0 bis ausleihen.size() - 1
    a : Ausleihe := ausleihen.get(i)
    wenn a.getDauer() > 60 dann
      index : integer := a.getStartStation() - 1            // Station 1 -> Index 0
      ergebnis[index] := ergebnis[index] + 1
    Ende wenn
  Ende zähle
  rückgabe ergebnis
Ende funktion
Bewertung a): Array mit anzahlStationen Elementen angelegt (mit 0 belegt) 1 P, Schleife über alle Ausleihen 1 P, Bedingung > 60 1 P, Index startStation - 1 2 P, Rückgabe 1 P. Wer das Array mit anzahlStationen + 1 Elementen anlegt und Index 0 ungenutzt lässt, erfüllt die Vorgabe „Index i für Station i + 1“ nicht (−2 P).

b) erzeugeStatistik (10 P)

funktion erzeugeStatistik(ausleihen : List<Ausleihe>) : List<KundenStatistik>
  statistik : List<KundenStatistik> := new List<KundenStatistik>()
  zähle i von 0 bis ausleihen.size() - 1
    kundenNr : integer := ausleihen.get(i).getKundenNr()
    vorhanden : boolean := false
    zähle j von 0 bis statistik.size() - 1
      wenn statistik.get(j).getKundenNr() = kundenNr dann
        vorhanden := true
      Ende wenn
    Ende zähle
    wenn nicht vorhanden dann
      statistik.add(new KundenStatistik(kundenNr))
    Ende wenn
  Ende zähle
  rückgabe statistik
Ende funktion

Gleichwertige Alternative mit Hilfsliste: nummern : List<integer> mitführen und mit wenn nicht nummern.contains(kundenNr) dann nummern.add(kundenNr); statistik.add(new KundenStatistik(kundenNr)) arbeiten.

Bewertung b): neue Liste 1 P, äußere Schleife über alle Ausleihen 2 P, Prüfung „Kundennummer schon vorhanden“ (innere Schleife oder Hilfsliste mit contains) 4 P, Objekt mit Konstruktor erzeugen und hinzufügen 2 P, Rückgabe 1 P. Nicht korrekt: statistik.contains(new KundenStatistik(kundenNr)) – ein neu erzeugtes Objekt ist nie in der Liste enthalten (Anlage 2: Objekte sind nur gleich, wenn es dasselbe Objekt ist), dadurch entstehen Duplikate (−3 P).

c) berechneStatistik (14 P)

funktion berechneStatistik(ausleihen : List<Ausleihe>, statistik : List<KundenStatistik>,
                           von : Datum, bis : Datum) : void
  zähle i von 0 bis statistik.size() - 1
    ks : KundenStatistik := statistik.get(i)
    anzahl : integer := 0                        // für jeden Kunden neu
    summe : integer := 0
    zähle j von 0 bis ausleihen.size() - 1
      a : Ausleihe := ausleihen.get(j)
      wenn a.getKundenNr() = ks.getKundenNr()
           und a.getDatum().liegtZwischen(von, bis) dann
        anzahl := anzahl + 1
        summe := summe + a.getDauer()
      Ende wenn
    Ende zähle
    ks.setAnzahl(anzahl)
    ks.setGesamtDauer(summe)
    wenn anzahl > 0 dann
      ks.setDurchschnitt(summe / anzahl)       // Kommazahl! Java: (double) summe / anzahl
    sonst
      ks.setDurchschnitt(0.0)
    Ende wenn
  Ende zähle
Ende funktion
Bewertung c): äußere Schleife über statistik 2 P, Zähler und Summe je Kunde (innerhalb der äußeren Schleife) auf 0 gesetzt 2 P, innere Schleife über ausleihen 2 P, Bedingung Kundennummer 1 P, Zeitraum mit liegtZwischen 2 P, Zählen und Aufsummieren 2 P, Werte per Setter gesetzt 1 P, Durchschnitt mit Behandlung „0 Ausleihen“ 2 P. Gleichwertig: ein Durchlauf über die Ausleihen, bei dem jeweils das passende Statistikobjekt gesucht und hochgezählt wird, danach eine Schleife für die Durchschnitte.

Java · Aufgabe_ALG_01_3.java (Auszug)

// a) Anzahl der Fahrten über 60 Minuten je Startstation; Index = stationNr - 1
static int[] langeFahrtenJeStation(List<Ausleihe> ausleihen, int anzahlStationen) {
    int[] ergebnis = new int[anzahlStationen];          // alle Elemente sind 0
    for (int i = 0; i < ausleihen.size(); i++) {
        Ausleihe a = ausleihen.get(i);
        if (a.getDauer() > 60) {
            int index = a.getStartStation() - 1;
            ergebnis[index] = ergebnis[index] + 1;
        }
    }
    return ergebnis;
}

// b) für jede Kundennummer genau ein KundenStatistik-Objekt (Werte noch 0)
static List<KundenStatistik> erzeugeStatistik(List<Ausleihe> ausleihen) {
    List<KundenStatistik> statistik = new ArrayList<>();
    for (int i = 0; i < ausleihen.size(); i++) {
        int kundenNr = ausleihen.get(i).getKundenNr();
        boolean vorhanden = false;
        int j = 0;
        while (j < statistik.size() && !vorhanden) {
            if (statistik.get(j).getKundenNr() == kundenNr) {
                vorhanden = true;
            }
            j = j + 1;
        }
        if (!vorhanden) {
            statistik.add(new KundenStatistik(kundenNr));
        }
    }
    return statistik;
}

// c) Werte je Kunde für den Zeitraum von..bis berechnen und setzen
static void berechneStatistik(List<Ausleihe> ausleihen, List<KundenStatistik> statistik,
                              Datum von, Datum bis) {
    for (int i = 0; i < statistik.size(); i++) {
        KundenStatistik ks = statistik.get(i);
        int anzahl = 0;                                   // für jeden Kunden neu!
        int summe = 0;
        for (int j = 0; j < ausleihen.size(); j++) {
            Ausleihe a = ausleihen.get(j);
            if (a.getKundenNr() == ks.getKundenNr()
                    && a.getDatum().liegtZwischen(von, bis)) {
                anzahl = anzahl + 1;
                summe = summe + a.getDauer();
            }
        }
        ks.setAnzahl(anzahl);
        ks.setGesamtDauer(summe);
        if (anzahl > 0) {
            ks.setDurchschnitt((double) summe / anzahl);   // keine Ganzzahldivision
        } else {
            ks.setDurchschnitt(0.0);
        }
    }
}

Ausgabe

a) langeFahrtenJeStation(ausleihen, 6) = [0, 0, 1, 0, 1, 1]
b) erzeugeStatistik: 5 Objekte
   Kunde 5001: anzahl=0, gesamtDauer=0, durchschnitt=0,00
   Kunde 5002: anzahl=0, gesamtDauer=0, durchschnitt=0,00
   Kunde 5003: anzahl=0, gesamtDauer=0, durchschnitt=0,00
   Kunde 5004: anzahl=0, gesamtDauer=0, durchschnitt=0,00
   Kunde 5005: anzahl=0, gesamtDauer=0, durchschnitt=0,00
c) berechneStatistik(..., 2026-09-16, 2026-09-17):
   Kunde 5001: anzahl=1, gesamtDauer=26, durchschnitt=26,00
   Kunde 5002: anzahl=0, gesamtDauer=0, durchschnitt=0,00
   Kunde 5003: anzahl=2, gesamtDauer=98, durchschnitt=49,00
   Kunde 5004: anzahl=1, gesamtDauer=33, durchschnitt=33,00
   Kunde 5005: anzahl=1, gesamtDauer=9, durchschnitt=9,00
   Kontrolle Gesamtzeitraum 2026-09-14 bis 2026-09-17:
   Kunde 5001: anzahl=3, gesamtDauer=66, durchschnitt=22,00
   Kunde 5002: anzahl=2, gesamtDauer=76, durchschnitt=38,00
   Kunde 5003: anzahl=3, gesamtDauer=173, durchschnitt=57,67
   Kunde 5004: anzahl=2, gesamtDauer=128, durchschnitt=64,00
   Kunde 5005: anzahl=2, gesamtDauer=40, durchschnitt=20,00

Erklärung: Die Aufgabe kombiniert drei Grundmuster: a) Ergebnis-Array „je Station“ (wie W24 „je Strecke“), b) eindeutige Liste (wie S26 a), c) verschachtelte Schleifen mit Summe/Durchschnitt und Zeitraumfilter (wie S26 b). Die häufigsten Punktverluste: Zähler und Summe nur einmal vor der äußeren Schleife initialisiert (dann summieren sich die Werte über alle Kunden auf), Ganzzahldivision (98 / 2 ginge gut, 173 / 3 würde in Java 57 statt 57,67 ergeben) und Division durch 0 bei Kunde 5002, der im Zeitraum nichts ausgeliehen hat.

ALG-02 · Arrays und Listen durchlaufen

Aufgabe ALG-02.1 · 5 Punkte

a) (je 1 P, zwei davon) Ein Array hat eine feste Größe, die beim Anlegen festgelegt wird; eine Liste wächst und schrumpft dynamisch. · Bei Arrays greift man mit feld[i] zu und fragt die Länge mit length ab, bei Listen über Methoden (get(i), size()). · Listen bieten Methoden zum Einfügen und Entfernen (add, remove), beim Array muss man dafür umkopieren. · In Java kann ein Array primitive Typen (int) speichern, eine Liste nur Objekte (Integer).

b) kleinster Index 0, größter Index 23. (1 P)

c) Index 24 existiert nicht. Java löst zur Laufzeit eine ArrayIndexOutOfBoundsException aus; wird sie nicht behandelt, bricht das Programm ab. Das Array wird nicht automatisch vergrößert. (1 P)

d) 7 · 24 = 168 Elemente; Zugriff: zaehler[4][17]. (1 P)

Java · Aufgabe_ALG_02_1.java (Auszug)

public static void main(String[] args) {
    int[] belegung = new int[24];
    System.out.println("belegung.length = " + belegung.length
            + ", kleinster Index = 0, größter Index = " + (belegung.length - 1));
    try {
        belegung[24] = 5;
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("belegung[24] = 5  ->  " + e.getClass().getSimpleName());
        System.out.println("   Meldung: " + e.getMessage());
    }

    int[][] zaehler = new int[7][24];
    System.out.println("zaehler: " + zaehler.length + " Zeilen x " + zaehler[0].length
            + " Spalten = " + (zaehler.length * zaehler[0].length) + " Elemente");
    zaehler[4][17] = 12;   // Freitag (Index 4), 17-Uhr-Stunde
    System.out.println("zaehler[4][17] = " + zaehler[4][17]);

    List<Integer> liste = new ArrayList<>();
    liste.add(7);
    liste.add(3);
    liste.add(9);
    System.out.println("Liste wächst dynamisch: size() = " + liste.size()
            + " " + liste);
}

Ausgabe

belegung.length = 24, kleinster Index = 0, größter Index = 23
belegung[24] = 5  ->  ArrayIndexOutOfBoundsException
   Meldung: Index 24 out of bounds for length 24
zaehler: 7 Zeilen x 24 Spalten = 168 Elemente
zaehler[4][17] = 12
Liste wächst dynamisch: size() = 3 [7, 3, 9]
Bewertung: a) 2 × 1 P für zwei verschiedene, richtig beschriebene Unterschiede; b)–d) je 1 P (d nur mit Anzahl und Zugriff).
Aufgabe ALG-02.2 · 10 Punkte

a) summeJeTag (4 P)

funktion summeJeTag(daten : integer[][]) : integer[]
  summen : integer[] := new integer[daten.length]
  zähle tag von 0 bis daten.length - 1
    summe : integer := 0
    zähle f von 0 bis daten[tag].length - 1
      summe := summe + daten[tag][f]
    Ende zähle
    summen[tag] := summe
  Ende zähle
  rückgabe summen
Ende funktion

b) summeJeZeitfenster (4 P)

funktion summeJeZeitfenster(daten : integer[][]) : integer[]
  anzahlFenster : integer := daten[0].length
  summen : integer[] := new integer[anzahlFenster]
  zähle f von 0 bis anzahlFenster - 1
    summe : integer := 0
    zähle tag von 0 bis daten.length - 1
      summe := summe + daten[tag][f]
    Ende zähle
    summen[f] := summe
  Ende zähle
  rückgabe summen
Ende funktion

Alternative für b): Ergebnis-Array anlegen und in beliebiger Schleifenreihenfolge summen[f] := summen[f] + daten[tag][f] rechnen.

c) Rückgabewerte (2 P)

summeJeTag = [57, 58, 61, 63, 68, 68, 55] · summeJeZeitfenster = [17, 73, 74, 95, 126, 45] (Kontrolle: beide ergeben zusammen 430 Ausleihen).

Java · Aufgabe_ALG_02_2.java (Auszug)

// a) Summe je Zeile (= je Wochentag)
static int[] summeJeTag(int[][] daten) {
    int[] summen = new int[daten.length];
    for (int tag = 0; tag < daten.length; tag++) {
        int summe = 0;
        for (int f = 0; f < daten[tag].length; f++) {
            summe = summe + daten[tag][f];
        }
        summen[tag] = summe;
    }
    return summen;
}

// b) Summe je Spalte (= je Zeitfenster)
static int[] summeJeZeitfenster(int[][] daten) {
    int anzahlFenster = daten[0].length;
    int[] summen = new int[anzahlFenster];
    for (int f = 0; f < anzahlFenster; f++) {
        int summe = 0;
        for (int tag = 0; tag < daten.length; tag++) {
            summe = summe + daten[tag][f];
        }
        summen[f] = summe;
    }
    return summen;
}

Ausgabe

a) summeJeTag         = [57, 58, 61, 63, 68, 68, 55]
b) summeJeZeitfenster = [17, 73, 74, 95, 126, 45]
Kontrolle: Gesamtsumme über Zeilen = 430, über Spalten = 430
Bewertung: a) und b) je: Ergebnis-Array in richtiger Größe 1 P, verschachtelte Schleifen mit richtigen Grenzen 1 P, Summe je Zeile/Spalte zurückgesetzt und gespeichert 1 P, Rückgabe 1 P. Feste Grenzen 7 bzw. 6 in b) −1 P. c) je Array 1 P.

Erklärung: Für Spaltensummen läuft die äußere Schleife über die Spalten (daten[0].length), die innere über die Zeilen (daten.length). Der häufigste Fehler ist, beide Grenzen mit daten.length zu bilden – bei 7 × 6 führt das zu einem Indexfehler.

Aufgabe ALG-02.3 · 16 Punkte

a) genutzteRaeder (5 P)

funktion genutzteRaeder(ausleihen : List<Ausleihe>) : List<integer>
  ergebnis : List<integer> := new List<integer>()
  zähle i von 0 bis ausleihen.size() - 1
    radNr : integer := ausleihen.get(i).getRadNr()
    wenn nicht ergebnis.contains(radNr) dann
      ergebnis.add(radNr)
    Ende wenn
  Ende zähle
  rückgabe ergebnis
Ende funktion

b) Ergebnis (1 P)

[101, 105, 106, 104, 110, 109, 111, 107, 112, 102] – 10 Radnummern; 101 und 106 kommen in den Ausleihen je zweimal vor, stehen aber nur einmal in der Liste.

c) entferneDefekte (7 P)

funktion entferneDefekte(raeder : List<Fahrrad>) : integer
  entfernt : integer := 0
  i : integer := raeder.size() - 1            // rückwärts durchlaufen
  solange i >= 0
    wenn raeder.get(i).isDefekt() dann
      raeder.remove(i)
      entfernt := entfernt + 1
    Ende wenn
    i := i - 1
  Ende solange
  rückgabe entfernt
Ende funktion

Gleichwertig vorwärts, wenn i nur erhöht wird, falls nichts entfernt wurde:

i : integer := 0
solange i < raeder.size()
  wenn raeder.get(i).isDefekt() dann
    raeder.remove(i)                // i bleibt: Nachfolger steht jetzt auf Position i
    entfernt := entfernt + 1
  sonst
    i := i + 1
  Ende wenn
Ende solange

Für die Testdaten liefert die Funktion 2 (Räder 103 und 108 werden entfernt).

d) Ursache (3 P)

Nach remove(1) (Rad 108) rücken alle folgenden Räder eine Position nach vorn: 103 steht nun auf Index 1, 109 auf Index 2. Die Schleife erhöht i aber auf 2 und prüft als Nächstes 109. Rad 103 wird übersprungen und bleibt in der Liste; das Ergebnis ist [107, 103, 109] mit Rückgabe 1 statt 2. Das passiert immer, wenn zwei zu entfernende Elemente direkt hintereinander stehen.

Java · Aufgabe_ALG_02_3.java (Auszug)

// a) jede Radnummer genau einmal, in der Reihenfolge des ersten Auftretens
static List<Integer> genutzteRaeder(List<Ausleihe> ausleihen) {
    List<Integer> ergebnis = new ArrayList<>();
    for (int i = 0; i < ausleihen.size(); i++) {
        int radNr = ausleihen.get(i).getRadNr();
        if (!ergebnis.contains(radNr)) {
            ergebnis.add(radNr);
        }
    }
    return ergebnis;
}

// b) defekte Räder entfernen - rückwärts, damit kein Element übersprungen wird
static int entferneDefekte(List<Fahrrad> raeder) {
    int entfernt = 0;
    for (int i = raeder.size() - 1; i >= 0; i--) {
        if (raeder.get(i).isDefekt()) {
            raeder.remove(i);            // remove(int index)
            entfernt = entfernt + 1;
        }
    }
    return entfernt;
}

Ausgabe (inklusive der fehlerhaften Variante aus d)

a) genutzteRaeder = [101, 105, 106, 104, 110, 109, 111, 107, 112, 102]
b) entferneDefekte(Testdaten) = 2, Rest: [101, 102, 104, 105, 106, 107, 109, 110, 111, 112]
c) richtig (rückwärts) auf [107, 108d, 103d, 109]: entfernt 2 -> [107, 109]
   falsch (vorwärts, i++)  auf [107, 108d, 103d, 109]: entfernt 1 -> [107, 103, 109]
Bewertung: a) neue Liste 1 P, Schleife 1 P, Duplikatprüfung mit contains 2 P, add und Rückgabe 1 P. b) 1 P (Reihenfolge muss stimmen). c) Zähler 1 P, Durchlauf ohne Überspringen (rückwärts oder bedingtes Erhöhen) 3 P, Prüfung isDefekt() mit remove(i) 2 P, Rückgabe 1 P. Eine Lösung, die eine neue Liste ohne defekte Räder zurückgibt, erfüllt die Aufgabe nicht (übergebene Liste soll verändert werden, Rückgabe ist die Anzahl) – höchstens 3 P. d) Index-Verschiebung erkannt 2 P, am Beispiel konkret erläutert 1 P.

ALG-03 · Minimum/Maximum, lineare Suche

Aufgabe ALG-03.1 · 5 Punkte

a) Die Funktion liefert 0 (richtig wäre 15, Rad 102). (1 P)

b) min wird mit 0 initialisiert. Da kein Akkustand kleiner als 0 ist, ist die Bedingung raeder[i].getAkkuStand() < min nie erfüllt; die Funktion gibt immer den Startwert 0 zurück. Außerdem wird raeder[0] nie betrachtet, weil die Schleife bei 1 beginnt – das ist nur richtig, wenn das erste Element als Startwert dient. (2 P)

c) min : integer := raeder[0].getAkkuStand() (1 P)

d) Das Array muss mindestens ein Element enthalten (darf nicht leer bzw. null sein), sonst führt der Zugriff auf raeder[0] zu einem Laufzeitfehler. (1 P)

Java · Aufgabe_ALG_03_1.java (Auszug)

// gegebene, FEHLERHAFTE Funktion
static int minAkkuFalsch(Fahrrad[] raeder) {
    int min = 0;
    for (int i = 1; i <= raeder.length - 1; i++) {
        if (raeder[i].getAkkuStand() < min) {
            min = raeder[i].getAkkuStand();
        }
    }
    return min;
}

// korrigierte Funktion: Initialisierung mit dem ersten Element
static int minAkku(Fahrrad[] raeder) {
    int min = raeder[0].getAkkuStand();
    for (int i = 1; i <= raeder.length - 1; i++) {
        if (raeder[i].getAkkuStand() < min) {
            min = raeder[i].getAkkuStand();
        }
    }
    return min;
}

Ausgabe

fehlerhaft: minAkkuFalsch(101..105) = 0
korrigiert: minAkku(101..105)      = 15
leeres Array -> ArrayIndexOutOfBoundsException (Vorbedingung: mindestens 1 Element)
Bewertung: b) 1 P für die Ursache (Startwert 0), 1 P für die Wirkung (Bedingung nie wahr, immer 0). c) Ein „großer Startwert“ wie 101 reicht hier nicht, weil die Schleife bei 1 beginnt und Element 0 dann fehlen würde (0 P), es sei denn, auch die Schleife wird auf 0 geändert – das war ausgeschlossen.
Aufgabe ALG-03.2 · 10 Punkte

a) findeRad (5 P)

funktion findeRad(raeder : List<Fahrrad>, radNr : integer) : Fahrrad
  gefunden : Fahrrad := null
  i : integer := 0
  solange i < raeder.size() und gefunden = null
    wenn raeder.get(i).getRadNr() = radNr dann
      gefunden := raeder.get(i)
    Ende wenn
    i := i + 1
  Ende solange
  rückgabe gefunden                          // null, wenn nicht vorhanden
Ende funktion

Gleichwertig: Zählschleife mit rückgabe raeder.get(i) direkt beim Treffer und rückgabe null nach der Schleife.

b) vollstesRad (5 P)

funktion vollstesRad(raeder : List<Fahrrad>, typ : string) : Fahrrad
  bestes : Fahrrad := null
  zähle i von 0 bis raeder.size() - 1
    r : Fahrrad := raeder.get(i)
    wenn r.getTyp() = typ und nicht r.isDefekt() dann
      wenn bestes = null oder r.getAkkuStand() > bestes.getAkkuStand() dann
        bestes := r
      Ende wenn
    Ende wenn
  Ende zähle
  rückgabe bestes
Ende funktion

Java · Aufgabe_ALG_03_2.java (Auszug)

// a) lineare Suche mit Abbruch; null, wenn nicht gefunden
static Fahrrad findeRad(List<Fahrrad> raeder, int radNr) {
    Fahrrad gefunden = null;
    int i = 0;
    while (i < raeder.size() && gefunden == null) {
        if (raeder.get(i).getRadNr() == radNr) {
            gefunden = raeder.get(i);
        }
        i = i + 1;
    }
    return gefunden;
}

// b) nicht defektes Rad eines Typs mit dem höchsten Akkustand; null, wenn keines passt
static Fahrrad vollstesRad(List<Fahrrad> raeder, String typ) {
    Fahrrad bestes = null;
    for (int i = 0; i < raeder.size(); i++) {
        Fahrrad r = raeder.get(i);
        if (r.getTyp().equals(typ) && !r.isDefekt()) {
            if (bestes == null || r.getAkkuStand() > bestes.getAkkuStand()) {
                bestes = r;
            }
        }
    }
    return bestes;
}

Ausgabe

a) findeRad(raeder, 109) = Rad 109 (E-Bike, Akku 55 %)
   findeRad(raeder, 200) = null
b) vollstesRad(raeder, "Lastenrad") = Rad 106 (Lastenrad, Akku 40 %)
   vollstesRad(raeder, "E-Bike")    = Rad 105 (E-Bike, Akku 95 %)
   vollstesRad(raeder, "Tandem")    = null
Bewertung: a) Schleife 1 P, Vergleich der Radnummer 1 P, Abbruch nach dem Fund 2 P, null bei Nichtfinden 1 P. b) Startwert null (bzw. erstes passendes Rad) 1 P, Bedingung Typ und nicht defekt 1 P, Maximumvergleich 2 P, Rückgabe des Objekts 1 P.

Erklärung: Bei b) ist die Falle der Startwert: Mit raeder.get(0) zu starten wäre falsch, weil Rad 101 ein E-Bike ist und bei der Suche nach Lastenrädern nicht in Frage kommt. Das defekte Lastenrad 103 hat zwar den höchsten Akkustand (60 %), richtig ist aber 106 (40 %).

Aufgabe ALG-03.3 · 20 Punkte

a) Entfernung (2 P)

d = √((6,0 − 4,0)² + (4,0 − 3,0)²) = √(4 + 1) = √5 ≈ 2,24 km

b) naechsteStation (12 P)

funktion naechsteStation(stationen : List<Station>, kx : double, ky : double,
                         benoetigt : integer) : Station
  beste : Station := null
  minEntfernung : double := 0.0                 // erst gültig, wenn beste <> null
  zähle i von 0 bis stationen.size() - 1
    s : Station := stationen.get(i)
    verfuegbar : integer := 0                   // je Station neu zählen
    zähle j von 0 bis s.getRaeder().size() - 1
      r : Fahrrad := s.getRaeder().get(j)
      wenn nicht r.isDefekt() und r.getAkkuStand() >= 30 dann
        verfuegbar := verfuegbar + 1
      Ende wenn
    Ende zähle
    wenn verfuegbar >= benoetigt dann
      e : double := entfernung(kx, ky, s.getX(), s.getY())
      wenn beste = null oder e < minEntfernung dann
        beste := s
        minEntfernung := e
      Ende wenn
    Ende wenn
  Ende zähle
  rückgabe beste                                // null, wenn keine Station passt
Ende funktion
Bewertung b): äußere Schleife über Stationen 1 P, Zähler je Station zurückgesetzt 1 P, innere Schleife über getRaeder() 2 P, Bedingung „einsatzbereit“ (nicht defekt und ≥ 30 %) 2 P, Vergleich >= benoetigt 1 P, Entfernung mit entfernung(…) 1 P, Minimumlogik mit korrektem Startwert 2 P, Station merken und zurückgeben (null, wenn keine passt) 2 P.

c) Ergebnis für (4,0 | 3,0) und benoetigt = 2 (3 P)

StationEntfernungeinsatzbereite Räderkommt in Frage?
1 Hauptbahnhof2,83 km1 (101)nein
2 Marktplatz0,71 km1 (105; 104 hat nur 22 %)nein
3 Uni-Campus2,24 km1 (106; 107 zu leer, 108 defekt)nein
4 Hafen3,91 km1 (109)nein
5 Klinikum4,12 km0 (110 hat nur 28 %)nein
6 Seepark4,74 km2 (111, 112)ja

Die Funktion liefert Station 6 Seepark (4,74 km). Die nächstgelegene Station 2 Marktplatz (0,71 km) hat nur ein einsatzbereites Rad und erfüllt die Bedingung „mindestens 2“ nicht; nur am Seepark stehen zwei einsatzbereite Räder.

d) Startwert 0 (3 P)

Entfernungen sind nie negativ. Mit minEntfernung := 0 wäre e < minEntfernung nie erfüllt (bei einer Bedingung ohne beste = null) – es würde nie eine Station gemerkt und die Funktion gäbe immer null zurück. Geeignete Alternativen: einen Merker verwenden (beste = null oder e < minEntfernung, wie oben), mit einem sehr großen Wert starten (z. B. Double.MAX_VALUE bzw. größer als jede mögliche Entfernung) oder die erste passende Station als Startwert nehmen.

Java · Aufgabe_ALG_03_3.java (Auszug)

/** Hilfsfunktion aus Anlage 5: Luftlinie in km. */
static double entfernung(double x1, double y1, double x2, double y2) {
    return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}

// b) nächstgelegene Station mit mindestens "benoetigt" einsatzbereiten Rädern
static Station naechsteStation(List<Station> stationen, double kx, double ky,
                               int benoetigt) {
    Station beste = null;
    double minEntfernung = 0.0;              // wird erst gültig, wenn beste != null
    for (int i = 0; i < stationen.size(); i++) {
        Station s = stationen.get(i);
        int verfuegbar = 0;
        for (int j = 0; j < s.getRaeder().size(); j++) {
            Fahrrad r = s.getRaeder().get(j);
            if (!r.isDefekt() && r.getAkkuStand() >= 30) {
                verfuegbar = verfuegbar + 1;
            }
        }
        if (verfuegbar >= benoetigt) {
            double e = entfernung(kx, ky, s.getX(), s.getY());
            if (beste == null || e < minEntfernung) {
                beste = s;
                minEntfernung = e;
            }
        }
    }
    return beste;
}

Ausgabe

a) Entfernung Kunde (4,0|3,0) - Station 3 (6,0|4,0) = 2,24 km
   Übersicht je Station:
   Station 1 Hauptbahnhof   Entfernung 2,83 km, einsatzbereit: 1
   Station 2 Marktplatz     Entfernung 0,71 km, einsatzbereit: 1
   Station 3 Uni-Campus     Entfernung 2,24 km, einsatzbereit: 1
   Station 4 Hafen          Entfernung 3,91 km, einsatzbereit: 1
   Station 5 Klinikum       Entfernung 4,12 km, einsatzbereit: 0
   Station 6 Seepark        Entfernung 4,74 km, einsatzbereit: 2
b/c) naechsteStation(stationen, 4.0, 3.0, 1) = Station 2 Marktplatz
b/c) naechsteStation(stationen, 4.0, 3.0, 2) = Station 6 Seepark
b/c) naechsteStation(stationen, 4.0, 3.0, 3) = null
Bewertung a), c), d): a) Ansatz 1 P, Ergebnis 1 P. c) Ergebnis 1 P, Begründung (Marktplatz nur 1 einsatzbereites Rad) 2 P. d) Begründung 2 P, Alternative 1 P.

Erklärung: Typischer „nächster Nachbar mit Bedingung“ wie in W23 – zusätzlich muss je Station in einer inneren Schleife gezählt werden. Weil die erste Station die Bedingung nicht erfüllen muss, ist der Merker beste = null der sicherste Startwert.

ALG-04 · Speicherbedarf berechnen / Zahlensysteme

Aufgabe ALG-04.1 · 6 Punkte

a) Einheiten (2 P)

EinheitAnzahl Byte
1 KiB2¹⁰ = 1 024
1 kB10³ = 1 000
1 MiB2²⁰ = 1 048 576
1 GB10⁹ = 1 000 000 000

b) 202 umwandeln (2 P)

202 : 2 = 101  Rest 0
101 : 2 =  50  Rest 1
 50 : 2 =  25  Rest 0
 25 : 2 =  12  Rest 1
 12 : 2 =   6  Rest 0
  6 : 2 =   3  Rest 0
  3 : 2 =   1  Rest 1
  1 : 2 =   0  Rest 1      Reste von unten nach oben lesen

202 (dezimal) = 1100 1010 (binär)
Hex: 1100 = C, 1010 = A  ->  202 = CA (hex)     Probe: 12 · 16 + 10 = 202

c) (1 P)

7E (hex) = 7 · 16 + 14 = 126; 1011 0110 (binär) = 128 + 32 + 16 + 4 + 2 = 182.

d) (1 P)

2¹² = 4 096 verschiedene Werte (vorzeichenlos 0 bis 4 095).

Kontrollrechnung mit Java · Aufgabe_ALG_04_1.java · Ausgabe

a) 1 KiB = 1024 Byte, 1 kB = 1000 Byte, 1 MiB = 1048576 Byte, 1 GB = 1000000000 Byte
b) 202 dezimal = 11001010 binär = CA hex
   Divisionsrestverfahren 202:
   202 : 2 = 101 Rest 0
   101 : 2 = 50 Rest 1
   50 : 2 = 25 Rest 0
   25 : 2 = 12 Rest 1
   12 : 2 = 6 Rest 0
   6 : 2 = 3 Rest 0
   3 : 2 = 1 Rest 1
   1 : 2 = 0 Rest 1
c) 7E hex = 126 dezimal, 10110110 binär = 182 dezimal
d) 12 Bit -> 2^12 = 4096 verschiedene Werte (0 bis 4095)
Bewertung: a) je Zeile 0,5 P. b) Binärzahl mit erkennbarem Rechenweg 1 P, Hexzahl 1 P (auch über Division durch 16: 202 : 16 = 12 Rest 10 → CA). c) 0,5 P je Wert. d) 1 P.
Aufgabe ALG-04.2 · 8 Punkte

a) Datentypen (4 P)

FeldWertebereichDatentypByteBegründung
ausleihNr1 bis 3 000 000int4größer als 32 767 (short)
kundenNr1 bis 200 000int4größer als 32 767
radNr1 bis 5 000short2größer als 127, kleiner als 32 767
startStation1 bis 60byte1passt in −128 … 127
startZeitca. 1,8 · 10¹²long8größer als 2,1 · 10⁹ (int)
dauer0 bis 1 440short2größer als 127
streckeKommazahl, einfache Genauigkeitfloat4einfache Genauigkeit reicht
preisCent0 bis 50 000int4größer als 32 767

b) Speicherbedarf (4 P)

Datensatz    = 4 + 4 + 2 + 1 + 8 + 2 + 4 + 4        =         29 Byte
pro Jahr     = 29 Byte · 2 400 Datensätze/Tag · 365 Tage
             = 25 404 000 Byte
in MiB       = 25 404 000 : 1 024 : 1 024             ≈      24,23 MiB

Kontrollrechnung mit Java · Aufgabe_ALG_04_2.java · Ausgabe

   ausleihNr     int    4 Byte
   kundenNr      int    4 Byte
   radNr         short  2 Byte
   startStation  byte   1 Byte
   startZeit     long   8 Byte
   dauer         short  2 Byte
   strecke       float  4 Byte
   preisCent     int    4 Byte
a/b) Datensatz = 29 Byte
   Wertebereiche: byte -128..127, short -32768..32767
   pro Jahr = 29 * 2400 * 365 = 25404000 Byte
   = 24,2271 MiB  (zum Vergleich 25,4040 MB)
Bewertung: a) je Feld 0,5 P (Typ und Byte). b) Datensatzgröße 1 P, Hochrechnung pro Jahr 1 P, Umrechnung mit 1 024 1 P, Ergebnis mit Einheit und Rundung 1 P. Folgefehler aus a) werden nicht erneut abgezogen. Wer für strecke double (8 Byte) wählt, verliert in a) 0,5 P; b) wird dann mit 33 Byte und 27,57 MiB als Folgerechnung anerkannt.

Erklärung: short reicht bis 32 767, byte bis 127 (vorzeichenbehaftet). Ein Zeitstempel in Millisekunden passt längst nicht mehr in int – das ist der Grund, warum Java System.currentTimeMillis() als long liefert.

Aufgabe ALG-04.3 · 12 Punkte

a) Speicherbedarf (6 P)

Pixel je Foto      = 4 000 · 3 000                           =        12 000 000 Pixel
Bit je Foto        = 12 000 000 Pixel · 3 Kanäle · 8 Bit      =       288 000 000 Bit
Byte je Foto       = 288 000 000 Bit : 8                      =        36 000 000 Byte
MiB je Foto        = 36 000 000 : 1 024 : 1 024               ≈             34,33 MiB

Fotos pro Jahr     = 2 Fotos · 2 400 Rückgaben/Tag · 365 Tage =         1 752 000 Fotos
Byte pro Jahr      = 36 000 000 Byte · 1 752 000              = 63 072 000 000 000 Byte
TiB pro Jahr       = 63 072 000 000 000 : 1 024⁴              ≈             57,36 TiB

Auch der Weg über den gerundeten Wert ist richtig: 34,33 MiB · 1 752 000 = 60 146 160 MiB; : 1 024 : 1 024 ≈ 57,36 TiB.

b) Festplatten (4 P)

komprimiert, 2 Jahre = 63 072 000 000 000 Byte · 2 : 15      = 8 409 600 000 000 Byte
                     = 8,41 TB  (SI, wie die Herstellerangabe)   ≈ 7,65 TiB
1 Festplatte "8 TB"  = 8 000 000 000 000 Byte                     ≈ 7,28 TiB
Anzahl               = 8,41 TB : 8 TB = 1,05  -> aufrunden       =  2 Festplatten

Falle: Wer 7,65 TiB mit „8 TB“ vergleicht, kommt fälschlich auf eine Platte – es muss in derselben Einheit verglichen werden (8,41 TB > 8 TB bzw. 7,65 TiB > 7,28 TiB).

c) Anzeige „7,27 TB“ (2 P)

Der Hersteller rechnet mit SI-Präfixen: 8 TB = 8 · 10¹² Byte. Das Betriebssystem rechnet binär (1 TiB = 2⁴⁰ = 1 099 511 627 776 Byte), beschriftet den Wert aber mit „TB“: 8 · 10¹² : 2⁴⁰ ≈ 7,28 TiB (angezeigt abgeschnitten bzw. abzüglich Dateisystem-Verwaltungsdaten ≈ 7,27). Es fehlt also kein Speicher, nur die Einheit ist eine andere.

Kontrollrechnung mit Java · Aufgabe_ALG_04_3.java · Ausgabe

a) Pixel pro Bild   = 12000000
   Bit pro Bild     = 288000000
   Byte pro Bild    = 36000000
   MiB pro Bild     = 34,3323
   Bilder pro Jahr  = 1752000
   Byte pro Jahr    = 63072000000000
   TiB pro Jahr     = 57,3636
b) JPEG 1:15, 2 Jahre = 8409600000000 Byte = 8,4096 TB = 7,6485 TiB
   8 TB = 7,2760 TiB
   benötigte Platten = 2
   (Falsch gerechnet mit TiB-Wert gegen 8: 7,65 / 8 -> 1 Platte)
Bewertung: a) Pixel · Farbtiefe 1 P, Bit → Byte 1 P, MiB je Foto 1 P, Anzahl Fotos pro Jahr (beide Kameras!) 1 P, Hochrechnung 1 P, TiB-Ergebnis 1 P; Rundungsdifferenzen werden toleriert. b) Kompression 1 P, zwei Jahre 1 P, Vergleich in gleicher Einheit 1 P, Ergebnis 2 Platten 1 P. c) SI vs. binär erklärt 1 P, Rechnung bzw. Zahlenbeleg 1 P.

Erklärung: Gleiches Schema wie W23 (GiB) und S26 (TiB): erst ein Bild in Bit, dann Byte, dann Hochrechnung, zuletzt Umrechnung. Die zwei Kameras je Rückgabe werden am häufigsten vergessen.

ALG-05 · Sortieren

Aufgabe ALG-05.1 · 5 Punkte

a) Durchläufe (3 P)

DurchlaufInhalt nach dem DurchlaufVertauschungen
1[17, 8, 33, 25, 42]4 (42↔17, 42↔8, 42↔33, 42↔25)
2[8, 17, 25, 33, 42]2 (17↔8, 33↔25)
3[8, 17, 25, 33, 42]0 → Abbruch
4––

b) Komplexität (2 P)

Bubblesort hat im schlechtesten Fall die Zeitkomplexität O(n²). Besser: Mergesort (O(n log n)), Heapsort (O(n log n)) oder Quicksort (im Mittel O(n log n)).

Kontrolle mit Java · Aufgabe_ALG_05_1.java · Ausgabe

Start:          [42, 17, 8, 33, 25]
nach Durchlauf 1: [17, 8, 33, 25, 42]  Vertauschungen: 4
nach Durchlauf 2: [8, 17, 25, 33, 42]  Vertauschungen: 2
nach Durchlauf 3: [8, 17, 25, 33, 42]  Vertauschungen: 0
Abbruch, weil im letzten Durchlauf nicht getauscht wurde.
Bewertung: a) je korrektem Durchlauf (Inhalt und Anzahl) 1 P; Durchlauf 3 zählt nur, wenn erkannt wird, dass 0 Vertauschungen zum Abbruch führen. b) je 1 P.
Aufgabe ALG-05.2 · 10 Punkte

a) Bubblesort nach Akkustand (6 P)

funktion sortiereNachAkku(raeder : Fahrrad[]) : void
  n : integer := raeder.length
  zähle i von 0 bis n - 2
    zähle j von 0 bis n - 2 - i
      wenn raeder[j].getAkkuStand() > raeder[j + 1].getAkkuStand() dann
        hilf : Fahrrad := raeder[j]              // ganzes Objekt tauschen
        raeder[j] := raeder[j + 1]
        raeder[j + 1] := hilf
      Ende wenn
    Ende zähle
  Ende zähle
Ende funktion

b) mit vorzeitigem Abbruch (2 P)

funktion sortiereNachAkku(raeder : Fahrrad[]) : void
  n : integer := raeder.length
  durchlauf : integer := 0
  getauscht : boolean := true                     // neu
  solange getauscht                               // geändert (statt zähle i …)
    getauscht := false                            // neu
    zähle j von 0 bis n - 2 - durchlauf
      wenn raeder[j].getAkkuStand() > raeder[j + 1].getAkkuStand() dann
        hilf : Fahrrad := raeder[j]
        raeder[j] := raeder[j + 1]
        raeder[j + 1] := hilf
        getauscht := true                         // neu
      Ende wenn
    Ende zähle
    durchlauf := durchlauf + 1
  Ende solange
Ende funktion

c) Stabilität (2 P)

Ein Sortierverfahren ist stabil, wenn Elemente mit gleichem Sortierschlüssel nach dem Sortieren in derselben Reihenfolge stehen wie vorher. Das Verfahren oben ist stabil, weil nur bei > getauscht wird: Räder mit gleichem Akkustand werden nie gegeneinander vertauscht. (Mit >= wäre es instabil.)

Java · Aufgabe_ALG_05_2.java (Auszug, Variante b)

// a/b) Bubblesort aufsteigend nach Akkustand, mit vorzeitigem Abbruch
static void sortiereNachAkku(Fahrrad[] raeder) {
    int n = raeder.length;
    boolean getauscht = true;
    int durchlauf = 0;
    while (getauscht) {
        getauscht = false;
        for (int j = 0; j <= n - 2 - durchlauf; j++) {
            if (raeder[j].getAkkuStand() > raeder[j + 1].getAkkuStand()) {
                Fahrrad hilf = raeder[j];
                raeder[j] = raeder[j + 1];
                raeder[j + 1] = hilf;
                getauscht = true;
            }
        }
        durchlauf = durchlauf + 1;
    }
}

Ausgabe

Ladereihenfolge (aufsteigend nach Akkustand):
   Rad 107 (E-Bike, Akku 8 %)
   Rad 102 (E-Bike, Akku 15 %)
   Rad 104 (E-Bike, Akku 22 %)
   Rad 110 (Lastenrad, Akku 28 %)
   Rad 112 (E-Bike, Akku 33 %)
   Rad 106 (Lastenrad, Akku 40 %)
   Rad 109 (E-Bike, Akku 55 %)
   Rad 103 (Lastenrad, Akku 60 %, defekt)
   Rad 108 (E-Bike, Akku 71 %, defekt)
   Rad 101 (E-Bike, Akku 85 %)
   Rad 111 (E-Bike, Akku 90 %)
   Rad 105 (E-Bike, Akku 95 %)
Bewertung: a) Schleifengrenzen (kein Zugriff über das Array-Ende) 2 P, Vergleich über den Getter 2 P, Tausch der Objekte mit Hilfsvariable 2 P. b) Merker richtig zurückgesetzt/gesetzt 1 P, Abbruchbedingung 1 P. c) Begriff 1 P, Aussage mit Begründung 1 P.
Aufgabe ALG-05.3 · 18 Punkte

a) sortiere mit Vergleichsfunktion (7 P)

funktion sortiere(ausleihen : Ausleihe[], vergleiche : Function) : void
  zähle i von 0 bis ausleihen.length - 2
    zähle j von 0 bis ausleihen.length - 2 - i
      wenn vergleiche(ausleihen[j], ausleihen[j + 1]) > 0 dann
        hilf : Ausleihe := ausleihen[j]
        ausleihen[j] := ausleihen[j + 1]
        ausleihen[j + 1] := hilf
      Ende wenn
    Ende zähle
  Ende zähle
Ende funktion

b) nachDauerAbsteigend (3 P)

funktion nachDauerAbsteigend(a : Ausleihe, b : Ausleihe) : integer
  wenn a.getDauer() > b.getDauer() dann
    rückgabe -1                                   // längere Ausleihe gehört nach vorn
  Ende wenn
  wenn a.getDauer() < b.getDauer() dann
    rückgabe 1
  Ende wenn
  rückgabe 0
Ende funktion

Kurzform (ebenfalls richtig): rückgabe b.getDauer() - a.getDauer().

c) nachStationDannPreis (5 P)

funktion nachStationDannPreis(a : Ausleihe, b : Ausleihe) : integer
  wenn a.getStartStation() <> b.getStartStation() dann
    rückgabe a.getStartStation() - b.getStartStation()   // aufsteigend
  Ende wenn
  wenn a.getPreis() > b.getPreis() dann          // gleiche Station: Preis absteigend
    rückgabe -1
  Ende wenn
  wenn a.getPreis() < b.getPreis() dann
    rückgabe 1
  Ende wenn
  rückgabe 0
Ende funktion

d) Reihenfolge und Stabilität (3 P)

Reihenfolge der Ausleihnummern: 11, 1, 9, 2, 3, 8, 4, 6, 5, 12, 7, 10.

Die Ausleihen 1 und 9 beginnen beide an Station 1 und kosten beide 0,00 € – die Vergleichsfunktion liefert 0. Ein stabiles Verfahren lässt sie in der ursprünglichen Reihenfolge (1 vor 9). Bei einem instabilen Verfahren könnte auch 9 vor 1 stehen.

Java · Aufgabe_ALG_05_3.java (Auszug)

// b) generischer Bubblesort: die Reihenfolge bestimmt allein die Vergleichsfunktion
static void sortiere(Ausleihe[] ausleihen, Comparator<Ausleihe> vergleiche) {
    for (int i = 0; i < ausleihen.length - 1; i++) {
        for (int j = 0; j < ausleihen.length - 1 - i; j++) {
            if (vergleiche.compare(ausleihen[j], ausleihen[j + 1]) > 0) {
                Ausleihe hilf = ausleihen[j];
                ausleihen[j] = ausleihen[j + 1];
                ausleihen[j + 1] = hilf;
            }
        }
    }
}

// c1) nach Dauer absteigend
static int nachDauerAbsteigend(Ausleihe a, Ausleihe b) {
    if (a.getDauer() > b.getDauer()) {
        return -1;
    } else if (a.getDauer() < b.getDauer()) {
        return 1;
    }
    return 0;
}

// c2) nach Startstation aufsteigend, bei gleicher Station nach Preis absteigend
static int nachStationDannPreis(Ausleihe a, Ausleihe b) {
    if (a.getStartStation() != b.getStartStation()) {
        return a.getStartStation() - b.getStartStation();
    }
    if (a.getPreis() > b.getPreis()) {
        return -1;
    } else if (a.getPreis() < b.getPreis()) {
        return 1;
    }
    return 0;
}

Ausgabe

sortiere(feld, nachDauerAbsteigend):
   Ausleihe  5: Kunde 5004, Rad 110, Station 5 -> 4, 2026-09-15,  95 min, 12,40 km,   6,50 EUR
   Ausleihe  3: Kunde 5003, Rad 106, Station 3 -> 5, 2026-09-14,  75 min,  9,80 km,   7,50 EUR
   Ausleihe  7: Kunde 5002, Rad 111, Station 6 -> 3, 2026-09-15,  64 min,  8,70 km,  13,80 EUR
   Ausleihe 12: Kunde 5003, Rad 106, Station 5 -> 3, 2026-09-17,  58 min,  9,10 km,   5,80 EUR
   Ausleihe  8: Kunde 5003, Rad 107, Station 3 -> 3, 2026-09-16,  40 min,  6,20 km,   4,00 EUR
   Ausleihe 10: Kunde 5004, Rad 112, Station 6 -> 1, 2026-09-16,  33 min,  7,10 km,   0,30 EUR
   Ausleihe  6: Kunde 5005, Rad 109, Station 4 -> 2, 2026-09-15,  31 min,  5,00 km,   7,20 EUR
   Ausleihe  9: Kunde 5001, Rad 101, Station 1 -> 6, 2026-09-16,  26 min,  7,30 km,   0,00 EUR
   Ausleihe  4: Kunde 5001, Rad 104, Station 3 -> 1, 2026-09-14,  22 min,  4,90 km,   0,00 EUR
   Ausleihe  1: Kunde 5001, Rad 101, Station 1 -> 3, 2026-09-14,  18 min,  4,60 km,   0,00 EUR
   Ausleihe  2: Kunde 5002, Rad 105, Station 2 -> 1, 2026-09-14,  12 min,  2,10 km,   3,40 EUR
   Ausleihe 11: Kunde 5005, Rad 102, Station 1 -> 2, 2026-09-17,   9 min,  1,90 km,   2,80 EUR
sortiere(feld, nachStationDannPreis):
   Ausleihe 11: Kunde 5005, Rad 102, Station 1 -> 2, 2026-09-17,   9 min,  1,90 km,   2,80 EUR
   Ausleihe  1: Kunde 5001, Rad 101, Station 1 -> 3, 2026-09-14,  18 min,  4,60 km,   0,00 EUR
   Ausleihe  9: Kunde 5001, Rad 101, Station 1 -> 6, 2026-09-16,  26 min,  7,30 km,   0,00 EUR
   Ausleihe  2: Kunde 5002, Rad 105, Station 2 -> 1, 2026-09-14,  12 min,  2,10 km,   3,40 EUR
   Ausleihe  3: Kunde 5003, Rad 106, Station 3 -> 5, 2026-09-14,  75 min,  9,80 km,   7,50 EUR
   Ausleihe  8: Kunde 5003, Rad 107, Station 3 -> 3, 2026-09-16,  40 min,  6,20 km,   4,00 EUR
   Ausleihe  4: Kunde 5001, Rad 104, Station 3 -> 1, 2026-09-14,  22 min,  4,90 km,   0,00 EUR
   Ausleihe  6: Kunde 5005, Rad 109, Station 4 -> 2, 2026-09-15,  31 min,  5,00 km,   7,20 EUR
   Ausleihe  5: Kunde 5004, Rad 110, Station 5 -> 4, 2026-09-15,  95 min, 12,40 km,   6,50 EUR
   Ausleihe 12: Kunde 5003, Rad 106, Station 5 -> 3, 2026-09-17,  58 min,  9,10 km,   5,80 EUR
   Ausleihe  7: Kunde 5002, Rad 111, Station 6 -> 3, 2026-09-15,  64 min,  8,70 km,  13,80 EUR
   Ausleihe 10: Kunde 5004, Rad 112, Station 6 -> 1, 2026-09-16,  33 min,  7,10 km,   0,30 EUR
Kontrolle mit Java-Comparator, Reihenfolge der Ausleihnummern: 11 1 9 2 3 8 4 6 5 12 7 10
Bewertung: a) Schleifengrenzen 2 P, Aufruf der übergebenen Funktion mit Auswertung > 0 2 P, Tausch 2 P, kein fest verdrahtetes Attribut 1 P (andere Verfahren wie Selection- oder Insertionsort ebenfalls volle Punkte). b) je Fall (größer, kleiner, gleich) 1 P. c) Stationsvergleich 2 P, Preisvergleich absteigend 2 P, Gleichheit 1 P. d) Reihenfolge 2 P, Stabilität erläutert 1 P.

Erklärung: Der Sortieralgorithmus kennt kein Attribut mehr – er fragt nur „steht a hinter b?“ (> 0). Dadurch genügt ein getesteter Algorithmus für beliebig viele Sortierkriterien. In Java entspricht die Vergleichsfunktion einem Comparator<Ausleihe>, der z. B. als Methodenreferenz Aufgabe_ALG_05_3::nachDauerAbsteigend oder als Lambda übergeben wird.

ALG-06 · Zeichenketten verarbeiten

Aufgabe ALG-06.1 · 4 Punkte

a) (2 P)

AusdruckErgebnis
code.length()9
code.charAt(3)'0' (Zeichen)
code.substring(3, 7)"0107"
code.indexOf('-')2

b) (2 P)

Strings sind Objekte. == vergleicht bei Objekten die Referenzen, also ob beide Variablen auf dasselbe Objekt im Speicher zeigen. Zwei inhaltsgleiche Strings (z. B. einer aus einer Benutzereingabe, einer im Programm) sind meist verschiedene Objekte – == liefert dann false. equals() vergleicht dagegen den Inhalt Zeichen für Zeichen. (Dass == bei zwei gleichen Literalen manchmal zufällig true liefert, liegt am String-Pool und ist nicht verlässlich.)

Kontrolle mit Java · Aufgabe_ALG_06_1.java · Ausgabe

code.length()        = 9
code.charAt(3)       = '0'
code.substring(3, 7) = "0107"
code.indexOf('-')    = 2
a == b      -> false   (vergleicht Referenzen)
a.equals(b) -> true    (vergleicht Inhalte)
Bewertung: a) je Ausdruck 0,5 P (bei charAt genügt 0). b) Referenzvergleich 1 P, Inhaltsvergleich mit equals 1 P.
Aufgabe ALG-06.2 · 10 Punkte

a) pruefePasswort (8 P)

funktion pruefePasswort(pw : string) : boolean
  wenn pw.length() < 10 dann
    rückgabe false
  Ende wenn
  sonderzeichen : string := "!?#+-_*"
  gross : boolean := false
  klein : boolean := false
  ziffer : boolean := false
  sonder : boolean := false
  zähle i von 0 bis pw.length() - 1
    c : char := pw.charAt(i)
    wenn c = ' ' dann
      rückgabe false                              // Leerzeichen verboten
    Ende wenn
    wenn istGrossbuchstabe(c) dann
      gross := true
    Ende wenn
    wenn istKleinbuchstabe(c) dann
      klein := true
    Ende wenn
    wenn istZiffer(c) dann
      ziffer := true
    Ende wenn
    wenn sonderzeichen.indexOf(c) >= 0 dann
      sonder := true
    Ende wenn
  Ende zähle
  rückgabe gross und klein und ziffer und sonder   // erst NACH der Schleife auswerten
Ende funktion

b) Ergebnisse (2 P)

PasswortErgebnisGrund
Rad2026!falsenur 8 Zeichen
auenrad#2026falsekein Großbuchstabe
Auenrad#2026truealle Regeln erfüllt
Auen Rad#2026falseenthält ein Leerzeichen

Java · Aufgabe_ALG_06_2.java (Auszug)

// a) Passwortrichtlinie der Auenrad-App
static boolean pruefePasswort(String pw) {
    if (pw.length() < 10) {
        return false;
    }
    String sonderzeichen = "!?#+-_*";
    boolean gross = false;
    boolean klein = false;
    boolean ziffer = false;
    boolean sonder = false;
    for (int i = 0; i <= pw.length() - 1; i++) {
        char c = pw.charAt(i);
        if (c == ' ') {
            return false;
        } else if (istGrossbuchstabe(c)) {
            gross = true;
        } else if (istKleinbuchstabe(c)) {
            klein = true;
        } else if (istZiffer(c)) {
            ziffer = true;
        } else if (sonderzeichen.indexOf(c) >= 0) {
            sonder = true;
        }
    }
    return gross && klein && ziffer && sonder;
}

Ausgabe

pruefePasswort("Rad2026!") = false
pruefePasswort("auenrad#2026") = false
pruefePasswort("Auenrad#2026") = true
pruefePasswort("Auen Rad#2026") = false
pruefePasswort("AUENRAD#2026x") = true
pruefePasswort("Auenrad2026") = false
Bewertung: a) Längenprüfung (< 10 → false, also „mindestens 10“) 1 P, Schleife über alle Zeichen (0 bis length() - 1) 1 P, je Zeichenklasse Merker korrekt initialisiert und gesetzt 4 × 1 P, Leerzeichenprüfung 1 P, Gesamtergebnis nach der Schleife 1 P. b) je Passwort 0,5 P.

Erklärung: „Mindestens ein …“ verlangt Merker, die erst nach der Schleife ausgewertet werden. Genau diese Fehler steckten im Struktogramm von W21: Länge mit > statt >= geprüft, Schleife bis ≤ length und ein Merker, der im sonst-Zweig wieder auf false gesetzt wurde.

Aufgabe ALG-06.3 · 14 Punkte

a) zerlegeName (6 P)

funktion zerlegeName(vollname : string) : string[]
  ergebnis : string[] := new string[2]
  pos : integer := vollname.indexOf(',')
  wenn pos = -1 dann                              // kein Komma
    ergebnis[0] := ""
    ergebnis[1] := vollname.trim()
  sonst
    ergebnis[1] := vollname.substring(0, pos).trim()                       // Nachname
    ergebnis[0] := vollname.substring(pos + 1, vollname.length()).trim()   // Vorname
  Ende wenn
  rückgabe ergebnis
Ende funktion

b) pruefeRadCode (6 P)

funktion pruefeRadCode(code : string) : boolean
  wenn code.length() <> 9 dann
    rückgabe false
  Ende wenn
  wenn code.substring(0, 3) <> "AR-" oder code.charAt(7) <> '-' dann
    rückgabe false
  Ende wenn
  summe : integer := 0
  zähle i von 3 bis 6
    c : char := code.charAt(i)
    wenn nicht istZiffer(c) dann
      rückgabe false
    Ende wenn
    summe := summe + zahlwert(c)
  Ende zähle
  p : char := code.charAt(8)
  wenn nicht istZiffer(p) dann
    rückgabe false
  Ende wenn
  rückgabe zahlwert(p) = summe mod 10
Ende funktion

c) Probleme beim Zerlegen (2 P)

Java · Aufgabe_ALG_06_3.java (Auszug)

// a) "Nachname, Vorname" -> [Vorname, Nachname]
static String[] zerlegeName(String vollname) {
    String[] ergebnis = new String[2];
    int pos = vollname.indexOf(',');
    if (pos == -1) {
        ergebnis[0] = "";
        ergebnis[1] = vollname.trim();
    } else {
        ergebnis[1] = vollname.substring(0, pos).trim();
        ergebnis[0] = vollname.substring(pos + 1, vollname.length()).trim();
    }
    return ergebnis;
}

// b) Format "AR-" + 4 Ziffern + "-" + Prüfziffer (= Summe der 4 Ziffern mod 10)
static boolean pruefeRadCode(String code) {
    if (code.length() != 9) {
        return false;
    }
    if (!code.substring(0, 3).equals("AR-") || code.charAt(7) != '-') {
        return false;
    }
    int summe = 0;
    for (int i = 3; i <= 6; i++) {
        char c = code.charAt(i);
        if (!istZiffer(c)) {
            return false;
        }
        summe = summe + zahlwert(c);
    }
    char p = code.charAt(8);
    if (!istZiffer(p)) {
        return false;
    }
    return zahlwert(p) == summe % 10;
}

Ausgabe

zerlegeName("Schulze-Ebert, Anna Lena") -> Vorname "Anna Lena", Nachname "Schulze-Ebert"
zerlegeName("Haas,Tom") -> Vorname "Tom", Nachname "Haas"
zerlegeName("  Nowak ,  Piotr ") -> Vorname "Piotr", Nachname "Nowak"
zerlegeName("Müller") -> Vorname "", Nachname "Müller"
pruefeRadCode("AR-0107-8") = true
pruefeRadCode("AR-0107-7") = false
pruefeRadCode("AR-01O7-8") = false
pruefeRadCode("AR-107-8") = false
pruefeRadCode("AR-0959-3") = true
pruefeRadCode("XR-0107-8") = false
Bewertung: a) Position des Kommas bestimmt 1 P, Fall ohne Komma 1 P, Nachname mit substring(0, pos) 1 P, Vorname ab pos + 1 1 P, trim 1 P, Array angelegt und zurückgegeben 1 P. b) Länge 1 P, „AR-“ und Bindestrich an Position 7 1 P, Schleife über die vier Ziffern mit Ziffernprüfung 2 P, Summe und Vergleich der Prüfziffer mit mod 10 2 P. c) je sinnvollem Problem 1 P.

Erklärung: Bei substring(von, bis) ist bis ausgeschlossen – substring(0, pos) endet also genau vor dem Komma. In Java muss der Präfixvergleich mit equals erfolgen (!code.substring(0, 3).equals("AR-")).

ALG-07 · Exceptions / Fehlerbehandlung

Aufgabe ALG-07.1 · 5 Punkte

a) Checked Exceptions erben von Exception (aber nicht von RuntimeException). Der Compiler erzwingt, dass sie gefangen (try/catch) oder mit throws in der Methodensignatur weitergereicht werden. Sie beschreiben äußere Umstände, die auch korrekter Code nicht verhindern kann, z. B. IOException, SQLException. Unchecked Exceptions erben von RuntimeException; der Compiler verlangt keine Behandlung. Sie entstehen meist durch Programmierfehler, z. B. NullPointerException, ArithmeticException, IllegalArgumentException.

b) Der finally-Block wird immer ausgeführt, nachdem der try-Block (und ggf. ein catch-Block) beendet ist – egal, ob eine Ausnahme aufgetreten ist, ob sie gefangen wurde und auch bei einem return im try-Block. Typischer Einsatz: Ressourcen freigeben, z. B. Datei oder Datenbankverbindung schließen, Sperre freigeben.

Bewertung: a) Unterschied (Behandlungspflicht durch den Compiler) 1 P, je Beispiel 1 P. b) Zeitpunkt 1 P, Einsatzzweck 1 P.
Aufgabe ALG-07.2 · 9 Punkte

a) Ausgaben und Rückgabewerte (6 P)

AufrufAusgabeRückgabewert / Ablauf
centProMinute("750", 30)A B E F25 (750 / 30, kein Fehler)
centProMinute("7,50", 30)A C E F−1: parseInt wirft NumberFormatException, ergebnis behält den Startwert
centProMinute("750", 0)A D E F0: Ganzzahldivision durch 0 wirft ArithmeticException, der catch-Block setzt 0
centProMinute("750", -5)A Ekein Rückgabewert: die IllegalArgumentException wird von keinem catch-Block gefangen; nach dem finally-Block (E) wird sie an den Aufrufer weitergereicht, „F“ und return werden nicht mehr ausgeführt

Kontrolle mit Java · Aufgabe_ALG_07_2.java · Ausgabe (der Aufrufer fängt die Ausnahme des 4. Aufrufs ab, um sie anzuzeigen)

1) centProMinute("750", 30): Ausgabe A B E F | Rückgabe 25
2) centProMinute("7,50", 30): Ausgabe A C E F | Rückgabe -1
3) centProMinute("750", 0): Ausgabe A D E F | Rückgabe 0
4) centProMinute("750", -5): Ausgabe A E | keine Rückgabe
   -> Ausnahme beim Aufrufer: IllegalArgumentException: Minuten negativ

b) Beurteilung (3 P)

Der Vorschlag ist schlecht. Ein leerer Block catch (Exception e) { } verschluckt jeden Fehler: Es gibt weder Meldung noch Protokolleintrag, und der Aufrufer rechnet mit −1 weiter, als wäre nichts passiert. Außerdem fängt er auch Ausnahmen ab, die gar nicht hier behandelt werden sollen (die IllegalArgumentException bei negativen Minuten, Programmierfehler wie NullPointerException), sodass Fehler verdeckt werden. Eine unterschiedliche Reaktion (Division durch 0 → 0, Formatfehler → −1) ist nicht mehr möglich. Besser: gezielt die erwarteten Ausnahmen fangen, sinnvoll reagieren und protokollieren.

Bewertung: a) je Aufruf 1,5 P (Ausgabe 1 P, Rückgabe/Ablauf 0,5 P); beim 4. Aufruf muss erkannt werden, dass F fehlt und kein Wert zurückkommt. b) 3 P für eine begründete Ablehnung mit mindestens zwei Argumenten (Fehler verschluckt/kein Log, zu allgemeiner Fang, keine differenzierte Reaktion).
Aufgabe ALG-07.3 · 14 Punkte

a) Fehlerfälle (3 P)

b) durchschnittsDauer (7 P)

funktion durchschnittsDauer(ausleihen : List<Ausleihe>, stationNr : integer) : double
  wenn ausleihen = null dann
    throw new IllegalArgumentException("Liste der Ausleihen fehlt")
  Ende wenn
  wenn stationNr < 1 dann
    throw new IllegalArgumentException("Ungültige Stationsnummer: " + stationNr)
  Ende wenn
  summe : integer := 0
  anzahl : integer := 0
  zähle i von 0 bis ausleihen.size() - 1
    a : Ausleihe := ausleihen.get(i)
    wenn a.getStartStation() = stationNr dann
      summe := summe + a.getDauer()
      anzahl := anzahl + 1
    Ende wenn
  Ende zähle
  wenn anzahl = 0 dann
    throw new KeineDatenException("Keine Ausleihen für Station " + stationNr)
  Ende wenn
  rückgabe summe / anzahl              // Kommazahl! Java: (double) summe / anzahl
Ende funktion

c) zeigeStationsbericht (4 P)

funktion zeigeStationsbericht(ausleihen : List<Ausleihe>, stationNr : integer) : void
  try
    d : double := durchschnittsDauer(ausleihen, stationNr)
    ausgabe("Station " + stationNr + ": durchschnittliche Dauer " + d + " min")
  catch (KeineDatenException e)
    ausgabe("Für diese Station liegen keine Ausleihen vor.")
  catch (IllegalArgumentException e)
    protokolliere(e.getMessage())
  Ende try
Ende funktion

Java · Aufgabe_ALG_07_3.java (Auszug) – in Java muss die geprüfte Ausnahme mit throws deklariert werden

/** eigene, geprüfte (checked) Ausnahme */
static class KeineDatenException extends Exception {
    private static final long serialVersionUID = 1L;

    KeineDatenException(String meldung) {
        super(meldung);
    }
}

// b) durchschnittliche Dauer der Ausleihen, die an stationNr beginnen
static double durchschnittsDauer(List<Ausleihe> ausleihen, int stationNr)
        throws KeineDatenException {
    if (ausleihen == null) {
        throw new IllegalArgumentException("Liste der Ausleihen fehlt (null)");
    }
    if (stationNr < 1) {
        throw new IllegalArgumentException("Ungültige Stationsnummer: " + stationNr);
    }
    int summe = 0;
    int anzahl = 0;
    for (int i = 0; i < ausleihen.size(); i++) {
        Ausleihe a = ausleihen.get(i);
        if (a.getStartStation() == stationNr) {
            summe = summe + a.getDauer();
            anzahl = anzahl + 1;
        }
    }
    if (anzahl == 0) {
        throw new KeineDatenException("Keine Ausleihen für Station " + stationNr);
    }
    return (double) summe / anzahl;
}

// c) Aufrufer: fängt die Ausnahmen und reagiert benutzerfreundlich
static void zeigeStationsbericht(List<Ausleihe> ausleihen, int stationNr) {
    try {
        double d = durchschnittsDauer(ausleihen, stationNr);
        System.out.printf(Locale.GERMANY, "Station %d: durchschnittliche Dauer %.1f min%n",
                stationNr, d);
    } catch (KeineDatenException e) {
        System.out.println("Station " + stationNr
                + ": Für diese Station liegen keine Ausleihen vor.");
    } catch (IllegalArgumentException e) {
        System.out.println("FEHLER-LOG: " + e.getMessage());
    }
}

Ausgabe

Station 3: durchschnittliche Dauer 45,7 min
Station 4: durchschnittliche Dauer 31,0 min
Station 7: Für diese Station liegen keine Ausleihen vor.
FEHLER-LOG: Ungültige Stationsnummer: 0
FEHLER-LOG: Liste der Ausleihen fehlt (null)
Bewertung: a) je Fehlerfall 1 P. b) Prüfungen auf null und < 1 mit throw 2 P, Schleife mit Summe und Anzahl 2 P, KeineDatenException bei Anzahl 0 (vor der Division) 2 P, Rückgabe als Kommazahl 1 P. c) Aufruf und Ausgabe im try-Block 1 P, catch für KeineDatenException mit Meldung 1 P, catch für IllegalArgumentException mit protokolliere(e.getMessage()) 1 P, korrekte Struktur (Ausgabe nur im Erfolgsfall) 1 P.

Erklärung: Die Prüfungen am Anfang („Wächter“) brechen bei Vertragsverletzungen sofort ab (fail fast). Die fachliche Situation „keine Daten“ wird als eigene, geprüfte Ausnahme gemeldet; der Aufrufer muss sich darum kümmern und kann eine verständliche Meldung anzeigen, statt eine falsche Zahl (0 oder NaN) auszugeben.

ALG-08 · Rekursion

Aufgabe ALG-08.1 · 4 Punkte

a) Der Basisfall ist die Abbruchbedingung: Für ihn liefert die Funktion ihr Ergebnis direkt, ohne sich selbst erneut aufzurufen (z. B. 0! = 1). Im Rekursionsfall ruft sich die Funktion mit einem kleineren bzw. einfacheren Teilproblem selbst auf und setzt aus dessen Ergebnis ihr eigenes zusammen (z. B. n! = n · (n − 1)!). Der Rekursionsfall muss sich dem Basisfall nähern.

b) Die Funktion ruft sich endlos selbst auf. Jeder Aufruf legt einen neuen Eintrag (Parameter, lokale Variablen, Rücksprungadresse) auf dem Aufrufstapel ab, bis der Stack-Speicher erschöpft ist. Java bricht dann mit einem StackOverflowError ab.

Bewertung: a) je Begriff 1 P. b) Endlosrekursion 1 P, Stacküberlauf/Programmabbruch 1 P.
Aufgabe ALG-08.2 · 8 Punkte

a) Schreibtischtest binaer(13) (5 P)

Aufrufnn div 2n mod 2Rückgabewert
11361binaer(6) + "1" = "110" + "1" = "1101"
2630binaer(3) + "0" = "11" + "0" = "110"
3311binaer(1) + "1" = "1" + "1" = "11"
4101binaer(0) + "1" = "" + "1" = "1"
50––"" (Basisfall)
6– (nicht benötigt)

b) Fehler und Korrektur (3 P)

Für n = 0 liefert die Funktion den leeren Text "" statt "0". Korrektur: Der Basisfall gilt für n < 2 und gibt die Ziffer selbst zurück.

funktion binaer(n : integer) : string
  wenn n < 2 dann
    rückgabe "" + n                               // "0" bzw. "1"
  Ende wenn
  rückgabe binaer(n div 2) + (n mod 2)
Ende funktion

Achtung: Nur den Basisfall auf wenn n = 0 dann rückgabe "0" zu ändern, wäre falsch – dann entstünde für jede Zahl eine führende Null (binaer(13) = "01101").

Java · Aufgabe_ALG_08_2.java (Auszug, korrigierte Funktion)

// korrigierte Funktion: Basisfall n < 2 liefert die Ziffer selbst
static String binaerKorrigiert(int n) {
    if (n < 2) {
        return "" + n;
    }
    return binaerKorrigiert(n / 2) + (n % 2);
}

Ausgabe (mit Aufrufprotokoll der ursprünglichen Funktion)

a) Schreibtischtest binaer(13):
binaer(13) aufgerufen
   binaer(6) aufgerufen
      binaer(3) aufgerufen
         binaer(1) aufgerufen
            binaer(0) aufgerufen
            binaer(0) liefert ""
         binaer(1) liefert "1"  (n div 2 = 0, n mod 2 = 1)
      binaer(3) liefert "11"  (n div 2 = 1, n mod 2 = 1)
   binaer(6) liefert "110"  (n div 2 = 3, n mod 2 = 0)
binaer(13) liefert "1101"  (n div 2 = 6, n mod 2 = 1)
   Ergebnis: "1101"
binaer(0) aufgerufen
binaer(0) liefert ""
b) binaer(0) = ""  <- falsch, erwartet "0"
   binaerKorrigiert(0) = "0"  Kontrolle: 0
   binaerKorrigiert(1) = "1"  Kontrolle: 1
   binaerKorrigiert(2) = "10"  Kontrolle: 10
   binaerKorrigiert(13) = "1101"  Kontrolle: 1101
   binaerKorrigiert(45) = "101101"  Kontrolle: 101101
   binaerKorrigiert(255) = "11111111"  Kontrolle: 11111111
Bewertung: a) je korrekt ausgefüllter Aufrufzeile 1 P (5 Aufrufe). b) Wert 0 erkannt 1 P, korrekte Korrektur 2 P (Lösungen mit führender Null nur 1 P).
Aufgabe ALG-08.3 · 12 Punkte

a) rekursive binäre Suche (6 P)

funktion suche(nummern : integer[], gesucht : integer,
               links : integer, rechts : integer) : integer
  wenn links > rechts dann
    rückgabe -1                                   // Basisfall 1: Suchbereich leer
  Ende wenn
  mitte : integer := (links + rechts) div 2
  wenn nummern[mitte] = gesucht dann
    rückgabe mitte                                // Basisfall 2: gefunden
  Ende wenn
  wenn nummern[mitte] < gesucht dann
    rückgabe suche(nummern, gesucht, mitte + 1, rechts)   // rechte Hälfte
  sonst
    rückgabe suche(nummern, gesucht, links, mitte - 1)    // linke Hälfte
  Ende wenn
Ende funktion

b) Aufrufe für gesucht = 112 (2 P)

Aufruflinksrechtsmittenummern[mitte]weiter
10115106rechts suchen
26118109rechts suchen
391110111rechts suchen
4111111112gefunden → Index 11

c) Rekursion vs. Iteration (4 P)

Java · Aufgabe_ALG_08_3.java (Auszug, rekursiv und iterativ)

// a) rekursive binäre Suche in einem aufsteigend sortierten Array; -1 = nicht gefunden
static int suche(int[] nummern, int gesucht, int links, int rechts) {
    String info = links <= rechts ? "  mitte = " + ((links + rechts) / 2) : "  -> links > rechts";
    System.out.println("   suche(nummern, " + gesucht + ", " + links + ", " + rechts + ")" + info);
    if (links > rechts) {
        return -1;                                      // Basisfall 1: Bereich leer
    }
    int mitte = (links + rechts) / 2;
    if (nummern[mitte] == gesucht) {
        return mitte;                                   // Basisfall 2: gefunden
    } else if (nummern[mitte] < gesucht) {
        return suche(nummern, gesucht, mitte + 1, rechts);  // rechte Hälfte
    } else {
        return suche(nummern, gesucht, links, mitte - 1);   // linke Hälfte
    }
}

// zum Vergleich: iterative Variante
static int sucheIterativ(int[] nummern, int gesucht) {
    int links = 0;
    int rechts = nummern.length - 1;
    while (links <= rechts) {
        int mitte = (links + rechts) / 2;
        if (nummern[mitte] == gesucht) {
            return mitte;
        } else if (nummern[mitte] < gesucht) {
            links = mitte + 1;
        } else {
            rechts = mitte - 1;
        }
    }
    return -1;
}

Ausgabe

b) Aufrufe für gesucht = 112:
   suche(nummern, 112, 0, 11)  mitte = 5
   suche(nummern, 112, 6, 11)  mitte = 8
   suche(nummern, 112, 9, 11)  mitte = 10
   suche(nummern, 112, 11, 11)  mitte = 11
   Ergebnis: Index 11
   Aufrufe für gesucht = 100:
   suche(nummern, 100, 0, 11)  mitte = 5
   suche(nummern, 100, 0, 4)  mitte = 2
   suche(nummern, 100, 0, 1)  mitte = 0
   suche(nummern, 100, 0, -1)  -> links > rechts
   Ergebnis: Index -1
   iterativ: 112 -> 11, 104 -> 3, 100 -> -1
   max. Anzahl Halbierungen bei 1000 Elementen: 10
Bewertung: a) Basisfall „Bereich leer“ 1 P, Berechnung von mitte 1 P, Basisfall „gefunden“ 1 P, rekursive Aufrufe mit korrekten Grenzen (mitte + 1 / mitte - 1) 2 P, Ergebnis des rekursiven Aufrufs zurückgegeben 1 P. b) 2 P für alle vier Aufrufe. c) je Vor- und Nachteil 1 P Nennung + 1 P Begründung.

ALG-09 · Nebenläufigkeit

Aufgabe ALG-09.1 · 4 Punkte

a) Eine Race Condition liegt vor, wenn mehrere nebenläufige Abläufe (Threads, Prozesse, Server) auf dieselbe Ressource zugreifen, mindestens einer davon schreibend, und das Ergebnis von der zufälligen zeitlichen Reihenfolge der Zugriffe abhängt. Folge sind inkonsistente Daten, z. B. verlorene Änderungen oder Doppelbuchungen.

b) (zwei davon) Zwei Kunden reservieren bzw. leihen gleichzeitig dasselbe (letzte) Rad aus. · Zwei Kunden geben gleichzeitig an einer fast vollen Station zurück (Zähler freier Stellplätze). · Station meldet den Akkustand, während die Werkstatt den Datensatz desselben Rades ändert. · Das Kundenkonto wird gleichzeitig in App und Kundenportal geändert (z. B. Guthaben/Abo).

Bewertung: a) gleichzeitiger Zugriff auf gemeinsame Daten 1 P, Ergebnis abhängig von der Reihenfolge/Folgen 1 P. b) je passendem Beispiel aus dem Szenario 1 P.
Aufgabe ALG-09.2 · 8 Punkte

a) Problem (4 P)

Es entsteht eine Doppelbuchung (Lost Update): Beide Threads lesen bei t1 und t2 den Wert 1, bevor einer von ihnen schreibt. Beide prüfen auf Basis dieses veralteten Werts (t3, t4), beide schreiben 0 und speichern eine Reservierung (t5, t6). Für ein einziges Lastenrad gibt es nun zwei Reservierungen; die Änderung von Thread A wird von B überschrieben, der Bestand müsste eigentlich „−1“ sein. Ursache: Die Folge Lesen – Prüfen – Schreiben ist ein kritischer Abschnitt, der nicht atomar bzw. nicht gegen gleichzeitigen Zugriff geschützt ist.

b) Pessimistisches Sperren (4 P)

Beim pessimistischen Sperren sperrt ein Thread den Datensatz vor dem Lesen (z. B. in einer Transaktion mit SELECT … FOR UPDATE, in Java mit synchronized/Lock). Andere Threads, die denselben Datensatz sperren wollen, müssen warten, bis die Sperre mit dem Ende der Transaktion (Commit) freigegeben wird. Dadurch kann immer nur ein Thread den kritischen Abschnitt durchlaufen.

ZeitThread A (Jana)Thread B (Tom)DB
t1sperrt Datensatz, liest 11 (gesperrt von A)
t2will sperren → wartet1
t3prüft 1 > 0 → jawartet1
t4schreibt 0, speichert Reservierung, Commit → Sperre freiwartet0
t5erhält Sperre, liest 00 (gesperrt von B)
t6prüft 0 > 0 → nein → „kein Lastenrad frei“, Sperre frei0

Java · Aufgabe_ALG_09_2.java (Auszug: geschützte Variante)

/** geschützt: kritischer Abschnitt unter einer Sperre (pessimistisch) */
static void reserviereGesperrt(String kunde) throws InterruptedException {
    synchronized (SPERRE) {
        int frei = freieLastenraeder;
        Thread.sleep(50);
        if (frei > 0) {
            freieLastenraeder = frei - 1;
            reservierungen++;
            System.out.println("   " + kunde + ": Reservierung bestätigt");
        } else {
            System.out.println("   " + kunde + ": leider kein Rad frei");
        }
    }
}

Ausgabe (Reihenfolge der Threadzeilen kann je Lauf variieren)

Ohne Sperre (1 freies Lastenrad, 2 gleichzeitige Anfragen):
   Thread B (Kunde Tom)  : Reservierung bestätigt
   Thread A (Kundin Jana): Reservierung bestätigt
   => Reservierungen: 2, freieLastenraeder: 0
Mit Sperre (synchronized):
   Thread A (Kundin Jana): Reservierung bestätigt
   Thread B (Kunde Tom)  : leider kein Rad frei
   => Reservierungen: 1, freieLastenraeder: 0
Bewertung: a) Problem benannt (Doppelbuchung/Lost Update) 2 P, Ursache am Ablauf erläutert (beide lesen den alten Wert, nicht atomar) 2 P. b) Prinzip der Sperre 2 P, geänderter Ablauf (B wartet und erhält danach 0) 2 P.
Aufgabe ALG-09.3 · 14 Punkte

a) Vor- und Nachteile (6 P)

VarianteVorteilNachteil
A (pessimistisch, Sperre bei Detailansicht)Wer die Detailansicht geöffnet hat, kann das Rad sicher reservieren; ein Konflikt beim Speichern ist ausgeschlossen, die Logik ist einfach.Das Rad ist bis zu 10 Minuten blockiert, auch wenn der Kunde nur schaut oder die App schließt; andere sehen es als „nicht verfügbar“. Viele offene Sperren belasten die Datenbank; Sperren müssen bei Verbindungsabbruch zuverlässig per Timeout aufgehoben werden.
B (optimistisch, Versionsnummer)Keine Blockade: Beliebig viele Kunden können das Rad gleichzeitig ansehen; Sperren dauern nur Millisekunden, das System skaliert gut, keine Deadlocks.Ein Konflikt fällt erst beim Tippen auf „Reservieren“ auf; der langsamere Kunde erhält eine Fehlermeldung und muss neu wählen. Zusätzliche Versionsspalte und Konfliktbehandlung nötig.

b) Entscheidung (3 P)

Empfohlen wird Variante B. Viele Kunden sehen sich Räder an, aber nur selten wollen zwei Kunden in denselben Sekunden dasselbe Lastenrad reservieren – Konflikte sind also selten, und dann ist eine Meldung „leider gerade vergeben“ zumutbar. Variante A hält eine Sperre über eine Benutzerinteraktion von bis zu 10 Minuten; das ist viel zu lang, weil das Rad in dieser Zeit für alle anderen blockiert ist. Sperren sollten nur so lange dauern wie die eigentliche Änderung (Millisekunden) – das leistet die unteilbare Operation in Variante B.

c) reserviere (5 P)

funktion reserviere(db : RadDB, radNr : integer, kundenNr : integer) : boolean
  info : RadInfo := db.leseRad(radNr)
  wenn info.getStatus() <> "frei" dann
    rückgabe false                       // schon reserviert oder ausgeliehen
  Ende wenn
  wenn db.setzeStatusWennVersion(radNr, "reserviert", info.getVersion()) dann
    db.legeReservierungAn(radNr, kundenNr)
    rückgabe true
  sonst
    rückgabe false                       // Version geändert: jemand war schneller
  Ende wenn
Ende funktion

Java · Aufgabe_ALG_09_3.java (Auszug, mit simulierter Datenbank und zwei gleichzeitigen Threads)

// c) Reservierung mit optimistischem Sperren
static boolean reserviere(RadDB db, int radNr, int kundenNr) {
    RadInfo info = db.leseRad(radNr);
    if (!info.getStatus().equals("frei")) {
        return false;                                   // schon vergeben
    }
    // ... Kunde bestätigt in der App, Zeit vergeht ...
    try { Thread.sleep(30); } catch (InterruptedException e) { }
    if (db.setzeStatusWennVersion(radNr, "reserviert", info.getVersion())) {
        db.legeReservierungAn(radNr, kundenNr);
        return true;
    }
    return false;                                       // Konflikt: jemand war schneller
}

Ausgabe

Gleichzeitige Reservierungsversuche für Rad 106: 2
davon erfolgreich: 1, abgewiesen (Konflikt): 1
Status danach: reserviert, Version: 8, gespeicherte Reservierungen: 1
Dritter Versuch später: reserviere(...) = false
Bewertung: a) je Vor- bzw. Nachteil 1,5 P. b) Entscheidung mit fachlicher Begründung 2 P, Bezug zur Sperrdauer 1 P (Variante A mit überzeugender Begründung, z. B. stark verkürzter Sperrdauer von 1–2 Minuten, ebenfalls volle Punktzahl). c) leseRad 1 P, Statusprüfung 1 P, setzeStatusWennVersion mit der zuvor gelesenen Version 2 P, Reservierung nur bei Erfolg und korrekte Rückgabewerte 1 P.

Erklärung: Entscheidend ist, dass Prüfen und Ändern in einer unteilbaren Operation passieren. Wer stattdessen erst den Status liest und dann einfach „reserviert“ schreibt, hat genau die Race Condition aus ALG-09.2. In einer echten Anwendung würden Statusänderung und Anlegen der Reservierung zusätzlich in einer Transaktion zusammengefasst, damit nicht nur eines von beiden gespeichert wird.