Rückblick
Testen und Laufzeitbetrachtung sind die beiden Werkzeuge, mit denen man ein Programm beurteilt, statt es nur zu schreiben. Beide beantworten Fragen, die der Übersetzer nicht stellt: Stimmt es auch in den Randfällen? Und bleibt es brauchbar, wenn die Daten wachsen?
Das kann ich jetzt
- Ich kann Testfälle systematisch auswählen: Normalfall, Grenzfälle, Sonderfälle. (7.1)
- Ich kann einen Test mit
assertEqualsschreiben und eine brauchbare Meldung dazu formulieren. (7.1) - Ich kann begründen, warum „sieht plausibel aus" kein Test ist. (7.1)
- Ich kann die Komplexitätsklassen benennen und einem Algorithmus zuordnen. (7.2)
- Ich kann aus dem Aufbau eines Programms auf seine Klasse schließen. (7.2)
- Ich kann Zeitbedarf und Speicherbedarf getrennt beurteilen. (7.2)
Gemischte Aufgaben
Aufgabe 1: Testfälle finden
Diese Methode soll das Maximum eines Feldes liefern:
public int maximum(int[] pWerte) {
int groesster = 0;
for (int i = 0; i < pWerte.length; i++) {
if (pWerte[i] > groesster) {
groesster = pWerte[i];
}
}
return groesster;
}
a) Nenne fünf Testfälle: einen Normalfall, zwei Grenzfälle und zwei Sonderfälle.
b) Bei welchem deiner Testfälle liefert die Methode ein falsches Ergebnis? Warum?
c) Berichtige die Methode.
d) Schreib die Testklasse mit @Test und lass sie im Testrunner laufen. Alle Tests müssen grün sein.
e) Ein Sonderfall lässt sich nicht durch Berichtigen retten. Welcher, und wie geht man damit um?
Tipp zu a): Wo suche ich Testfälle?
Immer an denselben Stellen:
- Normalfall: ein typisches Feld mit verschiedenen Werten.
- Grenzfälle: das Maximum steht ganz vorn; es steht ganz hinten; das Feld hat genau ein Element.
- Sonderfälle: alle Werte negativ; alle Werte gleich; ein leeres Feld.
Aufgabe 2: Klassen zuordnen
Bestimme für jeden Ausschnitt die Komplexitätsklasse in Abhängigkeit von n. Begründe mit einer der drei Faustregeln.
// a)
for (int i = 0; i < n; i++) {
summe = summe + i;
}
// b)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
IO.println(i + " " + j);
}
}
// c)
for (int i = 0; i < n; i++) {
summe = summe + i;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
produkt = produkt * 2;
}
}
// d)
int i = n;
while (i > 1) {
i = i / 2;
}
// e)
for (int i = 0; i < n; i++) {
int j = n;
while (j > 1) {
j = j / 2;
}
}
Ordne danach zu: lineare Suche, binäre Suche, Bubblesort, Mergesort, Zugriff auf ein Feldelement.
Aufgabe 3: Beurteilen und entscheiden
a) Ein Programm braucht für 1000 Datensätze 2 Sekunden. Wie lange braucht es für 10 000, wenn es in O(n), in O(n · log n) und in O(n²) läuft? Rechne für jede Klasse.
b) Warum ist O(n²) für 1000 Elemente meist völlig unproblematisch, für 100 000 aber nicht mehr? Nenne konkrete Zahlen.
c) Ein Verfahren ist in O(n), braucht aber zusätzlichen Speicher in O(n). Ein anderes ist in O(n · log n) und braucht O(1) zusätzlichen Speicher. Wann würdest du welches wählen? Nenne je eine Situation.
d) Eine Kollegin sagt: „Mein Algorithmus ist O(n), der andere O(n²) – meiner ist also immer schneller." Nimm Stellung.
e) Warum ist eine gemessene Laufzeit allein kein guter Beleg für die Güte eines Verfahrens? Nenne zwei Gründe.
Tipp zu a)
Rechne mit Verhältnissen. Wird n verzehnfacht, dann wächst
- O(n) um den Faktor 10,
- O(n²) um den Faktor 100,
- O(n · log n) um etwa 10 · (log 10 000 / log 1000) ≈ 13.