Unterprogramme (Funktionen)

Werbung
5 Unterprogramme
5- 1
Unterprogramme (Funktionen)
Inhalt
Einführung
Funktionen
Lokale Variablen und Parameter
Übergabe per Wert
Rekursive Unterprogramme
Arrays und Funktionen
Überladen von Funktionen
Beispiele
Übungsaufgaben
Einführung
Unterprogramme sind abgeschlossene Programmteile, die beim Aufruf einen neuen Abschnitt im
Stack erzeugen und dort ihre lokalen Variablen ablegen. Nach ihrer Beendigung kehren
Unterprogramme genau hinter die Stelle des Aufrufs zurück. Dazu merken sie sich die
Rücksprungstelle ebenfalls auf dem Stack.
Obendrein können Unterprogramme Parameter erhalten, die bei jedem Aufruf verschieden sein
können. Sie können außerdem einen Wert zurückgeben. Man spricht in diesem Fall von
Funktionen. Wenn die Unterprogramme oder Funktionen im Zusammenhang mit Klassen (dazu
später mehr) auftauchen, spricht man von Methoden der Klasse.
Ein typisches Unterprogramm, das keinen Wert zurückgibt, ist etwa
void print (double x)
{
System.out.println("Wert = "+x);
}
Dabei bedeutet void, daß kein Wert zurückgegeben wird. Der Name des Unterprogramms ist
print. Das Unterpogramm bekommt einen Parameter namens x, der ein double-Wert sein muß.
Ein Beispiel für den Aufruf ist
double x=Math.PI;
print(x); // druckt: Wert = 3.14152
Die allgemeine Deklaration eines Unterprogramms ist also
5 Unterprogramme
5- 2
ergebnistyp name (parameterliste)
anweisungsblock
Dabei ist
ergebnistyp
name
parameterliste
anweisungsblock
Typ des Rückgabewertes, oder void, falls kein Wert
zurückgegeben wird.
Name des Unterprogramms.
Mit Komma getrennte Parameter. Jeder Parameter sieht aus
wie eine Variablendeklaration und verhält sich auch lokal
wie eine Variable. Der Wert dieser Variablen wird beim
Aufruf übergeben.
Mit {...} geklammerte Folge von Anweisungen.
Aus Gründen, die erst später klar werden, müssen Unterprogramme, die aus main aufgerufen
werden, static deklariert werden. Ein vollständiges Programm sieht also so aus.
public class Test
{
public static void main (String args[])
{
double x=Math.PI;
print(x);
print(x*x);
print(4.5);
}
static void print (double x)
{
System.out.println("Wert = "+x);
}
}
Es kommt also noch der Modifier static hinzu.
muß also innerhalb der Klasse Test deklariert sein. Dort kann es von main aus einfach
mit dem Namen print aufgerufen werden. Wir werden später Unterprogramme in anderen
Klassen kennenlernen. Diese werden dann z.B. mit KlassenName.print(x) aufgerufen.
print
Wichtig: Man beachte, daß das x in print nichts mit dem x im Hauptprogramm main zu tun hat.
Die Bedeutung lokaler Variablen und Parameter wird später noch erläutert.
Funktionen
Funktionen sind Unterprogramme die nicht den Ergebnistyp void haben. Sie geben einen Wert
zurück und lassen sich deshalb in Ausdrücken einsetzen. Ein einfaches Beispiel ist die Funktion
x^2.
double sqr (double t)
{
return t*t;
}
Das Ergebnis wird also mit der Anweisung return zurückgegeben. Diese Funktion läßt sich
dann wie folgt verwenden.
5 Unterprogramme
5- 3
double x=1.2;
System.out.println(sqr(x));
System.out.println(sqr(Math.sin(x)));
Im zweiten Beispiel wird Math.sin(x) nur einmal berechnet. Verwendet man stattdessen
System.out.println(Math.sin(x)*Math.sin(x));
so wird natürlich der Sinus zweimal berechnet. Das Ergebnis ist allerdings das gleiche. Man
könnte jedoch auch
y=Math.sin(x);
System.out.println(y*y);
verwenden. Dies würde dann die Arbeitsweise des Funktionsaufrufs nachahmen, der ebenfalls
eine lokale Variable (namens t) verwendet.
Die Anweisung return beendet die Funktion von jeder Stelle aus. Also auch aus Schleifen
heraus.
Lokale Variablen und Parameter
Alle Variablen, die in einer Funktion (oder einem Unterprogramm, was wir nun nicht mehr
unterscheiden) deklariert werden, sind lokal und existieren nach dem Ende des Unterprogramms
nicht mehr. Sie werden ja auf dem Stack abgelegt, und dieser Bereich wird nach dem Ende des
Unterprogramms freigegeben.
Als Beispiel programmieren wir eine Funktion, die x^4 berechet.
double pow4 (double x)
{
double y=x*x;
return y*y;
}
Die Variable y ist lokal. Ebenso der Parameter x, der sich lokal wie eine Variable verhält.
Man beachte nochmals, daß der Name und der Speicherplatz einer lokalen Variablen nichts mit
dem aufrufenden Programm zu tun hat.
Übergabe per Wert
Parameter sind ebenfalls lokal. Ihr Speicherplatz hat nichts mit dem aufrufenden Programm zu
tun. Parameter sind im Prinzip nur lokale Variablen, denen beim Aufruf gewisse Werte (die
Werte der Parameter) zugewiesen werden. Eine Änderung der Parameter wirkt sich nicht auf das
aufrufende Programm aus. Ihr Name ist für das aufrufende Programm unwesentlich.
Beispiel
Wir übergeben an eine Funktion zwei Parameter a und b. Die Funktion soll dann die Iteration
a(n+1)=(a(n)+b(n))/2, b(n+1)=sqrt(a(n)*b(n))
ausführen, bis a(n) und b(n) nahe genug beieinander liegen. Gestartet wird mit a(0)=a und
b(0)=b. Diese Funktion sieht so aus.
5 Unterprogramme
5- 4
public class Test
{
public static void main (String args[])
{
double x=1,y=2;
System.out.println(agm(x,y));
System.out.println(x+","+y); // druckt immer noch 1,2
}
static double agm (double a, double b)
// berechnet das Arithmetisch-Geometrische Mittel iterativ
{
do
{
double h=Math.sqrt(a*b); // muss sein!
b=(a+b)/2;
a=h;
} while (b-a>1e-13); // Abbruchbedingung
// aus mathematischen Gruenden ist a immer kleiner gleich b
return (a+b)/2;
}
}
Man beachte, daß der neue Wert von a zunächst auf einer Hilfsvariablen gespeichert werden
muß, da der alte Wert bei der Berechnung von b verwendet werden muß. Das verwendete
Iterationsverfahren konvergiert sehr schnell.
Merke: Der Wert von x und y ändert sich durch die Zuweisung innerhalb agm nicht. Auch dann
nicht, wenn a und b dort x und y heißen würden.
Parameter werden also immer als Wert (value) übergeben. In Pascal und C gibt es eine einfache
Möglichkeit, Parameter als Variable zu übergeben. Dies kann man in Java nur mittels Objekten
nachahmen. Allerdings ist insbesondere die Übergabe von Variablen in Pascal verwirrend und
fehlerträchtig, ebenso wie die Übergabe als Zeiger in C.
Rekursive Unterprogramme
Falls ein Unterprogramm sich direkt oder indirekt selbst aufruft, spricht man von einem
rekursiven Aufruf. Da alle Variablen und Parameter des Unterprogramms lokal sind, stören sich
diese nicht mit den Variablen und Parametern des vorigen Aufrufs.
Mit solchen Rekursionen lassen sich sehr elegante Programme erzeugen. Allerdings kostet jeder
Aufruf Zeit und Stackplatz.
Beispielsweise berechnen wir die Fakultät n!=1*2*...*n rekursiv wie folgt.
double fak (int n)
// Fakultaet rekursiv
{
if (n<=1) return 1.0;
else return n*fak(n-1);
}
Es ist klar, daß hier auch eine iterative Lösung in Frage kommt, die erheblich schneller ist.
double fak (int n)
// Fakultät iterativ
{
double x=1.0;
int i;
for (i=2; i<=n; i++)
5 Unterprogramme
5- 5
{
x=x*i;
}
return x;
}
Diese Lösung sieht aber nicht so elegant aus. Eleganz ist aber auch ein wichtiger Grundpfeiler
für Übersichtlichkeit, Wartbarkeit und damit Fehlerfreiheit.
Es ist oft unklar, wie die rekursive Lösung eigentlich funktioniert. In diesem Fall werden die
einzelnen Werte von n auf dem Stack zwischengelagert, und zwar von n beginnend rückwärts bis
1. Beim Aufräumen des Stacks werden diese Werte dann benutzt um die Multiplikation
1*2*3*...*n zu berechnen.
Überladen von Funktionen
Funktionen werden in Java (wie in C++, aber nicht in C) nach ihren Parametertypen (der
sogenannten Signatur) unterschieden. Der Compiler sucht die korrekte Funktion (bzw. das
korrekte Unterprogramm) heraus, das diese Parameter verarbeiten kann.
Beispiel
public class Test
{
public static void main (String args[])
{
print(3.4); // druckt: double-wert 3.4
print(3); // druckt: int-wert 3
}
static void print (double x)
{
System.out.println("double-Wert "+x);
}
static void print (int n)
{
System.out.println("int-Wert "+n);
}
}
Die beiden Unterprogramme heißen gleich. Der Compiler (oder der Interpreter) unterscheiden
die Funktionen nach dem Typ der Parameter-Werte. (Funktionen, die sich nur im Ergebnistyp
unterscheiden, erzeugen eine Fehlermeldung.)
Arrays und Funktionen
Natürlich kann eine Funktion auch ein Array zurückgeben. Das sieht dann so aus.
static double[] generate (int n)
// gib ein Array mit n Zufallsvariablen zurück.
{
double R[]=new double[n];
int i;
for (i=0; i<n; i++) R[i]=Math.random();
return R;
}
Der Ergebnistyp wird also wie eine Array-Deklaration ohne Namen geschrieben. Das Programm
gibt natürlich nur eine Referenz auf das Array zurück. Das Array selbst befindet sich ja auf dem
Heap.
5 Unterprogramme
5- 6
Man kann auch ein Array (genauer eine Referenz darauf) als Parameter an eine Funktion
übergeben. Als Beispiel schreiben wir Unterprogramme, die den Mittelwert und die
Standardabweichung berechnen. Hier ein vollständiges Programm.
Beispiel
public class Test
{
public static void main (String args[])
{
double R[]=generate(1000);
double m=mean(R);
System.out.println("Mean value "+m);
System.out.println("Standard Deviation "+deviation(R,m));
}
static double[] generate (int n)
// generiere Zufallsvektor der Länge n
{
double R[]=new double[n];
int i;
for (i=0; i<n; i++) R[i]=Math.random();
return R;
}
static double mean (double x[])
// berechne Mittelwert von x
{
int n=x.length,i;
double sum=0;
for (i=0; i<n; i++) sum+=x[i];
return sum/n;
}
static double deviation (double x[], double mean)
// berechne Standard-Abweichung von x, wenn der Mittelwert
// bekannt ist
{
int n=x.length,i;
double sum=0;
for (i=0; i<n; i++) sum+=sqr(x[i]-mean);
return Math.sqrt(sum/(n-1));
}
static double sqr (double x)
// Hilfsfunktion, berechne x^2
{
return x*x;
}
}
Man beachte, daß nur ein einziges Array existiert. Alles, was übergeben oder zurückgegeben
wird, sind nur Referenzen auf dieses Array.
Beispiele
Bisektions-Verfahren
Hier noch einmal das Bisektionsverfahren mit Funktionen. Das Programm sieht dann viel klarer
aus.
public class Test
5 Unterprogramme
{
5- 7
public static void main (String args[])
{
System.out.println(bisect(0,1));
}
static double bisect (double a, double b)
// Bisektionsverfahren für die Funktion x
{
double m,y;
while (b-a>1e-13) // Abbruch, wenn Intervall klein genug
{
m=(a+b)/2; // Berechne Mitte
y=f(m); // und Wert in der Mitte
if (y>0) a=m; // Nimm rechtes Halb-Intervall
else b=m; // Nimm linkes Halb-Intervall
}
return (a+b)/2;
}
static double f (double x)
// Funktion, deren Nullstelle berechnet wird
{
return Math.exp(x)-4*x;
}
}
Der Nachteil ist, daß die Funktion f, deren Nullstelle zu berechnen ist, nicht so leicht an die
Funktion bisekt übergeben werden kann (im Unterschied zu C). Es gibt aber dafür verschiedene
Auswege.
Türme von Hanoi
Es gibt Probleme, die sich fast nur mit Rekursion lösen lassen. Eines davon ist das Problem der
Türme von Hanoi.
Die Ausgangssituation sei hier skizziert.
Die Aufgabe besteht darin, die Scheiben von links (Turm 0) in die Mitte (Turm 1) zu bewegen.
Dabei darf immer nur eine Scheibe bewegt werden, und nie darf eine größere über einer
kleineren liegen.
Die rekursive Lösung besteht darin, zuerst 3 Scheiben von 0 nach 2 zu bewegen, und dann die
unterste nach 1. Danach wieder 3 Scheiben von 2 nach 1. Also kann man 4 Scheiben bewegen,
wenn man weiß, wie 3 Scheiben bewegt werden. Dies läßt sich in folgendem Programm
umsetzen.
5 Unterprogramme
5- 8
public class Test
{
public static void main (String args[])
{
hanoi(4,1,2,3); // bewege 4 Scheiben von 0 nach 1
}
static void hanoi (int n, int from, int to, int using)
// Bewegt n Scheiben von from nach to, wobei using frei ist.
{
if (n==1)
{
System.out.println("Move disk from "+from+" to "+to);
}
else
{
hanoi(n-1,from,using,to);
System.out.println("Move disk from "+from+" to "+to);
hanoi(n-1,using,to,from);
}
}
}
Die Ausgabe des Programms ist eine Folge von Anweisungen, von wo nach wo die nächste
Scheibe zu verschieben ist.
D:\java\kurs>java Test
Move disk from 1 to 3
Move disk from 1 to 2
Move disk from 3 to 2
Move disk from 1 to 3
Move disk from 2 to 1
Move disk from 2 to 3
Move disk from 1 to 3
Move disk from 1 to 2
Move disk from 3 to 2
Move disk from 3 to 1
Move disk from 2 to 1
Move disk from 3 to 2
Move disk from 1 to 3
Move disk from 1 to 2
Move disk from 3 to 2
Das Programm sieht leicht aus, ist aber wohl am Anfang schwer zu verstehen.
5 Unterprogramme
5- 9
Quick-Sort
Als Beispiel für einen schnellen Sortier-Algorithmus geben wir hier das Quicksort-Verfahren an.
Die Logik des Programms ist etwas schwer zu verstehen.
Der verwendete Algorithmus ist folgender:
1. Der Haufen von Zahlen wird in kleine und große aufgeteilt. Kriterium für klein und groß
ist ein Element, das aus der Mitte herausgezogen wird. Dies kann von rechts und links
durch einfaches Vertauschen von Elementen geschehen. Man hofft, daß dadurch zwei
gleich große Haufen entstehen (zumindest im Mittel).
2. Dann werden beide Haufen sortiert. Damit ist die Gesamtheit sortiert. Dies ist natürlich
ein rekursives Verfahren.
public class Test
{
static public void main (String args[])
{
int n=100;
double v[]=new double[n];
int i;
for (i=0; i<n; i++) v[i]=Math.random(); // n Zufallszahlen
QuickSort(v,0,n-1); // sortieren
for (i=0; i<n; i++) System.out.println(v[i]);
// und ausgeben.
}
static void QuickSort (double a[], int loEnd, int hiEnd)
// Sortiere den Abschnitt zwischen a[loEnd] und a[hiEnd]
{
int lo=loEnd;
int hi=hiEnd;
double mid;
if (hiEnd>loEnd) // sonst ist nichts zu tun!
{
mid=a[(loEnd+hiEnd)/2];
// nimm mittelgrosses Element (hoffentlich)
// Trenne in kleinere und groessere:
while (lo<=hi)
{
while((lo<hiEnd) && (a[lo]<mid)) lo++;
// von links aussen nach rechts,
// solange nichts zu tun ist
while((hi>loEnd) && (a[hi]>mid)) hi--;
// von rechts aussen nach links
// solange nichts zu tun ist
if (lo<=hi) // zwei Elemente stehen falsch!
{
swap(a,lo,hi); // vertauschen
lo++; // nun stehen sie richtig
hi--;
}
}
// Sortiere rechts:
if(loEnd<hi) QuickSort(a,loEnd,hi);
// Sortiere alles kleiner mid
if(lo<hiEnd) QuickSort(a,lo,hiEnd);
// Sortiere alles groesser mid
}
}
5 Unterprogramme
5 - 10
static void swap (double a[], int i, int j)
// Vertausche im a[i] mit a[j]
{
double h;
h = a[i];
a[i] = a[j];
a[j] = h;
}
}
Potenzen einer Matrix
Sei A eine quadratische Matrix. Wir berechnen A^n für große n. Der verwendete Algorithmus
setzt
An=(An/2)2
wenn n gerade ist, bzw.
An=A*(A(n-1)/2)2
falls n ungerade ist. Dies funktioniert natürlich rekursiv und ist ein sehr effektives Verfahren.
Das Programm nimmt als Beispiel eine spezielle Matrix, deren Potenzen mit den FibonacciZahlen zusammenhängen.
Man sieht hier auch, wie zweidimensionale Arrays als Parameter und als Rückgabewerte
verwendet werden. Die Größe der Arrays wird nicht mit übergeben, sondern mittels length
ausgelesen. Man beachte, daß die Spaltenzahl einer quadratischen Matrix die Größe von a[0]
(das ist die erste Zeile von a) ist.
public class Test
{
static public void main (String args[])
{
double A[][]={{0,1},{1,1}};
double B[][]=pot(A,100);
print(B); // drucke A^n
System.out.println(B[0][0]+B[0][1]); // druckt 5.731478440138171E20
System.out.println(fibonacci(100)); // druckt dasselbe
}
// Berechne A^n mit dem Verfahren der sukzessiven Quadrierung
static double[][] pot (double a[][], int n)
{
if (n==1) return a;
if (n%2==0) return sqr(pot(a,n/2));
else return mult(sqr(pot(a,n/2)),a);
}
// Berechne das Produkt zweier Matrizen
static double[][] mult (double a[][], double b[][])
{
double c[][]=new double[a.length][b[0].length];
int i,j,k;
for (i=0; i<a.length; i++)
for (j=0; j<b[0].length; j++)
{
c[i][j]=0;
for (k=0; k<b.length; k++) c[i][j]+=a[i][k]*b[k][j];
}
return c;
5 Unterprogramme
5 - 11
}
// Berechne A^2
static double[][] sqr (double a[][])
{
return mult(a,a);
}
// Drucke eine Matrix
static void print (double a[][])
{
int i,j;
for (i=0; i<a.length; i++)
{
for (j=0; j<a[i].length; j++)
System.out.print(a[i][j]+" ");
System.out.println("");
}
}
// Berechne n-te Fibonacci-Zahl
static double fibonacci (int n)
{
int i;
double a=0,b=1,old;
for (i=0; i<n; i++)
{
old=b;
b=a+b;
a=old;
}
return b;
}
}
Übungsaufgaben
1. Schreiben Sie Unterprogramme für den Sinus hyperbolicus und den Kosinus
hyperbolicus.
2. Schreiben Sie ein Unterprogramm, das das größte Element eines Vektors
zurückgibt.
3. Versuchen Sie 1000 (10000) Zufallszahlen mit dem Quicksort-Verfahren zu
sortieren.
4. Benutzen Sie das Unterprogramm für die Fakultät, um die Auswahlfunktion (n
über m) in einem Unterprogramm (choose(n,k) ) zu berechnen. Berechnen Sie
die Anzahl der Möglichkeiten, 6 aus 49 auszuwählen.
5. Schreiben Sie 4. direkt, indem Sie in einer Schleife (n über 0) bis (n über k)
berechnen. Nutzen Sie außerdem aus, daß (n über k) gleich (n über n-k) ist.
6. Schreiben Sie ein Programm, das x^n für double-Werte x und int-Werte n
berechnet. Benutzen Sie dieselbe rekursive Methode wie bei den Potenzen der
Matrizen.
7. Schreiben Sie ein rekursives Unterprogramm, das den größten gemeinsamen
Teiler berechnet. Dazu beachten Sie, daß ggt(n,m)=ggt(n%m,m) gilt.
5 Unterprogramme
5 - 12
Lösungen.
Aufgaben ohne Lösung
1. Berechnen Sie die Fibonacci-Zahlen rekursiv als Funktion fibonacci(n).
Warum ist das ein sehr schlechtes Verfahren? Testen Sie es für größere n
(10,20,...).
2. Schreiben Sie ein Unterprogramm factor(n,p), das p aus n ausdividiert, bis p
die Zahl n nicht mehr teilt. Das Programm soll p^i ausgeben, wobei i die Anzahl
der Potenzen von p in n sei.
3. Schreiben Sie mit Hilfe von 2. ein Unterprogramm, das alle Teiler einer Zahl n
ausgibt.
4. Schreiben Sie ein Unterprogramm, das das maximale Element einer Matrix
double A[][] berechnet. Dabei sind die Zeilenlängen aus der Matrix auszulesen.
5. Schreiben Sie ein Unterprogramm, das die transponierte Matrix zurückgibt. Dabei
wird angenommen, das die Matrix rechteckig sei. Die Dimensionen von A sind
aus der Matrix auszulesen. A darf nicht verändert werden.
6. Nehmen Sie an, daß A eine quadratische Matrix ist. Schreiben Sie ein
Unterprogramm, daß A transponiert, ohne eine neue Matrix zu erzeugen. A wird
also verändert.
Herunterladen