Kapitel 3 Datenstrukturen und Algorithmen

Werbung
Kapitel 3
Datenstrukturen und Algorithmen
3.1 Elemente eines Programms
31
3.1.1 Programmkonstrukte ..................................................................................31
3.1.2 Daten...........................................................................................................32
3.1.3 Ein- und Ausgabe .......................................................................................33
3.1.4 Beispiel für ein Programm .........................................................................33
3.2 Datenstrukturen
34
3.2.1 Grundlagen der objektorientierten Datenmodellierung..............................34
3.2.2 Abstrakte Datentypen .................................................................................35
3.2.3 Graph ..........................................................................................................37
3.2.4 Liste ............................................................................................................40
3.2.5 Stapel ..........................................................................................................41
3.2.6 Schlange .....................................................................................................42
3.2.7 Baum...........................................................................................................43
3.3 Algorithmen
47
3.3.1 Komplexität ................................................................................................48
3.3.2 Algorithmenklassen....................................................................................53
3.4 Zusammenfassung
66
3.5 Literatur
67
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
31
Kapitel 3
Datenstrukturen und Algorithmen
3.1 Elemente eines Programms
Ein Prozess lässt sich durch die drei elementaren Bausteine Dateneingabe, Datenverarbeitung und Datenausgabe beschreiben. Diese Teilprozesse können beliebig aufeinander folgen
oder geschachtelt sein. Die Reihenfolge und Schachtelung ist abhängig von der Art und
Komplexität des Problems, welches durch den Prozeß beschrieben wird.
3.1.1 Programmkonstrukte
Ein Programm ist eine Liste von Anweisungen, die bei ihrer Ausführung den Gesamtprozeß
entstehen lassen. Reduziert man die Anordnung der Anweisungen oder Teilprozesse auf das
Grundlegendste, erkennt man die drei Elementarbausteine, aus denen jedes Programm besteht.
•
Sequenz: Erst wenn eine Anweisung abgearbeitet ist, darf mit der nächsten
begonnen werden. Dies schließt eine Parallelverarbeitung aus. Diese sequentielle Arbeitsweise wird in den meisten Programmiersprachen durch
das Unter- bzw. Hintereinanderschreiben von einzelnen Befehlen erreicht.
•
Alternative: Aufgrund von logischen Bedingungen kann eine Wahl zwischen mehreren Teilprozessen getroffen werden. Dies ermöglicht es, bestimmte Teilprozesse in Abhängigkeit des Zustandes der bearbeiteten Objekte auszulassen oder einzufügen.
•
Wiederholung: Ein bestimmter Teilprozeß wird mehrfach abgearbeitet.
Der Zustand der beteiligten Objekte läßt sich dabei durch Anweisungen ändern. Die Anzahl der Wiederholungen kann somit vom Zustand der Objekte
abhängig gemacht werden, oder es wird eine feste Anzahl von Wiederholungen vorgegeben.
Datenverarbeitung in der Konstruktion (DiK)
32
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
3.1.2 Daten
Die Objekte, die einem Rechner zur Verarbeitung gegeben werden, können in der Realität
sehr unterschiedliche Form haben, z.B. Meßreihen, Konstruktionspläne oder auch Zusammenhänge und Hierarchien. Da ein Computer nur Zahlen innerhalb eines bestimmten Wertebereichs verarbeiten kann, müssen die Objekte auf Zahlenwerte repräsentierende Variablen abgebildet werden. Dazu bieten Programmiersprachen abstrakte Datenstrukturen an, die
einen Mittelweg zwischen den zu verarbeitenden Objekten und reiner Binärinformation
bilden. Die Abbildung der Objekte auf die Datenstrukturen der Programmiersprachen ist
neben dem eigentlichen Codieren für die Effizienz des fertigen Programms von entscheidender Bedeutung. Die Datenstrukturen sind in verschiedene Datentypen aufgeteilt, die
jeweils einen begrenzten Wertebereich umfassen. Angesprochen werden die Daten über die
Variablen. Eine Variable wird im Programm unter Angabe ihres frei wählbaren Namens
deklariert, daß heißt, einem bestimmten Datentyp zugeordnet. Dann kann sie definiert werden, d.h. sie wird mit einem bestimmten Wert belegt.
•
Einfache Datentypen und Skalare: Dies ist eine geordnete Menge von
Werten eines festen Wertebereichs. Eine skalare Variable repräsentiert einen einzelnen Wert aus diesem Wertebereich. Von den meisten Programmiersprachen werden folgende skalare Datentypen angeboten:
integer: ganze Zahlen, deren Wertebereich von der Programmiersprache
oder auch Hardware abhängt,
real: reelle Zahlen in Gleitkommadarstellung, deren Wertebereich ebenfalls
von der Programmiersprache und auch Hardware abhängt,
boolean: logischer Typ mit den Werten false und true und
char: ein einzelnes Zeichen, meist ASCII-codiert, z.B. Buchstaben, Ziffern
oder Sonderzeichen.
•
Strukturierte Datentypen: Sie ermöglichen es, eine Anzahl von Werten zu
einer übergeordneten Menge zusammenzufassen und sie als Gesamtheit oder einzeln zu bearbeiten. Eine solche Variable repräsentiert also eine ganze
Struktur von Werten. Die grundlegenden strukturierten Datentypen sind:
array (Feld, Vektor): Eine ein- oder mehrdimensionale Matrix, deren Elemente alle denselben skalaren Datentyp haben und durch Indizierung der
Variable angesprochen werden. Um beispielsweise dem Feld, das durch die
Variable Matrix repräsentiert wird, in der 5. Reihe und 3. Spalte den Wert
8 zuzuweisen, würde man "Matrix[5,3] = 8" schreiben.
record (Verbund): Ein hierarchischer Datentyp, dessen Bestandteile aus
verschiedenen Datentypen aufgebaut sein können. Der Zugriff auf einzelne
Werte erfolgt durch Angabe des Weges durch die Hierarchie zum gewünschten Element. In der Programmiersprache PASCAL werden die Hierarchieebenen durch Punkte getrennt, so daß die Adresse eines bestimmten
Angestellten in der Personaldatei beispielsweise mit "Abteilung.Name"
angesprochen werden würde.
file (Datei): Eine Reihe von Daten gleichen Typs, wobei es sich dabei auch
um Verbunde oder Felder handeln darf. Im Unterschied zu den anderen Datentypen muß eine Datei zusätzlich zur Deklaration noch geöffnet und nach
der Bearbeitung wieder geschlossen werden. Zur Verarbeitung von Dateien
bieten die Programmiersprachen deshalb spezielle Befehle an, z.B. wie
Öffnen, Schließen, Nächstes Element lesen und Zurücksetzen
auf das erste Element.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
33
3.1.3 Ein- und Ausgabe
Bei der Programmierung darf nicht die Art und Weise der Datenein- und der Datenausgabe
übersehen werden. Es muß festgelegt werden, ob die zu verarbeitenden Daten ihre Werte
z.B. durch interaktive Eingabe, über externe Speichermedien, von anderen Teilprogrammen
oder per Definition im Programmtext erhalten. Ebenso muß klar sein, was mit den Ergebnisdaten geschieht und in welcher Form sie ausgegeben werden (z.B. über den Bildschirm,
Drucker oder in eine Datei).
3.1.4 Beispiel für ein Programm
Das folgende Pseudoprogramm soll die einzelnen Elemente eines Programms verdeutlichen. Deshalb wird hier kein Wert auf eine effiziente Codierung gelegt. Es soll eine Reihe
von positiven ganzen Zahlen eingelesen, die Summe der Zahlen gebildet und ausgegeben
werden. Bei Eingabe einer negativen Zahl soll das Programm beendet werden.
Variablendeklaration:
NÄCHSTER WERT = integer; // NÄCHSTER WERT erhält Typ integer
SUMME = integer;
// SUMME erhält Datentyp integer
Programmcode:
SUMME = 0;
// Setze SUMME auf 0
NÄCHSTER WERT = 0;
// Setze NÄCHSTER WERT auf 0
NÄCHSTER WERT = eingabe (Tastatur); // Gib nächste Zahl von der Tastatur
// Wenn die Eingabe > 0 ist, dann summiere die Eingabe auf die Summe und
// lasse dir die nächste Eingabe von der Tastatur geben. Wenn die Eingabe < 0
// ist, beende die Schleife und gehe zur Ausgabe
solange NÄCHSTER WERT > -1 berechne
SUMME = SUMME + NÄCHSTER WERT;
NÄCHSTER WERT = eingabe ( Tastatur );
end_solange
ausgabe(SUMME,bildschirm); // Gibt Summe auf dem Bildschirm aus
Die Verarbeitung der Anweisungen eines Programmes erfolgt von oben nach unten, es sei
denn, durch eine Alternative wird die sequentielle Verarbeitung unterbrochen und gemäß
dieser festgesetzt. Es werden zunächst die Variablen SUMME und NÄCHSTER WERT auf 0
gesetzt. Die Funktion eingabe(Tastatur) liest eine Ziffer von der Tastatur und weist
diese Ziffer NÄCHSTER WERT zu. Ist diese Ziffer größer als -1 wird diese Ziffer zur Summe
hinzugezählt und eine weitere Ziffer von der Tastatur angefordert. Ist NÄCHSTER WERT
kleiner als 0 wird die Summierung nicht vollzogen und die Ausgabe der Summe durch die
Funktion ausgabe(SUMME) veranlaßt. Bei der Ausgabe wird definiert, wohin die Ausgabe
erfolgt (z.B. Drucker, Bildschirm, etc.). Diese wird in der Funktion ausgabe später genau
spezifiziert werden und ist auch für diesen Programmteil nicht relevant. Man sieht deutlich
die Modularisierung des kleinen Programms: ohne auf die Detailfragen wie ausgabe oder
eingabe einzugehen, wird die grobe Struktur des Programms sichtbar - anstelle von eingabe(Tastatur) könnte auch eingabe(Datei) stehen, falls die notwendigen Daten aus
einer Datei gelesen werden sollen.
Datenverarbeitung in der Konstruktion (DiK)
34
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
3.2 Datenstrukturen
Datenstrukturen sind Anordnungen von Objekten, deren Beziehungen untereinander nach
bestimmten Gesetzmäßigkeiten aufgebaut sind. Zur Beschreibung von Datenstrukturen
werden in der objektorientierten Datenmodellierung Objektklassen verwendet.
Die Verbindungen zwischen zwei Objekten einer Datenstruktur werden meist durch besondere Attribute dargestellt, die auf andere Objekte verweisen können (oft auch referenzieren
genannt), die sogenannten Zeiger oder Referenzen.
In vielen, nicht-objektorientierten Programmiersprachen werden jedoch anstelle der Objektklassen und Objekte einfachere Konstrukte zur Beschreibung einer Datenstruktur verwendet. Dabei entsprechen den Objekten die sogenannten Variablen und den Objektklassen
die Datentypen.
Eine Variable hat drei Bestandteile: ihren Namen, ihren Wert und ihren Datentyp. In einem
Programm kann auf eine Variable durch Nennung ihres Namens Bezug genommen werden.
Ihr Datentyp beschreibt die Struktur und den Wertebereich des Variablenwertes. Ausschließlich der Wert der Variablen kann durch Operationen, die für Variablen dieses Datentyps in der Programmiersprache definiert sind, manipuliert werden. Der Name und der Datentyp einer Variable werden explizit vor der ersten Benutzung durch eine Deklaration
festgelegt.
Beispiel: (Variablen-Deklaration)
In der Programmiersprache FORTRAN können Variablen explizit deklariert
werden. Variablen der Datentypen INTEGER (ganze Zahl) oder REAL (rationale
Zahl als Annäherung zur reellen Zahl) können jedoch auch implizit bei der ersten Benutzung deklariert werden. Hier entscheidet der Anfangsbuchstabe des
Variablennamens darüber, von welchem Typ die Variable ist: beginnt er mit I, J,
K, L, M oder N, so hat die Variable den Typ INTEGER, beginnt er mit einem
anderen Buchstaben, so ist die Variable vom Typ REAL.
In der Programmiersprache PASCAL müssen alle Variablen vor der ersten Benutzung deklariert werden. Ein PASCAL-Programm besteht daher aus einem
Deklarationsteil und einem Ausführungsteil (dem sogenannten Block). Eine
Komponente des Deklarationsteils ist die Variablendeklaration, die mit dem
Wort VAR eingeleitet wird. Die Deklaration einer REAL-Variablen 'Messwert'
und einer INTEGER-Variablen 'Anzahl' hat beispielsweise das folgende Aussehen:
VAR
Messwert : REAL;
Anzahl
: INTEGER;
Datenstrukturen werden ausführlich in [Knut-73, Kap.2], [Mehl-88, Kap. III], [Wirt-86,
Kap.1 und 4] behandelt. Von der Seite der Programmiersprachen werden Datenstrukturen in
[Loud-93, Kap.6] näher besprochen. Als richtungweisendes Standardwerk zu Datenstrukturen und deren Benutzung (und auch zu strukturierter Programmierung) ist [DaDH-74, Kap.
II] zu nennen.
3.2.1 Grundlagen der objektorientierten Datenmodellierung
Ausgangsphase für die Entwicklung eines Softwaresystems sind die Modellierungen eines
Wirklichkeitsausschnitts, ohne auf dessen konkrete Implementierung besondere Rücksicht
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
35
zu nehmen. Der Wirklichkeitsausschnitt muss zunächst erklärt, analysiert, strukturiert und
modelliert werden, bevor ein brauchbares Informationssystem erstellt werden kann. Das
Ergebnis dieser Modellierung bezeichnet man als Datenmodell. Der Schlüssel der Datenmodellierung liegt darin, einen Wirklichkeitsausschnitt genau zu erfassen und in adäquater
Weise abzubilden, d.h. den Ausschnitt widerspruchsfrei, vollständig, formal richtig und
möglichst ohne Redundanz zu beschreiben [Nguy-99]. Im objektorientierte Datenmodell
werden Informationen über einen Wirklichkeitsausschnitt als Objekte dargestellt.
Daher wird in der objektorientierten Modellierung zunächst entschieden, welche realen
Objekte und welche ihrer Eigenschaften relevant sind (Abstraktion). Darauffolgend wird
eine Abbildung dieser Objekte, ihrer Eigenschaften und ihrer Beziehungen zueinander in
eine implementierbare Form vorgenommen; es wird das objektorientierte Datenmodell für
die Aufgabenlösung erstellt. Dazu werden die in dem Problem vorkommenden Objekttypen
in Klassen ähnlicher Objekte, die sogenannten Objektklassen, eingeteilt. Klassen sind, wie
in Kapitel 2.3 beschrieben, formalisierte Begriffe, die wir durch Abstraktionen von Gegenständen der Wirklichkeit gewonnen haben. Sowohl Klassen als auch Objekte erhalten eindeutige Namen. Die Objekte einer Objektklasse werden durch eine Menge von Eigenschaften, den Attributen, und eine Menge von Operationen beschrieben, die die Abfrage und
Manipulation der Attribute erlauben. Die Erzeugung eines konkreten Objektes aus einer
Objektklasse wird Ausprägung oder Instanziierung genannt, weshalb ein Objekt auch als
Objektinstanz oder nur als Instanz bezeichnet wird.
Von außerhalb der Objekte kann auf deren Attribute nur über die zugeordneten Operationen
zugegriffen werden (Kapselung). Dies bedeutet insbesondere, daß im Objekt Attribute versteckt sein können, auf die ein Zugriff über Operationen nicht oder nur indirekt möglich ist,
die aber für die Beschreibung des Objektzustandes wichtig sind.
Zur objektorientierten Datenmodellierung werden in Kapitel 4 Entwicklungsmethoden vorgestellt die diesen Vorgang unterstützen.
3.2.2 Abstrakte Datentypen
Datentypen, die in nicht-objektorientierten Programmiersprachen zur Verfügung stehen,
setzen sich aus Elementartypen und strukturierten Datentypen zusammen. Komplexere Datenmodelle besitzen jedoch auch Strukturen, die oberhalb der Abstraktionsebene von strukturierten Datentypen angesiedelt sind. Diese Strukturen lassen sich in Klassen einteilen, zu
denen jeweils ein generischer Datentyp definiert werden kann, auf den dann alle in einer
Klasse vorkommende Strukturen zurückgeführt werden können. Solche generischen Datentypen können in objektorientierten als auch in nicht-objektorientierten Programmiersprachen als abstrakte Datentypen (ADT) realisiert werden.
Ein abstrakter Datentyp präsentiert sich einem Benutzer wie ein in der Programmiersprache
schon verfügbarer Datentyp, auf den eine definierte Menge von Operationen anwendbar
sind. Der abstrakte Datentyp muß jedoch in nicht-objektorientierten Programmiersprachen
basierend auf schon vorhandenen Datentypen definiert worden sein, wobei seine Operationen in Ablaufkonstrukten der Programmiersprache ausformuliert sein müssen.
Beispiel: (Vektor als abstrakter Datentyp)
Ein Vektor Vn ist ein n-Tupel von: Vn=(v1, v2, ..., vn). Beispielsweise kann ein
Tripel V3=(x, y, z) verwendet werden, um einen Punkt im dreidimensionalen
Raum zu beschreiben, während etwa V2=(x, y) einen Punkt in der Ebene bezeichnen könnte. Ob die Komponenten des Vektors ganze oder reelle Zahlen
Datenverarbeitung in der Konstruktion (DiK)
36
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
sind, ist für die Operationen, die auf Vektoren ausführbar sind, unerheblich. Mit
Vektoren können neben den Grundrechenarten Addition, Subtraktion und komponentenweise Multiplikation mit einem Faktor auch das Skalarprodukt und das
Kreuzprodukt (für n=3) gebildet werden. Bei allen Operationen bis auf das Skalarprodukt ist das Ergebnis wieder ein Vektor, beim Skalarprodukt hat das Ergebnis den gleichen Typ wie die Komponenten.
In der Programmiersprache ADA kann ein objektorientierter Datentyp Vektor
wie folgt beschrieben werden. Die Deklarationen im generic-Teil legen die
grundlegenden Typen, Variablen und Operationen fest, die der Anwender des
abstrakten Datentyps bei dessen Benutzung näher zu spezifizieren hat. In der
package-Deklaration werden der Datentyp vektor und die auf ihn anwendbaren Operationen beschrieben. Diese beiden Teile sind dem Benutzer sichtbar.
Der darauffolgende Teil package body enthält die Implementation des abstrakten Datentyps. Er wird vor dem Benutzer verdeckt gehalten. Dieser Teil ist der
Kürze wegen hier nur mit einer Funktion ausformuliert (Schlüsselworte, die zur
Programmiersprache gehören, sind unterstrichen; das Beispiel ist an [KKUG-83,
S. 186] angelehnt).
generic
type komponente is private;
nullelement : komponente;
with function "+"(x, y : komponente) return komponente
is <>;
with function "-"(x, y : komponente) return komponente
is <>;
with function "*"(x, y : komponente) return komponente
is <>;
package ADT_vektor is
type vektor is array(integer range <>) of komponente;
function "*"(s : komponente; x : vektor) return vektor;
function "+"(x, y : vektor) return vektor;
function "-"(x, y : vektor) return vektor;
function "*"(x, y : vektor) return komponente;
function kreuz(x, y : vektor) return vektor;
end ADT_vektor;
package body ADT_vektor is
M
function "*"(x, y : vektor) return komponente is
resultat : komponente := nullelement;
begin
for index in x’range loop
resultat := resultat + x(index) * y(index);
end loop;
return resultat;
end "*";
M
end ADT_vektor;
In diesem Beispiel lassen sich zwei wichtige Eigenschaften moderner Programmiersprachen
erkennen: der Polymorphismus von Operationen und das Überladen von Operatoren. Polymorphismus wurde schon in der objektorientierten Datenmodellierung erwähnt und setzt
sich insbesondere bis in objektorientierte Programmiersprachen fort. Er drückt sich in die-
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
37
sem Beispiel darin aus, daß zwei Operationen "*" deklariert werden, die sich nur durch die
Typen der Übergabe- und Rückgabeparameter unterscheiden. Dadurch können für vom Sinn
her gleichartige Operationen auch die gleichen Namen benutzt werden. Das Überladen von
Operatoren bezeichnet die Möglichkeit, Funktionsnamen nicht nur aus Buchstaben und
Ziffern zu bilden, sondern auch Operationszeichen wie +, *, / zuzulassen, die normalerweise nur auf Werte von in der Programmiersprache eingebauten, elementaren Datentypen
anwendbar sind. Dadurch können Operationen mit abstrakten Datentypen so kurz und prägnant wie mit den elementaren Datentypen ausgedrückt werden. Allerdings wird dadurch die
Analyse von Programmen bei Fehlern erschwert, da neben den Operatoren auch die Operandentypen für eine eindeutige Kennzeichnung der aufgerufenen Operationen notwendig
sind.
Einige abstrakte Datentypen werden als generische Datentypen in der Praxis besonders häufig für die Datenmodellierung verwendet. Der grundlegende abstrakte Datentyp ist dabei
der sogenannte Graph. Auf ihn bauen die abstrakten Datentypen Liste, Stapel, Schlange und
Baum auf.
Abstrakte Datentypen werden meist zusammen mit der Implementierung der zugehörigen
Operationen in [Wirt-86], [Mehl-88] und [Knut-73] ausführlich betrachtet. Auf die formalen Grundlagen und die Realisierung von abstrakten Datentypen in verschiedenen Programmiersprachen geht [Loud-93, Kap. 8] ein.
3.2.3 Graph
Ein ungerichteter Graph G besteht aus zwei Mengen: einer Menge N von Knoten (Punkten,
engl. nodes) und einer Menge V von Kanten (engl. vertices), d.h. G=(N, V). Eine Kante a
aus V verbindet stets zwei Knoten A und B aus N miteinander. Wir schreiben
a={A,B}={B,A}. Die Anzahl der Kanten, die mit dem Knoten verbunden sind, wird der
Grad eines Knotens genannt. Eine graphische Darstellung eines ungerichteten Graphen ist
in dem folgenden Beispiel zu sehen.
Beispiel: (Ungerichteter Graph)
a
B
A
c
b
C
Dieser Graph besitzt die Knoten A, B und C und die Kanten a={A,B}={B,A},
b={A,C}={C,A} und c={B,C}={C,B}.
Neben den ungerichteten Graphen existieren auch gerichtete Graphen, deren Kanten mit
einer Vorzugsrichtung versehen sind. Bei ihnen sind die Kanten a={A,B} und a´={B,A}
verschieden. Dies wird in der graphischen Darstellung durch einen Pfeil gekennzeichnet.
Die Anzahl der auf einen Knoten zeigenden Kanten wird Ingrad des Knotens, die Anzahl
der aus dem Knoten herausgehenden Kanten wird Ausgrad genannt.
Datenverarbeitung in der Konstruktion (DiK)
38
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Beispiel: (Gerichteter Graph)
a
B
A
c
b
C
Der hier dargestellte Graph besitzt die Knoten A, B und C und die Kanten
a={A,B}, b={A,C} und c={B,C}.
Die Kanten ungerichteter Graphen stellen nur die Beziehungen zwischen den Knoten dar,
weisen jedoch keine Richtung aus. Sie können durch äquivalente gerichtete Graphen ausgedrückt werden, bei denen jeder Kante des ungerichteten Graphen zwei Kanten des gerichteten Graphen zugeordnet werden, die jedoch einander entgegengesetzt ausgerichtet sind.
Beispiel: Der zu dem ungerichteten Graphen des vorherigen Beispiel äquivalente
gerichtete Graph besitzt die Knoten A, B und C und die Kanten a={A,B},
a´={B,A}, b={A,C}, b´={C,A}, c={B,C} und c´={C,B}.
a
A
B
a´
c´ c
b
b´
C
Ein Pfad zwischen zwei Knoten A und B existiert genau dann, wenn eine Folge
aus Kanten und Knoten existiert, die von Knoten A ausgeht und zu Knoten B
führt. Ein gerichteter oder ungerichteter Graph besitzt einen Zyklus, wenn von
einem Knoten ein Pfad über mindestens einen weiteren Knoten wieder zu dem
ersten Knoten zurück existiert. Ein Graph, der keine Zyklen besitzt, wird zyklenfrei genannt.
Im Gegensatz zu ungerichteten Kanten können gerichtete Kanten direkt auf Variablen eines
Datentyps abgebildet werden, der in allen modernen Sprachen vorkommt: den Zeiger (engl.
pointer, siehe Kapitel 2). Ein Knoten kann z.B. in die in PASCAL formulierte Datenstruktur
abgebildet werden:
TYPE
Knoten
Kantenliste
= RECORD
knoteninhalt
kanten
END;
= RECORD
kante
nächsteKante
END;
: IrgendeinTyp;
: ^Kantenliste
: ^Knoten;
: ^Kantenliste
VAR graph, k, k1, k2 : ^Knoten
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
39
Jeder Knoten besitzt also einen Inhalt und eine Liste von Verweisen auf Knoten. Diese
Verweise entsprechen den Kanten des Graphen.
Wesentliche Operationen auf Graphen sind (in objektorientierter Schreibweise als Methodenaufruf ausgedrückt)
•
das Erzeugen von Knoten (neuerknoten(k)),
•
das Erzeugen einer Kante (k1.verweisezu(k2)),
•
das Entfernen einer Kante (k1.verweisenichtzu(k2)) und
•
das Traversieren des Graphen, d.h. das Besuchen aller Knoten. Das Traversieren von Knoten wird z.B. genutzt, um auf den Nutzinformationen jedes
Knotens des Graphen Operationen auszuführen. Diese Operationen können
wieder in einer Methode zusammengefaßt werden, die der TraversierMethode als Parameter mitgegeben wird:
(graph.traversiere(manipulationsmethode))
Zum Traversieren existieren zwei prinzipiell verschiedene Vorgehensweisen: die sogenannte Breitensuche und die Tiefensuche. Bei der Breitensuche werden ausgehend von einem
Anfangsknoten alle Knoten besucht, zu denen Kanten vom Anfangsknoten aus verlaufen.
Danach werden all die Knoten besucht, auf die Kanten der eben besuchten Knoten verweisen. Der Graph wird somit schichtweise, in der Breite durchwandert. Die Tiefensuche geht
anders vor. Von einem Anfangsknoten ausgehend wird eine Kante gewählt, dessen verbundener Knoten als nächstes besucht wird. Dort wird wiederum eine Kante ausgesucht, der
den durch sie ausgewiesenen Knoten besucht, u.s.w., bis auf ein Knoten verwiesen wird, der
schon besucht wurde. In diesem Fall wird die Kante des vorher besuchten Knotens verwendet. Es wird also zunächst tief in den Graph hineingewandert und erst, wenn alle Knoten
dort besucht worden sind, wird sich aus der Tiefe näher an die Oberfläche bewegt.
Beispiel: Die Knoten der beiden dargestellten Graphen sind mit Zahlen markiert, die
die Reihenfolge des Besuchs der Knoten bei der Durchwanderung des Graphen
angeben. Der linke Graph wurde nach der Breitensuche, der rechte Graph nach
der Tiefensuche durchwandert. Da die Reihenfolge, in der die ausgehenden Kanten ausgewählt werden, bei beiden Suchmethoden beliebig ist, sind die gezeigten
Numerierungen nicht die einzig möglichen.
1
1
9
2
4
2
6
3
7
5
7
6
3
5
8
9
8
4
Die Kanten eines Graphen können ebenso wie die Knoten mit Attributen versehen sein.
Meist sind die Attribute Zahlen, die beispielsweise als Aufwand interpretiert werden können, um die zugehörige Kante entlangzuwandern. Solche Graphen werden daher gewichtete
Graphen genannt. Zur Realisierung der Gewichte kann in dem oben beschriebenen Verbund
Kantenliste eine Komponente gewicht vom Typ INTEGER oder REAL eingeführt werden. Gewichtete Graphen werden zur Modellierung von vernetzten Strukturen verwendet,
Datenverarbeitung in der Konstruktion (DiK)
40
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
bei denen nicht jeder Weg zu einem Knoten gleich einfach begangen werden kann.
Beispiel: In einer Produktionsstraße stehen für jeden Produktionsschritt mehrere Maschinen zur Verfügung. Jeder Produktionsschritt ist durch eine Zwischenstation
getrennt, die die Bauteile aus dem vorhergehenden Produktionsschritt sammelt
und sie auf die Maschinen des nächsten Produktionsschrittes verteilt. Es soll ein
Datenmodell gefunden werden, das computergestützt eine gleichmäßige Auslastung der Maschinen erlaubt. Als Basis für das Datenmodell wird ein gewichteter
Graph verwendet, bei dem die Knoten die Maschinen und Zwischenstationen
darstellen und die gerichteten Kanten die Wege zwischen den Maschinen und
Zwischenstationen repräsentieren. Die Kanten werden mit Gewichten versehen,
die die Anzahl der vor den Maschinen wartenden Werkstücke angeben. Wenn
nun ein Werkstück eine Zwischenstation passiert, bestimmt man das Minimum
der Gewichte und schickt das Teil zu der momentan am wenigsten belasteten
Maschine.
Weiterführende Literatur zu Graphen ist z.B. [Jung-90], [AhHU-74, Kap. 5] und [Sedg-91,
Kap. 29-34].
3.2.4 Liste
Die Liste (engl. list) ist ein vereinfachter Graph, denn jeder Knoten außer den beiden Endknoten ist mit genau zwei anderen Knoten über je eine Kante verbunden. Von den beiden
Listenenden gehen jeweils nur eine Kante ab. Die Darstellung einer Liste in PASCAL könnte wie folgt aussehen:
TYPE
Liste =
Listenknoten =
VAR
RECORD
ende1, ende2
END;
RECORD
nutzinformation
vorgänger
nachfolger
END;
:^Listenknoten
: IrgendeinTyp;
:^Listenknoten;
:^Listenknoten
liste:
^Liste;
l,l1,l2: ^Listenknoten;
Der Knoten, auf den ende1 zeigt, hat keinen vorgänger, und der Knoten auf den ende2
zeigt, hat keinen nachfolger. In diesen Fällen besitzen die Komponenten vorgänger
bzw. nachfolger den vordefinierten Wert NIL, der anzeigt, daß die Komponenten zur Zeit
auf keinen Knoten verweisen.
Die obige Datenstruktur wird auch doppelt verkettete Liste genannt. Dies ist eine Liste mit
ungerichteten Kanten (siehe die äquivalente gerichtete Darstellung eines ungerichteten Graphen), die in beide Richtungen traversiert werden kann. Beispielhaft sei hier eine doppelt
verkettete Liste graphisch dargestellt. Die gepunkteten Zeiger beschreiben die Änderungen,
wenn ein neuer Knoten in die Liste eingefügt wird. Der Ort des Einfügens ist bei einer Liste
beliebig.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Ende 1
41
Ende 2
.
.
Neben der doppelt verketteten Liste existieren auch
•
die einfach verkettete Liste, die einer Liste mit gerichteten Kanten entspricht und nur einen Verweis nachfolger auf den nächsten Knoten besitzt; sie hat statt ende1 und ende2 nur einen Listenanfang, den kopf;
•
die Ringliste, die einen Einfüge- und gleichzeitig Entnahmepunkt hat und
bei der jeder Knoten einen nachfolger und einen vorgänger besitzt
(keine NIL-Werte an den Enden, da keine Enden mehr existieren).
Gebräuchliche Operationen auf Listen sind
•
das Erzeugen eines Listenknotens
(neuerknoten(l1)),
•
das Einfügen eines Knotens nach oder vor einem anderen Knoten
(liste.einfügenach(l,l1),liste.einfügevor(l,l2)),
•
das Entfernen eines Knotens
(liste.enfernen(l)),
•
das Verweisen auf den Vorgänger bzw. Nachfolger
(liste.nächster(l),liste.voriger(l)),
•
das Erfragen ob die Liste leer ist
(liste.leer) und
•
das Feststellen der Anzahl der Listenknoten
(liste.länge).
Beispiel: Die beim Kauf eines Kraftfahrzeuges ausgewählten Ausstattungsoptionen
können als Liste dargestellt werden. Dabei sind in jedem Listenknoten die für die
Option notwendigen Informationen abgelegt. Möchte der Kunde eine neue Option gegen eine schon ausgewählte Option eintauschen, so muß nur der Knoten für
die alte Option aus der Liste genommen und der Knoten für die neue Option hineingenommen werden.
3.2.5 Stapel
Der Stapel (engl. stack, bzw. last in first out, LIFO) oder auch Keller ist eine einfach verkettete Liste, bei der ein Knoten nur am Listenkopf eingefügt oder entfernt werden kann.
Eine Darstellung eines Stapels in PASCAL könnte sein:
Datenverarbeitung in der Konstruktion (DiK)
42
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
TYPE
Stapelknoten
VAR
= RECORD
nutzinformation
nachfolger
END;
: IrgendeinTyp;
:^Stapelknoten
stapel, s : ^Stapelknoten;
Ein Beispiel für einen Stapel mit drei Knoten und einem einzufügenden Knoten ist im folgenden graphisch dargestellt. Die für das Einfügen zu ändernden Zeiger sind gepunktet
gezeichnet.
S
.
Die Stapeloperationen haben üblicherweise englische Namen. Sie sind heute in jedem
Computerprozessor
als
Maschineninstruktionen
realisiert,
um
z.B.
den
Unterprogrammstapelspeicher zu verwalten:
•
das Erzeugen eines Stapelknotens
(neuerknoten(s)),
•
das Einfügen eines Stapelknotens
(stapel.push(s)),
•
das Entfernen eines Knotens
(stapel.pop),
•
das Feststellen, welcher Knoten oben auf dem Stapel liegt
(stapel.top),
•
das Feststellen, ob der Stapel leer ist
(stapel.empty) und
•
das Feststellen der Anzahl Knoten auf dem Stapel
(stapel.amount).
Beispiel: Stapel werden in Prozessoren verwendet, um die Rücksprungadresse vor
einem Unterprogrammaufruf abzulegen. Dadurch können Unterprogramme ineinander geschachtelt werden. Die Rücksprungadresse wird trotzdem korrekt
aufgefunden, da immer die zuletzt auf den Stapel gelegte Adresse wieder heruntergeholt wird. Die Existenz eines Stapels für Rücksprungadressen ist Voraussetzung für die Realisierung rekursiver Algorithmen (siehe Abschnitt 3.4.2.3).
3.2.6 Schlange
Die (Warte-) Schlange (engl. queue bzw. first in first out, FIFO) ist eine doppelt verkettete
Liste, bei der Knoten nur an dem einen ende1 eingefügt und an dem anderen ende2 entfernt werden. Die Darstellung der Datenstruktur in PASCAL entspricht der Datenstruktur
der doppelt verketteten Liste aus Abschnitt 3.3.3. Auch die graphische Darstellung einer
Schlange unterscheidet sich nicht sehr von einer doppelt verketteten Liste bis auf die Tatsache, daß nur Knoten an einem Ende der Schlange eingefügt werden dürfen.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Ende 1
43
Ende 2
.
.
.
Die Operationen, die auf einer Schlange ausgeführt werden können, sind:
•
das Erzeugen eines Schlangenknotens
(neuerknoten(q)),
•
das Einreihen eines Knotens an das Schlangenende
(schlange.einreihe(q)),
•
das Entfernen eines Knotens am Schlangenanfang
(schlange.ausreihe),
•
das Feststellen, ob die Schlange leer ist
(schlange.leer) und
•
das Feststellen der Knotenanzahl in der Schlange
(schlange.anzahl).
Beispiel: In einem vorherigen Beispiel gaben die Gewichte der Kanten, die in die
Knoten für die Maschinen eingingen, an, wie viele Werkstücke vor der Maschine
auf die Bearbeitung warten. In einer Detaillierung des Datenmodells für die Produktionsstraße können Schlangen vor den Maschinen eingeführt werden, deren
Knoten die wartenden Werkstücke repräsentieren. Das Werkstück, das am längsten in der Warteschlange steht, wird als nächstes bearbeitet, während ein von einer Zwischenstation kommendes Werkstück sich hinten anstellen muß, also am
anderen Ende in die Schlange eingereiht wird.
3.2.7 Baum
Ein Baum ist ein gerichteter Graph mit Knoten, auf die, bis auf eine Ausnahme, nur jeweils
eine Kante zeigen. Die Ausnahme ist die Wurzel des Baumes. Sie besitzt keine eingehende
Kante. Von den Knoten eines Baumes weisen ein oder mehrere Kanten zu weiteren Knoten,
deren ausgehende Kanten wiederum auf Knoten verweisen können. Ein Knoten wird zusammen mit allen über seine Kanten referenzierten Knoten, dessen Referenzen, u.s.w. Teilbaum genannt. Knoten, von denen keine Kanten ausgehen, werden Blätter genannt, alle
anderen Knoten heißen innere Knoten. Die Anzahl der Kanten von der Wurzel des Baumes
zu einem Knoten heißt die Weglänge des Knotens. Die Wurzel hat demnach die Weglänge
0. Die größte Weglänge, über alle Knoten betrachtet, heißt Tiefe (oder Höhe) des Baumes.
Einem Baum, dessen Knoten nur jeweils eine ausgehende Kante besitzen, entspricht eine
einfach verketteten Liste. Sie wurde schon oben betrachtet. Die häufigste Baumart ist der
binäre Baum, dessen Knoten genau zwei ausgehende Kanten besitzen, die meist mit links
und rechts benannt werden. Besitzen Baumknoten mehr als zwei ausgehende Knoten, so
werden die dazugehörenden Bäume B-Bäume oder Vielweg-Bäume genannt (sie werden im
folgenden nicht mehr behandelt, können jedoch in der Literatur nachgelesen werden).
Datenverarbeitung in der Konstruktion (DiK)
44
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Bäume werden meist dazu benutzt, um Daten im Arbeitsspeicher oder auf Massenspeichern
geordnet abzulegen und schnell wiederzufinden. Aus diesem Grund besitzen die Knoten
eines Baumes neben den Nutzinformationen und den Verweisen auf die nachfolgenden
Knoten auch einen oder bei B-Bäumen mehrere Schlüssel, nach deren Werten der Knoten in
den Baum eingefügt wird. Zudem existiert eine Ordnungsrelation kleinergleich, die
Antwort darauf gibt, ob der Schlüssel kleiner oder gleich einem anderen Schlüssel ist. Dies
ist notwendig, da Schlüssel nicht nur Zahlen sondern auch beliebig komplexere Strukturen
sein können, auf denen sich eine Ordnung definieren läßt.
Binärer Baum
Die Knoten eines binären Baums besitzen einen Ausgrad von 2, sie verweisen also auf zwei
weitere Knoten, die Nachfolger des aktuellen Knotens, der seinerseits der Vorgänger der
beiden Knoten ist. Ein binärer Baum kann in PASCAL wie folgt dargestellt werden:
TYPE
Knoten = RECORD
schluessel:
nutzinformation:
links, rechts:
END;
wurzel, k: ^Knoten;
VAR
IrgendeinTyp;
IrgendeinWeitererTyp;
^Knoten
Die Operationen auf einen binären Baum sind:
•
das Erzeugen eines Baumknotens
(neuerknoten(k)),
•
das Einfügen eines Knotens in einen Baum anhand des eingetragenen
Schlüssels und der auf ihm definierten Ordnungsrelation
(wurzel.einfuege(k,relation)),
•
das Löschen eines Knotens aus dem Baum
(wurzel.loesche(k)),
•
das Suchen nach einem Knoten anhand eines Schlüssels
(wurzel.suche(schluessel,relation)) (zurückgegeben wird ein
Zeiger auf den gefundenen Knoten oder NIL, wenn kein passender Knoten
gefunden wurde) und
•
das Traversieren des Baumes, d.h. das Ausführen einer Operation auf jeden
Baumknoten
(wurzel.traversiere(operation)).
Das Suchen in einem Baum ist sehr einfach, da ab der Wurzel für jeden besuchten Knoten
nur die folgenden Fragen beantwortet werden müssen:
•
Stimmen der vorgegebene Schlüssel und der Knotenschlüssel überein, so ist
der Knoten gefunden.
•
Ist der vorgegebene Schlüssel von der Ordnungsrelation kleiner als der
Knotenschlüssel, so befindet sich der gesuchte Knoten, wenn überhaupt, in
dem linken Teilbaum des aktuellen Knotens.
•
Ist der vorgegebene Schlüssel größer als der Knotenschlüssel, so befindet
sich der gesuchte Knoten, falls er existiert, im rechten Teilbaum des aktuellen Knotens.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
45
Da die Suche von der Wurzel ausgeht und spätestens in einem Blatt endet, wurden höchstens soviele Knoten besucht, wie der Baum tief ist. Da im optimalen Falle an jedem inneren
t
Knoten zwei Nachfolger hängen, befinden sich in einem Baum der Tiefe t n = 2 Knoten.
Wenn also in einer Menge von n Knoten zu suchen ist, so müssen nur maximal t = log2 n
Knoten besucht werden, um den passenden Knoten zu finden. Dies bedeutet beispielsweise
bei einer Gesamtzahl von n=1.000.000 Knoten, daß nur t = log2 n = log 2 1.000.000 ≅ 20
Knoten besucht werden müssen.
Das Traversieren eines Baumes kann auf drei Arten geschehen:
•
Bei der Präorder-Traversierung wird in einem Knoten die Operation zuerst
durchgeführt, dann wird der linke Teilbaum traversiert und danach der rechte Teilbaum.
•
In der Inorder-Traversierung, wird zunächst der linke Teilbaum traversiert,
dann die Operation auf dem Knoten ausgeführt und danach der rechte Teilbaum traversiert.
•
Die Postorder-Traversierung handelt zuerst die Traversierung des linken
und des rechten Teilbaums ab und führt als letztes die Operation auf dem
Knoten aus.
Auch das Einfügen kann verschiedenartig realisiert werden:
•
Bei einem naturwüchsigen Baum werden neue Knoten stets als Blätter an
den Baum gehängt. Dies kann bei einer auf- oder absteigend vorsortierten
Folge von einzufügenden Blättern eine Degenerierung des Baums zu einer
einfach verketteten Liste zur Folge haben.
•
Bei einem vollständig ausgeglichenen Baum können Knoten auch als innere
Knoten einfügt werden, die prinzipiell zwei Nachfolger besitzen. Jeder innere Knoten eines vollständig ausgeglichenen Baums besitzt daher zwei
Teilbäume mit gleicher Tiefe. Dies hat zur Folge, daß zwar der Aufwand
sehr hoch ist, nach dem Einfügen eines Knotens diese Bedingung wiederherzustellen, der Suchaufwand aber nach wie vor außerordentlich gering ist.
•
Bei einem ausgeglichenen Baum (AVL-Baum, nach seinen Entdeckern Adelson-Velskii und Landis benannt) dürfen sich die Tiefen der Teilbäume
jedes Knotens um 1 unterscheiden. Da dieser Unterschied zum vollständig
ausgeglichenen Baum nur eine geringe Verschlechterung von ggf. einem
zusätzlich zu untersuchenden Knoten darstellt, das Einfügen in den Baum
jedoch erheblich vereinfacht wird, ist der AVL-Baum der derzeit am
weitesten verbreitete binäre Baum.
Beispiel: Die Verbundkomponente schluessel sei vom Typ INTEGER. Der folgende naturwüchsige Baum ist durch das Einfügen von Knoten in der folgenden
Reihenfolge der Schlüssel entstanden: 9, 7, 12, 15, 1, 3.
Datenverarbeitung in der Konstruktion (DiK)
46
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Wurzel
9
7
12
.
.
1
15
.
.
.
3
.
.
Beispiel: Um komplexe Volumina aus einfachen Körpern aufzubauen, kann man z.B.
die Methode CSG (Constructive Solid Geometry) verwenden. Ihre Funktionalität
kann man sich mit Hilfe eines Binärbaums veranschaulichen, dessen Blätter einfache Körper wie Quader, Prismen oder Zylinder sind, und desses sonstige Knoten aus Mengenoperationen bestehen. Betrachten wir das folgende Beispiel:
Knoten
Blatt
Wenn man die beiden Operationen Mengendifferenz und Mengenvereinigung
durchgeführt hat, erhält man etwa folgenden Körper als Ergebnis:
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
47
3.3 Algorithmen
Die Methoden eines Objektes nehmen anhand von beim Aufruf mitgegebenen Parametern
und dem aktuellen Zustand des Objektes Änderungen an den Objektattributen vor oder lassen Abfragen zum Inhalt der Attribute zu.
Während der Entwurfsphase werden die Signatur und die Semantik einer Methode in der
Spezifikation festgelegt. Die Signatur einer Methode besteht aus einer Zusammenstellung
der Datentypen ihrer Ein- und Ausgabeparameter und deren Reihenfolge. Die Semantik
einer Methode ist eine Beschreibung, was die Methode leistet (sog. Nachbedingung), wenn
die Eingabeparameter bestimmte Eigenschaften erfüllen (sog. Vorbedingung). Vor- und
Nachbedingung können umgangssprachlich oder formal (z.B. über Prädikatenlogik) formuliert werden. Letzteres eröffnet die Möglichkeit, die Korrektheit des gesamten Entwurfs mit
mathematischen Methoden maschinell zu prüfen (verifizieren) oder zumindest rechnergestützt nachzuvollziehen (validieren). Die Verifikation eines Entwurfs und dessen Implementierung ist für Softwaresysteme eminent wichtig. Insbesondere dürfen bei fehlerhaften
Softwarekomponenten Leben oder hohe Sachwerte nicht gefährdet werden.
Beispiel: (Signatur und Semantik)
Der in einem früheren Beispiel beschriebene abstrakte Datentyp Vektor kann als
Objektklasse interpretiert werden, wobei die Typdeklaration vektor zu den
Attributen der Klasse gehört und die Operationen Methoden der Klasse sind. Die
Signatur der Operationen ergibt sich aus deren mathematischer Definition:
+:Vektor × Vektor → Vektor
−:Vektor × Vektor → Vektor
⋅:Komponente × Vektor → Vektor
⋅:Vektor × Vektor → Komponente
×:Vektor × Vektor → Vektor
Die erste Multiplikation ist die Multiplikation mit einem Skalar, die zweite das
Skalarprodukt und die dritte das Kreuzprodukt, im ADA-Programmbeispiel mit
Cross bezeichnet.
Die Semantik der Operationen kann informell beschrieben werden: z.B. ist das
Skalarprodukt die Summe aus den Produkten der an den gleichen Positionen stehenden Vektorkomponenten. Die Definition der Semantik kann jedoch auch
formal erfolgen (formuliert in einer Prädikatenlogik für den Fall des Skalarprodukts):
n
∀ a, b ∈ Vektor ( x1 , x 2 , ..., x n ); c, xi ∈ Komponentec = a ⋅ b ⇔ c = ∑ a i ⋅ bi
i =1
Aus der Semantik des Skalarprodukts ist erkennbar, daß dieses auf der Existenz
von Addition und Multiplikation von Komponenten beruht. Daher sind diese
Operationen im generic-Teil des ADA-Beispiels explizit angegeben.
In der Implementierungsphase wird festgelegt, wie die Methode das leistet, was sie entsprechend ihrer Semantik leisten soll. Ein Verfahren zur Lösung einer Aufgabe wird Algorithmus genannt. Im allgemeinen existieren mehrere Möglichkeiten, eine Semantik zu implementieren. Diese Algorithmen, die bei Einhalten der Vorbedingung die Nachbedingung
Datenverarbeitung in der Konstruktion (DiK)
48
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
einer Semantik erfüllen, können zu Klassen zusammengefaßt werden.
Obwohl alle Algorithmen einer Klasse gleiche (oder ähnliche) Aufgaben lösen, tun sie dies
teilweise auf vollkommen unterschiedliche Art und Weise. Der die Implementierungsphase
bearbeitende Softwareentwickler wird daher einen Algorithmus nach verschiedenen Kriterien auswählen (in der Reihenfolge der Wichtigkeit):
•
Rechenzeitaufwand,
•
Speicherplatzaufwand für Zwischenspeicher,
•
Verständlichkeit und damit Wartbarkeit,
•
leichte Verifizierbarkeit der Korrektheit und
•
leichte Implementierbarkeit.
Gerade der Rechenzeit- und Speicherplatzaufwand eines Algorithmus´ ist meist ausschlaggebend für dessen Auswahl, da während des Betriebs des Softwaresystems beides kostenerzeugende Größen sind, die von Rechenzentren abgerechnet werden. Zudem müssen beide
Größen bei der Auswahl der Rechnerhardware berücksichtigt werden. Deshalb hat die Informatik seit den sechziger Jahren erhebliche Anstrengungen unternommen, neue Algorithmen für bekannte Aufgaben zu entwickeln, die in den beiden Aufwandsgrößen Verbesserungen aufweisen. Für die Einordnung verschiedener Algorithmen einer Klasse in Bezug
auf ihren Rechenzeit- und Speicherplatzaufwand ist es wichtig zu wissen, wie der Aufwand
eines Algorithmus´ gemessen wird und wie die Aufwandsgrößen zweier Algorithmen sinnvoll miteinander verglichen werden können.
Zu Algorithmen existiert eine weiter Bereich von Literatur. Einige Standardwerke sind
[DaDH-74], [Knut-73] und [AhHU-74]. In der anschaulichen Darstellung von grundlegenden Algorithmen nimmt [Wirt-86] einen besonderen Rang ein. Unter den neueren Publikationen ist [Sedg-91] empfehlenswert. Die formale Spezifikation von Algorithmen ist beispielsweise in [Loud-93, Kap.8] zu finden. Zum Entwurf von korrekten Programmen,
insbesondere von Programmen, deren Korrektheit formal nachgewiesen werden kann, seien
[Futs-89] und als Standardwerk [Grie-81] empfohlen.
3.3.1 Komplexität
Die Komplexität eines Algorithmus´ ist der Aufwand, den Algorithmus auf einem Rechner
oder per Hand auszuführen. Die Komplexität hängt vom Umfang der zu bearbeitenden Daten, der sogenannten Problem- oder Aufgabengröße, ab.
Zur Quantifizierung der Komplexität wird ein abstraktes Kostenmaß (keine Geldbeträge!)
eingeführt. Diese Kosten können sich einerseits auf die Ausführungszeit einer Elementaroperation (z.B. eines logischen Vergleiches) oder auf den Speicherplatzbedarf beziehen.
Bezieht sich die Komplexität auf ein rechenzeitbezogenes Kostenmaß, so wird sie Zeitkomplexität genannt, bezieht sie sich auf ein speicherplatzbezogenes Kostenmaß, so heißt sie
Platzkomplexität. Die Komplexität stellt somit einen funktionalen Zusammenhang zwischen
dem Umfang der zu bearbeitenden Daten und der Höhe der abstrakten Kosten her.
Im folgenden wird vereinfacht angenommen, daß der Umfang der zu bearbeitenden Daten
durch eine positive, ganzzahlige Größe n beschrieben wird.
Beispiel: (Komplexität von Bubblesort)
Der Komplexitätsbegriff soll nun an dem einfachen Sortierverfahren Bubblesort
beispielhaft erläutert werden.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Die Aufgabe ist es, n Zahlen, die in einem eindimensionalen Feld A (engl. array,
einer der strukturierten Datentypen) gespeichert sind, aufsteigend zu sortieren.
Diese Aufgabe wird von sogenannten Sortieralgorithmen gelöst. Sie werden in
Abschnitt 3.4.2.1 näher behandelt. Einer dieser Algorithmen ist Bubblesort: Zunächst stellt man sich das Datenfeld in Spaltenform vor, wobei das erste Element
des Feldes oben steht. Das Feld kann man jetzt sortieren, indem man zunächst
die kleinste Zahl in das oberste Element transportiert. Dazu wird mit dem untersten Element beginnend jede Zahl mit dem über ihr liegenden Nachbarn verglichen. Wird bei einem solchen Vergleich festgestellt, daß die Zahl im aktuellen
Element kleiner als der Nachbar ist, so werden beide Zahlen vertauscht. Daher
befindet sich nach diesen Vergleichen die kleinste Zahl des Feldes im ersten Element. Nun muß nur noch das restliche Feld ab dem zweiten Element betrachtet
und die in diesem Teilfeld kleinste Zahl in das zweite Element transportiert werden. Ist das restliche Feld auf das unterste Element geschrumpft, so ist das gesamte Feld sortiert und der Algorithmus ist beendet. Das Hochsteigen der kleineren Zahlen in der Spaltendarstellung hat Ähnlichkeit mit dem Aufsteigen von
Gasblasen in Flüssigkeiten - daher der Name Bubblesort.
Der Algorithmus kann in der Programmiersprache PASCAL wie folgt ausgedrückt werden:
CONST
VAR
n = 10;
A : ARRAY [1..n] OF INTEGER;
PROCEDURE BubbleSort;
VAR i, j, hilf : INTEGER
BEGIN
FOR i:=2 TO n DO BEGIN
FOR j:=n DOWNTO i DO BEGIN
IF A[j] < A[j-1] THEN BEGIN
hilf:=A[j-1];
A[j-1]:=A[j];
A[j]:=hilf
END
END
END;
Vor der Bestimmung der Komplexität des Algorithmus´ muß zunächst das Kostenmaß festgelegt werden, nach dem sie quantifiziert sein soll. In Sortieralgorithmen ist als Kostenmaß der Vergleich zweier Zahlen gebräuchlich. Da während eines Sortierprozesses jede zu sortierende Zahl mindestens einmal
angesehen und mit einer anderen Zahl verglichen werden muß, ist die untere
Schranke für die Komplexität eines Sortieralgorithmus´ gleich der Anzahl n der
zu sortierenden Zahlen.
Der Bubblesort-Algorithmus besteht aus zwei geschachtelten FOR-Schleifen. In
der inneren Schleife liegt in der IF-Anweisung der als Kostenmaß zu verwendende Vergleich. Anfänglich hat i den Wert 2. Daher wird der Vergleich in der
inneren Schleife n-i+1=n-2+1=n-1 mal ausgeführt. Im nächsten Durchlauf der
äußeren Schleife hat i den Wert 3. Folglich sind n-i+1=n-3+1=n-2 Vergleiche
durchzuführen. Dies setzt sich fort, bis i gleich n ist, in welchem Fall noch ein
Vergleich zu realisieren ist. Insgesamt sind demnach
n −1
K (n ) = (n − 1) + (n − 2 ) + (n − 3) + ... + 2 + 1 = ∑ m =
m =1
n(n − 1) 1 2 1
= n − n
2
2
2
Vergleiche durchgeführt worden. Die Komplexität von Bubblesort ist also
Datenverarbeitung in der Konstruktion (DiK)
49
50
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
KBubblesort (n) =
1 2 1
n − n
2
2
Wäre als Kostenmaß die Anzahl der für den Algorithmus zusätzlich benötigten
Speicherzellen definiert worden (der Speicherplatz für die Eingabedaten A zählt
hier nicht), so ergäbe sich eine Platzkomplexität von 1, da nur die Variable hilf
zum Austausch zweier Zahlen eingeführt werden mußte. Diese Platzkomplexität
ist also unabhängig von n und somit sehr günstig.
Verschiedene Algorithmen für eine Aufgabe können sich für kleine Aufgabegrößen n sehr
ähnlich bezüglich ihres Rechenzeitaufwands verhalten. Interessant wird jedoch die Betrachtung der Komplexität für große n.
Beispiel:
Für die Analyse der Verformung eines mechanischen Bauteils nach der
Methode der finiten Elemente (FEM) wird das Bauteil in kleine Würfel oder
komplexere Körper (die sogenannten Finiten Elemente) aufgeteilt. Ihr physikalisches Verhalten wird über Gleichungen beschrieben, die von den Zuständen der
benachbarten Würfel und allgemeinen Größen wie Werkstoffkenngrößen, Druck,
u.a. beeinflußt werden. Da der Aufwand zur Berechnung einer FE-Analyse von
der Anzahl der zu lösenden Gleichungen abhängt und die Anzahl der Gleichungen pro Element meist konstant ist, kann die Komplexität der FE-Analyse über
die Anzahl der Elemente ausgedrückt werden. Dem Kostenmaß liegt also das Element zugrunde.
Ohne Beschränkung der Allgemeinheit sei angenommen, daß das zu betrachtende Bauteil ein Würfel sei. Zur Durchführung der FEM werde jede Kante des
Würfels in n Teile geteilt, so daß der Würfel in insgesamt n3 kleine Würfel zerlegt wird. Die Komplexität einer dreidimensionalen FE-Analyse ist somit
K3(n)=n3. Für die Werte n=2 und n=3 ergeben sich demnach 23=8 bzw. 33=27
kleine Würfel. Im zweiten Fall sind also 27-8=19 Würfel mehr in die Rechnung
einzubeziehen als im ersten Fall. Wird n verdoppelt, also jeder Kantenteil nochmals halbiert, so ergibt sich eine Anzahl von (2n)3=8n3 Würfeln gegenüber n3
Würfeln bei einfachem n. Bei Verdopplung sind somit 8n3-n3=7n3 Würfel mehr
zu
berechnen.
Für
n=1000
ergibt
eine
Verdopplung
somit
3
7·1000 =7.000.000.000 zusätzlich zu berechnende Würfel.
Wegen dieser riesigen Zuwächse reduziert man die Aufgabe durch eine Verringerung von drei auf zwei Dimensionen, indem man von dem Bauteil nur ein
Schnitt betrachtet und es dann in zweidimensionale Elemente zerlegt. Betrachtet
man einen zu einer Würfelseite parallelen Schnitt, so erhält man ein Quadrat.
Der obigen Vorgehensweise entsprechend ergeben sich bei einer Unterteilung
einer Seite des Quadrats in n Teile eine Anzahl von n2 kleinen Quadraten. Die
Komplexität einer zweidimensionalen FE-Analyse ist demnach K2(n)=n2. Bei
einer Verdoppelung von n erhält man hier (2n)2=4n2 Quadrate, also 4n2-n2=3n2
Quadrate mehr als bei einfachem n. Für n=1000 ergeben sich dann
3·10002=3.000.000 zusätzliche Quadrate. Gegenüber der dreidimensionalen Analyse spart man 4n3/3n2=4/3·n Elemente bei einer Verdopplung von n ein, die
Einsparung wächst also linear mit n.
Zusammenfassend läßt sich sagen, daß die dreidimensionale FE-Analyse mit
einer Komplexität K3(n)=n3 erheblich mehr Aufwand bereitet als die zweidimensionale FE-Analyse mit einer Komplexität von K2(n)=n2.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
51
Trotz der in dem Beispiel offensichtlich schlechteren Komplexität der dreidimensionalen
Analyse ist es nicht einfach zu entscheiden, ob ein Algorithmus signifikant besser oder
schlechter ist als ein anderer, insbesondere, wenn einer der beiden Algorithmen für jeden
auf das Kostenmaß bezogenen Rechenschritt mehr Rechenzeit benötigt als der andere.
Beispiel: Angenommen, in dem oben beschriebenen Sortieralgorithmus Bubblesort
laufe die innere Schleife nicht von n bis i sondern von n hinunter bis 1, also über
das gesamte, auch schon teilweise geordnete Feld. Scheinbar verschlechtert sich
der Algorithmus dadurch, denn jetzt sind in jeder Bearbeitung der inneren
Schleife n Vergleiche durchzuführen, insgesamt also n2. Die Komplexität
KBubblesort'(n) dieses veränderten Algorithmus´ verschlechtert sich demnach
von (n2-n)/2 zu n2. Da jedoch
KBubblesort (n) =
n2 − n 1 
1 2 1
=  n −  −
2
2
2
8
gilt, nimmt die Komplexität des ersten Algorithmus´ gleichfalls parabolisch mit
n zu und ist daher nicht signifikant besser als seine Variante.
Ein anderer Sortieralgorithmus, der Quicksort (vgl. Abschnitt 3.4.2.1) besitzt
eine Komplexität KQuicksort(n)=n·log2 n (vgl. z.B. [Wirt-86, S. 102]). Sie
nimmt also fast linear mit n zu. Die Anzahl der Anweisungen, die bei Erfülltsein
des als Kostenmaß benutzten Vergleichs bei Bubblesort und bei Quicksort auszuführen sind, schlagen sich in einem Vorfaktor c in der Komplexität nieder
(K(n)=c·n·log n). Daher gibt es ein n, ab dem der Quicksort immer weniger Vergleiche benötigt als Bubblesort. Benötigt beispielsweise der Quicksort 2.000 mal
mehr Anweisungen, wenn ein Vergleich erfolgreich ist, als der Bubblesort
(c=2.000), so gilt (unter Vernachlässigung des Logarithmus´, der selbst für große
n sehr klein bleibt)
KQuicksort < K Bubblesort’ ⇔ c ⋅n ⋅ log n < n2
⇒
⇔
c⋅n < n
c<n
2
und mit c=2000 folgt, daß ab n=2001 der Quicksort immer weniger Vergleiche
benötigt als der Bubblesort und somit dessen Komplexität signifikant besser ist.
Zum Vergleich der Komplexität verschiedener Algorithmen für die gleiche Aufgabe dient
ein besonderes Ordnungsschema, die 1894 von Paul Bachmann eingeführte O-Notation.
Definition 3-1: Es seien f(n) und g(n) zwei von n abhängige Funktionen. Man
schreibt
f (n) = O(g(n))
(gesprochen: f ist von der Ordnung g), wenn zwei Konstanten n0 ≥ 0 und c existieren, so daß für alle n ≥ n0 die Ungleichung
f (n) ≤ c ⋅g(n)
gilt. 1
1Da die Ungleichung für ein festes g für viele f gilt, ist das Gleichheitszeichen in der obigen Glei-
chung etwas irreführend. Vorteilhafter wäre das Zeichen ⊆ für unechte Untermenge, jedoch hat sich
das Gleichheitszeichen in der Fachliteratur etabliert (siehe hier auch die Diskussion in [GrKP-88, S.
432f]).
Datenverarbeitung in der Konstruktion (DiK)
52
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Beispiele:
Die Komplexität der zweiten Variante des Bubblesort, KBubblesort'(n)=n2, ist von der Ordnung O(n2), da für jedes n>0 (n0=1)
n 2 ≤ c ⋅ n2 ⇔ 1 ≤ c gilt und somit unendlich viele Konstanten c existieren, für
die die Ungleichung gilt.
1
2
3
Jedoch gilt auch n = O(n ) , da n 2 ≤ c ⋅ n3 ⇔ 1≤ c ⋅ n ⇔ ≤ n , wenn für ein bec
1
liebiges c n0 ≥ ist.
c
2
Die Komplexität KBubblesort (n) = (n − n) / 2 ist auch von der Ordnung O(n2), da
n2 − n
1
1
n2 − n ≤ c ⋅ n2 ⇔
≤ c ⇔1 − ≤ c ⇔
≤ n , wenn c>1 ist. Ange2
n
n
1− c
nommen, der Bubblesort liefe auf einem m mal so schnellen Rechner. Die Komplexität der zweiten Variante ist KBubblesort',m=1/m·KBubblesort'=1/m·n2. Sie
ist jedoch noch immer von der gleichen Ordnung O(n2), da
1 2
1
n ≤ c ⋅ n2 ⇔ ≤ c gilt, für jedes m also beliebig viele c existieren, für die die
m
m
Ungleichung erfüllt ist.
Die Komplexität der zweiten Variante des Bubblesort ist nicht von der Ordnung
n
O(n·log n), da n 2 ≤ c ⋅ n⋅ log n ⇔ n ≤ c ⋅log n ⇔
≤ c ist und für jedes c unlog n
endlich viele n existieren, für die die Ungleichung nicht gilt.
Aus den Beispielen kann man erkennen, dass
•
Polynome von der Ordnung der höchsten, in ihnen vorkommenden Potenz sind,
•
ein konstanter Faktor in der Komplexität an deren Ordnung nichts ändert (ein
schlechter Algorithmus wird durch einen schnelleren Rechner nicht signifikant
besser!) und
•
ein Klassensystem existiert, dessen Klassen durch die Ordnungsfunktionen gebildet werden, die nicht kommutativ angewendet werden können, mit anderen
Worten: seien f=O(g) und h=O(i). Wenn g=O(i) ist, dann gilt auch f=O(i). Ist
jedoch i•O(g), dann gilt nicht für jedes h, für das h=O(i) gilt, auch h=O(g). Beispielsweise ist zwar die Komplexität des Quicksort (n·log n) auch von der Ordnung O(n2), jedoch ist die Komplexität des Bubblesort, wie im obigen Beispiel
ausgeführt, nicht von der Ordnung O(n·log n), weil n·log n=O(n2), jedoch
n2•O(n·log n) ist.
Die gebräuchlichsten Ordnungsklassen sind (nach aufsteigender Wachstumsgeschwindig2n
keit) log log n, log n, n, n·log n, n2, n3, ..., 2n, nn, 2 .
Zusammenfassend läßt sich sagen, daß Algorithmen, die von der gleichen Ordnung sind,
qualitativ gleichwertig bezüglich ihres Aufwands sind. Ziel der Entwicklung eines Algorithmus´ sollte neben der reinen Erfüllung der Aufgaben stets sein, seine Komplexität möglichst günstig zu halten. Dabei ist zu beachten, daß für ein Problem immer eine Mindestkomplexität existiert, die von einem Algorithmus nicht unterschritten werden kann.
Zur Vertiefung der O-Notation sei [GrKP-88, Kap. 9] empfohlen. Die Komplexität von
verschiedenen Algorithmen wird beispielsweise in [Wirt-86] näher behandelt. Als allgemeine Einführung in die Komplexitätstheorie kann [Paul-78] dienen.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
53
3.3.2 Algorithmenklassen
Schon die Begriffsdefinition des Algorithmus´ als allgemeines Lösungsverfahren für eine
Aufgabe impliziert, daß zu jedem lösbaren Problem auch mindestens ein Lösungsverfahren
existiert. Um nun diese riesige Menge von Algorithmen besser handhaben zu können, ist es
sinnvoll, eine Einteilung der Algorithmen nach bestimmten Kriterien vorzunehmen. Ein
Kriterium kann direkt aus dem ersten Satz abgeleitet werden: alle Algorithmen, die ein bestimmtes Problem lösen, können zu Klassen zusammengefaßt werden. Solche Klassen sind
beispielsweise Sortier- und Suchalgorithmen. Da auch Probleme zu Klassen zusammengefaßt werden können, kann eine entsprechende Einteilung der Algorithmen geschehen. Beispiele hierfür sind geometrische Algorithmen, Graphenalgorithmen, kryptographischen
Algorithmen, Kodierungsalgorithmen und numerischen Algorithmen. Zusätzlich können
Algorithmen nach den in ihnen verwendeten Methoden zur Problemlösung unterschieden
werden. Hier sind z.B. die rekursiven Algorithmen zu nennen.
Die aufgeführten Beispiele sollen nur einen groben Eindruck von der Vielfalt der existierenden Algorithmen, Algorithmenklassen und -klassifikationen geben. Sie erheben keinen
Anspruch auf Vollständigkeit, stellen jedoch wichtige Vertreter aus der Menge der Algorithmen dar.
Sortieren
Das Sortieren von Objekten nach bestimmten Kriterien ist eine der weitestverbreiteten Aufgaben, die derzeit mit Computern gelöst werden. Gemeinhin im Zusammenhang mit Zahlen
behandelt, können Sortierverfahren jedoch auch beliebige andere Objekte sortieren, auf
denen eine sogenannte Ordnungsrelation definiert werden kann. Eine Ordnungsrelation •
setzt jeweils zwei Objekte der Menge M der zu sortierenden Objekte in eine Beziehung, für
die das Idempotenzgesetz (für alle i aus M gilt: i • i) und das Transitivgesetz (für alle a, b, c
aus M gilt: a ≤ b ∧ b ≤ c ⇒ a ≤ c ) gilt.
Für die Auswahl eines passenden Sortieralgorithmus ist es wichtig zu wissen, in welcher
Datenstruktur die zu sortierende Objektmenge vorliegt. Wird im Arbeitsspeicher des Computers sortiert, so wird meist ein eindimensionales Datenfeld verwendet, das einen direkten
Zugriff auf jedes Element der Menge zuläßt. Große Datenmengen, die nicht in den Arbeitsspeicher passen, können in Dateien auf Massenspeichern vorliegend sortiert werden. Dateien bieten meist nur einen sequentiellen Zugriff auf die in ihnen enthaltenen Daten, es ist
also nur ein Objekt nach dem anderen zugreifbar.
Beim Sortieren in Feldern macht man sich drei Prinzipien zunutze: das Einfügen eines neuen Elements in eine schon geordnete Teilmenge an die durch die Ordnungsrelation vorgegebene Stelle, das Auswählen eines bezüglich der Ordnungsrelation ausgezeichneten Elements der unsortierten Menge (z.B. des kleinsten Elements x) und das Austauschen von
Elementen.
Das Sortieren durch Einfügen wurde schon in Abschnitt 3.2.6.1 im Zusammenhang in binären Bäumen gezeigt. Dem Sortieren durch Auswahl entspricht ein leicht veränderter
Bubblesort, bei dem nicht die benachbarten Feldelemente miteinander verglichen werden,
sondern das kleinste Feldelement durch sequentielles Durchwandern des Feldes festgestellt
und mit dem ersten Feldelement vertauscht wird. Danach muß nur noch das restliche Feld in
Bezug auf die dort kleinste Zahl betrachtet werden. Typisches Beispiel für das Sortieren
durch Austauschen ist der normale Bubblesort.
Datenverarbeitung in der Konstruktion (DiK)
54
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Ein sehr bekannter und höchst effizienter Algorithmus ist der Quicksort. Er beruht auf einem der Software-Entwurfsprinzipien für Algorithmen, dem Herrsche und Teile (engl. divide and conquer), also dem Aufteilen des Problems in Teilprobleme und deren separate
Lösung. Aus den in einem Datenfeld vorliegenden Objekten wird zufällig ein x, ausgesucht.
Nun beginnt man mit dem Feldelement i mit dem kleinsten Index und testet, ob die Ordnungsrelation i • x gilt. Ist dies der Fall so wird zum Element mit dem nächsthöheren Index
gegangen. Anderenfalls wird nun von rechts beginnend ein Element gesucht, für das die
Relation x • i nicht gilt. Diese beiden Elemente werden miteinander vertauscht. Dieser Prozeß wird solange durchgeführt, bis man sich von links und rechts kommend trifft. Das Feld
befindet sich dann in einem Zustand, daß links von dem Element x alle Elemente • x und
rechts von x alle Elemente • x angeordnet sind. Diese beiden sogenannten Partitionen sind
zwar noch nicht sortiert, wendet man aber den eben beschriebenen Algorithmus auf die
beiden Partitionen an, so kann man auch hier wieder Partitionierungen hervorrufen. Die
rekursive Anwendung des Algorithmus´ auf die immer kleiner werdenden Partitionen führt
zu einer Partitionsgröße von 1, so daß bei deren Erreichen das gesamte Feld sortiert ist.
Dem Herrsche von "Herrsche und Teile" ist demnach die Auswahl des Elements x und das
optionale Vertauschen von Elementen zuzuordnen, dem Teile das Aufteilen in die beiden
Partitionen. Der Quicksort ist ein typisches Beispiel aus der Klasse der rekursiven Algorithmen.
Neben den genannten Sortieralgorithmen auf Feldern existieren noch weitere Algorithmen,
auf die hier nicht weiter eingegangen werden soll. Die wichtigsten Vertreter sind der Shellsort, benannt nach seinem Entwickler D. L. Shell, und der Heapsort.
Das Sortieren auf Dateien beruht auf dem Prinzip des Mischens. Die Datei mit den zu sortierenden Objekte kann beispielsweise in der Mitte geteilt werden. Nun werden die jeweils
ersten Elemente der beiden Teile gelesen, über die Ordnungsrelation miteinander verglichen
und ggf. vertauscht in eine neue Datei geschrieben. Dies wird für die restlichen Paare aus
den beiden Teilen durchgeführt, bis das Ende der ursprünglichen Datei erreicht ist. Auf der
neuen Datei wird die Zweiteilung, das Lesen, Vergleichen und Schreiben der Paare erneut
ausgeführt, wobei jedoch von der korrekten Reihenfolge der Elemente aus dem ersten Mischen profitiert werden kann. Dadurch erhält man Vierergruppen, in denen eine geordnete
Reihenfolge besteht. Der Prozeß wird solange wiederholt, bis die entstehende Gruppe die
gesamte Datei umfaßt.
Ein umfassendes Kompendium zum Sortieren ist [Knut-73]. Anschauliche Darstellungen
von Sortieralgorithmen sind in [Wirt-86] und [Sedg-91] zu finden.
Suchen
Sortierte Daten können besonders gut weiter verarbeitet werden. Insbesondere bieten sie
auch die Grundlage für das Wiederauffinden von Informationen, das die sogenannten Suchalgorithmen übernehmen. Sie benötigen eine Suchbedingung, die entscheidet, ob das zu
testende Objekt mit dem gesuchten Objekt übereinstimmt. Datenstrukturen, auf denen meist
gesucht wird, sind sequentielle Dateien, Felder und Bäume.
Mit dem sequentiellen Suchen kann in sequentiellen Dateien und in Feldern gesucht werden, indem die Suchbedingung vom Beginn der Datei oder Feldes an nacheinander auf alle
Objekte angewendet wird, bis sie erfüllt ist. Das gesuchte Objekt ist dann gefunden.
Das binäre Suchen auf Feldern macht sich die Ordnung des Feldes zunutze, indem ein Objekt zufällig ausgewählt und über die Suchbedingung entschieden wird, ob das Objekt schon
gefunden wurde. Ist dies nicht der Fall, so kann anhand der Ordnungsrelation für das Sortie-
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
55
ren herausgefunden werden, ob das zu suchende Objekt links oder rechts vom aktuellen
Objekt liegt. Diese Maßnahme reduziert den noch zu durchsuchenden Feldteil erheblich.
Eine solche Intervallschachtelung liegt auch den binären Suchbäumen zugrunde.
Das Tabellen-Suchen (engl. hashing) basiert nicht auf dem Durchsuchen des Gesamtdatenbestandes anhand einer Suchbedingung, sondern auf der Aufstellung einer Tabelle vor dem
eigentliche Suchprozeß. Dazu ist es wichtig zu wissen, daß die Ordnungsrelation zum Sortieren meist über eine Funktion mit mehreren Eingangsparametern und einem Wahrheitswert als Rückgabeparameter realisiert wird, einem sogenannten Prädikat. Als Eingangsparameter dienen bestimmte Objektattribute, die sogenannten Sortierschlüssel. Eine
Suchbedingung testet dann üblicherweise, ob die Schlüssel des zu findenden Objekts, die
vor der Suche als Anhaltspunkt vorliegen, mit den Schlüsseln des aktuell zu testenden Objektes übereinstimmen. Stattdessen wird die oben genannte Tabelle aufgebaut, indem mit
einer sogenannten Hashfunktion eine eindeutige Abbildung von den Schlüsseln zu einem
Eintrag der Tabelle hergestellt wird. Da im allgemeinen weniger Tabelleneinträge als zu
sortierende Objekte existieren, fallen auf einen Tabelleneintrag mehrere Objekte, die Hashfunktion ist demnach nicht unbedingt bijektiv (eineindeutig). Daher ist jeder Tabelleneintrag als Liste von Objekten ausgeführt. Beim Suchen eines Objekts anhand eines Schlüssels
wird auf diesen die Hashfunktion angewendet, die dann zu einem Tabelleneintrag und der
zugehörigen Objektliste führt. Nun können die in der Liste eingetragenen Objekte durch
sequentielles Suchen untersucht werden. Existiert das gesuchte Objekt überhaupt, dann muß
es sich ja in der Liste des Tabelleneintrags befinden, weil beim Einsortieren des Objekts
dieselbe Hashfunktion angewendet wurde wie beim Suchen. Die Tabellensuche ist u.a. in
[Wirth-86] beschrieben.
Als letzte Gruppe von Suchalgorithmen ist die Mustersuche (engl. pattern matching) anzuführen. Deren Aufgabe ist es, in einem Text den Ort einer Zeichenkette auszumachen. Neben der direkten Mustersuche, bei der jedes Zeichen des Textes mit dem Beginn der Zeichenkette verglichen wird, bei Erfolg das nächste Zeichen an die Reihe kommt, bei
Mißerfolg jedoch die Zeichenkette nur um ein Textzeichen weitergeschoben wird, wurden
zwei Algorithmen entwickelt, die die Mustersuche im Durchschnitt erheblich beschleunigen
können: der Knuth-Morris-Pratt- und der Boyer-Moore-Algorithmus. Beide wurden bis
heute mehrfach verbessert und sind so ausgefeilt, daß ihre Besprechung den gesetzten Rahmen sprengen würde. Näheres ist in [Wirt-86], den Originalartikeln [BoMo-77] und
[KnMP-77], sowie [HuSu-91] zu finden.
Rekursive Algorithmen
Benutzt ein Algorithmus während seiner Durchführung sich selbst, um nach dem Prinzip
des Herrsche und Teile einen Teil der gestellten Aufgabe zu lösen und dann diese Teillösung zur Lösung der gesamten Aufgabe zu verwenden, so nennt man ihn rekursiv.
Damit ein rekursiver Algorithmus korrekt arbeitet, muß er zwei Bedingungen erfüllen: einerseits muß er eine Abbruchbedingung besitzen, die das Selbstaufrufen in bestimmten Fällen unterbindet, andererseits muß das dynamische Verhalten des Algorithmus´ so gestaltet
sein, daß die Abbruchbedingung in endlicher Zeit erfüllt werden kann.
Beispiel: Die Fakultät n! ist definiert als n! = n ⋅ (n − 1) ⋅ (n − 2 ) ⋅ ... ⋅ 2 ⋅ 1 . Die Definition kann jedoch auch als Rekurrenzbeziehung ausgedrückt werden:
,n = 0
1
n! = 
n ⋅ (n − 1)! ,sonst
In dieser Definition wird von dem Vorhandensein der zu definierenden Funktion
Datenverarbeitung in der Konstruktion (DiK)
56
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
ausgegangen. Die rekurrente Fakultätsdefinition kann direkt in einen Algorithmus umgesetzt und in PASCAL wie folgt formuliert werden:
FUNCTION Fakultät(n : INTEGER) : INTEGER;
BEGIN
IF n = 0 THEN
Fakultät:=1
ELSE
Fakultät:=n*Fakultät(n-1)
END;
Der Algorithmus weist die beiden, oben genannten Forderungen nach einer Abbruchbedingung (n=0) und dem dynamischen Verhalten in Richtung der Abbruchbedingung (Fakultät(n-1)) auf. Der Algorithmus hat jedoch eine Unzulänglichkeit: da n vom Typ INTEGER ist, kann n auch negativ werden. In diesem
Fall besitzt der Algorithmus zwar noch die Abbruchbedingung, das Argument n
wird jedoch bei jeder weiteren Rekursion „negativer“ und bewegt sich daher von
der Abbruchbedingung weg. Der Algorithmus terminiert nicht. Um diesem Fall
zu begegnen, kann entweder vereinbart werden, daß die Funktion nur mit positiven Argumenten aufgerufen wird (es wird eine sogenannte Vorbedingung vereinbart, die erfüllt sein muß, wenn der Algorithmus seine Aufgabe erfüllen soll)
oder die IF-Abfrage wird um den Fall n<0 erweitert, dessen Auftreten zu einer
Fehlermeldung führt.
Gerade im Zusammenhang mit sich selbst wiederholenden Datenstrukturen, wie Graphen,
Bäumen oder Listen werden rekursive Algorithmen zur Implementierung der auf den Datenstrukturen anwendbaren Operationen verwendet. Eine übersichtliche Beschreibung von
rekursiven Algorithmen kann in [Wirt-86] gefunden werden.
Graphenalgorithmen
Viele praktische Probleme lassen sich auf Graphen abbilden und somit algorithmisch lösen.
Dem gewichteten Graphen (siehe Abschnitt 3.2.2) kommt in vielen Fällen eine besondere
Rolle zu, da die Kanten mit Gewichten versehen werden, welche die problemzugehörigen
(Kosten, Längen, ...) für den Lösungsalgorithmus verfügbar machen.
Die Tiefen- und Breitensuche wurde schon in Abschnitt 3.2.2 behandelt.
Häufige Problemstellungen, bei denen gewichtete Graphen zum tragen kommen, sind die
Suche nach der kürzesten Verbindung zwischen zwei Punkten (z.B. automatische Fahrplanerstellung) oder die Suche nach der kostengünstigsten Möglichkeit, alle Punkte miteinander zu verbinden (Traveling Salesman Problem). Im ersten Falle ist der kürzeste Pfad, der
hier nicht weiter erläutert wird, im zweiten der minimale Spannbaum gesucht.
Ein minimaler Spannbaum eines gewichteten Graphen ist eine Menge von Kanten, die alle
Knoten des Graphen so verbindet, daß die Summe der Kantengewichte minimal ist. Der
minimale Spannbaum muß nicht eindeutig sein. Um den minimalen Spannbaum zu erzeugen, verknüpft man jeden Knoten mit dem benachbarten Knoten, der ihm „am nächsten“
liegt, also dem Knoten mit der am niedrigsten gewichteten Kante, sofern er nicht bereits im
minimalen Spannbaum enthalten ist. Sollte es mehrere Knoten mit gleichgewichteter Verbindungskante zum Ausgangsknoten geben, kann ein beliebiger Knoten unter ihnen gewählt
werden.
Ein anderes Verfahren zur Erzeugung des minimalen Spannbaumes geht auf J. Kruskal
zurück. Dem zunächst leeren Baum werden, beginnend mit der am niedrigsten gewichteten
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
57
Kante, nach aufsteigenden Gewichten die Kanten des Graphen hinzugefügt. Die Knoten, die
durch eine zum Baum hinzugenommene Kante verbunden werden, werden in Mengen zusammengefaßt. Bei der Hinzunahme einer Kante wird nun in allen schon existierenden
Mengen nachgesehen, ob eine der beiden Kanten enthalten ist. Ist eine Menge gefunden, so
wird geprüft, ob der zweite zur Kante gehörende Knoten auch Element dieser Menge ist. Ist
dies der Fall, so wird die Kante nicht in den Baum aufgenommen, da offenbar schon ein
besserer Pfad zu diesem Knoten existiert. Ist der zweite Knoten nicht in der Menge, so wird
die Kante in den Baum übernommen und die beiden Knotenmengen werden durch ihre Vereinigungsmenge ersetzt.
Beispiel (aus: [AhHU-74, S. 174f]):
Gegeben sei der folgende gewichtete Graph:
20
v1
23
v2
1
4
36
v6
9
v7
25
28
15
16
3
17
v5
v3
v4
Zunächst ist der minimale Spannbaum leer, also S={}, und man sortiert alle
Kanten nach ihren Kantengewichten aufsteigend, erhält also die Liste (v1,v7)1,
(v3,v4)3, (v2,v7)4, (v3,v7)9, (v2,v3)15, (v4,v7)16, (v4,v5)17, (v1,v2)20, (v1,v6)23,
(v5,v7)25, (v5,v6)28, (v6,v7)36.
Jetzt wird diese Liste der Reihe nach durchsucht, und es werden Kanten zum
minimalen Spannbaum S nur dann hinzugefügt, wenn das Ergebnis wieder einen
Baum bildet. Nach dem ersten Schritt ist also S={(v1,v7)1}. Im zweiten Schritt
wird die nächste Kante der Liste hinzugefügt, es ist also S={(v1,v7)1, (v3,v4)3}.
Nach dem dritten Schritt ist S={(v1,v7)1, (v3,v4)3, (v2,v7)4}, nach dem vierten
S={(v1,v7)1, (v3,v4)3, (v2,v7)4, (v3,v7)9}. Die dann folgende Kante (v2,v3)15 wird
nicht zu S hinzugefügt, da sich sonst ein Kreis in S bilden würde und S damit
kein Baum mehr wäre. Aus demselben Grund wird auch die Kante (v4,v7)16 nich
hinzugefügt. Die Kante (v4,v5)17 dagegen kann hinzugenommen werden, da das
Ergebnis nachwievor ein Baum ist: S={(v1,v7)1, (v3,v4)3, (v2,v7)4, (v3,v7)9,
(v4,v5)17,}. Dagegen wird die Kante (v1,v2)20 wieder nicht übernommen, da sich
sonst ein Kreis in S bilden würde. Nachdem im letzten Schritt auch die Kante
(v1,v6)23 zu S hinzugefügt wurde, erhält man als Ergebnis den minimalen
Spannbaum S={(v1,v7)1, (v3,v4)3, (v2,v7)4, (v3,v7)9, (v4,v5)17, (v1,v6)23}.
v1
23
v2
1
v6
4
v7
9
v3
v3
3
v5
17
v4
Eine Einführung in die Graphenalgorithmen ist in [Sedg-91] zu finden. Erschöpfend behan-
Datenverarbeitung in der Konstruktion (DiK)
58
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
delt [Jung-90] die Graphentheorie.
Kryptographische Algorithmen
Kommunizieren zwei Kommunikationspartner (Menschen, Tiere oder Maschinen) miteinander, so übermittelt stets einer der Kommunikationspartner, der Sender, Nachrichten über
ein Übertragungsmedium an den Empfänger. Nach [BeHW-83, S. 10] kann die Übertragung
beeinträchtigt werden durch:
•
zufällige Störungen,
•
systematische (physikalisch bedingte) Störungen,
•
passive Beeinträchtigung und
•
aktive Beeinträchtigung.
Unter passiver Beeinträchtigung wird das Abhören des Übertragungsmediums durch einen
Lauscher verstanden, während die aktive Beeinträchtigung durch einen Fälscher zustande
kommt, der sich in das Übertragungsmedium einschaltet, Nachrichten mithört und dem
Empfänger gefälschte Nachrichten zukommen läßt.
Ziel der Kryptographie (griech.: verborgen schreiben) ist es
•
Nachrichten vor Lauschangriffen durch Verschlüsselung zu schützen
(Chiffrierung) und
•
dem Empfänger zu ermöglichen, daß er feststellen kann, ob die Nachrichten
in der Tat von dem erwarteten Sender kommen oder durch einen Fälscher
eingespielt wurden (Authentisierung).
Die Chiffrierung einer Nachricht ist eine eindeutige Abbildung des Nachrichteninhaltes in
eine verschlüsselte Form, dem Chiffre, das mit sogenannten kryptoanalytischen Angriffen
eines Fälschers nicht oder nur mit viel Aufwand zu entschlüsseln ist. Viel Aufwand bedeutet hier, daß ein entsprechender Algorithmus für einen kryptanalytischen Angriff mindestens eine polynomiale Komplexität hohen Grades oder eine exponentielle Komplexität hat.
Im Unterschied zum Fälscher ist der autorisierte Empfänger der verschlüsselten Nachricht
mit einem Schlüssel ausgestattet, von dem er außerhalb des für die Nachricht benutzten
Übertragungsmediums Kenntnis erlangt hat. Algorithmen zur Chiffrierung erzeugen also
das Chiffre und den Schlüssel, mit dem sich das Chiffre wieder entschlüsseln läßt. Algorithmen zur Dechiffrierung nutzen Chiffre und Schlüssel, um wieder den Klartext zu erhalten.
Bei Algorithmen zur Chiffrierung und Dechiffrierung sind drei Klassen verbreitet:
•
Algorithmen für sequentielle Chiffren mit nichtlinearen PseudoZufallszahlen-Generatoren zerlegen die Nachricht in eine Bitfolge, die mit
Bits aus einem Pseudo-Zufallszahlen-Generator (PSG) verknüpft werden.
Ein Pseudo-Zufallszahlen-Generator erzeugt Zahlen in einem festgelegten
Bereich, deren Reihenfolge von einem Beobachter, der die Rechenvorschrift nicht kennt, als nicht vorhersagbar und daher zufällig eingestuft
wird. Da die Zahlen jedoch mittels einer Rechenvorschrift entstanden sind,
sind sie nicht wirklich zufällig (daher Pseudo-Zufallszahlen). Die Zufallszahlenfolge, die ein PSG erzeugt, ist wiederholbar, wenn die Initialisierung
des PSG bekannt ist. Diese Initialisierung wird dem Empfänger als Schlüssel für das Chiffre mitgeteilt.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
•
Algorithmen für Blockchiffren teilen die Nachricht in Blöcke einer festen
Größe ein. Die Zeichen eines Blocks werden dann gemeinsam der Chiffrierung unterworfen. Als Beispiel sei hier der Data Encryption Standard (DES)
des amerikanischen National Bureau of Standards (NBS) genannt. Dieser
Algorithmus teilt die Nachricht in 64-Bit-Blöcke ein, die über einen 56 Bit
langen Schlüssel mittels verschiedener boolescher Funktionen verschlüsselt
werden. Das Verfahren ist im Detail zu kompliziert, um hier angeführt zu
werden. Zudem ist der Export von Programmen, die den DES verwenden,
aus den USA nicht erlaubt.
•
Algorithmen für Chiffren aus Einwegfunktionen benutzen sogenannte Einwegfunktionen (engl. trapdoor functions), deren Funktionswert mit geringem Aufwand zu berechnen ist. Das Finden der zu einem Funktionswert
gehörenden Funktionsstelle jedoch ist nur mit sehr viel mehr Aufwand
möglich. Allerdings erhält der Empfänger vom Sender über ein anderes Übertragungsmedium eine Zusatzinformation, die ihm die Berechnung der
Umkehrfunktion erleichtert. Ein Beispiel für eine Falltürfunktion ist in dem
sehr bekannten, von R. Rivest, A. Shamir und L. Adleman entwickelten
RSA-Algorithmus zur Berechnung von öffentlichen Schlüsseln (engl. public key system, s.u.) enthalten [RiSA-78].
59
Die Authentisierung einer Nachricht ist der Nachweis, daß die Nachricht von dem Sender
stammt, von dem sie erwartet wurde. Eine Nachricht gilt als authentisch, wenn dieser
Nachweis erbracht wurde. Die Authentisierung kann dadurch ermöglicht werden, daß die
Nachricht oder Teile von ihr in einer Art verschlüsselt worden sind, die nachweislich nur
der erwartete Sender vorgenommen haben kann. Eine Erweiterung der Authentisierung ist
die elektronische Unterschrift (elektronische Signatur), mit der der Empfänger nicht nur die
Authentizität der Nachricht feststellen kann, sondern auch, ob die Nachricht auf dem Übertragungsmedium verfälscht wurde. Wäre nämlich bei der Authentisierung der Nachricht
deren Inhalt nicht miteinbezogen worden, so könnte ein Fälscher die Nachricht in seinem
Sinne verändern, den die Authentisierung betreffenden Teil jedoch unverfälscht lassen. Ist
der Inhalt einer Nachricht nicht vertraulich, soll sichergestellt sein, daß die Nachricht authentisch ist und nicht verfälscht wurde. In diesem Fall kann eine elektronische Unterschrift
verwendet, auf eine weitergehende Chiffrierung der Nachricht aber verzichtet werden.
Algorithmen zur Authentisierung berechnen u.a. Schlüsselpaare, mit denen die Nachricht
chiffriert und dechiffriert werden kann. Insbesondere kann eine Nachricht nur dann mit dem
zweiten Schlüssel eines Schlüsselpaars entschlüsselt werden, wenn sie mit dem ersten
Schlüssel verschlüsselt wurde. Dadurch kann die Urheberschaft der Nachricht sichergestellt
werden.
In den Privaten Schlüsselsystemen (engl.: privat key systems) sind beide Schlüssel gleich.
Somit könnte der Besitzer des dekodierenden Schlüssels auch gefälschte Nachrichten
versenden. Da jedoch dieser Besitzer üblicherweise mit dem Empfänger übereinstimmt, ist
die Gefahr des Mißbrauchs gering. Für die langlebigere Verwendung von Authentisierungsschlüsseln wurden Öffentliche Schlüsselsysteme (engl. public key systems) entwickelt, bei
denen sich der chiffrierende von dem dechiffrierenden Schlüssel unterscheidet. Der chiffrierende Schlüssel wird privater Schlüssel genannt, der dechiffrierende öffentlicher Schlüssel.
Bei der Herstellung des Schlüsselpaares wird der öffentliche Schlüssel aus dem privaten
Schlüssel über eine Einwegfunktion berechnet. Aufgrund der Tatsache, daß die mit dem
privaten Schlüssel chiffrierte Nachricht mit dem öffentlichen Schlüssel dekodierbar ist,
kann daher die Urheberschaft der Nachricht sichergestellt werden. Das Herstellen authentischer Nachrichten nur mit dem öffentlichen Schlüssel ist jedoch nicht möglich, da dieser
nur dekodierende Eigenschaften hat.
Datenverarbeitung in der Konstruktion (DiK)
60
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
Beispiel: Der oben genannte RSA-Algorithmus verwendet als Grundlage für den
privaten Schlüssel d zwei große Primzahlen p und q und als öffentlichen Schlüssel das Zahlenpaar n=p·q und e, wobei e die multiplikative Inverse zu d bezüglich des Produkts von (p-1) und (q-1) ist:
d ⋅e mod((p − 1) ⋅(q − 1)) ≡ 1mod((p − 1) ⋅(q − 1)) ≡ 1
Wird nun die Nachricht m in das Chiffre c = m mod n verschlüsselt, so kann
der Empfänger durch Kenntnis von n und e die Nachricht entschlüsseln, denn
d
c mod n = (m mod n) mod n = m
e
d
e
d ⋅e
mod n = m mod n = m
1
Den Besitzern des öffentlichen Schlüssels nutzt jedoch die Kenntnis von n und e
nichts, da sie zur Berechnung von d die beiden Primzahlen p und q kennen müssen. Diese Primzahlen können zwar aus n durch Primfaktorzerlegung berechnet
werden, der derzeit beste Algorithmus zur Primfaktorzerlegung einer n-stelligen
Zahl hat aber eine Komplexität der Ordnung
(log n )⋅ (log log n)
O(e
).
Die Primfaktorzerlegung einer 80-stelligen Zahl würde somit etwa
e
(ln1080 ) ⋅(ln ln10 80 )
≅e
184⋅ 5
=e
920
≅ 3,5 ⋅ 10
399
kostenrelevante Einzeloperationen benötigen. Wenn eine solche Operation 1ns
dauert, so müssen für die gesamte Primfaktorzerlegung etwa 10380 Jahre Rechenzeit aufgewendet werden. Es ist jedoch auch möglich, daß ein Algorithmus
mit einer besseren Komplexitätsordnung gefunden wird ([BeHW-83, S. 36f]).
Kodierungsalgorithmen
Die Kodierung von Informationen wird verwendet, um
•
die Informationen kompakter zu speichern oder zu übermitteln (Datenkomprimierungskodierung),
•
die Informationen gegen Speicher- oder Übertragungsfehler zu sichern (Kodierung zur Fehlererkennung und -korrektur) und
•
die Struktur der Informationen an die Gegebenheiten des Speicher- oderÜbertragungsmediums anzupassen (Kanalkodierung).
Eine Komprimierung von Informationen ist möglich, weil sie meist redundante Anteile enthalten, die weggelassen werden können, ohne den Informationsgehalt der Nachricht zu verändern.
Während der Speicherung und Übertragung von Informationen können Fehler auftreten, die
durch physikalische Einflüsse auf das Speichermedium oder eine schlechte Qualität des
Übertragungsmediums bedingt sind. Um solche Fehler zu erkennen oder sogar zu beheben,
läßt man neben den reinen Nutzinformationen weitere, redundante Informationen in die
Speicherung oder Übertragung einfließen. Sie werden vom Sender aus den Nutzinformationen berechnet und ermöglichen dem Empfänger eine Überprüfung der übermittelten Informationen und, bei Auftreten eines Fehlers, die Behebung desselben.
Oft besitzt das Speicher- oder Übertragungsmedium bestimmte physikalische Eigenschaften, die das reine, bitweise Abspeichern oder Senden verhindern. In diesem Fall wird auf
die möglicherweise schon mit einer Komprimierung und Kodierung zur Fehlererkennung
und -korrektur versehenen Informationen eine Kanalkodierung angewendet, die die Infor-
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
61
mationen an die Möglichkeiten des Speicher- oder Übertragungsmediums anpasst.
Numerische Algorithmen
Numerische Algorithmen sind Berechnungsverfahren zur Lösung mathematischer Probleme
unter vorwiegender Verwendung der Grundrechenarten und der in Programmiersprachen
zur Verfügung stehenden mathematischen Funktionen.
Die existierenden numerischen Algorithmen sind so vielfältig, daß hier nur ein grober Überblick in Form einer Klasseneinteilung gegeben werden kann. Die Klasseneinteilung orientiert sich an der Struktur von [EnRe-88].
Numerische Verfahren zur Lösung algebraischer und transzendentaler Gleichungen
Mit Hilfe des Newtonschen Verfahrens, des Regula Falsi Verfahrens, des Verfahrens von
Steffensen oder des Pegasus-Verfahren kann man transzendentale Gleichungen wie z.B.
cos(x) − x = 0 numerisch lösen.
Algebraische Gleichungen kann man z.B. mit dem Horner-Schema dem Verfahren von
Muller dem Verfahren von Bauhuber oder dem Verfahren von Jenkins und Traub lösen.
Numerische Verfahren zur Lösung linearer Gleichungssysteme
Es gibt direkte und iterative Methoden, um lineare Gleichungssysteme zu lösen. Der GaußAlgorithmus und das Verfahren von Cholesky sind die bekanntesten Vertreter direkter Verfahren. Sie haben die Eigenschaft, daß sie lineare Gleichungssysteme theoretisch exakt lösen können. In der Praxis spielen jedoch Rundungsfehler eine große Rolle. Daher verwendet
man bei großen linearen Gleichungssystemen oft iterative Verfahren, die zwar die Lösung
nur annähern, jedoch sehr schnell arbeiten. Das Gauß-Jordan-Verfahren ist ein Beispiel für
solche iterativen Verfahren.
Insbesondere für spezielle Formen von Gleichungssystemen mit z.B. tridiagonalen Matrizen
oder allgemeinen Bandmatrizen gibt es sehr schnelle Algorithmen. Ein kleines Beispiel für
ein tridiagonales Gleichungssystem könnte lauten:
2 1
1 2

