Algorithmen und Datenstrukturen
Werner Struckmann
Wintersemester 2005/06
2. Imperative Algorithmen
2.1 Variable, Anweisungen und Zustände 2.2 Felder
2.3 Komplexität von Algorithmen 2.4 Korrektheit von Algorithmen 2.5 Konzepte imperativer Sprachen 2.6 Rekursionen
Paradigmen zur Algorithmenbeschreibung
In einem imperativen Algorithmus gibt es Variable, die
verschiedene Werte annehmen können. Die Menge aller Variablen und ihrer Werte sowie der Programmzähler beschreiben den
Zustand zu einem bestimmten Zeitpunkt. Ein Algorithmus bewirkt eine Zustandstransformation.
Ein funktionaler Algorithmus formuliert die Berechnung durch Funktionen. Die Funktionen können rekursiv sein; auch gibt es Funktionen höherer Ordnung.
Paradigmen zur Algorithmenbeschreibung
In einem objektorientierten Algorithmus werden Datenstrukturen und Methoden zu einer Klasse zusammengefasst. Von jeder
Klasse können Objekte gemäß der Datenstruktur erstellt und über die Methoden manipuliert werden.
Ein logischer (deduktiver) Algorithmus führt Berechnungen durch, indem er aus Fakten und Regeln durch Ableitungen in einem
logischem Kalkül weitere Fakten beweist.
Beispiel: Algorithmus von Euklid
Der folgende, in einer imperativen Programmiersprache formulierte Algorithmus von Euklid berechnet den größten gemeinsamen
Teiler der Zahlen x
,
y∈
N mit x≥
0 und y>
0:a := x;
b := y;
while b # 0
do r := a mod b;
a := b;
b := r od
Anschließend gilt a
=
ggT(
x,
y)
.Beispiel: Algorithmus von Euklid
Variable z2 z5 z8 z11 z14
r – 36 16 4 0
a 36 52 36 16 4
b 52 36 16 4 0
ggT
(
36,
52) =
4 Durchlaufene Zustände: z0,
z1,
z2, . . . ,
z14 Zustandstransformation: z07−→
z14Imperative Konzepte
◮ Variable: Abstraktion eines Speicherbereichs, ein Wert eines gegebenen Datentyps kann gespeichert und beliebig oft
gelesen werden, solange nicht ein neuer Wert gespeichert und der alte damit überschrieben wird.
◮ Zustand: Abstraktion des Speicherinhalts, Gesamtheit der momentanen Werte aller Variablen, ändert sich durch die Ausführung von Anweisungen.
◮ Anweisung: Vorschrift zur Ausführung einer Operation, ändert im Allgemeinen den Zustand.
Datentypen
◮ Datentyp: Die Zusammenfassung von Wertebereichen und Operationen zu einer Einheit.
◮ Abstrakter Datentyp: Schwerpunkt liegt auf den
Eigenschaften, die die Operationen und Wertebereiche besitzen.
◮ Konkreter Datentyp: Beschreibt die Implementierung eines Datentyps.
Grundlegende Datentypen
Oft werden mathematische Konzepte als Grundlage für einen Datentyp verwendet. Ein Datentyp besteht aus einem
Wertebereich und einer Menge von Operationen.
◮ bool: die booleschen Werte „wahr“ und „falsch“
Operationen: logische Verknüpfungen, wie zum Beispiel „und“
und „oder“.
◮ int: ganze Zahlen,
{
minint, · · · , −
1,
0,
1, · · · ,
maxint} ⊆
Z minint und maxint besitzen etwa den gleichen Betrag.Operationen: arithmetische und Vergleichsoperationen.
Die Rechengesetze gelten in der Regel nicht, wenn man über die Darstellungsgrenzen gerät.
Grundlegende Datentypen
◮ real, float: Näherungswerte für reelle Zahlen, dargestellt durch Gleitpunktzahlen:
r
=
m·
bem
∈
Z Mantisse, b∈
N Basis, e∈
Z Exponent Die Darstellung erschließt den Zahlenbereich mit konstanter relativer Genauigkeit.Operationen: arithmetische und Vergleichsoperationen.
Die Rechengesetze gelten im Allgemeinen nur näherungsweise.
Gleitpunktzahlen sollten nicht auf Gleichheit überprüft
werden, stattdessen sollte
|
x−
y| < ε
für einen kleinen WertGrundlegende Datentypen
◮ char: Zeichen aus einem Alphabet
Mit Vergleichsoperationen und den Funktionen
ord
:
char→
int und chr:
int→
char, die eine Zuordnungzwischen den Zeichen aus dem Alphabet und ganzen Zahlen herstellen.
Variable
◮ Variable x : t: Abstraktion eines Speicherplatzes und Zuordnung eines Datentyps
X Menge der Variablen x
:
t x∈
Xt
= τ(
x)
Typ von x v= σ(
x)
Wert von xv
∈
W(
t)
Wertemenge von t◮ Deklaration: Ein Variablenname wird einem Speicherbereich und einem Typ zugeordnet.
var i, j: int;
var r: real;
var c: char;
Zustand
Ein Zustand bezeichnet die Belegung aller Variablen zu einem Zeitpunkt.
◮ Modellierung:
σ :
x17→
v1, . . . ,
xn7→
vn xi∈
X,
vi∈
W(
ti) ∀
i∈ {
1, . . . ,
n}
◮ Tabellarisch: x1
· · ·
xn v1· · ·
vn◮ Mathematisch: Abbildung
σ :
X→
W= {
v1, . . . ,
vn}
, wobeiσ(
xi) ∈
W(
ti) ∀
i∈ {
1, . . . ,
n}
σ(
x)
ist die Belegung der Variablen x im Zustandσ
.Zuweisung
◮ Syntax: x
←
v v∈
W(τ(
x))
◮ Zuweisung, Grundbaustein: Nach Ausführung der Zuweisung gilt
σ(
x) =
v.
Ist
σ :
X→
W ein Zustand und wählt man eine Variable x∈
X sowie einen Wert v vom passenden Typ, so ist der transformierte Zustandσ
<x←v> wie folgt definiert:σ
<x←v>(
y) =
( v falls x
=
yσ(
y)
sonstSemantik
Die Semantik beschreibt die Bedeutung eines Algorithmus.
Sie ist eine (partielle) Funktion, die jeder Anweisung eine Zustandsänderung zuordnet:
[ ] :
A→ ((
X→
W) → (
X→
W))
mit[
a](σ) = σ
′ Semantik einer Zuweisung:[
x←
v](σ) = σ
<x←v>Ausdrücke/Terme
Beispiele für Ausdrücke:
◮ Konstante: 3, 2
.
7182, ′A′◮ Variable: x1, x2, y
◮ Operatoren und Funktionsaufrufe: x1
+
3, f(
x1)
◮ Zusammengesetzte Ausdrücke: f
(
x1+
3) −
2.
7182+
yFalls ein Ausdruck t die Variablen x1, . . . , xn enthält, schreiben wir t
(
x1, . . . ,
xn)
.Beispiel eines booleschen Ausdrucks: x
≤
5∧
y<
4Wert eines Ausdrucks
Die Auswertung von Ausdrücken mit Variablen ist
zustandsabhängig. An die Stelle der Variablen wird ihr aktueller Wert gesetzt.
Beispiel: Für den Ausdruck 2
·
x+
1 ist sein Wert im Zustandσ
durch 2σ(
x) +
1 gegeben.Der so bestimmte Wert des Ausdrucks t
(
x1, . . . ,
xn)
im Zustandσ
wird mitσ(
t(
x1, . . . ,
xn))
bezeichnet.Beispiel:
σ(
2·
x+
1) =
2σ(
x) +
1Zuweisungen
Diese Festlegung erlaubt Wertzuweisungen mit Ausdrücken auf der rechten Seite
y
←
t(
x1, . . . ,
xn)
Der transformierte Zustand hierfür ist wie folgt definiert:
[
y←
t(
x1, . . . ,
xn)](σ) = σ
<y←σ(t(x1,...,xn))>Zuweisungen
Beispiel: Semantik der Zuweisung
x
←
2·
x+
1Transformation:
[
x←
2·
x+
1](σ) = σ
<x←2·σ(x)+1>Hier handelt es sich nicht um eine rekursive Gleichung für x, da auf der rechten Seite der „alte“ Wert von x benutzt wird.
Wertzuweisungen sind die einzigen elementaren Anweisungen imperativer Algorithmen. Aus ihnen werden zusammengesetzte Anweisungen gebildet, aus denen imperative Algorithmen
bestehen.
Zusammengesetzte Anweisungen
Elementare Anweisungen können auf unterschiedliche Arten zu komplexen Anweisungen zusammengesetzt werden:
1. sequenzielle Ausführung, 2. bedingte Ausführung,
3. wiederholte Ausführung,
4. Ausführung als Unterprogramm,
5. rekursive Ausführung eines Unterprogramms.
Diese Möglichkeiten werden als Kontrollstrukturen bezeichnet. Wir betrachten jetzt die ersten drei dieser zusammengesetzten
Anweisungen. Auf die anderen kommen wir am Schluss des Kapitels zu sprechen.
Sequenzielle Ausführung von Anweisungen
◮ Definition: Sind a1 und a2 Anweisungen, so auch a1
;
a2◮ Informelle Bedeutung: „Führe erst a1, dann a2 aus.“
◮ Semantik:
[
a1;
a2](σ) = [
a2]([
a1](σ))
◮ Darstellung im Flussdiagramm:
a1
a2
Bedingte Ausführung von Anweisungen
◮ Definition: Sind a1 und a2 Anweisungen und P ein boolescher Ausdruck, so ist auch
if
Pthen
a1else
a2fi
eine Anweisung.◮ Voraussetzung:
σ(
P)
kann ausgewertet werden (ansonsten ist die Anweisung undefiniert)◮ Informelle Bedeutung: „Falls P gilt, führe a1 aus, sonst a2.“
Bedingte Ausführung von Anweisungen
◮ Semantik:
[ if
Pthen
a1else
a2fi ](σ) =
[
a1](σ)
fallsσ(
P) =
true[
a2](σ)
fallsσ(
P) =
false◮ Darstellung im Flussdiagramm:
a1 a2
P
true false
Wiederholte Ausführung von Anweisungen
◮ Definition: Ist a eine Anweisung und P ein boolescher Ausdruck, so ist auch
while
Pdo
aod
eine Anweisung.◮ Voraussetzung:
σ(
P)
kann ausgewertet werden (ansonsten ist die Anweisung undefiniert)◮ Informelle Bedeutung: „Solange P gilt, führe a aus.“
Wiederholte Ausführung von Anweisungen
◮ Semantik:
[ while
Pdo
aod ](σ) =
σ
fallsσ(
P) =
false[ while
Pdo
aod ]([
a](σ))
sonst Diese Definition ist rekursiv.While-
Schleifen müssen nicht terminieren.◮ Darstellung im Flussdiagramm:
a P true
false
Flussdiagramme
Normierte Methode (DIN 66001) zur Darstellung von Programmen
◮ Beginn: Start
◮ Ende: Stop
◮ Elementare Anweisung: a
◮ Entscheidung durch booleschen Ausdruck:
P
◮ Eingabe nach n: Eingabe n
◮ Ausgabe von p: Ausgabe p
Konkrete Umsetzung
◮ Sprachen mit nur diesen Anweisungen sind bereits berechnungsuniversell.
◮ In existierenden Programmiersprachen gibt es fast immer diese Anweisungen, oft jedoch mehr.
Beispiel:
case-
oderrepeat
-Anweisung.◮ Schlüsselwörter und Syntax der Kontrollstrukturen variieren von Sprache zu Sprache.
Beispiel:
end if
stattfi
.◮ Hierarchische Struktur der Anweisungen: Anweisungen können Bestandteil anderer Anweisungen sein.
Imperative Algorithmen
◮ Es werden die Datentypen int, real und bool verwendet.
◮ Aufbau imperativer Algorithmen (Syntax):
<Programmname>
var
x,y,. . . :int
; p,q:bool
; Variablendeklarationeninput
: x1, . . . ,
xn;
Eingabevariablena; Anweisung(en)
output
: y1, . . . ,
ym Ausgabevariablen◮ Die Semantik wird durch eine Zustandsüberführungsfunktion beschrieben.
Imperative Algorithmen
Die Semantik eines imperativen Algorithmus ist eine partielle Funktion
[
PROG] :
W1× · · · ×
Wn→
V1× · · · ×
Vm[
PROG](
w1, . . . ,
wn) = (σ(
y1), . . . , σ(
ym)) σ = [
a](σ
0)
σ
0(
xi) =
wi∈
Wi für i=
1, . . . ,
n wobeiPROG: <Programmname>
Wi
:
Wertebereich des Typs von xi, für i=
1, . . . ,
n Vj:
Wertebereich des Typs von yj, für j=
1, . . . ,
mImperative Algorithmen
◮ Der Algorithmus legt eine Zustandstransformation fest. Die Eingabe befindet sich im Zustand
σ
0.◮ Die Ausführung imperativer Algorithmen besteht aus einer Abfolge von Zuweisungen. Diese Folge wird mittels
„Sequenz“, „bedingter Ausführung“ und „wiederholter Ausführung“ aus den Zuweisungen gebildet.
◮ Jede Zuweisung definiert eine Transformation des Zustands.
Die Semantik des Algorithmus ist durch die Kombination all dieser Zustandstransformationen festgelegt.
◮ Falls die Auswertung von a nicht terminiert, ist die Funktion
[
PROG]
nicht definiert.Algorithmus von Euklid
Der Algorithmus von Euklid lautet in dieser Notation:
EUKLID
var a, b, r, x, y: int;
input x, y;
a ← x;
b ← y;
while b # 0 do r ← a mod b;
a ← b;
b ← r;
od;
output a;
Fakultätsfunktion
k
! =
1·
2·
3· · ·
kFAK
var x, y: int;
input x;
y ← 1;
while x > 1 do y ← y * x;
x ← x - 1;
od;
output y;
Es gilt:
[
FAK](
w) =
(w
!
für w>
0 1 sonstStart Eingabe x
y ← 1
x > 1 true
y ← y · x x ← x − 1
false
Ausgabe y Stop
Auswertung der Fakultätsfunktion
◮ Signatur der Semantikfunktion:
[
FAK] : int → int
◮ Das Ergebnis der Berechnung ist die Belegung der Variablen y im Endzustand:
σ(
y)
.◮ Der Endzustand
σ
ist laut Definitionσ = [
a](σ
0)
, wobei a die Folge aller Anweisungen des Algorithmus ist.◮
σ
0 ist der Startzustand. Die Eingabe befindet sich inσ
0(
x)
.◮ Da nur die zwei Variablen x und y auftreten, können wir einen Zustand
σ
als Paar(σ(
x), σ(
y))
schreiben.◮
⊥
bedeutet, dass der Wert der Variablen keine Rolle spielt bzw. undefiniert ist.Auswertung der Fakultätsfunktion
Gesucht:
[
FAK](
3)
σ = [
a](σ
0) = [
a](
3, ⊥ )
= [
y←
1; while
x>
1do
y←
y∗
x;
x←
x−
1od ](
3, ⊥ )
= [ while
x>
1do
y←
y∗
x;
x←
x−
1od ]([
y←
1](
3, ⊥ ))
=: [ while
Bdo β od ]([
y←
1](
3, ⊥ ))
= [ while
Bdo β od ](
3,
1)
=
(
σ
fallsσ(
B) =
false[ while
Bdo β od ]([β](σ))
sonst=
(
(
3,
1)
fallsσ(
x>
1) =
false[ while
Bdo β od ]([
y←
y∗
x;
x←
x−
1](σ))
sonstAuswertung der Fakultätsfunktion
· · · = [ while
Bdo β od ]([
y←
y∗
x;
x←
x−
1](
3,
1))
= [ while
Bdo β od ]([
x←
x−
1]([
y←
y∗
x](
3,
1)))
= [ while
Bdo β od ]([
x←
x−
1](
3,
3))
= [ while
Bdo β od ](
2,
3)
= [ while
Bdo β od ]([
y←
y∗
x;
x←
x−
1](
2,
3))
= [ while
Bdo β od ](
1,
6)
=
(
(
1,
6)
fallsσ(
x>
1) =
false[ while
Bdo β od ]([
y←
y∗
x;
x←
x−
1](
1,
6))
sonst= (
1,
6)
[FAK](3) = 6
Fibonacci-Zahlen (iterativ)
f0
=
0,
f1=
1,
fn=
fn−1+
fn−2,
n>
1 FIB:var x,a,b,c: int;
input x;
a ← 0;
b ← 1;
while x > 0 do c ← a + b;
a ← b;
b ← c;
x ← x - 1;
od;
output a;
[
FIB](
x) =
( die x-te Fibonacci-Zahl, falls x
>
0,
Fibonacci-Zahlen (rekursiv)
Einzelheiten zu rekursiven Programmen werden später behandelt!
f0
=
0,
f1=
1,
fn=
fn−1+
fn−2,
n>
1 FIB:var n, x: int;
input n;
if n = 0 then x ← 0 fi;
if n = 1 then x ← 1 fi;
if n > 1 then x ← FIB(n - 1) + FIB(n - 2) fi;
output x;
Ein rekursiv ausgedrückter Algorithmus ist häufig eleganter als sein iteratives Äquivalent. Nachteil ist evtl. eine längere Laufzeit.
Felder
◮ Definition: Ein Feld ist eine indizierte Menge von Variablen des gleichen Typs. Felder sind generische Datentypen.
◮ Deklaration:
var x[I]: T;
var coords[1..3]: real;
var str[0..4095]: char;
◮ Indexmenge: I endlich, häufig I
⊆
N, aber andere Indexmengen sind möglich.◮ Zugriff: x
[
i]
sowohl lesend als auch schreibend, wobei i∈
I. Längenänderungen von Feldern sind nicht in allenProgrammiersprachen möglich.
Verbreitete Spezialfälle von Feldern
◮ Vektor:
var v[1..3]: real;
◮ Matrix:
var v[1..4, 1..4]: real;
◮ Strings:
var str[0..102]: char;
For-Schleife
Die For-Schleife ist eine weitere Kontrollstruktur zur Wiederholung von Anweisungen.
Es seien j
,
k∈
N Konstante,var
i: int
eine Variable und a eine Anweisung.for i ← j to k do a; od
entsprichti ← j; while i ≤ k do a; i ← i + 1; od
i wird Laufvariable der Schleife genannt.Viele Programmiersprachen enthalten Varianten dieser Schleife.
Maximale Summe eines Teilfelds
Die folgenden Programme berechnen die maximale Summe aufeinanderfolgender Zahlen in einem Feld
var
x[
1..
n] : int
.m
=
max
j
X
k=i
x
[
k] |
1≤
i≤
j≤
n
x sei das Feld:
1 2 3 4 5 6 7 8 9 10
31
−
41 59 26−
53 58 97−
93−
23 84 Die maximale Summe aufeinanderfolgender Zahlen istx
[
3] +
x[
4] +
x[
5] +
x[
6] +
x[
7] =
187. Keine andere Summe besitzt einen höheren Wert. Beispielsweise istx
[
1] +
x[
2] +
x[
3] +
x[
4] =
75.Maximale Summe eines Teilfelds
Idee: Berechne alle möglichen Summen.
MaxSum1
var x[1..n]: int;
var i,j,k,s,m: int;
input x;
m ← 0; // Wert der leeren Summe for i ← 1 to n do
for j ← i to n do s ← 0;
for k ← i to j do s ← s + x[k];
od;
m ← max(m, s);
od;
od;
output m;
Maximale Summe eines Teilfelds
Idee: Einsparen der inneren Schleife durch Wiederverwenden bereits bekannter Summen.
MaxSum2
var x[1..n]: int;
var i,j,s,m: int;
input x;
m ← 0; // Wert der leeren Summe for i ← 1 to n do
s ← x[i];
m ← max(m, s);
for j = i+1 to n do s ← s + x[j];
m ← max(m, s);
od;
od;
output m;
Dieser Algorithmus besitzt zwei ineinandergeschachtelte Schleifen.
Maximale Summe eines Teilfelds
Idee: Genau überlegen, was eine hohe Summe ausmacht. Diese Lösung stammt von M. Shamos (1977).
MaxSum3
var x[1..n]: int;
var i,s,m: int;
input x;
m ← 0;
s ← 0;
for i ← 1 to n do
s ← max(0, s + x[i]);
m ← max(m, s);
od;
output m;
Dieser Algorithmus hat nur noch eine Schleife.
Einführung
◮ In der Regel lässt sich ein Problem durch verschiedene Algorithmen lösen (zum Beispiel „maximale Summe aufeinanderfolgender Elemente“).
◮ Welcher Algorithmus soll gewählt werden?
◮ Die Algorithmen müssen hinsichtlich ihres Verhaltens verglichen werden.
◮ Man benötigt ein Maß für den Aufwand eines Algorithmus.
Komplexität von Algorithmen und Problemen
◮ Unter der Komplexität eines Algorithmus versteht man den Aufwand, den der Algorithmus zur Lösung des Problems benötigt. Typischerweise ist hier die
◮ Laufzeit des Algorithmus oder der
◮ Speicherbedarf des Algorithmus gemeint. Man unterscheidet die
◮ Komplexität im günstigsten Fall (best-case), die
◮ Komplexität im mittleren Fall (average-case) und die
◮ Komplexität im ungünstigsten Fall (worst-case).
◮ Unter der Komplexität eines Problems versteht man die
Komplexität des besten Algorithmus zur Lösung des Problems im ungünstigsten Fall.
Komplexität eines Algorithmus
◮ Umfang n eines Problems:
„Anzahl der Eingabewerte“ oder „Größe der Eingabewerte“
oder . . .
◮ Aufwand T(n) eines Algorithmus:
„Anzahl der Schritte“ oder „Anzahl bestimmter Operationen“
oder „Anzahl der benötigten Speicherplätze“ oder . . . , die der Algorithmus braucht, um ein Problem vom Umfang n zu lösen.
Um sinnvolle Aussagen über die Komplexität eines Algorithmus zu treffen, müssen n und T
(
n)
mit Bedacht gewählt werden.Beispiel: Im Algorithmus „Sortieren durch Einfügen“ war n die Anzahl der zu sortierenden Zahlen und T
(
n)
die Anzahl der benötigten Vergleiche.Wachstum von Funktionen
◮ In der Regel stellt die Wachstumsrate der Laufzeit ein
einfaches und geeignetes Kriterium zur Messung der Effizienz eines Algorithmus dar.
◮ Die Wachstumsrate erlaubt es uns, die relative
Leistungsfähigkeit alternativer Algorithmen zu vergleichen.
◮ Die Wachstumsrate von Algorithmen wird meistens mithilfe der asymptotischen Notation angegeben.
◮ Die asymptotische Notation lässt konstante Faktoren unberücksichtigt.
Asymptotische Notation
Es sei eine Funktion g
:
N−→
R gegeben.Θ(
g) = {
f:
N−→
R| ∃
c1>
0,
c2>
0,
n0>
0∀
n≥
n0.
0
≤
c1g(
n) ≤
f(
n) ≤
c2g(
n) }
O
(
g) = {
f:
N−→
R| ∃
c>
0,
n0>
0∀
n≥
n0.
0≤
f(
n) ≤
cg(
n) } Ω(
g) = {
f:
N−→
R| ∃
c>
0,
n0>
0∀
n≥
n0.
0≤
cg(
n) ≤
f(
n) }
o
(
g) = {
f:
N−→
R| ∀
c>
0∃
n0>
0∀
n≥
n0.
0≤
f(
n) <
cg(
n) }
ω(
g) = {
f:
N−→
R| ∀
c>
0∃
n0>
0∀
n≥
n0.
0≤
cg(
n) <
f(
n) }
Gebräuchliche Wachstumsklassen
Θ(
1)
konstantes WachstumΘ(
log(
n))
logarithmisches WachstumΘ(
n)
lineares WachstumΘ(
n log(
n))
„fast lineares“ WachstumΘ(
n2)
quadratisches WachstumΘ(
nk)
polynomiales WachstumΘ(
2n)
exponentielles WachstumO
(
n2)
höchstens quadratisches WachstumΩ(
n)
mindestens lineares WachstumBeispiel: Die Laufzeit beim „Sortieren durch Einfügen“ beträgt im günstigsten Fall
Θ(
n)
und im ungünstigsten FallsΘ(
n2)
. DieLaufzeit kann also durch
Ω(
n)
nach unten und durch O(
n2)
nach oben abgeschätzt werden. Die Laufzeit wird auch durch O(
n3)
nach oben abgeschätzt! In der Praxis werden häufig möglichstGraphen einiger Funktionen
1 log(n) nlog(n)
n
Graphen einiger Funktionen
n2 n3
2n
Exemplarische Werte
n
=
1 10 100 1000 10000log2
(
n) ≈
0 3 7 10 13n log2
(
n) ≈
0 33 664 9966 132877 n2=
1 100 10000 1000000 100000000 2n≈
2 103 1030 10301 103010Maximales n bei gegebener Zeit, Ann.: 1 Schritt benötigt 1
µ
s 1 Min. 1 Std. 1 Tag 1 Woche 1 Jahr n 6·
107 3.
6·
109 8.
6·
1010 6·
1011 3·
1013 n2 7750 6·
104 2.
9·
105 7.
9·
105 5.
6·
106n3 391 1530 4420 8450 31600
2n 25 31 36 39 44
Asymptotische Notationen in Gleichungen
◮ 2n2
+
3n+
1= Θ(
n2)
heißt 2n2+
3n+
1∈ Θ(
n2)
.◮ 2n2
+
3n+
1=
2n2+ Θ(
n)
heißt: Es gibt eine Funktion f(
n) ∈ Θ(
n)
mit 2n2+
3n+
1=
2n2+
f(
n)
.◮ 2n2
+ Θ(
n) = Θ(
n2)
heißt: Für jede Funktion f(
n) ∈ Θ(
n)
gibt es eine Funktion g(
n) ∈ Θ(
n2)
mit 2n2+
f(
n) =
g(
n)
.◮ In Gleichungsketten
2n2
+
3n+
1=
2n2+ Θ(
n) = Θ(
n2)
werden die Gleichungen einzeln gelesen: Die erste Gleichung besagt, dass es eine Funktion f
(
n) ∈ Θ(
n)
mit2n2
+
3n+
1=
2n2+
f(
n)
gibt. Die zweite Gleichung sagt aus, dass es für jede Funktion g(
n) ∈ Θ(
n)
eine Funktion h(
n) ∈ Θ(
n2)
mit 2n2+
g(
n) =
h(
n)
gibt. Aus derEigenschaften der asymptotischen Notation
Eine Funktion f
:
N−→
R heißt asymptotisch positiv, wenn gilt:∃
n0>
0∀
n≥
n0.
f(
n) >
0.
Für alle asymptotisch positiven Funktionen f
,
g:
N−→
R gelten die folgenden Aussagen.Transitivität:
f
(
n) = Θ(
g(
n)) ∧
g(
n) = Θ(
h(
n)) = ⇒
f(
n) = Θ(
h(
n)),
f(
n) =
O(
g(
n)) ∧
g(
n) =
O(
h(
n)) = ⇒
f(
n) =
O(
h(
n)),
f(
n) = Ω(
g(
n)) ∧
g(
n) = Ω(
h(
n)) = ⇒
f(
n) = Ω(
h(
n)),
f
(
n) =
o(
g(
n)) ∧
g(
n) =
o(
h(
n)) = ⇒
f(
n) =
o(
h(
n)),
f(
n) = ω(
g(
n)) ∧
g(
n) = ω(
h(
n)) = ⇒
f(
n) = ω(
h(
n)).
Eigenschaften der asymptotischen Notation
Reflexivität:
f
(
n) = Θ(
f(
n)),
f(
n) =
O(
f(
n)),
f(
n) = Ω(
f(
n)).
Symmetrie:
f
(
n) = Θ(
g(
n)) ⇐⇒
g(
n) = Θ(
f(
n)).
Austausch-Symmetrie:
f
(
n) = Θ(
g(
n)) ⇐⇒
f(
n) =
O(
g(
n)) ∧
f(
n) = Ω(
g(
n)),
f(
n) =
O(
g(
n)) ⇐⇒
g(
n) = Ω(
f(
n)),
f
(
n) =
o(
g(
n)) ⇐⇒
g(
n) = ω(
f(
n)).
Beispiele
8
= Θ(
1)
3n2
−
5n+
8= Θ(
n2)
3n2−
5n+
8=
O(
n2)
3n2−
5n+
8= Ω(
n2)
loga
(
n) =
logb(
n)
logb(
a) Θ(
loga(
n)) = Θ(
logb(
n))
12 log10(
n) = Θ(
log(
n))
12 log2
(
n) = Θ(
log(
n))
Laufzeit von imperativen Algorithmen
Folgende Annahmen werden zur Analyse der Laufzeit von imperativen Algorithmen getroffen:
◮ Zuweisung: Die Laufzeit ist konstant.
◮ Sequenz: Die Laufzeit ist die Summe der Laufzeiten der Einzelanweisungen.
◮ Alternative: Die Laufzeit ist im ungünstigsten Fall die Laufzeit der Bedingungsauswertung plus dem Maximum der
Laufzeiten der Alternativen.
◮ Iteration: Die Laufzeit errechnet sich aus dem Produkt der Laufzeit der inneren Anweisung und der Anzahl der
Iterationen. Hinzu kommt die Laufzeit für einen weiteren Test.
Sortieren durch Einfügen
Code Kosten Anzahl
insertionSort(A)
j ← 2;
c1 1while j ≤ length(A) do
c2 nkey ← A[j];
c3 n−
1i ← j - 1;
c4 n−
1while i > 0 und A[i] > key
c5 Pn j=2 tjdo
A[i + 1] ← A[i];
c6 Pnj=2
(
tj−
1)
i ← i - 1;
c7 Pnj=2
(
tj−
1) od;
A[i + 1] ← key;
c8 n−
1j ← j + 1;
c9 n−
1od;
Abschätzung durch die O-Notation
Code Kosten
insertionSort(A)
j ← 2;
O(
1)
while j ≤ length(A) do
O(
n)
key ← A[j];
O(
n)
i ← j - 1;
O(
n)
while i > 0 und A[i] > key;
O(
n2) do
A[i + 1] ← A[i];
O(
n2)
i ← i - 1;
O(
n2)
od;
A[i + 1] ← key;
O(
n)
j ← j + 1;
O(
n)
od;
Korrektheit von Softwaresystemen
In vielen Situationen ist eine korrekte Funktionsweise eines
Softwaresystems von großer Bedeutung. Dies gilt insbesondere, wenn das System
◮ sicherheitskritisch (z. B. Atomreaktor),
◮ kommerziell kritisch (z. B. massenproduzierte Chips) oder
◮ politisch kritisch (z. B. Militär) ist.
Korrektheit von Softwaresystemen
Es gibt mehrere Möglichkeiten, die Zuverlässigkeit von Softwaresystemen zu erhöhen:
◮ Software Engineering:
Maßnahmen während des gesamten Softwareentwicklungsprozesses.
◮ Programmierung:
Beispiel: Ausnahmebehandlung, Zusicherungen.
◮ Validation:
Systematische Tests unter Einsatzbedingungen; Tests zeigen die Anwesenheit, aber nicht die Abwesenheit von Fehlern.
◮ Verifikation:
Mathematischer Nachweis der Korrektheit von Algorithmen.
Korrektheit von Softwaresystemen
Um ein System zu verifizieren zu können benötigt man Methoden, Werkzeuge und Sprachen, zur
◮ Modellierung von Systemen auf hoher Abstraktionsebene,
◮ Spezifikation nachzuweisender Eigenschaften dieser Systeme (Terminierungsverhalten, berechnete
Funktionswerte, . . . ) und zur
◮ Verifikation, d. h. zum formalen Beweis, dass ein
implementiertes System die spezifizierten Eigenschaften hat.
In diesem Abschnitt behandeln wir eine Möglichkeit zur
Spezifikation und Verifikation imperativer Algorithmen.
Hoaresche Logik
Es seien ein Algorithmus S sowie Bedingungen p und q gegeben.
◮ Wir schreiben in diesem Fall
{p} S {q}
und nennen
◮ p Vorbedingung,
◮ q Nachbedingung und
◮ (p,q) Spezifikation von S.
Hoaresche Logik
◮ S heißt partiell-korrekt bezüglich der Spezifikation (p,q), wenn jede Ausführung von S, die in einem Zustand beginnt, der p erfüllt und die terminiert, zu einem Zustand führt, der q erfüllt.
| =
{p} S {q}◮ S wird total-korrekt bezüglich der Spezifikation (p,q) genannt, wenn jede Ausführung von S, die in einem Zustand beginnt, der p erfüllt, terminiert und zu einem Zustand führt, der q erfüllt.
Beispiele
◮
| = {
true}
x←
1{
x=
1}
◮
| = {
x=
1}
x←
x+
1{
x=
2}
◮
| = {
y=
a}
x←
y{
x=
a∧
y=
a}
◮
| = {
x=
a∧
y=
b}
z←
x;
x←
y;
y←
z{
x=
b∧
y=
a}
◮
| = {
false}
x←
1{
x=
42}
◮
| = {
true} while
0=
0do
x←
1od {
x=
23}
◮
| = {
x>
0} while
x , 0do
x←
x−
1od {
x=
0}
◮
| = {
true} while
x , 0do
x←
x−
1od {
x=
0}
◮
| = {
true} while
p(
x) do α od {¬
p(
x) }
◮
| = {
x+
y=
a} while
x , 0do
x←
x−
1;
y←
y+
1od {
x=
0∧
x+
y=
a}
Hoarescher Kalkül
Zuweisung:
{
pxt}
x←
t{
p}
Sequenz:
{
p}
S1{
r} , {
r}
S2{
q} {
p}
S1;
S2{
q}
If:
{
p∧
e}
S1{
q} , {
p∧ ¬
e}
S2{
q} {
p}
if e then S1 else S2 fi{
q}
While:
{
p∧
e}
S{
p}
{
p}
while e do S od{
p∧ ¬
e}
Anpassungsregel: p
⊃
q, {
q}
S{
r} ,
r⊃
s{
p}
S{
s}
Hoarescher Kalkül
◮ Der hoaresche Kalkül besteht aus dem Axiomenschema für die Zuweisung und Ableitungsregeln.
◮ Falls {p} S {q} mithilfe des hoareschen Kalküls hergeleitet werden kann, schreibt man
⊢
{p} S {q}.Schleifeninvariante
Die Bedingung p in der While-Regel heißt Schleifeninvariante.
{
p∧
e}
S{
p}
{
p}
while e do S od{
p∧ ¬
e}
Eine Schleifeninvariante gilt vor jedem Schleifendurchlauf und damit auch nach jedem Schleifendurchlauf, speziell also nach Beendigung der Wiederholungsanweisung. Dann gilt zudem
¬
e.Schleifeninvariante
Beispiel: Für den folgenden Algorithmus ist q
=
k−
1 eine Schleifeninvariante.{
q=
0∧
k=
1}
while
k , n+
1do q ← q + 1;
k ← k + 1;
od;
{
q=
n}
Nach Ausführung der Schleife gilt: q
=
k−
1∧ ¬ (
k , n+
1)
Daraus folgt: q=
nEigenschaften des Kalküls
Korrektheit:
⊢ {
p}
S{
q} = ⇒ | = {
p}
S{
q}
relative Vollständigkeit:
⊢ {
p}
S{
q} ⇐ = | = {
p}
S{
q}
Beispiel: Division mit Rest
◮ Es sollen zwei ganze Zahlen x
,
y∈
Z mit x≥
0 und y>
0 durcheinander dividiert werden.◮ Das Ergebnis der Division x
/
y ist der Quotient q und der Rest r mit x=
qy+
r∧
0≤
r<
y.
var x, y, q, r: int;
input x, y;
q ← 0;
r ← x;
while r >= y do r = r - y;
q = q + 1;
od;
output q, r;
Beispiel: Division mit Rest
◮ Mithilfe des hoareschen Kalküls leiten wir jetzt den Ausdruck
{
x≥
0}
S{
x=
qy+
r∧
0≤
r<
y}
her, wobei S der obige imperative Algorithmus ist.
◮ Wegen der Korrektheit des Kalküls können wir dann
schließen, dass der Algorithmus bezüglich der angegebenen Vor- und Nachbedingung partiell-korrekt ist.
◮ Die Bedingung y
>
0 wird zum Nachweis der totalen Korrektheit benötigt.Beispiel: Division mit Rest
Wir zeigen zuerst, dass
x
=
qy+
r∧
0≤
reine Schleifeninvariante ist. Dies ergibt sich aus:
x
=
qy+
r∧
0≤
r∧
r≥
y= ⇒
x= (
q+
1)
y+ (
r−
y) ∧
0≤
r−
yBeispiel: Division mit Rest
Die Behauptung folgt dann aus den beiden Aussagen x
≥
0= ⇒
0≤
xund
x
=
qy+
r∧
0≤
r∧ ¬ (
r≥
y) = ⇒
x=
qy+
r∧
0≤
r<
y.
Beispiel: Division mit Rest
◮ Da nach Voraussetzung y
>
0 gilt, durchläuft die Variable r eine monoton streng fallende Folge natürlicher Zahlen:r0
,
r1,
r2, . . .
◮ Da y konstant ist, wird deshalb für ein i die Bedingung ri
<
ywahr. Das heißt, das Programm terminiert schließlich und ist deshalb total-korrekt bezüglich der angegebenen
Bedingungen.
Bemerkung: Es gibt auch Kalküle zum Nachweis der totalen Korrektheit.
Beispiel: Division mit Rest
Vor- und Nachbedingung, Schleifeninvariante als Annotation:
static private int remainder (int x, int y) {
assert x >= 0 && y > 0;
int q = 0, r = x;
assert x == q * y + r && 0 <= r;
while (r >= y) { r = r - y;
q = q + 1;
assert x == q * y + r && 0 <= r;
}
assert x == q * y + r && 0 <= r && r < y;
return r;
}
Wunschtraum
Es ist ein Algorithmus gesucht, der für beliebige p, S und q beweist oder widerlegt, dass
⊢
{p} S {q}gilt.
Solch ein Algorithmus kann nicht existieren!
Dann existiert natürlich erst recht kein analoger Algorithmus für die totale Korrektheit.
Totale Korrektheit
◮ Zum Nachweis der totalen Korrektheit muss zusätzlich zur partiellen Korrektheit die Terminierung gezeigt werden.
◮ Um die Terminierung von Schleifen nachzuweisen, gibt es keine allgemeine Methode, oft funktioniert aber die
Vorgehensweise vom obigen Beispiel:
1. Man suche einen Ausdruck u, dessen Wert eine natürliche Zahl ist.
2. Man beweise, dass u bei jedem Schleifendurchlauf echt kleiner wird.
3. Da die natürlichen Zahlen wohlgeordnet sind, muss die Schleife terminieren.
Beispiel: Insertionsort
P
(
j)
: Das Feld A[
1· · ·
j]
ist eine Permutation des Ausgangsfeldes A[
1· · ·
j]
.S
(
j)
: Das Feld A[
1· · ·
j]
ist sortiert.A∗
[
j]
: Der ursprünglich in A[
j]
enthaltene Wert.Pk∗
(
j)
: Das Feld A[
1· · ·
k−
1,
k+
1· · ·
j]
ist eine Permutation des Ausgangsfeldes A[
1· · ·
j−
1]
.Sk∗
(
j)
: Das Feld A[
1· · ·
k−
1,
k+
1· · ·
j]
ist sortiert.Beispiel: Insertionsort
{
n≥
1}
{
P(
1) ∧
S(
1) ∧
n≥
1} j ← 2;
{
P(
j−
1) ∧
S(
j−
1) ∧
2≤
j≤
n+
1} while j ≤ n do
key ← A[j];
i ← j - 1;
while i > 0 ∧ A[i] > key do A[i+1] ← A[i];
i ← i - 1;
od;
A[i+1] ← key;
j ← j + 1;
od;
{
P(
j−
1) ∧
S(
j−
1) ∧
2≤
j≤
n+
1∧
j>
n}
{
P(
n) ∧
S(
n) }
Beispiel: Insertionsort
{
P(
j−
1) ∧
S(
j−
1) ∧
2≤
j≤
n+
1∧
j≤
n} {
P∗j
(
j) ∧
S∗j
(
j) ∧
0≤
j−
1} key ← A[j];
i ← j - 1;
{
Pi∗+1(
j) ∧
Si∗+1(
j) ∧
key<
A[
i+
2], . . . ,
A[
j]
∧
0≤
i≤
j−
1∧
key=
A∗[
j] }
while i > 0 ∧ A[i] > key do A[i+1] ← A[i];
i ← i - 1;
od;
A[i+1] ← key;
j ← j + 1;
{
P(
j−
1) ∧
S(
j−
1) ∧
2≤
j≤
n+
1}
Konzepte imperativer Sprachen
◮ Anweisungen
◮ primitive Anweisungen: Zuweisung, Block, Prozeduraufruf
◮ zusammengesetzte Anweisungen: Sequenz, Auswahl, Iteration
◮ Ausdrücke
◮ primitive Ausdrücke: Konstante, Variable, Funktionsaufruf
◮ zusammengesetzte Ausdrücke: Operanden/Operatoren
◮ Datentypen
◮ primitive Datentypen: Wahrheitswerte, Zeichen, Zahlen, Aufzählung
◮ zusammengesetzte Datentypen: Felder, Verbund, Vereinigung, Zeiger
Konzepte imperativer Sprachen
◮ Abstraktion
◮ Anweisung: Prozedurdeklaration
◮ Ausdruck: Funktionsdeklaration
◮ Datentyp: Typdeklaration
◮ Weitere Konzepte
◮ Ein- und Ausgabe
◮ Ausnahmebehandlung
◮ Bibliotheken
◮ Parallele und verteilte Berechnungen
◮ . . .
Blöcke
◮ Motivation: Ein Block fasst mehrere Anweisungen und Deklarationen zu einer Einheit zusammen.
◮ Syntax: Die Abgrenzung erfolgt durch syntaktische Elemente, wie z. B. Schlüsselwörter (
begin
,end
), Klammerung oderEinrückung. Blöcke dürfen überall statt einer einzelnen Anweisung stehen.
◮ Kontrollfluss: Die Ausführung eines Blocks beginnt mit der ersten Anweisung und wird im Normalfall nach der letzten beendet. Es gibt auch Anweisungen zum Verlassen des Blocks:
break, return
.◮ Lokale Variablen: Innerhalb eines Blocks können Variablen deklariert werden, die nur in diesem Block verfügbar sind.
Blöcke
◮ Globale Variable: Innerhalb eines Blocks sind alle
Bezeichner aus den umschließenden Blöcken sichtbar, soweit sie nicht von einer inneren Deklaration überdeckt werden.
◮ Gültigkeitsbereich (scope): Ein Bezeichner ist innerhalb des Blocks gültig, in dem er deklariert wurde, nicht aber
außerhalb. Die Gültigkeit ist eine statische Eigenschaft, die sich aus dem Programmtext ableitet.
◮ Lebensdauer: Ist eine dynamische Eigenschaft und
bezeichnet den Zeitraum der Verfügbarkeit eines Wertes
während der Laufzeit. Werte von überdeckten Variablen sind nach Beendigung des überdeckenden Blocks wieder
verfügbar.
Blöcke
Code Gültig Sichtbar
var x,y: int; | | | |
· · · do | | | |
var x: int; | | | | |
· · · | | | | |
· · · | | | | |
od; x | | x | |
· · · | | | |
y x y x
Einzelheiten lernen Sie in der Vorlesung „Programmieren“.
Prozeduren
◮ Abstraktion: Eine Prozedur fasst mehrere Anweisungen zusammen und gibt ihnen einen Namen. Der Aufruf einer
Prozedur führt die Anweisungen aus und wirkt dabei wie eine elementare Anweisung. Über Parameter können die
Anweisungen gesteuert werden.
◮ Wiederverwertung: Eine Prozedur wird nur einmal deklariert und kann beliebig oft verwendet werden.
◮ Modularisierung: Die Implementation der Prozedur muss dem aufrufenden Programm nicht bekannt sein.
Veränderungen innerhalb der Prozedur erfordern keine Änderung des aufrufenden Programms.
Deklaration von Prozeduren
◮ Deklaration:
proc P(
p1, . . . ,
pn) begin a; end
◮ Name: P ist der Name der Prozedur und frei wählbar.
◮ Parameter: p1
, . . . ,
pn sind die Parameter der Prozedur. Sie sind lokale Variablen mit eigenem Typ (formale Parameter), denen beim Aufruf der Prozedur Werte (aktuelle Parameter) zugewiesen werden. Die Gültigkeit der Parameter ist auf den Rumpf der Prozedur beschränkt.◮ Rumpf: a ist der Rumpf der Prozedur. Er enthält die auszuführenden Anweisungen.
Werte- und Referenzparameter
Werteparameter (call by value):
◮ Der aktuelle Parameter kann ein Ausdruck oder speziell auch eine Variable sein.
◮ Es wird der Wert des Ausdrucks übergeben.
◮ Die Deklaration erfolgt (zum Beispiel) ohne das Präfix
var
.Werte- und Referenzparameter
Referenzparameter (call by reference):
◮ Der aktuelle Parameter muss eine Variable sein.
◮ Es wird die Variable (Adresse) übergeben.
◮ In der Deklaration werden Referenzparameter (zum Beispiel) mit
var
bezeichnet.Es gibt weitere Arten der Parameterübergabe.
Beispiel
Aufgabe: Vertausche die Inhalte der Variablen x und y und addiere zu beiden den Wert a.
proc vertausche(a: int; var x, y: int) begin var z: int; // lokale Variable
z ← x;
x ← y;
y ← z;
x ← x + a;
y ← y + a;
end
Die Wirkung ist die „simultane“ Ersetzung.
(
x,
y) ← (
y+
a,
x+
a)
Beispiel
Aufrufen lässt sich die Prozedur in unterschiedlichen Umgebungen mit verschiedenen aktuellen Parametern:
vertausche(0, i, j);
vertausche(3, a[1], a[2]);
vertausche(a[3], a[1], a[2]);
vertausche(i, i, j);
Funktionen
◮ Abstraktion: Funktionen sind Abstraktionen von Ausdrücken.
Der Aufruf einer Funktion berechnet einen Wert eines Typs
τ
.◮ Deklaration:
func F(
p1, . . . ,
pn): τ begin a; end
◮ Auswertung: Der Rückgabewert der Funktion wird (zum
Beispiel) durch eine spezielle
return
-Anweisung angegeben.Diese Anweisung verlässt den Funktionsblock mit sofortiger Wirkung. Häufig wird auch der letzte Term innerhalb einer
Funktion als Rückgabewert genommen, oder eine Zuweisung zum Funktionsnamen legt den Rückgabewert fest.
◮ Seiteneffekt: Wenn der Aufruf einer Funktion den Wert einer globalen Variablen verändert, spricht man von einem
Seiteneffekt.
Beispiel
func EUKLID(x, y: int): int begin var a,b,r: int
a ← x;
b ← y;
while b # 0 do r ← a mod b;
a ← b;
b ← r;
od;
return a;
end
Für negative Werte der Parameter x und y hängt das Verhalten der Funktion von der Implementierung des mod-Operators ab.