Programmierung 1 Studiengang MI / WI
Dipl.-Inf., Dipl.-Ing. (FH) Michael Wilhelm
Hochschule Harz
FB Automatisierung und Informatik
mwilhelm@hs-harz.de
http://mwilhelm.hs-harz.de
Raum 2.202
Tel. 03943 / 659 338
Inhalt der Vorlesung
Überblick:
•
Erste Beispiele, Interaktion
•
elementare Datentypen
•
Variablen und Kontrollstrukturen
•
Arrays und Funktionen
•
Objekte und Methoden, Interface
•
Rekursion, Suchen, Sortieren
•
Algorithmen und Pseudocode
•
Laufzeitverhalten
•
Simulation
•
Bibliotheken
• Folien basierend auf Daniel Schiffman “Learning Processing” und Donald W. Smith
Grundlegende Algorithmen und Methoden:
•
Suchen und Sortieren
•
Hashing
•
Rekursion
•
Graphen
•
Dynamische
Programmierung
Von Processing zu Java
Kapitel
Rekursion
•
Definition
•
Fakultät
•
Fibonacci
•
m über n
Suchen
• Lineares Suchen
• Intervallhalbiereungsverfahren
Sortieren
Rekursion
Berechnung der Fakultät;
•
int f=0;
•
int n=3;
•
for (int i=2; i<n; i++) {
•
f=f*i;
•
}
•
System.out.println("Ergebnis: "+f);
Rekursion
Berechnung der Fakultät;
•
int f=1;
•
int n=3;
•
for (int i=2; i<=n; i++) {
•
f=f*i;
•
}
•
System.out.println("Ergebnis: "+f);
Rekursion
Besser wäre die Umsetzung mittels der mathematischen Notation:
n!
1 falls n=0
1 falls n=1
n*(n-1)! sonst
Rekursion
rekursiv definierte Funktionen (Beispiele):
a) Fakulatät n! = n * (n-1)! falls n > 1, 1! = 0! = 1
Benutzung
Anzahl der Möglichkeiten n unterscheidbare Elemente anzuordnen
b) Fibonacci-Folge: f(n) = f(n-1) + f(n-2) falls n > 1, f(1) = 1,
f(0) = 0
Benutzung
Wachstum (Wachstum einer Kaninchenpopulation) Abstand der Blätter
Grundlegende Schritte der Rekursion:
Rekursive Implementierung der Fakultät
long calcFakulteat2(int n) { long result = 1;
if(n > 1) {
result = n * calcFakulteat2(n-1);
}
else if (n < 0) { result = 0;
}
return result;
}
Fragen:
•
Was passiert in calcFakultaet2 für größere Werte von n?
Zwei Processing-Beispiele zur Rekursion:
Baum, Koch´sche Schneeflocke
Rekursive Implementierung der Fibonacci-Folge
long calcFibonacci2(int n) { long result = 0;
•
if(n > 1) { // Verhindert eine unendliche Rekursion
result = calcFibonacci2(n-1)+calcFibonacci2(n-2);
} else if(n == 1) { result = 1;
}
return result;
}
Weshalb ist diese rekursive Implementierung keine gute Idee?
•
Fibonacci-Funktion wird 2 mal rekursiv aufgerufen.
•
Insgesamt für das
Argument n also 2n+1 mal.
•
Dabei werden dieselben Werte
mehrfach berechnet.
Beispielrechner:
•
Taktfrequenz 2,8 GHz
Rekursive Implementierung der Fibonacci-Folge
n Java Zeit (iterativ)
Java Zeit
C# Delphi C / C++
20 1000 ns 0 ms 0 ms 0 ms 0 ms
30 1000 ns 16 ms 0 ms 16 ms 7 ms
35 1000 ns 46 ms 0,187 s 0,109 s 0,080 s 40 1000 ns 0,6 sec 1,8 sec 0,952
sec
1,02 s 45 1000 ns 6,7 sec 29,1 sec 12,2 sec 14,2 s 50 1000 ns 1:47
min
5:40 min 2:54 min
2:40 min 55 1000 ns 21 min 01:05:52 34:13
min 60 1000 ns 4 Std
70 1200 ns Jahre
Fibonacci ohne Rekursion
•
Grundlegende Idee der dynamischen Programmierung:
•
Speichere
Zwischenergebnisse, falls Sie später
nochmals benutzt werden können.
public class Fibonacci { private long[] value = null;
private final int MAX = 100;
public long fib(int n) { long result = -1;
if(value == null) {
value = new long[NVALS];
value[0] = 0;
value[1] = 1;
for(int i=2; i<NVALS; i++) {
value[i] = value[i-1] + value[i-2];
} // for } // if
if(n < NVALS) { result = value[n];
}
return result;
Berechnung von k aus n
Die Anzahl M der Möglichkeiten beim Ziehen von k
Elementen aus einer Gesamtheit von n Elementen ist gegeben durch:
=
= !
! · − ! = · − 1 · − 2 … · ( − + 1) − · − + 1 ·. . .· 2 · 1
Weshalb ist diese Formel nicht gut für eine Computer- Implementierung geeignet?
Die Rekursionsformel
=
+
,
auch als Pascal´sches Dreieck bekannt, ist besser
geeignet. Beachte, dass
=
= 1
Pacal‚sche Dreieck
Datenstrukturen: Stack (Stapel-/Kellerspeicher)
•
Ein Stapel kann eine beliebige Menge von Elementen
speichern.
•
Dabei kann ein neues Element immer nur oben auf den
bestehenden Stapel darauf gelegt werden (push).
•
Ebenso kann ein Element nur von dort entfernt werden (pop).
•
Anwendungen
Anwendung eines Stacks
•
Java/Processing teilt jedem Programm Speicher (genannt Heap) und einen Stack zu.
•
Der Stack dient als Speicher für lokale Variablen
•
Was passiert bei einem Methoden-/Funktionsaufruf?
public void test1() { int m = add(3, 4);
System.out.println(m);
}
public int add(int a, int b) { int c=a+b;
return c;
}
…
Lokale Variable int j Parameter int a(3) Parameter int b(3) Rücksprungadresse Return-Wert 7
Lokale Variable m
Anwendung eines Stacks
•
Speichern lokaler Variablen
•
Umgekehrte Polnische Notation
•
5, 4, +
•
5,4+,2,5+x
•
5 Enter 4 + 2 Enter 5 + x
•
Programmiersprache Forth
•
Parsen eines Postfix-Ausdruckes
Stack: Java-Implementierung
Beachte: Jede Klasse in Java ist auch vom Typ Object.
In Java ist die Stack-Klasse in java.util vorimplementiert!
public class Stack {
private Object[] elements;
private int topOfStack;
public Stack() { topOfStack = 0;
elements = new Object[10];
}
public Object pop() { Object value = null;
if(topOfStack > 0) {
value = elements[--topOfStack];
elements[topOfStack+1] = null;
}
return value;
}
public void push(Object o) {
if(topOfStack >= elements.length) {
growStack();
}
elements[topOfStack++] = o;
}
private void growStack() { int len = elements.length;
Object[] e = new Object[2*len];
for(int i=0; i<len; i++) { e[i] = elements[i];
elements[i] = null;
}
elements = e;
}
Umgang mit einem Stack
public static void main(String[] args) { Object o;
Stack meinStack = new Stack();
for(int i=0; i<15; i++) {
o = new String("Zeichenkette " + i);
meinStack.push(o);
} // for
while(true) {
Object p = meinStack.pop();
if(p == null) break;
System.out.println(p);
}
Zeichenkette 14 Zeichenkette 13 Zeichenkette 12 Zeichenkette 11 Zeichenkette 10 Zeichenkette 9 Zeichenkette 8 Zeichenkette 7 Zeichenkette 6 Zeichenkette 5 Zeichenkette 4 Zeichenkette 3 Zeichenkette 2 Zeichenkette 1 Zeichenkette 0
Ausgabe
Umgang mit einem Stack
public static void main(String[] args) { Object o;
Stack meinStack = new Stack();
for(int i=0; i<15; i++) {
o = new String("Zeichenkette " + i);
meinStack.push(o);
} // for
Object p= meinStack.pop();
while(p!=null) {
System.out.println(p);
p = meinStack.pop();
} }
Zeichenkette 14 Zeichenkette 13 Zeichenkette 12 Zeichenkette 11 Zeichenkette 10 Zeichenkette 9 Zeichenkette 8 Zeichenkette 7 Zeichenkette 6 Zeichenkette 5 Zeichenkette 4 Zeichenkette 3 Zeichenkette 2 Zeichenkette 1 Zeichenkette 0
Umgang mit einem Stack: Beispiel Pyramide
Algorithmus
Ein Algorithmus ist eine Handlungsvorschrift, die eindeutig beschreibt, wie ein Problem mit endlich vielen Schritten gelöst
werden kann.
•
Diese Definition schließt die Berechnung der Fibonacci-Zahlen mittels Rekursion mit ein, auch wenn die Rechenzeiten “etwas”
länger sein können. Daher unterscheiden wir zwischen
“theoretischer” Berechenbarkeit und “faktischer”
Berechenbarkeit.
•
Hierzu betrachten wir sich die Laufzeit (oder der Speicherbedarf) verändert, wenn die zu bearbeitende Problemgröße
systematische geändert wird.
•
CPU
•
Speicher
•
Netzwerk
•
Die Laufzeit wird in einer asymptotischen Notation
beschrieben, die weitgehend von unwesentlichen Details Die Komplexität eines Algorithmus ist eine Abschätzung des
Aufwands seiner Berechnung auf dem Computer Formale Definition der O-Notation:
∈ ↔ ∃ ,
0∀ ≥
0∶ ≤ ()
Definition:
Funktion f(n) ist in der Menge O(g(n)), wenn es ein c> 0 und ein n
0∈N gibt, so dass für alle n ≥ n
0gilt:
f(n) ≤ c* g(n)
Die Komplexität eines Algorithmus
•
Groß-Oh-Notation:
•
Sie bringt zum Ausdruck, dass eine Funktion f(n)
höchstens so schnell wächst, wie eine andere Funktion g(n). g(n)Ist also die obere Schranke für f(n).
•
Die O-Notation geht auf den Zahlentheoretiker Edmund Landau (1877-1938) zurück; daher wird das „O“ auch als Landau-Symbol bezeichnet
Die Komplexität eines Algorithmus
Die Komplexität eines Algorithmus ist eine Abschätzung des Aufwands seiner Berechnung auf dem Computer
Formale Definition der O-Notation:
•
O(1): konstanter Aufwand: Zugriff auf ein Feldelement, Berechnung eines Ausdrucks
•
O(log n): logarithmischer Aufwand: allgemeine Suchverfahren
•
O(n): linearer Aufwand: zeichnen einer Linie über n Pixel, Einfügen eines Elements in einen Array
•
O(n log n): quasilinearer Aufwand: z.B. sortieren mit Quicksort
•
O(n
2): quadratischer Aufwand: sortieren mit BubbleSort
•
O(n
k): polynomialer Aufwand: Matrizenmultiplikation O(n
3)
•
O(2
n): exponentieller Aufwand: rekursive Fibonacci-Berechnung, entscheidungsbasierte Spiele
∈ ↔ ∃ ,
0∀ ≥
0∶ ≤ ()
Wachstum für Komplexitätsbereiche
Beispiele:
// O(1)
•
x = x + 1;
•
y = a[i];
// O(n)
•
for(int i=0;i<n;i++) {
•
x = x + 1;
•
}
// O(n
2)
•
for(int j=0;j<n;j++){
•
for(int i=0;i<n;i++){
•
x = x+1;
Lineare Suche
•
schnellstmöglicher Algorithmus,
•
falls an die zu durchsuchende Menge keine weiteren Bedingungen geknüpft sind.
•
falls die zu durchsuchende Menge jedoch geordnet ist, kann ein besserer Algorithmus gefunden werden.
•
Um die Elemente einer Menge M anordnen zu können,
müssen die Elemente vergleichbar sein, d. h. es ist möglich für a, b ∈ M zu entscheiden, ob gilt:
•
a < b oder a == b oder a > b
Sollen Klasseninstanzen vergleichbar sein, so wird üblicherweise die Methode int compareTo in der Klasse implementiert.
Mathematik Java Klasseninstanzen Normaler Wert a < b a.compareTo(b) < 0 -1
a = b a.compareTo(b)==0 ß
a>b a.compareTo(b)>0 +1
Beachten Sie:
falls a.compareTo(b) == 0,
dann sollte auch a.equals(b) == true sein, und umgekehrt.
Suchen
bessere Suchmethode: Suche 450
Voraussetzung: die zu durchsuchende Menge ist sortiert.
2 4 7 8 22 45 47 99 120 450 550
bessere Suchmethode: Suche 450
Voraussetzung: die zu durchsuchende Menge ist sortiert.
2 4 7 8 22 45 47 99 120 450
mittlere Element, 45<450
mittlere Element, 120<450
gefunden
bessere Suchmethode
Feld der Größe n mit Ganzzahlwerten:
Wie viele Vergleiche sind nötig bis ein Element x gefunden wird?
Binäre Suchmethode
Verbale Beschreibung der Binärsuche:
•
Suche in einem sortierten Datenbestand den Wert der in der Mitte liegt und vergleiche ihn mit dem gesuchten Wert.
•
Ist der gesuchte Wert kleiner, so wiederhole den Vorgang nur mit der linken Hälfte,
•
ist der gesuchte Wert größer, so wiederhole den Vorgang nur mit der rechten Hälfte.
•
Wiederhole den Vorgang, bis das gesuchte Element gefunden
Binäre Suchmethode
•
deutlich verbesserter Suchalgorithmus
•
basiert auf dem Teile und Herrsche Prinzip (“Mantra der Informatik”)
•
die Laufzeit der binären Suche ist O(log n).
•
Teile-und-Herrsche wird in der Informatik häufig benutzt:
•
Betrachte ein Gesamtproblem und teile es in zwei (meist etwa gleich große) Teile.
•
Löse die Teilprobleme (oft durch ähnliches Vorgehen)
•
Berechne aus den Teillösungen die Gesamtlösung des
Problems.
Binäre Suche im Ganzzahl-Array
int binarySearch ( int[] array, int key ) { int u = 0; // untere Grenze
int o = array.length -1; // obere Grenze while( u <= o ) {
int m = (u + o ) / 2; // lese den Wert des mittleren Index if ( array[m] == key ) { // bester Fall
return m;
} else if ( array[m] > key ) {
o = m-1; // Wert in linker Haelfte oder nicht vorhanden } else {
u = m+1; // Wert in rechter Haelfte oder nicht vorhanden } // if
} // while
Sortier-Verfahren
Anwendungen:
•
Suchen in großen Datensätzen ist viel effizienter, wenn es sortiert ist (vgl. binäre vs. lineare Suche)
•
Aber Suche nach Kundennummer, Nachname ???
•
Finden doppelter Werte
•
Effizientes Einfügen neuer Werte
Nachfolgende Verfahren werden auf einen 1D-Integer-Array
angewendet. Andere Datenstrukturen sind jedoch möglich.
Sortier-Verfahren
•
Bubble Sort
•
Selection Sort (hier nicht besprochen)
•
Insertion Sort (hier nicht besprochen)
•
Quicksort
•
Eingabe für das Sortierverfahren:
•
ein Feld hat beliebig angeordnete Datensätze
•
Ziel:
•
Sortiere die Datensätze aufsteigend, so dass gilt
•
E[i] < E[i+1] für alle i.
•
prog1
Bubble-Sort
Sei n Anzahl der Elemente Wiederhole:
setze vertauscht auf falsch
Für alle noch nicht sortierten Elemente 0 ... n-1 Vergleiche E[i] mit E[i+1].
Falls E[i] > E[i+1], dann vertausche die Elemente setze vertauscht auf wahr n = n - 1
solange vertauscht und n > 1
Laufzeit: O(n
2)
Bubble-Sort
public void bubbleSort( int[] array ) { boolean swapped = true;
while(swapped) { swapped = false;
for(int i=0; i< array.length; i++) { if(array[i] > array[i+1]) {
int temp = array[i];
array[i] = array[i+1];
array[i+1] = temp;
swapped = true;
} //if
} // for
public void bubbleSort(int[] array) { boolean swapped = true;
int n = array.length;
while(swapped) { swapped = false;
n--;
for(int i=0; i<n; i++) {
if(array[i] > array[i+1]) { int temp = array[i];
array[i] = array[i+1];
array[i+1] = temp;
swapped = true;
} // if } // for } // while
Bubble-Sort
Quick-Sort
meist O(n log n), im schlechtesten Fall jedoch O(n
2) Idee:
•
Wähle ein Referenzelement aus der Folge (das Element in der Mitte)
•
Zerlege Folge in zwei Teilfolgen, wobei alle Elemente der einen Folge kleiner als das Referenzelement sind, alle Elemente der anderen Folge sind größer als das Referenzelement.
•
Wiederhole den Vorgang mit den Teilfolgen solange bis die Länge
einer Teilfolge 0 oder 1 beträgt (diese sind dann trivialerweise
Algorithmus: Zerteilen in Teilfolgen durch Vertauschen
o: obere Grenze u: untere Grenze
p: Referenzelement bei (o+u)/2
l: linkes äußeres Element das größer oder gleich p ist r: rechtes äußeres Element das kleiner oder gleich p ist Zerteilen:
•
Es wird ein Teil des Arrays mit o und u bestimmt.
•
Bestimme das Referenzelement (beliebiger Wert aus der
Folge, meist (o+u)/2)
Tauschen:
•
solange(u < o)
•
l suchen (von links nach rechts nach erstem Element größer order gleich p suchen.
•
r suchen (von rechts nach links nach erstem Element kleiner oder gleich p suchen.
•
falls r größer als l: vertausche die Elemente E[l] und E[r]
•
Im abgearbeiteten Bereich sind links vom Referenzelement nur Elemente, die kleiner als dieses sind, rechts vom Referenzelement nur Elemente, die größer sind.
•
Diese Teilfolgen sind noch nicht sortiert, daher rekursiver Aufruf für jede der beiden Teilfolgen:
•
für die linke Teilfolge: falls u < r, teile linke Seite erneut
Methode teile(u, o) l = u
r = o
p = daten[(u+o)/2]
wiederhole solange l <= r
solange daten[l] <= p und l < o l = l + 1
solange daten[r] >= p und r > u r = r - 1
falls l < r, dann
vertausche daten[l] mit daten[r]
l = l + 1 r = r - 1
Methode quicksort(u, o) m = teile(u, o)
falls u < m-1
quicksort(u, m-1) falls m < o
quicksort(m, o)
Stufe 1 Stufe 1 Stufe 1 Stufe 1
• Wähle ein Pivotelement p
• erste, letzte, mittlere, random
• Tausche die Elemente in eine
• linke Liste, a(i) <= p
• rechte Liste, a(i) > p
• Stufe 2
• Wähle ein Pivotelement p pro Liste
• erste, letzte, mittlere, random
• Tausche die Elemente in eine
• linke Liste, a(i) <= p
• rechte Liste, a(i) > p
Quick-Sort: Pseudocode
funktion funktion funktion
funktion quicksort(links, rechts) falls
falls falls
falls links < rechts dann ann ann ann
teiler := teile teile teile teile(links, rechts) quicksort(links, teiler-1) quicksort(teiler+1, rechts) ende
ende ende ende ende ende ende ende
Quick-Sort: Pseudocode
Quick-Sort: Beispiel
1 13 5 27 7 14 3 8 2
Pivotelement
•
Index i startet am Anfang der Liste
•
i++, bis a[i]>p
1 13 5 27 7 14 3 8 2
Index i Index j
1 13 5 27 7 14 3 8 2
13 >7 2<=7
1 2 5 27 7 14 3 8 13
1 2 5 3 7 14 27 8 13
Weiter mit Rekursion
1 2 5 3 7 14 27 8 13 Pivotelemente
1 2 3 5 7 13 27 8 14
13 8 27 14
1 2 3 5 7
void quickSort(int[] arr, int left, int right) { int i = left, j = right;
int tmp;
int pivot = arr[ (left + right) / 2 ];
while (i <= j) {
while (arr[i] < pivot) i++;
while (arr[j] > pivot) j--;
if (i <= j) {
tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
i++;
j--;
} };
if (left < j)
quickSort(arr, left, j);
if (i < right)
quickSort(arr, i, right);
Sortierung mit BubbleSort und QuickSort
BubbleSort BubbleSort QuickSort QuickSort
n Zeit ns Zeit ms Zeit ns Zeit ms
100 145516 0 42518 0
200 583164 0 71108 0
300 266108 0 108862 0
400 483832 0 41786 0
500 775230 0 39220 0
600 1142503 0 46550 0
700 1532500 0 55347 0
800 2025495 15 64877 0
900 2570172 0 73674 0
1000 3247537 0 79906 0
2000 13133829 16 178871 0
3000 29703228 31 257677 0
4000 52558773 47 373870 0
5000 81858074 78 475768 0
6000 115988361 125 549442 0
7000 156244983 156 657571 0
8000 198897304 202 760569 0