1


  x1   1
1   x 2   2
=
2 1  x 3   3
   
1 2  x 4   4
Numerische Verfahren zur Lösung von Systemen nichtlinearer Gleichungen
Systeme nichtlinearer Gleichungen wie z.B.
x 2 + y + sin(z) = 0
x + e y − z +1 = 0
y − z3 −1 = 0
Datenverarbeitung in der Konstruktion (DiK)
62
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
werden meist mit dem Newtonsche Verfahren gelöst. Weitere Verfahren sind Regula Falsi,
das Gradientenverfahren und das Verfahren von Brown.
Verfahren zur Berechnung von Eigenwerte und Eigenvektoren von Matrizen
Ist A eine quadratische Matrix, so nennt man den Vektor x Eigenvektor zum Eigenwert λ ,
falls die Beziehung
A⋅ x = λ ⋅x
gilt. Um Eigenwerte und Eigenvektoren von Matrizen näherungsweise zu berechnen, gibt es
z.B. das Iterationsverfahren nach von Mises. Direkte Methoden wie z.B. das Verfahren von
Krylov oder das Verfahren von Martin, Parlett, Peters, Reinsch und Wilkinson können zwar
Eigenwerte theoretisch exkat berechnen, werden aber in der Praxis meist als iterative Methoden benutzt.
Numerische Approximation stetiger Funktionen
Die Fehlerquadrat-Methode nach Gauß ist wohl das bekannteste Verfahren, um Funktionen
zu approximieren, von denen manche Funktionswerte durch Meßreihen bestimmt wurden.
Falls die zu approximierenden Funktionen Polynome sind, bieten sich für die Approximation die sogenannten Tschebyscheff-Polynome an. Die Fourier-Transformation benutzt man
hingegen eher für die Approximation von (elektronischen) Signalen.
Numerische Interpolation
Bei der numerischen Interpolation geht es darum, zu gegebenen Funktionswerten Funktionen (meist Polynome oder sogenannte Splines) zu finden, die an den gegebenen Stellen
genau die gewünschten Funktionswerte besitzen. Bekannte Verfahren sind die Interpolation
nach Lagrange, das Interpolationsschema von Aitken, oder die Interpolation nach Newton.
Bei der Spline-Interpolation werden Polynom-Splines dritten Grades, Hermite-Splines fünften Grades oder Bezier-Splines verwendet. Auch rationale Funktionen werden für die Interpolation benutzt, man spricht dann von Rationaler Interpolation.
In CAD-Systemen sind Interpolationsverfahren von entscheidenter Wichtigkeit. Zum Beispiel kann man als Benutzer sogenannte Stützpunkte definieren, aus denen dann die CADSoftware mit Hilfe von Interpolationsmethoden Kurven oder Flächen generiert.
Numerische Differentiation
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
63
Durch eine Formel gegebene Funktionen lassen sich einfach symbolisch differenzieren. Oft
jedoch sind Funktionen nicht in dieser Art vorgegeben und man benötigt numerische Verfahren für die Differentiation. Solche Verfahren sind z.B. die Differentiation mit Hilfe eines
Interpolationspolynoms, die Differentiation mit Hilfe interpolierender kubischer Splines
oder die Differentiation nach dem Romberg-Verfahren.
Numerische Integration
Für die numerische Integration gibt es eine Fülle von Verfahren, von denen wir hier einige
nur nennen wollen. Bekannt sind z.B. die Newton-Cotes-Formeln (Sehnentrapezformel,
Simpsonsche Formel, 3/8-Formel), die Maclaurin-Formeln (Tangententrapezformel, u.a.),
die Euler-Maclaurin-Formeln und die Tschebyscheffschen Integrationsformeln. Vergleichsweise sehr exakt arbeiten die Gauß- Integration und das Verfahren von Romberg.
Numerische Verfahren für Anfangswertprobleme bei gewöhnlichen Differentialgleichungen
erster Ordnung
Um Anfangswertprobleme gewöhnlicher Differentialgleichungen numeisch zu lösen gibt es
Verfahren, die sich grob aufteilen lassen in Einschrittverfahren, Mehrschrittverfahren und
Extrapolationsverfahren. Bei den Einschrittverfahren sind das einfache Polygonzugverfahren von Euler-Cauchy, das Verfahren von Heun oder komplexere Runge-Kutta-Verfahren
zu nennen. Bekannte Mehrschrittverfahren wurden z.B. von Adams-Bashforth, AdamsMoulton und von Gear entwickelt.
Numerische Verfahren für Anfangswertprobleme bei Systemen von gewöhnlichen Differentialgleichungen erster Ordnung und bei Differentialgleichungen höherer Ordnung
Handelt es sich um Systeme von gewöhnlichen Differentialgleichungen erster Ordnung, so
können ebenfalls Runge-Kutta-Verfahren oder diverse Mehrschrittverfahren verwendet
werden. Typische Anwendungsgebiete im Maschinenbau für solche Verfahren sind z.B.
Mehrkörpersysteme wie Radaufhängungen in Fahrzeugen, bei denen das Bewegungsverhalten einzelner Komponenten simuliert wird.
Randwertprobleme bei gewöhnlichen Differentialgleichungen
Randwertprobleme sind zumindet bei gewöhnlichen Differentialgleichungen numerisch
schwieriger zu behandeln als Anfangswertprobleme. Bekannte Verfahren sind zum Beispiel
Schießverfahren oder Kollokationsverfahren.
Numerische Verfahren zur Lösung partieller Differentialgleichungen
Die meisten numerischen Probleme, die in der Praxis entstehen, resultieren daraus, daß
partielle Differentialgleichungen zwar bei der Modellierung von technischen Systemen
entstehen, jedoch nur mit numerischen Näherungsverfahren gelöst werden können. Im Maschinenbau gehören z.B. die überaus wichtige Methode der Finiten Elemente, aber auch
sogenannte Differenzenverfahren, die Linienmethode oder Finite Volumen Verfahren zur
Klasse der Verfahren für die Behandlung partieller Differentialgleichungen. Prinzipiell gilt,
daß bei der Lösung komplexer partieller Differentialgleichungen auch Verfahren aus den
Datenverarbeitung in der Konstruktion (DiK)
64
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
anderen Klassen verwendet werden. So müssen z.B. bei der Methode der Finiten Elemente
sehr viele sehr große lineare Gleichungssysteme gelöst werden.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
65
Wesentliches Problem bei der Implementierung numerischer Algorithmen auf Computern
ist die Beschränkung des darstellbaren Zahlenbereichs von ganzen und reellen Zahlen, wie
sie in Abschnitt 1.2 beschrieben ist. Ganze Zahlen besitzen auf dem Computer eine größten
und kleinsten darstellbaren Wert, während reelle Zahlen in der Anzahl der abgespeicherten
Nachkommastellen und in der Größe des Exponenten in der Gleitkommadarstellung beschränkt sind. Nach [EnRe-88, S. 6ff] treten bei der Verwendung numerischer Algorithmen
immer drei Fehlerquellen in Erscheinung: die Verfahrensfehler, die Eingangsfehler und die
Rechnungsfehler.
Die Verfahrensfehler entstehen dadurch, daß anstelle der eigentlichen Problemlösung nur
eine Näherung an die Lösung möglich ist oder aus technischen Gründen angegangen wird.
Beispielsweise werden Funktionen numerisch dadurch integriert, daß an ihren Funktionsgraph Graphen anderer Funktionen (Polynome) oder geometrischer Objekte (Trapeze) angenähert werden, bei denen der Flächeninhalt unter dem Graphen leichter zu berechnen ist.
Die Approximation führt zu den Verfahrensfehlern.
Die Eingangsfehler kommen zustande, wenn Funktionen, die den numerischen Algorithmen
als Eingangsdaten dienen, selbst nur z.B. durch Stützstellen angenähert werden. Dieser Fehler pflanzt sich dann in der Rechnung bis in das Ergebnis fort.
Die Rechnungsfehler entstehen durch die oben genannte eingeschränkte Darstellbarkeit der
ganzen und reellen Zahlen im Rechner. Bei der Durchführung arithmetischer Operationen
kommen Rundungsfehler zustande, die sich je nach Algorithmus akkumulieren oder gegenseitig aufheben können.
Zu jeder Verwendung eines numerischen Algorithmus gehört eine Fehlerabschätzung, die
bei einem vorgegebenen Eingangsfehler den zu erwartenden Fehler im berechneten Ergebnis angibt. Es ist also unsinnig, ein Ergebnis auf 20 Stellen nach dem Komma ausgeben zu
lassen, wenn der Algorithmus bei gewissen Eingangsdaten nur eine Genauigkeit von drei
Stellen erwarten läßt.
Datenverarbeitung in der Konstruktion (DiK)
66
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
3.4 Zusammenfassung
In Abschnitt 3.1 stellten wir als wichtige Programmbausteine typische Konstrukte wie die
Sequenz, die Alternative und die Wiederholung vor. Die Datentypen, die bei der Programmierung verwendet werden, unterteilten wir in einfache, skalare Typen für die Darstellung
von Zahlen, Zeichen und Wahrheitswerten und in strukturierte Typen wie Felder, Verbände
und Dateien.
In Abschnitt 3.2 nutzen wir die Sichtweise aus der objektorientierten Programmierung, um
den Begriff des abstrakten Datentyps (ADT) einzuführen. Mit Hilfe des ADT stellten wir
dann die wichtigsten Vertreter abstrakter Datentypen, den Graph, die Liste, den Stapel, die
Schlange und den Baum, vor. Anhand von Beispielen wurden Eigenschaften dieser abstrakten Datentypen erläutert.
Nachdem wir Datenstrukturen als solche untersucht hatten, behandelten wir im Abschnitt
3.3 Algorithmen, die als Methoden auf den Objektdaten operieren. Als Maß für den Aufwand und damit die Qualität eines Algorithmus führten wir in Abschnitt 3.3.1 als abstraktes
Kostenmaß die Zeit- und Speicherplatzkomplexität ein. Eine Einteilung der Algorithmen in
die wichtigsten Vertreter Sortierverfahren, Suchalgorithmen, rekursive Algorithmen, Graphenalgorithmen und numerische Algorithmen nahmen wir in Abschnitt 3.3.2 vor.
Datenverarbeitung in der Konstruktion (DiK)
Grundlagen der Datenverarbeitung: Datenstrukturen und Algorithmen
67
3.5 Literatur
[AhHU-74]
A. V. Aho, J. E. Hopcroft, J. D. Ullman: The Design and Analysis of Computer Algorithms. Addison-Wesley, Reading, Mass., 1974
[BeHW-83]
T. Beth, P. Heß, K.Wirl: Kryptographie. Teubner-Verlag, Stuttgart, 1983
[Biae-89]
C. Biaesch-Wiebke: CD-Player und R-DAT-Recorder. Vogel, Würzburg, 1989
[BoMo-77]
R. S. Boyer, J. S. Moore: A fast string searching algorithm.
In: Communcations of the ACM, Bd. 20, Nr. 10, 1977
[DaDH-72]
O.-J. Dahl, E. W. Dijkstra, C. A. R. Hoare: Structured Programming. Academic
Press, London, New York, 1972
[EnRe-88]
G. Engeln-Müllges, F. Reutter: Formelsammlung zur Numerischen Mathematik mit
MODULA-2-Programmen. BI-Wissenschaftsverlag, Mannheim, Wien, Zürich, 1988
[Futs-89]
G. Futschek: Programmentwicklung und Verifikation. Springer-Verlag, New York,
Berlin, Heidelberg, 1989
[Grie-81]
D. Gries: The Science of Programming. Springer-Verlag, New York, Berlin, Heidelberg, 1981
[GrKP-88]
R. L. Graham, D. E. Knuth, O. Patashnik: Concrete Mathematics. Addison-Wesley,
Reading, Mass., 1988
[HoLa-92]
J. Hoschek, D. Lasser: Grundlagen der geometrischen Datenverarbeitung. TeubnerVerlag, Stuttgart, 1992
[HuSu-91]
A. Hume, D. Sunday: Fast string searching. In: Software - Practice and Experience,
Bd. 21, Nr. 11, 1991
[Jung-90]
D. Jungnickel: Graphen, Netzwerke und Algorithmen. BI-Wissenschaftsverlag,
Mannheim, Wien, Zürich, 1990
[KKUW-83]
E. Kaucher, R. Klatte, Chr. Ullrich, J. Wolf v. Gudenberg: Programmiersprachen im
Griff, Bd. 4: ADA. BI-Wissenschaftsverlag, Mannheim, Wien, Zürich, 1983
[KnMP-77]
D. E. Knuth, J. H. Morris, V. R. Pratt: Fast pattern matching in strings. In: SIAM
Journal of Computing, Bd. 6, Nr. 2, 1977
[Knut-73]
D. E. Knuth: The Art of Computer Programming, Bd. 1: Fundamental Algorithms.
Addison-Wesley, Reading, Mass., 1973
[Loud-93]
K.C. Louden: Programming Languages -- Principles and Practice. PWS Publishing
Company, Boston, 1993
[Mehl-88]
K. Mehlhorn: Datenstrukturen und effiziente Algorithmen, Bd1: Sortieren und Suchen. Teubner, Stuttgart, 1988
[Nguy-99]
Thanh Hai, Nguyen. Erkenntnistheoretische und begriffliche Grundlagen der
objektorientierten Datenmodellierung, Institut für Informatik, Uni Leipzig, 1999
[Paul-78]
W.J. Paul: Komplexitätstheorie. Teubner-Verlag, Stuttgart, 1978
[RBPE-91]
J. Rumbaugh, M. Blaha, W. Premerlani, F. Eddy, W. Lorensen: Object-oriented
Modeling and Design. Prentice Hall, Englewood Cliffs, New Jersey, 1991
[RiSA-78]
R. L. Rivest, A. Shamir, L. Adleman: A method for obtaining digital signatures and
public-key cryptosystems. In: Communications of the ACM, Bd. 21, Nr. 2, 1978
[Sedg-91]
R. Sedgewick: Algorithmen. Addison-Wesley, Bonn, Paris, Reading, Mass., 1991
[TzHa-91]
H. Tzschach, G. Haßlinger: Skript zur Vorlesung Codierungstheorie WS 91/92.
Technische Hochschule Darmstadt, FB Informatik, 1991
[Wirt-86]
N. Wirth: Algorithmen und Datenstrukturen in Modula-2. Teubner, Stuttgart, 1986
Datenverarbeitung in der Konstruktion (DiK)
Herunterladen