Binäre Bäume
Wiederholung Listen
Überall in der Informatik treten Baumstrukturen auf, etwa als Verzeichnisbäume eines Betriebssystems. Baumstrukturen können entweder als Erweiterung von Listenstrukturen aufgefasst werden oder sie können mit Hilfe von Listen aufgebaut werden.
Einfache verkettete Liste
Dabei ist ein Knoten ein RECORD der aus dem eigentlichen Listeneintrag (Wert) und einem Zeiger (next) auf den nächsten Knoten besteht. Das Ende der Liste wird durch einen Knoten bestimmt bei dem der Zeiger den Wert NIL hat. Die Liste wird zugänlgich durch einen Zeiger l, der auf den ersten Knoten zeigt. Die Listeneinträge können von beliebige Datentypen sein.
Es müssen zwei neue Datentypen definiert werden: einen Typ für Knoten und einen für Zeiger auf Knoten.
type PKnotentyp = ^Knotentyp; {Zeiger auf Knoten}
Knotentyp= record
name : string;
vorname : string;
next :PKnotentyp; {Knoten}
end;
Ist k eine Variable für einen Knoten vom Typ Knotentyp , dann bezeichnet k.wert den Wert dieses Knotens, k.next den Zeiger auf den Folgeknoten. Dabei ist es möglich, dass k.next den Wert NIL hat, wenn es sich um den letzten Knoten der Liste handelt.
Aufbau einer Liste im einfachsten Fall (schematisch).
Es werden zwei Variablen vom Zeigertyp (PKnotentyp) benötigt:
l steht für den Zeiger auf die aufzubauende Liste, merke steht für einen zwischendrin benötigten Hilfszeiger auf Knoten
Die Liste wird vom Ende her aufgebaut, indem man zunächst mit der leeren Liste beginnt und mit jedem Schritt ein neues
Element vorne an die Liste anfügt. Es werden die folgende Listen schrittweise erzeugt:
Schritte | schematisch | Erklärung | Code | |||
1. |
|
Start : l als leere Liste erzeugen. Der erste Schritt ist fertig. l repräsentiert jetzt die leere Liste |
l:=nil | |||
2. |
l ist noch immer die leere Liste, Platz für einen neuen Knoten reservieren, und die Adresse für diesen Speicherplatzin der Hilfsvariablen merke festhalten |
new(merke) | ||||
l ist noch immer die leere Liste Letztes Listenelement als Wert für den neuen Knoten zuweisen |
merke^.wert:=DS_1 | |||||
l ist noch immer die leere Liste. Das Ziel von l als next - Zeiger für den erzeugten Knoten definieren. next zeigt dorthin, wo l hinzeigt (das ist die im vorangehenden Schritt erzeugte Liste) |
merke^.next:=l | |||||
l jetzt auf den neuen Knoten zeigenlassen: „Zeiger umhängen". l und merke zeigen gegenwärtig auf den gleichen Speicherplatz. l repräsentiert jetzt die Liste |
l:=merke | |||||
3. |
l repräsentiert die Liste [DS_1 ] Platz für einen neuen Knoten reservieren und die Adresse für diesen Speicherplatz in der Hilfsvariablen merke festhalten |
new(merke) | ||||
l repräsentiert noch die Liste [DS_1 ] Nächstes Listenelement als Wert für den neuen Knoten zuweisen |
merke^.wert:=DS_2 | |||||
l repräsentiert noch die Liste [DS_1 ] Das Ziel von l als next - Zeiger für den erzeugten Knoten definieren. next zeigt dorthin, wo l hinzeigt. merke repräsentiert jetzt die Liste [DS_2 , DS_1 ] |
merke^.next:=l | |||||
Zeiger l „umhängen", so dass l die Liste [DS_2 , DS_1 ] repräsentiert. |
l:=merke |
Zugriff auf Listen – LIFO- und FIFO-Strukturen
Repräsentiert ein Zeiger l eine Liste, dann bedeutet
l^.wert das erste Element der Liste, also den Wert DS_4,
l^.next die Restliste ohne das erste Element der Liste, also die Liste [ DS_3 , DS_2 , DS_1].
Dies zeigt, dass man auf die Elemente eine Liste nur schrittweise zugreifen kann, und zwar über die beiden Operationen „erstes Element der Liste" und „Restliste ohne erstes Element". Wenn man noch eine Operation hinzu nimmt, die den Prozess zum Listenaufbau durch „vorne Anfügen eines Wertes e an die Liste l" unterstützt, dann sieht man, dass man bei einer Liste auf denjenigen Wert zuerst zugreift, der zuletzt hinzugefügt wurde.
Solche Strukturen nennt man LIFO-Strukturen als Abkürzung von „Last In, First Out". Listen sind also LIFO-Strukturen. Standardbeispiel für den Einsatz von LIFO-Strukturen ist der in der Informatik häufig gebrauchte Stapel (stack). Man denkt dabei an einen Stapel von Blättern, die man übereinander ablegt. Beim Hinzufügen eines Blattes wird dies oben auf dem Stapel abgelegt, und beim Zugreifen nimmt man das zuletzt abgelegte Blatt als erstes wieder auf.
Unter diesem Aspekt des Anfügens und Zugreifens betrachtet man auch FIFO-Strukturen, Abkürzung von „First In, First Out".Das Modell für diese Struktur ist die Warteschlange (queue), bei der immer derjenige zuerst bedient wird, der zuerst gekommen ist. Diese Struktur entspricht dem einer Liste, bei der Werte hinten angefügt und vorne entnommen werden. Die Konstruktion einer FIFO-Struktur ist etwas komplizierter als die einer Liste, da man zum Anfügen und zum Entnehmen zwei Zeiger für den Anfang und das Ende der Struktur nötig sind.Ein sehr bekanntes Beispiel für eine FIFO-Struktur ist der Tastaturpuffer eines Computers: Wenn ein Benutzer Zeichen über die Tastatur eingibt, die der Computer nicht unmittelbar verarbeiten kann, dann werden diese in eine Warteschlange gestellt, aus der der Computer die zuerst eingegebenen Zeichen auch zuerst entnimmt, wenn er wieder Zeit dafür hat.
Beispielprogramm für einfachste Listenverarbeitung
Typendeklaration
Liste einlesen
Liste ausgeben (ohne die Liste zu „verlieren")
Liste löschen (Speicherplatz freigeben)
PROGRAM liste_2; USES crt; TYPE PKnotentyp = ^Knotentyp; Knotentyp = RECORD wert:longint; next:PKnotentyp; END; VAR liste:PKnotentyp;
{-------------------------------------------------------------------}
PROCEDURE liste_einlesen (VAR l : PKnotentyp);
VAR merke : PKnotentyp;
zahl : longint;
BEGIN l:=nil; REPEAT WRITE('Zahl : ');READLN(zahl); IF zahl<>0 THEN BEGIN new(merke); merke^.wert:=zahl; merke^.next:=l; l:=merke; END; UNTIL zahl=0; END;
{-------------------------------------------------------------------}
PROCEDURE liste_ausgeben(l : PKnotentyp); { Zerstoert die Liste l im Hauptprogramm nicht, da l kein Var-Parameter ist }
BEGIN WHILE l<>nil DO BEGIN WRITELN(l^.wert); l:=l^.next; END; END;
{-------------------------------------------------------------------}
PROCEDURE liste_loeschen(l :PKnotentyp); VAR merke:PKnotentyp;
BEGIN
WHILE l<>nil DO
BEGIN
merke:=l^.next;
dispose(l);
l:=merke;
END;
END;
{------------------------ HAUPTPROGRAMM -----------------------------}
BEGIN
clrscr;
WRITELN(sizeOf(Knotentyp));
WRITELN('Heap : ',MemAvail);
liste_einlesen(liste);
liste_ausgeben(liste);
WRITELN('Heap : ',MemAvail);
liste_loeschen(liste);
WRITELN('Jetzt Liste wieder geloescht : ');
WRITELN('Heap : ',MemAvail);
READLN;
END.
Mit "dispose" kann der Speicherplatz (und der zugehörige Datensatz) wieder dem System zur Verfügung gestellt werden, auf den der Zeiger <zeigervariable> zeigt. Durch die Verwendung von "dispose" bekommt der Heap aber "Löcher". Diese Löcher werden durch erneute Anwendung von "new" wieder aufgefüllt, bevor neuer Platz im HEAP angefordert wird.
sizeOf
Die Funktion SizeOf(variable) /
SizeOf(datentyp) ist allgemein verwendbar und liefert die Größe einer Variablen oder eines Daten-typs.
Beispiel: Write(SizeOf(s1^));
In Verbindung mit dynamischen Variablen ist es wichtig zu wissen, dass ab Turbo-Pascal Version 6.0 der belegte Speicher im Heap-Speicher immer auf volle 8-Byte-Gruppen aufgefüllt wird. In der Regel wird also mehr Speicher reserviert,
als tatsächlich benötigt wird. Besteht z.B. ein dynamischer Rekord aus zwei Real-Feldern (je 6 Byte), so liefert SizeOf 12, reserviert werden aber 16 Byte im Heap-Speicher. Auf diesen Umstand ist bei der Ermittlung der maximalen Anzahl der
Daten im Heap durch eigene programmtechnische Maßnahmen einzugehen, da SizeOf alleine im allgemeinen ein falsches Ergebnis liefert.
Weiter
Binäre Bäume