Informatik II Prof. Jansen

Werbung
Informatik II
Prof. Jansen
Tim Dauer, [email protected]
Sommersemester 2004
Stand: 24. Mai 2004
Der Autor übernimmt keinerlei Garantie für Vollständigkeit!
1
Informatik 2 - Script 1. Teil
Inhaltsverzeichnis
1 Grundlagen
4
1.1
Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.2
Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.3
Ausdrücke und Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.4
Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
1.4.1
Das While-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
1.4.2
Das Do-While-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
1.4.3
Das For-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
1.4.4
Das If-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
1.4.5
Das Switch-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
Einfache Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
1.5.1
Fließkomma-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
1.5.2
Integer-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
1.5.3
Der Typ Boolean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
1.5.4
Der Typ Char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
Strings und Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
1.6.1
Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
1.6.2
Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
1.6.3
Binäre Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
1.6.4
Problem: Mischen von sortierten Sequenzen . . . . . . . . . . . . . . . . . . . . . .
17
1.6.5
Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
1.5
1.6
2 Prinzipien der objektorientierten Programmierung
20
2.1
Objekte, Methoden und Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
2.2
Formale Beschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
2.2.1
Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
2.2.2
Variablen (Deklaration von Instanz- und Klassenbariablen): . . . . . . . . . . . . .
27
2.2.3
Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
Stacks und Queues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
2.3
2
2.3.1
Stacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
2.3.2
Queues (Schlangen) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
2.4
Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
2.5
Einfach verkettete Listen
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
Realisation eines Stacks durch eine einfach verkettete Liste . . . . . . . . . . . . .
42
Doppelt verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
2.6.1
47
2.5.1
2.6
Exkurs: Prinzipien der objektorientierten Programmierung . . . . . . . . . . . . .
3
1
Grundlagen
1.1
Algorithmus
Ein Algorithmus besteht aus einer Folge von Anweisungen (engl. Statements), die vorswchreiben, was
zu tun ist. Einzelne Anweisungen werden durch ein Semikolon angeschlossen.
Mögliche Anweisungen:
1. Zuweisung v = w (v Variable, w arithmetischer Ausdruck)
2. bedingte Zuweisung
if (...) Bedingung → Boolescher Ausdruck
... → Zweige
else
... → Folge von Anweisungen
3. Schleifen
while (...) → Bedingung = bool
... → Rumpf = Folge von Anweisungen
Algorithmen, die auf Anweisungen basieren, heißen imperative Algorithmen - im Gegensatz zu funktionalen Agorithmen, die auf geschachtelten Funktions abstraktionen basieren.
1.2
Syntax
Wir möchten die Syntax von Java im Weiteren etwas formaler beschreiben. Dazu verwenden wir den Erweiterten-Backus-Naur-Formalismus (EBNF). Dieser enthält Regeln,
gemäß denen syntaktisch korrekte Programme abgeleitet werden können.
4
Der EBNF enthält 4 Ableitungsregeln:
1. A := BC → Satz A besteht aus B gefolgt von C
2. A := B | C → Satz A besteht entweder aus B oder C
3. A := [B] → Satz A besteht aus B oder nichts
4. A := {B}* → Satz A besteht aus beliebig vielen B’s oder nichts
Beispiel: Syntax einer Programmiersprache, die nur natürliche Zahlen programmieren
kann:
<Programm> := { <Anweisung> } *
<Anweisung> := [<Name> = <Term> + <Term>;]
<Name> := A|B|C|...|Z
<Ziffer> := 0|1|2|...|9
Ein Hauptprogramm in Java, das keine weiteren Klassen verwendet, hat folgende Syntax:
<Java Programm> := public static void main (string[] args)
{
<StatementSequence >
}
<StatementSequence> := {<Statement>;}*
<Statement> := <Assignment>|<whileStatement>|<forStatement>|
<DoWhileStatement>|<IfStatement>|<SwitchStatement>|
<MethodCall>|<ReturnStatement>|<BreakStatement>|
<ContinueStatement>|...
1.3
Ausdrücke und Anweisungen
Eine Anweisung ist eine Aktion zur Änderung des Zustands des Programms.
Dabei ist ein Zustand die Folge der Werte aller Programmvariablen. Die einfachste
Anweisung ist eine Zuweisung:
<assignment> := <variable> = <expression>
5
Eine Variable wird durch einen frei wählbaren Namen bezeichnet. Jede Variable hat
einen bestimmten Typ (”Boolean”, ”Character”, ”Integer”, etc.) und die Typen auf der
linken und der rechten Seite müssen identisch oder zumindest verträglich sein.
Ein Variablenname besteht aus einem Buchstaben gefolgt von einer beliebigen Folge von
Ziffern und Buchstaben. Ausdrücke sind zusammengesetzt aus Konstanten (Literale),
Variablen und Operatoren.
Arithmetische Ausdrücke:
Konstanten: 17, -52, 3.14
Operatoren: +, -, ∗ , /, % (modulo Operation)
Bitweise Operatoren für Integer:
∼: Komplement
&: AND
!: OR
ˆ : XOR
« , »: SHIFT-Operatoren
≫: zyklischer SHIFT
Boolesche Ausdrücke:
Konstanten: true, false
Operatoren: && (AND), ||(OR), !(NOT), ˆ(XOR), &(AND), |(OR)
&& und ||haben gegenüber & und | den Vorteil, dass sie den zweiten Ausdruck auf der
rechten Seite nicht mehr auswerten, wenn das Ergebnis nach dem ersten Ausdruck schon
feststeht.
Zeichenbasierte Ausdrücke:
Character-Literale: ’a’, ’ !’
String-Literale: ”Example”
Operator: + (Konkatenation)
In Java gibt es noch andere Zuweisungen der Form:
<assignment> := <variable><op> = <expression>
Dies ist äquivalent zu:
<assignment> := <variable> <op> <expression>
6
Dabei kann <op> sein:
+, -, ∗, /, %, &, |, ˆ, «, », ≫
Beispiel:
i += 2;
c -= 5;
(Diese Operatoren sollten Sparsam verwendet werden.)
1.4
1.4.1
Kontrollstrukturen
Das While-Statement
Das whileStatement sieht in Java wie folgt aus:
<whileStatement > := while(<BooleanExpression )
<BodyStatement >
<BodyStatement> := <Statement>|{ <StatementSequence>}
Semantik:
1. berechne den Booleschen Ausdruck.
Falls Wert = true ist, gehe zu (2), andernfalls beende die Anweisung.
2. führe Anweisung in BodyStatement aus und gehe zu (1)
Beispiel:
Berechnung der Fakultät:
Für eine natürliche Zahl n ist n! = 1 ∗ 2 ∗ 3 ∗ ... ∗ n
i = 1;
f = 1;
while (i<=n) {
f = f ∗ i;
i = i + 1;
}
Das DoWhileStatement ist eine Schleife, bei der die Schleifenbedingung am Ende jeder
Iteration ausgewertet wird.
7
1.4.2
Das Do-While-Statement
<DoWhileStatement> := do <BodyState> while <BoolExp>
Die Anweisung im Schleifenrumpf werden zunächst einmal ausgeführt.
Am Ende jeder Iteration wird der Boolesche Ausdruck ausgewertet. Ist er wahr, so
wiederholt man den Schleifenrumpf. Andernfalls bricht man die Anweisung ab.
Beispiel:
i = 1;
f = 1;
do {
f = f ∗ i;
i = i + 1;
}
while (i <= n);
1.4.3
Das For-Statement
<forStatement> := for([<Init.>];[Cond.];[<Inc.>])
<BodyStatement>
<Initialization> → Initialisierung einer Indexvariable oder gemeinsame Deklaration.
Beispiele:
for(i = 0;<Condition>;<Increment>)
for(int i = 0;<Condition>;<Increment>)
<Condition> → Boolescher Ausdruck, der zu Beginn jeder Iteration ausgewertet wird.
<Increment> → Ausdruck, der die Indexvariable erhöht oer verringert. Wird am Ende
jeder Iteration ausgeführt.
8
Beispiel:
(a) Berechnung der Fakultät
int f = 1;
for(int i = 1;i <= n;i++)
f = f ∗ i;
(b) berechne für natürliche Zahl n die Zahl 2n
int p = 1;
for(int i = 1;i <= n;i++)
p = p ∗ 2;
1.4.4
Das If-Statement
Das if-Statement ist formal wie folgt definiert:
<ifStatement > := if (<BooleanExpression>)
<BodyStatement>
[else <BodyStatement >]
Die booleschen Ausdrücke nach ”if” und dem optionalen ”else if” werden nacheinander
ausgewertet. Sobald ein Ausdruck wahr ist, führt man die zugehörigen Anweisungen im
BodyStatement aus, womit die Ausführung des If-Statements abgeschlossen ist.
Ist kein boolescher Ausdruck wahr, so wird der ”else”-Zweig ausgeführt (sofern
vorhanden).
Beispiel: Berechnung der Signumfunktion
if (x == 0);
sg = 0;
else if (x < 0)
sg = -1;
else sg = 1 ;
9
1.4.5
Das Switch-Statement
Das Switch-Statement führt in Abhängigkeit von einem ganzzahligem Ausdruck eine von
mehreren Alternativen aus:
<SwitchStatement > := switch (<Expression>) {
{case (<ConstantExpression>) :
[<StatementSequence >] } *
[default: <StatementSequence >]
}
Der Ausdruck nach ”switch” muss einen Integer-Wert liefern und die Ausdrücke nach
”case” müssen Integer-Konstanten und paarweise verschieden sein.
Der Ausdruck nach ”switch” wird zunächst ausgewertet und nacheinander mit den
Kontanten verglichen. Stimmen zwei Werte überein, so führt man die zugeordneten
Anweisungen aus. Andernfalls führt man die Anweisung nach dem optionalen
”default”-Zweig aus. Bei der Ausführung des ”switch”-Statements werden normalerweise
alle case-Alternativen durchlaufen.
Die kann man durch ein ”break”-Statement am Ende einer StatementSequence
unterbinden (mehr später).
Beispiel:
switch (sg) {
case 0 : System.out.println(”Die Zahl ist 0”);
case -1 : System.out.println(”Die Zahl ist negativ”);
default : System.out.println(”Die Zahl ist positiv”);
}
1.5
Einfache Datentypen
Jede benutzte Variable muss in eine Variablendeklaration eingeführt werden und hat
einen Typ.
<Declaration > := [<VariableModifier >] <VariableType >
<VariableList >
<VariableList > := <VariableName > [=<InstantValue >]
{ <VariableName > [=<InitialValue] }*
10
Beispiele:
int x;
int height = 10, width = 20;
char c;
boolean a;
float f;
Vaiablen desselben Typs können in einer Liste zusammengefasst werden (müssen aber
nicht). Eine Variable kann gleich initialisiert werden.
<VariableType > := <SimpleType >|<ComplexType >
<SimpleType > := boolean|char|byte|int|short|float|...
Die komplexeren Datentypen wie Arrays und Objekte (auch Strings) werden später
behandelt.
<VariableList > := <VariableName > [=<InstantValue >]
{ <VariableName > [=<InitialValue] }*
Typ
boolean
char
byte
short
int
long
float
double
1.5.1
Inhalt
true, false
1 Zeichen
Integer mit Vorzeichen
Integer mit Vorzeichen
Integer mit Vorzeichen
Integer mit Vorzeichen
Fließkommazahl
Fließkommazahl
Fließkomma-Typen
float, double
Literale: 3.14, 4.52E3 (→ zur Basis 10!)
Operatoren: +, -, ∗, /
1.5.2
Integer-Typen
byte, short, int, long
Literale: 17, -52
Arithmetische Operatoren: +, -, ∗, /, %
11
Größe
1 Bit
16 Bit
8 Bit
16 Bit
32 Bit
64 Bit
32 Bit
64 Bit
Bitweise Operatoren: &, |, ˆ, ∼ , «, », ≫
Für beide Typen sind Inkrement und Dekrement Operatoren definiert:
++ → Plus 1
- - → Minus 1
Beispiel: n = i++
Wird solch ein Operator einer Variable vorausgestellt, so wird 1 addiert/subtrahiert
bevor der Wert in den Ausdruck eingelesen wird .
Wird der Operator angehängt, so wird erst eingelesen und dann die Veränderung
vorgenommen.
Beispiel:
int i = 8;
System.out.println(i++ + ”, ” + ++i + ”, ” + i- -);
produziert → 8, 10, 10.
1.5.3
Der Typ Boolean
Literale: true, false
Operatoren && (AND), ! (NOT), || (OR), ˆ (XOR)
Mit den Operatoren <, <=, >, >=, ==, != kann man boolesche Ausdrücke aufbauen.
Beispiele:
(1==2) → false
(1<=2) → true
(1>=2) → false
Prioritäten: ! vor && vor ||
Beispiel: !x || y → (!x) || y
1.5.4
Der Typ Char
Literale: 1 Zeichen (eingeschlossen durch ’ ’)
Beispiel: ’a’, ’?’
Sonderzeichen: \t, \n
Der Typ char speichert Unicode Zeichen.
12
1.6
Strings und Arrays
1.6.1
Strings
Der Stringtyp ist eine Klasse (zu finden unter java.lang.string). Sie können in einfachen
Datentypen deklariert werden.
Ein String-Literal wird durch Anführungszeichen angegeben:
String s = ”Now”;
String t = s + ” is the time.”;
t → ”Now is the time”
Übrigens: + ist in diesem Beispiel ein Operator, der String verkettet (Konkateniert)!
Die Klasse java.lang.string stellt Methoden zur Verarbeitung von Strings zur
Verfügung, z. B.
∗.equals() → zum Vergleich von Strings
⇒ Wert: Boolean
∗.length() → Anzahl der Character im String
∗.compareTo() → zum Vergleich von Strings (Wert: Integer)
Stringobjekte sind unveränderlich (d.h. es gibt keine Methoden, die den Inhalt von
Strings verändern können). Die Aufrufe der Methoden besprechen wir später.
Möchte man den Inhalt verändern, so muss man zunächst aus dem String-Objekt ein
sogenanntes String-Buffer-Objekt erzeugen, den Inhalt des Stringbuffers verändern und
dann einen neuen String mit dem Inhalt des String-Buffers erzeugen (mehr in Klasse
java.lang.StringBuffer).
1.6.2
Arrays
Ein Feld oder Array ist eine Folge von Werten des gleichen Typs (genannt Basistyp).
Zunächst schauen wir uns 1-Dimensionale Felder an:
Beispiel:
A:
Wert
Index 0
1
2
...
... n
Beispiele für Deklarationen:
int[] A = new int[N];
double[] V = new double[N];
13
Variablen A und V besteht aus N Feldern vom Typ Integer bzw. Double. Feldelemente
sind in Java immer von 0 bis N-1 durchnummeriert. Ein neues Array wird mit dem
Schlüsselwort new erzeugt. Ein Array-Literal kann wie folgt erzeugt werden:
int[] A = {2,4,6,8,10 }
Jedes Feldelement kann durch Indizierung identifiziert werden: A[i], wobei 0 ≤ i ≤ n-1!
Beispiele:
1. Belegung eines Arrays:
for(int i = 0;i < N; i++) A[i]=1;
2. Aufsummieren von Feldelementen:
int sum = 0;
for(int i = 0;i < N; i++) sum = sum + A[i];
3. Berechnung des kleinsten Wertes in A und zugehörigen Index:
int min = A[0];
int m = 0;
for(int i = 0;i<N;i++) {
if (A[i] < min) }
min = A[i];
m = i;
}
4. Suche nach einem Element im Array:
Gegeben: Array A und Integer x
Gesucht: Index i mit A[i] = x
Versuch:
int i = 0;
while (A[i] != x)
i++
Dieser Ansatz funktioniert nicht, wenn x nicht in A auftritt.
Vereinbarung: Tritt x nicht auf, dann sei i = N;
Lösung 1:
int i = 0;
while ((i<N) && (A[i] != x))
i++;
Wir nutzen aus: wenn i = N ist, dann wird der zweite Ausdruck (A[i] != x) nicht
mehr ausgewertet.
Lösung 2: verwende ein Array mit N + 1 Elementen und setze A[N] = x.
int[] A = new int [n + 1];
A[N] = x;
int i = 0;
while (A[i] != x)
i++;
14
Bew: Ist am Ende der Schleife i = N, so gilt:
A[i] ungleich x, ∀ i= 0,. . .,i-1
Analyse der Anzahl der benötigten Operationen (Zuweisungen, Vergleiche,
Additionen). Die Anzahl hängt von dem Wert i = K nach dem Durchlauf der
Schleife ab.
Lösung 1: 1 Zuweisung, K Additionen
2(k+1) Vergleiche falls K<N und 2N+1 Vergleiche, falls K=N
K + 1 — && -Anweisung falls K < N und N
&& -Anweisungen falls K = N.
Im schlimmsten Fall (worst-case) für K = N ergeben sich 4 N + 2 Operationen.
Lösung 2: 2 Zuweisungen, K Additionen, K + 1 Vergleiche; d.h. im schlimmsten
Fall für K = N ergeben sich 2N+3 Operationen.
Bem.(1): Ist das Array ungeordnet, so gibt es keine Möglichkeit diese Schranke im
worst-case wesentlich zu verbessern.
Bem.(2): Verbesserte Lösungen sind möglich, wenn das Array sortiert ist.
1.6.3
Binäre Suche
Gegeben: ein Feld A mit A[i] ≤ A[i+1] für alle i = 0,...,N-2 und Integer x.
Gesucht: Index i mit A[i] = x
A=
Wert 2
Index 0
3
1
5
2
7
3
11
4
13
5
17
6
19
7
Idee: Vergleiche x mit dem Element in der Mitte
A[mid] wobei mid = (N-1) / 2
Drei Fälle sind möglich:
1. (A[mid] == x) → Element gefunden
2. (A[mid] < x) → x liegt an der oberen Hälfte des Arrays zwischen mid+1 und N-1
3. (A[mid] > x) → x liegt an der unteren Hälfte des Arrays zwischen mid-1 und 0
Während der Suche speichern wir Indizes i und j für die untere bzw. oberste Grenze des
aktuellen Suchbereichs.
15
Am Anfang: i = 0; j = N-1;
Algorithmus:
int i = 0; j = N-1, mid;
boolean found = false;
while ((i <= j) && (!found)) {
mid = (i + j) / 2;
if (x < A[mid])
j = mid -1;
else if (x > A[mid])
i = mid + 1;
else
found = true; }
Wichtig: i ≤ mid ≤ j solange i ≤ j!
Analyse der Anzahl der Rechenschritte (Operationen) im worst-case:
Im schlimmsten Fall halbiert der Alg. den Suchbereich iterativ bis die Anzahl der
Elemente im Suchbereich auf 0 zusammengeschrumpft ist
→ x ist nicht im Feld A!
Beispiel:
X = 6, i = 0, j = 7, mid = 7/2 = 3;
Wert 2
Index 0
Zeiger i
3
1
5
2
7
11
3
4
mid
13
5
17
6
19
7
j
A[mid] = 7 > 6 = x
⇒ Wir betrachten nur noch folgendes Teilarray:
i = 0, j = 2, mid = 2/2 = 1;
Wert 2
Index 0
Zeiger i
3
5
1
2
mid j
...
...
6 = X > A[mid] = 3
Es folgt also:
Wert ... 5 ...
Index ... 2 ...
Zeiger
i,j
16
Bei obigen Beispiel gilt i = j = 2 und
mid = (2+2)/2 = 2
Somit: A[mid] = 5;
→ i = 3, j = 2;
⇒ Abbruch der Schleife!
Formal: Wie oft muss der Suchbereich halbiert werden?
Es sei Bi für i = 0,...,K die Anzahl der Elemente im Suchbereich (nachdem des i-te Mal
der Bereich halbiert wurde).
Es gilt B0 = N. Es sei K der erste Index mit BK = 0.
Betrachte die Folge B0 , B1 ,...,BK
Ziel: bestimme Abschätzung für K.
Es gilt weiter Bi ≤ Bi − 1 / 2 ∀ i > 0
⇒ Bi ≤ B0 / 2i = N / 2i
Wenn N / 2i < 1 ⇔ N < 2i ⇔ log2 N < i
Für K = log2 N + 1 gilt die Bedingung und damit benötigt der Algorithmus höchstens
log2 N + 1 Schleifendurchläufe.
In jeder Schleifeniteration werden maximal 4 Vergleiche und 2 Zuweisungen ausgeführt.
Aufwand: c log N
1.6.4
Problem: Mischen von sortierten Sequenzen
Gegeben sind zwei sortierte Felder:
A mit N Zahlen
B mit M Zahlen
Diese sollen in ein Feld C der Länge N + M gemischt werden (d.h. alle Elemente aus A
und B sollen in C enthalten sein und C soll aufsteigend sortiert sein).
A: 1
5
7
8
B: 2
3
9
10
⇒ C: 1
2
3
5
7
8
9
10
Deklaration:
int[ ] A = new int[N];
int[ ] B = new int[M];
int[ ] C = new int[N+M];
17
Idee: nimm jeweils das kleinere der Elemente in den Feldern A und B und stelle es an
die nächste Position in C.
Am Ende des Algorithmus kann der Rest von B (d.h. 9,10) direkt kopiert werden.
Folgende Indizes werden benutzt:
i : index bzgl. Feld A (der Teil A[i],...,A[N-1] ist nicht bearbeitet worden. Ist i = N, so
ist A leer.
j : Index bzgl. Feld B (analog)
k : Index bzgl. Feld C (erste freie Position in C)
A+B = C:
Wert
Quellarray
1 2
A B
3
B
5 7 8 9
A A A B
10
B
int i = 0, j = 0, k = 0;
while((i < N) || (j < M)) {
if (i = = N) { //A ist abgearbeitet [(1)]
kopiere Rest von B nach C;
}
else if (j = = M) { //B ist abgearbeitet [(2)]
kopiere Rest von A nach C
}
else { //A und B ist nicht abgearbeitet [(3)]
kopiere kleineres der Elemente A[i], B[j] nach C[k] und erhöhe Indizes
}
Verfeinerung des Algorithmus:
1. for(int l = j;l < M;l++) {
c[k] = B[l];
k++;
}
j = M;
2. for(int l = i;l < M;l++) {
c[k] = A[l];
k++;
}
i = N;
18
3. if (A[i] <= B[j]) {
C[k] = A[i];
i++;
}
else {
c[k] = B[j];
j++;
}
k++;
Worst-Case: M + N Vergleiche
1.6.5
Mehrdimensionale Arrays
Die Dimension ergibt sich aus der Anzahl der [ ]-Paare bei der Deklaration.
Beispiel:
int [ ] [ ] matrix = new int [N] [M];
double [ ] [ ] [ ] cube = new double [N] [M] [L];
Mehrdimensionale Arrays sind implementiert als Arrays von Arrays.
Beispiel: 2-dimensionales Array
0
0
1
2
..
.
M-1
1
2
...
...
...
...
N-1
...
Hierbei ist jedes Feldelement in der 1. Dimension ein Array mit M Feldern.
Bemerkung: Bei der Erzeugung eines mehrdimensionalen Arrays müssen nicht alle
Dimensionsgrößen direkt spezifiziert werden.
Regel: bei dem ersten n ≥ 1 Dimensionen muss die Anzahl der Elemente angegeben sein.
Zulässig: int [ ] [ ] [ ] cube = new int [5] [3] [ ];
nicht Zulässig: int [ ] [ ] [ ] cube = new int [5] [ ] [3];
19
Feldelemente können angesprochen werden wie bei einem Ein-Dimensionalen Array:
matrix[i] [j] = ...
cube[i] [j] [k] = ...
Analog können Literale definiert werden:
int [ ] [ ] products =
{ {0,0,0,0,0 }, {0,1,2,3,4 }, {0,2,4,6,8 }, etc.}
Die Größe eines Arrays ist in einer Variable length gespeichert und kann abgefragt
werden durch <ArrayName>.length
Beispiel:
l = A.length;
l = matrix[1].length;
2
Prinzipien der objektorientierten Programmierung
2.1
Objekte, Methoden und Klassen
Ein Java-Programm besteht aus einer Menge von einer oder mehreren
zusammenhängenden Klassen. Klassen sind ein Mittel zur Beschreibung der
Eigenschaften und Fähigkeiten der Objekte der realen Welt.
Eine Klasse ist genau genommen eine Sammlung von Daten sowie Methoden, die auf
diesen Daten arbeiten. Die Daten und Methoden definieren den Inhalt und die
Fähigkeiten von einem Objekt.
Beispiel: Klasse, die Kreisobjekte definiert
Idee:
• beschreibe Kreis durch x / y Koordinaten, des Mittelpunktes und des Radius r.
• stelle Funktionen zur Berechnung von Umfang und Fläche zur Verfügung.
20
public class Circle {
public double x,y; // Koordinaten des Mittelpunkts
public double r; //Radius
public double circumference(){ //berechnet Umfang
return 2 + 3.141 * r;
}
public double area() { //berechnet Fläche
return 3.141 * r * r;
}
}
Die Klasse Circle stellt einen neuen Datentyp zur Verfügung. Um Kreise verwenden zu
können, müssen wir eine Variable vom Typ Circle deklarieren und ein Objekt von
diesem Typ erzeugen:
Circle c = new Circle();
c → Name, der auf Objekt verweist
new → erzeugt Objekt
Objekte sind Instanzen einer Klasse. Nach der Erzeugung eines Objekts können wir mit
den Datenfeldern arbeiten bzw. auf diese zugreifen. Dazu verwendet man den
Punkt-Operator:
Die Programmzeilen
Circle c = new Circle();
c.x = 1.0;
c.y = 1.0;
c.r = 2.0;
Circle d = new Circle();
d.x = c.x;
d.y = c.y;
d.r = c.r * 2;
erzeugen zwei Kreise an der Stelle (1,1) mit Radien 2 und 4. Die Methoden einer Klasse
werden in ähnlicher Weise angewandt:
double a = c.area();
Wichtig hierbei ist das Objekt c und nicht der Funktionsaufruf area(). Deshalb spricht
man von objektorientierter Programmierung.
21
Bemerkung:
1. in c.area() wird kein Parameter übergeben.
2. Das Gleiche gilt für die Definition der Methode area(). Sie hat keine Parameter.
Java sieht vor, dass eine Methode an einer Instanz arbeitet!
Die area()-Methode verwendet das Datenfeld r. Da die area()-Methode Teil der
gleichen Methode ist, wie das Datenfeld r, muss die Variable nicht übergeben werden.
Der Radius r ist Teil des Objektes c.
Java arbeitet implizit mit einem Argument this, das auf das Objekt verweist. Wir
können das Schlüsselwort this auch explizit verwenden wie folgt:
public double area() {
return 3.141 * this.r * this.r;
}
Das implizite this-Argument tritt in Methodendefinitionen nicht auf, da es im
allgemeinen nicht notwendig ist.
Bemerkung: Das this-Schlüsselwort ist aber notwendig, wenn ein Methodenargument
oder eine lokale Variable den gleichen Namen wie das Feld einer Klasse hat.
Beispiel:
public void setRadius(double r) {
this.r = r;}
}
Bei der Objekterzeugung wird eine Funktion
Circle c = new Circle()
bzw. Konstruktormethode aufgerufen. Wir können Konstruktoren selber definieren (mit
dem gleichen Namen wie die Klasse):
public Circle(double x, double y, double r) {
this.x = x;
this.y = y;
this.r = r;
}
Mit diesem Konstruktor können wir einen Kreis wie folgt erzeugen:
Circle c = new Circle(1.0,1.0,2.0);
22
Wir können auch mehrere Konstruktoren verwenden (sie müssen aber unterschiedliche
Parameterlisten haben):
public Circle(double r) {
this.x = 0.0;
this.y = 0.0;
this.r = r;
}
Die bisher verwendeten Variablen (im Beispiel: x, y, r) werden Instanzvariablen genannt.
Jede Instanz der Klasse Circle besitzt eine eigene Kopie der Variablen x, y, r. Es gibt
noch statische Variablen (Klassenvariablen). In dem Fall gibt es nur eine Kopie der
Variablen. Diese statischen Variablen werden mit dem Schlüsselwort static deklariert.
Beispiel: zähle die Anzahl der erzeugten Kreise
public class Circle {
static int num-circles = 0;
public double x,y,r;
public Circle(double x, double y, double r) {
this.x = x;
this.y = y;
this.r = r;
num-circles++;
}
..
.
}
Auf die Klassenvariable können wir wie folgt zugreifen:
System.out.println(”Anzahl der erzeugten Kreise: ” + Circle.num-circles;)
Wir können auch Konstanten über Klassenvariablen definieren:
public class Circle {
public class final double PI = 3.14159;
public double x,y,r;
}
Häufig möchte man Methoden definieren, die nicht an einer speziellen Instanz arbeiten.
Diese Methoden nennt man Klassenmethoden. Sie verwenden das Schlüsselwort static
bei der Deklaration:
23
public Circle {
public double x,y,r;
public static Circle bigger (Circle a, Circle b) {
//liefert den größeren zweier Kreise
if (a.r > b.r)
return a;
else
return b;
}
}
Die Klassenmethode bigger könnte man auch als Instanzmethode definieren:
public Circle bigger (Circle that) {
if (this.r > that.r)
return this;
else)
return that;
}
Angenomme wir haben zwei Kreise a, b deklariert:
Circle a = new Circle(1.0,1.0,2.0);
Circle a = new Circle(1.0,1.0,3.0);
Die Methoden bigger können wie folgt aufgerufen werden:
Circle biggest = Circle.bigger(a,b);
//bei Klassenmethode
Circle biggest = a.bigger(b);
//bei Instanzmethode
2.2
2.2.1
Formale Beschreibung
Klassen und Objekte
Klassen bestehen im wesentlichen aus Instanz- und Klassenvariablen sowie Methoden.
Klassen können Subklassen enthalten und verwandte Klassen können zu Paketen
zusammengefasst werden.
24
Aufbau eines Programms
Superklasse
Klasse
z
Subklasse
Klasse
Klasse
}|
+ Instanz- u. Klas- + Methoden
senvariablen
{
Deklaration einer Klasse:
[<ClassModifier>] class <className> [extends <SuperClassName>]
[implements <InterfaceName>] {
//Def. von Variablen und Methoden
..
.
}
Das Schlüsselwort extends wird verwendet um eine Subklasse zu definieren. Das
Schlüsselwort implements wird verwendet um Schnittstellen (Interfaces) zu benennen.
Interfaces können insbesondere zur Definition von abstrakten Datentypen verwendet
werden (d.h. man stellt dem Benutzer einen Datentyp mit einer Reihe von Operationen
und Methoden zur Verfügung, ohne die interne Realisation zu zeigen). In dem Fall
kapselt man Daten und Methoden (siehe Ende des Kapitels).
Der <ClassModifier> kann verschiedene Formen haben:
• public: Klasse darf instanziert oder erweitert werden
– im selben Paket
– überall wo die Klasse importiert wurde
• final: keine Subklasse möglich
• abstract:
– keine Instanzierung möglich
– Klasse enthält abstrakte Methoden, die in der Klasse nicht implementiert sind
(sondern in den Subklassen)
• ...
25
Ist ein <ClassModifier> spezifiziert, so können wir die Klasse überall im selben Paket
verwenden.
Bemerkung: Im wesentlichen arbeiten wir mit public.
Objekterzeugung:
Syntax:
<VariableName> = new <ClassName>
([<Parameter>, {, <Parameter>} *]);
Was passiert, wenn beim Aufruf des new-Operators?
1. Speicher wird für das neue Objekt bereitgestellt. Instanzvariablen werden mit
Default-Werten initialisiert.
2. Konstruktormethode mit den spezifizierten Parametern wird aufgerufen.
3. Eine Referenz (Speicheradresse) auf die neue Instanz wird zurückgeliefert.
Variable c ⇒ Adr. −→ 0xABCDEF01
..
.
In imperativen Programmiersprachen gibt es zusammengesetzte Datentypen wie
Records.
Diese werden in Java durch Klassen realisiert.
Beispiel:
Student
• Matrikelnummer
• Name
• Fachrichtung
• Semesterzahl
public class Student {
public int Matrikelnummer;
public String Name;
public String Fachrichtung;
public byte Semesteranzahl;
}
26
2.2.2
Variablen (Deklaration von Instanz- und Klassenbariablen):
[<VariableModifierList>]<VariableType> <VariableName>[=<InitialValue>]
{,<VariableName>[=<InitialValue>]} *
Der <VariableModifier> spezifiziert den Sichtbarkeitsbereich einer Variablen (d.h. von
wo aus auf die Variable zugegriffen werden kann):
• public: Zugriff von überall möglich (wo die Klasse verfügbar ist).
• protected: Zugriff von der Klasse (ohne Subklasse) möglich.
• private: Zugriff nur von der Klasse aus möglich (nicht von Subklassen).
Ist keiner der obigen Schlüsselwörter spezifiziert, so ist ein Zugriff auf die Variable
innerhalb desselben Pakets möglich.
Weitere Schlüsselwörter:
• static: Klassenvariablen, die zu einer Klasse aber nicht zu den einzelnen Instanzen
der Klasse gehören.
• final: Variablen, deren anfangs zugewiesener Wert nicht verändert werden kann.
• static final: Konstanten
Methoden können lokale Variablen haben. Sie existieren solange wie die Methode
ausgeführt wird.
Deklaration:
<VariableType> <VariableName> [= <InitialValue>]
{,<VariableName> [= <InitialValue>]} *;
Haben in einer Klasse verschiedene Variablen den gleichen Namen, so können
Sichtbarkeitsbereiche überdeckt werden. Es ist jeweils immer die Variable im inneren
Block sichtbar.
27
public class Scope {
public static int x = 2; //Klassenvariable
Beginn der Sichtbarkeit x = 2
public static void proc(){
int x = 4; //lokale Variable
Beginn der Sichtbarkeit x = 4
System.out.println(”Zahl ist ” + x);
}
Ende der Sichtbarkeit x = 4
public static void main(String[] args){
int x = 6; //lokale Variable
Beginn der Sichtbarkeit x = 6
proc();
System.out.println(”Zahl ist ” + x);
System.out.println(”Zahl ist ” + scope.x);
}
Ende der Sichtbarkeit x = 6
}
Ende der Sichtbarkeit x = 2
2.2.3
Methoden
Zunächst: Konstruktormethoden
Der Name einer Konstruktormethode ist immer mit dem Klassennamen indentisch. Ist
in einer Klasse kein Konstruktor definiert, so fügt Java automatisch den folgenden
Konstruktor ein:
public <ClassName>(){
super(); //Konstruktor der Superklasse
}
Ist keine Superklasse spezifiziert (meißt extends), dann ist die Superklasse die Klasse
Object (java.lang.object).
28
Bemerkung:
1. Der Befehl super() ist der erste Befehl in jedem Konstruktor, selbst wenn er nicht
explizit aufgeführt ist.
2. Mit dem Schlüsselwort this() wir ein anderer Konstruktor aufgerufen (muss aber
1. Statement sein)
public Circle (double x, double y, double r) {
this.x = x; this.y = y; this.r = r;
num_circles++;
}
Bei Methoden unterscheiden wir:
Funktionen (sie liefern einen Wert zurück)
Prozeduren (sie liefern keinen Wert zurück)
[<MethodModifierList>]<ReturnType><MethodName>(<ParameterList>) {
<MethodBody>
}
<ParameterList> := [<ParameterType> <ParameterName>
{,<ParameterType> <ParameterName>}*]
Methoden-Modifikatoren:
public|protected|private|abstract|final|static
Wie bei Instanzvariablen schränken die Methoden-Modifikatoren den
Sichtbarkeitsbereich (wie bei public, protected, private) oder die Methode ein (wie
bei abstract, final, static).
Bei Funktionen muss der ReturnType den Typ des Rückgabewerts spezifizieren. Dies ist
ein einfacher Datentyp oder ein Objekt. Der Wert wird mit dem Befehl return
zurückgeliefert.
Bei Prozeduren ist der ReturnType void (d.h. ohne Wert).
Parameterübergabe:
Parameter werden in Java per Wert übergeben (”Call by Value”).
Das bedeutet, dass beim Aufruf der Methode eine Kopie der Variablen angelegt wird.
Während der Ausführung der Methode wird mit der Kopie gearbeitet.
29
Einfache Datentypen:
Ist ein Parameter ein einfacher Datentyp, so kann die Methode den Variablenwert nicht
verändern.
public static void inc(int x){
x++;
}
Hier ist der Wert von x nach Abschluss der Methode inc nicht verändert!
Besser ist eine Funktion:
public static void inc(int x){
return ++x;
}
Objektvariablen:
Objektvariablen haben eine Referenz auf ein Objekt und enthalten das Objekt selber
nicht. Daher können Objekte in Methoden verändert werden.
public static void reset(Circle c){
c.x = 0.0; c.y = 0.0; c.r = 1.0;
}
2.3
Stacks und Queues
Stacks (Keller) und Queues (Schlangen) sind einfach und zugleich wichtige
Datenstrukturen (z. B. werden sie auf der Systemebene verwendet).
2.3.1
Stacks
Ein Stack (Keller) ist ein Behälter, bei dem Objekte gemäß der LIFO-Regel
(Last-In-First-Out) eingefügt bzw. entfernt werden. Wir können uns das wie einen
Stapel vorstellen, bei dem Objekte obne draufgepackt und nur von oben wieder entfernt
werden können.
Hinzufügen:
Neues Element
% bestehender
Stack
30
Entfernen:
bestehender
Stack
& Neues Element
Beispiele:
• Internet WEB Browser speichern die Adressen von ”recently visited pages” in Form
eines Stacks.
• Texteditoren stellen einen UNDO-Mechanismus zur Verfügung, um die letzten
Editor-Operationen rückgängig machen zu können.
Folgende Operationen werden von einem Stack unterstützt:
push(e): fügt Element e oben im Stack ein
pop(): entfernt oberstes Element aus dem Stack und gibt es aus [Ausgabe Objekt]
top(): gibt oberstes Element aus ohne es zu entfernen [Ausgabe: Objekt]
isEmpty(): prüft, ob der Stack leer ist [Ausgabe: boolean]
size(): gibt Anzahl der Elemente im Stack aus [Ausgabe: integer]
top →
3
2
1
0
a
e
b
f
Zunächst: Implementierung von Stacks mit Hilfe von Arrays. Variable top speichert die
Position des obersten Elements im Stack (top = -1 ⇔ Stack leer)
public class Stack {
public static final int CAPACITY = 1000;
//Default-Länge des Arrays
public int capacity;
private int[] S;
private int top = -1;
public Stack(){
this(CAPACITY);
}
public Stack(int cap){
capacity = cap;
S = new int[capacity];
}
public int size(){
return(top+1);
}
31
public boolean isEmpty(){
return(top < 0);
}
public void push(int element){
if(size < capacity)
S[++top] = element;
else → Fehlerbehandlung
}
public int top(){
if(!isEmpty())
return S[top];
else → Fehlerbehandlung
}
public int pop(){
if(!isEmpty()){
int element = S[top–];
return element;
}
else → Fehlerbehandlung
}
}
Bemerkung: zur Initialisierung
int[] S = new int[capacity]; ⇔ int[] S;
S = new int[capacity];
Operation Output Stack
Array
()
push(5)
(5)
S[0] =
push(3)
(5,3) S[0] =
S[1] =
pop()
3
(5)
S[0] =
push(7)
(5,7) S[0] =
S[1] =
..
..
..
..
.
.
.
.
32
5
5
3
5
5
7
top
-1
0
1
0
1
..
.
2.3.2
Queues (Schlangen)
Eine Queue (Schlange) ist ein Behälter, bei dem Objekte gemäß der FIFO-Regel
(First-In-First-Out) eingefügt bzw. entfernt werden. Objekte werden stets vom Kopf der
Schlange entfernt. Neue Objekte werden am Ende der Schlange eingefügt.
←
Anfügen:
←
Entfernen:
Eine Schlange unterstützt folgende Operationen:
enqueue(o): fügt Objekt o am Ende der Schlange ein
dequeue(): entfernt Objekt am Kopf der Schlange und gibt es aus [Ausgabe: Objekt]
isEmpty(): gibt einen Boolean-Wert zurück (wahr, wenn Schlange leer ist)[Ausgabe:
boolean]
size(): gibt Anzahl der Objekte in der Schlange aus [Ausgabe: integer]
front(): gibt Objekt am Kopf der Schlange aus (ohne es zu löschen) [Ausgabe: Objekt]
Realisation 1: Wir verwenden ein Array
Objekt
Zeiger
Index
0
1
2
X
f↑
3
X X X X
4
5
6
7
r↑
8
9
10
11
f: Index der Zelle, die das erste Objekt speichert (sofern Schlange nicht leer ist).
r: Index der ersten freien Position.
Ist Schlange leer, so soll gelten f = r (am Anfang f = r = 0).
Folgendes Problem eröffnet sich:
Startarray sei folgendes...
Objekt
Zeiger
Index
X X
f↑
r↑
0 1 2
3
4
5
6
7
,→ diverse Berechnungen später sieht es so aus...
Objekt
Zeiger
Index
0
1
X
f↑
2
X X
3
4
r↑
5
6
7
33
,→ wiederum diverse Berechnungen später sieht es schließlich so aus!
Objekt
Zeiger
Index
0
1
2
3
X X
f↑
r↑
5 6 7
4
Resultat: Bei einem erneuten Anhängen würden wir bei bisheriger Implementierung
einen Überlauf bekommen, da r auf 8 gesetzt werden muss.
Wir können einen Überlauf bekommen, obwohl das Feld schwach besetzt ist.
Realisation 2: Wir verwenden wieder ein Array
Objekt
Zeiger
Index
0
1
X
f↑
3
2
X X X X X
4
5
6
7
8
9
10
11
S=6
f: Index der Zelle, die das erste Objekt speichert (sofern Schlange nicht leer ist).
S: Anzahl der Elemente in der Schlange
Anfangs f = 0; S = 0;
Beachte: Es ist auch folgende Konfiguration erlaubt:
Objekt
Zeiger
Index
X X X
0
1
2
3
4
5
6
7
X
f↑
8
X
X
X
9
10
11
S=6
Die Position des letzten Objekts berechnet sich durch
(f + S - 1) % capacity
34
public class Queue{
public static final int CAPACITY = 1000;
private int capacity;
private int[] Q;
private int f = 0, s = 0;
public Queue(){
this(CAPACITY);
}
public Queue(int cap){
capacity = cap;
Q = new int[capacity];
}
public size() {
return s;
}
public boolean isEmpty(){
return (s == 0);
}
public void enqueue(int element{
int r;
if (s < capacity) {
r = (f+s)% capacity;
Q[r] = element;
s++;
}
else → Fehlerbehandlung
}
public int dequeue(){
int element;
if (s > 0) {
element = Q[f];
f = (f+1) % capacity;
s–;
return element;
}
else → Fehlerbehandlung
}
}
Operation Output Queue
enqueue(5)
(5)
enqueue(3)
(5,3)
dequeue()
..
.
5
..
.
(3)
..
.
35
Array
Q[0] = 5
Q[0] = 5
Q[1] = 3
Q[1] = 3
..
.
f
0
0
s
1
2
1
..
.
1
..
.
2.4
Fehlerbehandlung
Exception: Ereignis (Ausnahme), das während der Ausführung eines Programms auftritt.
Eine Ausnahmehandlung ermöglicht
1. Korrektur des Fehlers oder
2. kontrollierten Abbruch des Programms.
Exceptions sind Objekte in Java bzw. Instanzen der Klasse java.lang.Throwable
oder deren Subklassen
java.lang.Error
java.lang.Exception
java.io.IOException
java.lang.RuntimeException
IndexOutOfBoundsException
FileNotFoundException
Da Exceptions Objekte sind, können sie Daten enthalten und Methoden definieren.
Insbesondere kann man beim Erzeugen des Objekts eine String-Nachricht mitgeben, die
eine Fehlermeldung enthält und die später mit der Methode getMessage() vom Typ
String ausgegeben werden kann.
Eine Beschreibung, wie man Exceptions definiert, erzeugt und behandelt (am Beispiel
des Stacks) folgt hier:
Definition einer Subklasse von Exception:
public class StackException extends Exception{
public StackException(String err){
}
}
Erzeugen von Exceptions: Tritt in einer Anweisung eine Ausnahme (Exception) auf,
wird eine Exception-Instanz erzeugt und von der Methode (in der die Anweisung steht)
ausgeworfen (throws an Exception). Die eigentliche Exception muss mit einer
throws-Vereinbarung deklariert sein und wird mit dem throw-Befehl erzeugt. In diesem
Fall wird der Codeblock (wo die Exception auftritt) abgebrochen.
public void push (int element) throws StackException {
if (size() == capacity)
throw new StackException(”Stack ist voll!”);
s[++top] = element;
}
36
public void pop() throws StackException {
int element;
if (isEmpty())
throw new StackException(”Stack ist leer!”);
element = s[top- -];
return element;
}
Behandlung von Exceptions: Exceptions werdem mit dem Befehl try-catch abgefangen.
Folgende Syntax:
try <StatementBlock>
{catch(<ExceptionType><identifier>)
<StatementBlock>}*
[finally <StatementBlock>]
Im try-Block stehen Anweisungen, die Instanzen der Klasse Exception auswerfen
können. Danach folgen catch-Blöcke, die angeben wie die einzelnen Exceptions zu
behandeln sind. Danach folgt ein optionaler finally-Block, der immer im Anschluss an
den try-catch-Block ausgewertet wird (egal, ob eine Exception aufgetreten ist oder
nicht).
Beispiel:
try {
push(1);
pop();
top();
}
catch(StackException e){
System.out.println(e.getMessage());
}
catch(IOException e){
.
.
.
}
37
2.5
Einfach verkettete Listen
head
↓
a
→ b
→ c
→ d
→ null
• Eine einfach verkettete Liste besteht aus einer Folge von Knoten. Jeder Knoten
enthält:
1. ein Datenelement
2. einen Zeiger auf das nächste Element
3. Zeiger des letzten Knotens ist gleich null
• Eine solche Liste hat zusätzlich ein Element head, das auf den ersten Knoten der
Liste zeigt.
Wir nummerieren die Listenpositionen mit 1,2,3,... durch. Die folgenden Operationen
sollen für eine Liste realisiert werden:
size(): gibt die Anzahl der Knoten in der Liste aus
isEmpty(): prüft, ob die Liste leer ist
insert(e,p): fügt neues Element e an Position p ein
delete(p): löscht Element an Position p und gibt es aus
locate(e): gibt Position von Element e in der Liste aus
retrieve(p): gibt Element an Position p aus
Annahme: Die Elemente sind wieder Integer!
public class node {
private int element;
private Node next;
private Node (){
this(0,null);
}
public node (int e, Node n){
element = e;
next = n;
}
}
38
public class List{
private Node head;
private int size;
private List(){
head = null;
size = 0;
}
public boolean isEmpty(){
return (size == 0);
}
public int size(){
return size;
}
private Node nodeAtPosition(int p) throws ListException{
Node n;
if ((p < 1) || (p > size))
throw new ListException (”Unzulässige Position!”);
n = head;
for(int i = 1;i < p;i++)
n = n.next;
return n;
}
public void insert (int e, int p) throws ListException {
Node n = new Node(e,null);
Node prev;
if (p == 1){
n.next = head;
head = n;
}
else {
prev = nodeAtPosition(p-1);
n.next = prev.next;
prev.next = n;
}
size++;
}
39
public void delete (int p) throws ListException{
Node n;
int e;
if((p < 1) || (p > size))
throw new ListException(”Falsche Position!”);
if (p == 1){
e = head.element;
head = head.wert;
}
else {
n = nodeAtPosition(p-1);
e = n.next.element;
n.next = n.next.next;
}
size- -;
return e;
}
public int locate(int e) ...{
.
.
.
}
public int retrieve(int p) ...{
.
.
.
}
}
Skizzierungen
Vorher: Neues Element n und bestehende Liste
n
→ null
head
↓
a
→ b
→ ...
40
Ellement e an Position 1 einfügen
Ergebnis:
1. neue Verknüpfung von n zu a
2. ”umgelegte” Verknüpfung von head nach n
head
↓
n
→ a
→ b
→ ...
Ellement n an Position p > 1 einfügen
Ergebnis:
1. neue Verknüpfung von a zu n
2. neue Verknüpfung von n nach b
head
↓
a
→ n
→ b
→ ...
Ellement n an Position 1 entfernen
Element a entfernen!
Ergebnis:
1. neue Verknüpfung von head zu b
2. a und dessen Verknüpfung bleiben ungenutzt im Speicher
head
a
↓
→ b
→ c
→ ...
41
Ellement n an Position p > 1 entfernen
Ergebnis:
1. neue Verknüpfung von a zu c
2. b und dessen Verknüpfung zu c bleiben ungenutzt im Speicher
head
↓
−→ c
a
b
→ ...
%
Für den folgenden Abschnitt schauen wir uns nochmal unsere Klasse node an:
public class node {
public int element;
public Node next;
public Node (){
this(0,null);
}
public node (int e, Node n){
element = e;
next = n;
}
}
2.5.1
Realisation eines Stacks durch eine einfach verkettete Liste
public class Stack {
private Node top;
private int size;
public Stack(){
top = null; size = 0;}
public int size(){
return size;
}
42
public boolean isEmpty(){
if (top==null)
return true;
return false;
}
public void push (int elem) {
Node v = new Node(elem,top);
top = v;
size++;
}
public int top() throws StackException{
.
.
.
}
public int pop() throws StackException{
if (isEmpty())
throw new StackException(”Stack ist leer”);
int e = top.element;
top = top.next;
size- return e;
}
Auch eine Queue kann man durch einfach verkettete Liste realisieren.
Nachteil: Beim einfügen muss man einmal durch die Liste laufen.
Vermeidbar ist dies durch zusätzliche Variable tail.
head
tail
↓
a
→ b
→ c
↓
→ d
→ null
public class Queue {
privtae Node head, tail;
private int size;
public Queue(){
head = null; tail = null; size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return (size==0);
}
43
public void enqueue(int e){
Node n = new Node(e,null);
if (size == 0)//1. Fall
head = n;
else //2. Fall
tail.next = n;
tail = n;
size++;
}
public int dequeue throws QueueException{
if (size == 0)
throw new QueueException(”Queue ist leer!”);
int e = head.element;
head = head.next;
size - -;
if (size == 0)
tail = null;
return e;
}
}
1.Fall: Leere Queue
head
tail
↓
NIL
↓
NIL
→ NIL
Neues Element n: e
Ergebnis:
head
tail
&
.
→ NIL
e
2.6
Doppelt verkettete Listen
Skizze:
head
↓
←−
a
tail
−→
←−
b
−→
←−
−→
−→
...
←−
←−
c
44
↓
x
−→
null
Wir haben:
• eine Folge von Knoten (jeder Knoten hat Zeiger auf Vorgänger und Nachfolger in
der Liste, Vorgänger vom ersten Knoten = null; Nachfolger vom letzten Knoten =
null).
• Variablen head, tail die auf den ersten bzw. letzten Knoten der Liste zeigen
class DLNode{
int element;
DLNode prev,next;
DLNode(){
this(0,null,null);
}
DLNode(int e, DLNode p, DLNode n){
element = e;
prev = p;
next = n;
}
}
public class DLList{
private Node head, tail;
private int size;
public DLList(){
head = null; tail = null;
size = 0;
}
private DLNode dlnodeAtPosition(int p) throws DLListException{
DLNode n;
if ((p < 1)||(p > size))
throw new DLListException(”Unzulässige Position!”);
if (p <= size/2){
n = head;
for(int i = 1;i < p;i++)
n = n.next;
}
else {
n = tail;
for(int i = size;i > p;i - -)
n = n.prev;
}
return n;
}
45
public void insert (int e, int p) throws DLListException{
if (p == 1){
DLNode n = new DLNode(e,null,null);
if (head != null)
head.prev = n;
else
tail = n;
head = n;
}
else if (p == (size+1)){
DLNode n = new DLNode(e,tail,null);
tail.next = n;
tail = n;
}
else {
DLNode m = nodeAtPosition(p-1);
DLNode n = new DLNode(e,m,m.next);
m.next.prev = n;
m.next = n;
}
size++;
}
}
Beachte: Bei den obigen Methoden insert und delete sind verschiedene
Fallunterscheidungen nötig.
Vermeidbar ist dieser Umstand durch zwei Hilfsknoten header und trailer die keine
Datentypen (bzw. die Zahl 0) enthalten.
/
−→
←−
a
−→
←−
b
−→
←−
c
−→
←−
/
−→
null
Anfangs gilt:
NIL←−
/
−→
←−
/
−→NIL
Listenerzeugung ist aufwendiger, Methoden vereinfachen sich (siehe Übungen!)
public class DLLista{
DLNode header,trailer;
int size;
.
.
.
}
46
Kapselung der Node-Klassen:
class DLNode{
private int element;
private DLNode prev,next;
DLNode(){
this(0,null,null);}
DLNode(int e,DLNode p, DLNode n){
element = e; prev = p; next = n;}
int getElement(){ return element;}
void setElement(int e){ element = e;}
DLNode getPrev(){ return prev;}
void setPrev(DLNode p){ prev = p;}
DLNode getNext(){ return next;}
void setNext(DLNode n){ next = n;}
}
Der Inhalt der Knoten kann jetzt nur noch durch die Methoden getElement, etc.
verändert werden.
public class DLList{
.
.
.
public void insert(int e, int p) throws DLListException{
.
.
.
}
public int delete(int p) throws DLListException{
.
.
.
}
2.6.1
Exkurs: Prinzipien der objektorientierten Programmierung
• Abstraktion
• Kapselung
• Modularität
Abstraktion: Ein abstrakter Datentyp (ANT) ist ein mathematisches Modell für eine
Datenstruktur. Es spezifiziert:
1. welche Daten gespeichert werden und
2. welche Operationen unterstützt werden.
47
Ein ANT spezifiziert jedoch nicht, wie die Operationen realisiert werden. In Java kann
man Implementationen von Methoden verstecken, indem man ein Interface
implementiert.
Beispiel:
public interface Stack {
public int size();
public boolean isEmpty();
public int top() throws StackException;
public void push(int element) throws StackException;
public int pop() throws StackException;
}
public class ArrayStack implements Stack {
public static final int CAPACITY = 1000;
private int capacity;
.
.
.
}
Kapselung realisiert das Prinzip ”Information hiding”, Daten können nur über
Methoden verändert werden. Die Realisierung von Methoden wird nicht gezeigt.
Modularität: Softwaresystem ist in mehrere eigenständige Komponenten zerlegt.
48
Herunterladen