Lösungsblatt

Transcription

Lösungsblatt
Technische Universität München
Institut für Informatik
Lehrstuhl für Computer Graphik & Visualisierung
Praktikum: Grundlagen der Programmierung
Prof. R. Westermann, A. Lehmann, R. Fraedrich, F. Reichl
WS 2010
Lösungsblatt 3
Anmerkung: Da viele Fragen zur Diskussion anregen sollen, sind die hier vorgestellten Antworten
lediglich als Lösungsskizze zu verstehen.
Funktionen und Rekursion
3.1 (Ü) Fragen zu Funktionen und Rekursion
(a) Was für Vorteile hat das Programmieren mit Funktionen?
(b) Was sind lokale Variablen im Zusammenhang von Funktionen?
(c) Was ist mit dem Überladen“ von Funktionen”gemeint?
”
(d) Was sind rekursive Funktionen?
(e) Kann man jedes iterative Problem in einen rekursiven Algorithmus überführen? Wie sieht es
mit der entgegengesetzten Richtung aus?
(f) Was macht folgende Funktion?
void func ( int a ) {
func ( a );
}
(g) Wie verhalten sich Funktionsparameter und lokale Variablen in Funktionen bei verschiedenen
Rekursionstiefen? Verdeutlichen Sie dies am Beispiel der Fakultätsfunktion:
int fak ( int n ) {
if ( n <= 1) {
return 1;
} else {
int tmp = n * fak ( n - 1);
return tmp ;
}
}
Lösungsvorschlag
(a) Modularisierung (Divide-and-Conquer-Prinzip), isolierte Testbarkeit einzelner Module, Wiederverwendbarkeit für ähnliche Probleme, Lesbarkeit des Codes durch bessere Strukturierung,
1
Abstraktion im Hauptprogramm durch Funktionsufrufe statt vollständigem Programmcode
für Teilprobleme.
(b) Lokale Variablen sind nur gültig in der jeweiligen Funktion und außerhalb dieser nicht sichtbar
(d.h. weder Lese- noch Schreibzugriff).
(c) Funktionen mit gleichen Namen können unterschiedliche Signaturen (Parameter) haben. Je
nach den beim Aufruf übergebenen Parametern wird dann automatisch die richtige Funktion
ausgeführt.
Ein Beispiel:
// function A
public static void greetReader () {
System . out . println ( " Hello Reader " );
}
// function B
public static void greetReader ( String name ) {
System . out . println ( " Hello " + name );
}
// function A will be chosen
greetReader ();
// function B will be chosen
greetReader ( " Mike " );
(d) Rekursive Funktionen definieren sich durch sich selbst, d.h. die Funktion selbst wird im Rumpf
(mit anderen Parametern) wieder aufgerufen.
(e) Geht in beide Richtungen, allerdings ist die resultierende Implementierung in manchen Fällen
sehr komplex. Hier ein Beispiel für die iterative Version der Fakultätsfunktion (siehe Angabenblatt für die rekursive Version):
int fak ( int n ) {
int result = 1;
for ( int i = n ; i >= 1; i - -) {
result = result * i ;
}
return result ;
}
(f) Endlosrekursion, da die Funktion stets mit unveränderten Paramtern aufgerufen wird. Dadurch kommt es technisch schnell zu einem sogenannten Stack Overflow, d.h. die Zahl der
noch nicht vollständig abgearbeiten Rekursionsaufrufe übersteigt den zur Verfügung stehenden Arbeitsspeicher.
2
(g) Es wird jedes Mal eine eigene Instanz von Funktionsparametern und lokalen Variablen angelegt
und gespeichert. Sowohl die aufrufende ( Caller“) als auch die aufgerufene ( Callee“) Funk”
”
tion können nicht gegenseitig auf ihre Variablen zugreifen, auch wenn es sich beim rekursiven
Aufruf um dieselbe Funktion handelt.
24
fak(4)
tmp = 4 * fak(3)
return tmp
6
fak(3)
tmp = 3 * fak(2)
return tmp
2
fak(2)
tmp = 2 * fak(1)
return tmp
1
fak(1)
return 1
Abb. 1: Illustration des rekursiven Aufrufs
3
3.2 (Ü) PC Einkauf
In Übungsblatt 1 haben Sie ein Programm geschrieben, das den Einkaufspreis für PC-Komponenten
berechnet.
1) Kapseln Sie nun die einzelnen Berechnungsschritte wie folgt in Funktionen:
a) public static void line()
Zeichnet eine Linie.
b) public static void addItem(double cost)
Fügt einen neuen Gegenstand, bei dem nur der Preis bekannt ist, zum Einkaufszettel
hinzu und gibt ihn aus. Als Name für den Artikel wird Div.“ angenommen.
”
c) public static void addItem(String name, double cost)
Fügt einen neuen Gegenstand, bei dem sowohl Name als auch Preis bekannt sind, zum
Einkauszettel hinzu und gibt beides auf der Konsole aus.
d) public static void currentSum(String text)
Gibt die Gesamtsumme auf der Konsole aus.
e) public static void applyDiscount(double percent)
Berechnet die Ersparnis durch einen in Prozent angegebenen Rabatt und gibt sie aus.
f) public static void applyVoucher(int value)
Berechnet die Ersparnis durch einen als Wert in Cent gegebenen Gutschein und gibt sie
aus.
g) public static void showSalesTax()
Gibt die enthaltene Mehrwertsteuer von 19% aus.
Speichern Sie das Ergebnis für die Berechnung in einer globalen“ Variable (Klassenvariable).
”
Fertigen Sie eine Ausgabe ähnlich der folgenden mithilfe Ihrer Funktionen an. Runden Sie in
jeder Funktion auf ganze Cent.
PC
599.00
Monitor
189.90
Maus
20.79
Div.
49.99
Drucker
129.90
-----------------------------Rechnungsbetrag:
989,58
Abzug Gutschein:
100,00
-----------------------------Neuer Rechnungsbetrag: 889,58
Abzug Rabatt (20%):
177,92
-----------------------------Endbetrag:
711,66
(19% enthaltene MwSt: 113,63)
Erweitern Sie die Klasse Shopping entsprechend Ihrer Lösung.
4
2) Verwenden Sie nun die oben implementierten Funktionen, um folgenden Einkauf zu berechnen:
• PC im Wert von e 749,• TFT im Wert von e 229,• Drucker im Wert von e 125,90
Sie haben einen Rabattgutschein für 10% des Gesamteinkaufpreises und einen Warengutschein
zu e 25,-.
Lösungsvorschlag
Siehe beiligende .java-Files.
3.3 (Ü) Iteration und Rekursion
In der Klasse IterativeToRecursive finden Sie einen Algorithmus, der eine while-Schleife
enthält. Vervollständigen Sie die leere Funktion
public static int recursive(int a, int b) ,
die genau dasselbe berechnen soll, jedoch keine for- oder while-Schleife enthält – dafür aber
rekursiv ist.
Die main-Methode enthält bereits Code, mit dem Sie Ihre Implementierung testen können.
Lösungsvorschlag
Es handelt sich um die ggT-Funktion.
Siehe beiligende .java-Files.
3.4 (Ü) Türme von Hanoi
Spielprinzip
Das Spiel besteht aus drei Stäben A, B und C, auf die mehrere gelochte Scheiben gelegt werden,
die alle verschieden groß sind. Zu Beginn liegen alle Scheiben auf Stab A, der Größe nach geordnet,
mit der größten Scheibe unten und der kleinsten oben. Ziel des Spiels ist es, den kompletten
Scheiben-Stapel von A nach C zu versetzen.
Bei jedem Zug darf die oberste Scheibe eines beliebigen Stabes auf einen der beiden anderen Stäbe
gelegt werden, vorausgesetzt, dort liegt nicht schon eine kleinere Scheibe. Folglich sind zu jedem
Zeitpunkt des Spieles die Scheiben auf jedem Feld der Größe nach geordnet.
Aufgabe
Machen Sie sich mit dem Spielprinzip vertraut und versuchen Sie gemeinsam auf die Lösung des
Spieles zu kommen.
Erweitern Sie anschließend die Funktion move(...), so dass für eine beliebige Menge von Steinen
die Einzelschritte (z.B. “Move top stone from A to B.”) zur Lösung des Spiels auf der Konsole
5
ausgegeben werden.
Lösungsvorschlag
Hier handelt es sich um ein durch einen rekursiven Algorithmus sehr gut lösbares Problem.
Grundsätzlich ist klar: Die unterste Scheibe muss zuerst auf den rechten Stab C gelegt werden,
bevor andere Scheiben dort (endgültig) platziert werden können. Um das zu erreichen, muss der
rechte Stapel C leer sein und der linke Ausgangsstapel A bis auf die unterste Scheibe komplett
auf den mittleren Stab B verschoben werden.
Dieses Problem ist grundsätzlich identisch mit der Ausgangslage, allerdings ist der zu verschiebende
Turm um eine Scheibe niedriger (Rekursion!). Die so entstehenden Teilprobleme können so lange
weiter verkleinert werden, bis beim letzten rekursiven Aufruf lediglich eine einzige Scheibe verschoben werden muss (Divide-and-Conquer-Prinzip: Zerlegung eines großen Problems in beherrschbare
Teilaufgaben).
Beispiel der Zerlegung des ersten Schritts für 3 Scheiben:
• Hauptziel: 3 Scheiben von A nach C
– Verschiebung von 2 Scheiben von A nach B
∗ Verschiebung von 1 Scheibe von A nach C
∗ Verschiebung von 1 Scheibe von A nach B
∗ Verschiebung von 1 Scheibe von C nach B
– Verschiebung von 1 Scheibe von A nach C
– Verschiebung von 2 Scheiben von B nach C
∗ Verschiebung von 1 Scheibe von B nach A
∗ Verschiebung von 1 Scheibe von B nach C
∗ Verschiebung von 1 Scheibe von A nach C
Für das Verständnis des theoretischen Vorgehens zur Lösung des Problems kann folgendes
Java-Applet: http://www.mathematik.ch/spiele/hanoi_mit_grafik/
zum Ausprobieren hilfreich sein.
Siehe beiliegende .java-Files.
3.5 (Ü) Fibonacci-Folge
Anmerkung: Diese Aufgabe ist optional! Bearbeiten Sie sie nur, wenn es die Zeit trotz angemessener Besprechung der vorausgehenden Aufgaben erlaubt.
Die Folge der Fibonacci-Zahlen ist definiert als:
n0 = 0
n1 = 1
ni = ni−1 + ni−2 für i > 1
Schreiben Sie ein Programm, das nach Eingabe des Index i die Fibonaccizahl ni berechnet und
ausgibt.
Implementieren Sie sowohl eine rekursive, als auch eine iterative Lösung.
6
Erweitern Sie hierzu die Funktionen
public static long recursive(long i)
und
public static long iterative(long i)
in der Klasse Fibonacci.
Vergleichen Sie die beiden Lösungen hinsichtlich ihrer Laufzeit. Wie unterscheiden sich beide
Lösungen hinsichtlich großer i?
Lösungsvorschlag
Hier wird deutlich, dass sich rekursive Probleme auch iterativ lösen lassen. Während die rekursive
Lösung meist leichter verständlich und eleganter ist, ist die iterative Lösung schneller.
Siehe beiligende .java-Files.
3.6 (H) Tagerechner
(+++)
Ziel dieser Aufgabe ist es, einen Tagerechner zu programmieren, mit dem man beispielsweise berechnen kann, welches Datum n Tage nach einem bestimmten Datum liegt.
Nach dem Prinzip Divide and Conquer“ ( Teile und Herrsche“) sollen die Aufgaben in verschie”
”
dene Funktionen der Klasse DayCalculator zerlegt werden, bei der jede spätere Funktion (soweit
möglich) die vorangegangenen nutzen soll. Implementieren Sie dazu folgende Funktionen und achten
Sie darauf, dass Sie exakt die jeweils angegebene Funktions-Signatur1 verwenden.
(a) public static boolean isLeapYear(int y)
Prüft, ob es sich bei der Jahresangabe y um ein Schaltjahr handelt. Die Regel dabei lautet,
dass alle durch 4 teilbaren Jahre Schaltjahre sind (z.B. 1996), außer wenn sie ebenfalls durch
100 teilbar sind (z.B. 1900). Durch 400 teilbare Jahre bilden davon wiederum eine Ausnahme
und sind Schaltjahre (z.B. 2000).
(b) public static int daysInYear(int y) und
public static int daysInMonth(int m, int y)
Geben an, wieviele Tage es in einem Jahr y gibt bzw. wieviele Tage es im Monat m eines
bestimmten Jahres y gibt.
(c) public static boolean isValidDate(int d, int m, int y)
Prüft, ob das angegebene Datum mit Tag d, Monat m und Jahr y existiert.
(d) public static String getDateAsString(int d, int m, int y)
Gibt das Datum im Format 1.11.2009“ als String zurück. Bei einem unzulässigen Datum
”
soll die Zeichenkette “Invalid Date” zurückgegeben werden.
(e) public static int remainingDaysInMonth(int d, int m, int y)
Gibt an, wieviele Tage abzüglich des gegebenen Datums im Monat verbleiben. Bei einem
unzulässigen Datum soll der Wert 0 zurückgegeben werden.
1
d.h. korrekte Verwendung von Schlüsselworten, richtiger Rückgabetyp, korrekter Funktionsname (inkl. Groß- und
Kleinschreibung) sowie richtige Anzahl, Reihenfolge und Typen der Funktionsparameter
7
(f) public static int remainingDaysInYear(int d, int m, int y)
Gibt an, wieviele Tage abzüglich des gegebenen Datums im Jahr verbleiben. Bei einem unzulässigen Datum soll der Wert 0 zurückgegeben werden.
(g) public static String daysAdd(int d, int m, int y, int numDays)
Gibt an, welches Datum numDays Tage hinter dem angegebenen Datum liegt. Das Ergebnis
soll auf der Konsole ausgegeben werden. Falls das initiale Datum unzulässig ist, soll die
Zeichenkette “Invalid Date” zurückgegeben werden.
(h) Sofern noch nicht geschehen, soll die Funktion daysAdd so erweitert werden, dass auch
negative Zahlen zulässig sind.
(i) Kommentieren Sie alle Funktionen inklusive aller Parameter und Rückgabewerte entsprechend
der JavaDoc-Richtlinien.
Anmerkungen:
• Sie dürfen für diese Aufgabe natürlich nicht auf die Klasse Calendar aus der Standardbibliothek zurückgreifen.
• Für die Teilaufgaben (g) ist es sinnvoll, sich vorab einen Algorithmus zu überlegen, wie man
für beliebige Parameter möglichst einfach und schnell zum Ergebnis kommt. Versuchen Sie
dabei, nach dem Prinzip Teile und Herrsche“ das allgemeine Problem in Teilaufgaben zu
”
zerlegen, die einfach zu implementieren sind.
• Versuchen Sie bei der Teilaufgabe (h), einen möglichst einfachen Weg für die Lösung des
Problems zu finden.
Lösungsvorschlag
Siehe beiligende .java-Files.
8
3.7 (H) Ackermann-Funktion
(++)
Die Ackermann-Funktion ist definiert als:

für m = 0
 n+1
ack(m − 1, 1)
für n = 0
∀m, n ∈ N0 : ack(m, n) :=

ack(m − 1, ack(m, n − 1)) sonst
Sie liefert sehr schnell sehr große Resultate und Verschachtelungstiefen. Implementieren Sie die
Funktion
public static int recursive(int m, int n)
in der Klasse Ackermann.
Erstellen Sie nun ein Programm in
public static void main(String[] args) ,
das Werte für m und n mit int Tools.readInt() einliest und eine kleine Tabelle für alle Ergebnisse
zwischen 0 und der Benutzereingabe ausgibt.
Beispiel für m=4 und n=4:
m\n
0:
1:
2:
3:
4:
0
1
2
3
5
13
1
2
3
5
13
x
2
3
4
7
29
x
3
4
5
9
61
x
4
5
6
11
125
x
Kann ein Tabelleneintrag nicht mit maximal 10.000.000 Funktionsaufrufen berechnet werden, soll
an dieser Stelle ein x ausgegeben werden.
Lösungsvorschlag
Siehe beiligende .java-Files.
9