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