Ubungspaket 32 ¨
Einfach verkettete, sortierte Liste
Ubungsziele: ¨
1. Aufbau einer einfach verketteten, sortierten Liste 2. Traversieren von Listen
3. Vereinfachung durch ein Dummy-Element Skript:
Kapitel: 75 und insbesondere ¨ Ubungspaket 29 und 31 Semester:
Wintersemester 2021/22 Betreuer:
Thomas, Tim und Ralf Synopsis:
In diesem ¨ Ubungspaket werden wir endlich eine einfach verkettete, sor-
tierte Liste programmieren. Dabei orientieren wir uns an der in der
Vorlesung vorgestellten Wirth’schen Variante, die das Programmieren
deutlich vereinfacht. Da das Programmieren sortierter Listen erst mal
recht komplex ist, werden wir die wesentlichen Aspekte wiederholen
bzw. gezielt darauf hin arbeiten. Also, nicht verzweifeln, sondern ein-
fach probieren. . .
Teil I: Stoffwiederholung
Aufgabe 1: Allgemeine Fragen zu Listen
Erkl¨are mit eigenen Worten, was eine einfach verkettete, sortierte Liste ist.
Was versteht man unter
”Traversieren“?
Was ist der Unterschied zwischen einer linearen, sortierten Liste und einem Stack?
Aufgabe 2: Detailfragen zu Listen
Die folgenden Fragen beziehen sich immer auf eine einfach verkettete, sortierte Liste oder deren Elemente.
Wie viele Nachfolger hat jedes Element?
Wie viele Vorg¨anger hat jedes Element?
Woran erkennt man das Listenende?
Wie findet man den Listenanfang?
Wie kann man eine Liste ausgeben?
Wie kann man sie invertiert ausgeben?
Wie muss man sich das vorstellen?
Aufgabe 3: Einf¨ ugen neuer Elemente in eine Liste
Im Skript haben wir recht ausf¨uhrlich ¨uber die algorithmische Umsetzung des sortierten Einf¨ugens geschrieben. Zur Rekapitulation haben wir eine Reihe von Fragen zusammenge- tragen.
Welche vier F¨alle muss man beim Einf¨ugen neuer Elemente beachten?
1.
2.
3.
4.
In wie vielen F¨allen wird der Startzeiger ver¨andert?
Ist das programmiertechnisch gut oder schlecht?
Warum?
In den meisten F¨allen benutzt man eine Schleife, um diejenige Stelle zu finden, an der man einf¨ugen m¨ochte. Welches Programmierproblem ergibt sich dadurch?
Aufgabe 4: Die Wirth’sche Variante
Im Skript haben wir auch die Variante von Wirth diskutiert. Was ist die wesentliche Idee?
Die Wirth’sche Idee hat einige Besonderheiten und Vorteile. Hierzu folgende Fragen:
Wird der Startzeiger ver¨andert?
Wie viele F¨alle werden unterschieden?
Wof¨ur ist das Dummy Element?
Wo bleibt der
”Suchzeiger“ stehen?
Wie wird dann aber eingef¨ugt?
Welchen Algorithmus nehmen wir?
Teil II: Quiz
Aufgabe 1: Positionierung innerhalb von Listen
Nehmen wir an, wir haben die folgende Definition einer Liste sowie die folgende Funktion zur Positionierung eines Zeigers.
1 t y p e d e f s t r u c t u s e r { int i ; } D A T A ; 2
3 t y p e d e f s t r u c t _ e l e m e n t {
4 s t r u c t _ e l e m e n t * n e x t ;
5 D A T A d a t a ;
6 } ELEMENT , * EP ;
7
8 EP p o s i t i o n ( EP listp , int val )
9 {
10 w h i l e ( listp - > d a t a . i < val ) 11 l i s t p = listp - > n e x t ;
12 r e t u r n l i s t p ;
13 }
Nun gehen wir davon aus, dass wir bereits die folgende Liste (in unserem Hauptprogramm) aufgebaut haben:
start next:
i : 1
next:
i : 3
next:
i : 5
next:
i : 7
Auf welche Elemente zeigen die Zeiger start und p nach folgenden Funktionsaufrufen:
Funktionsaufruf start p Anmerkung
p = position( start, 3 ) . . . . . . . . . . . .
p = position( start, -1 ) . . . . . . . . . . . .
p = position( start->next, -1 ) . . . . . . . . . . . .
p = position( start, 6 ) . . . . . . . . . . . .
p = position( start, 7 ) . . . . . . . . . . . .
p = position( start, 8 ) . . . . . . . . . . . .
Schlussfolgerung:
Aufgabe 2: Positionierung: zweiter Versuch
Aufgrund des vorherigen Programmabsturzes bei Suchwerten, die gr¨oßer als das gr¨oßte Element der Liste waren, haben wir unser Programm wie folgt ver¨andert:
1 t y p e d e f s t r u c t u s e r { int i ; } D A T A ; 2
3 t y p e d e f s t r u c t _ e l e m e n t {
4 s t r u c t _ e l e m e n t * n e x t ;
5 D A T A d a t a ;
6 } ELEMENT , * EP ;
7
8 EP p o s i t i o n ( EP listp , int val )
9 {
10 w h i l e ( listp - > next - > d a t a . i < val ) 11 l i s t p = listp - > n e x t ;
12 r e t u r n l i s t p ;
13 }
Wir betrachten wieder die gleiche Liste sowie die gleichen Funktionsaufrufe:
start next:
i : 1
next:
i : 3
next:
i : 5
next:
i : 7
Auf welche Elemente zeigen die Zeiger start und p nach folgenden Funktionsaufrufen:
Funktionsaufruf start p Anmerkung
p = position( start, 3 ) . . . . . . . . . . . .
p = position( start, -1 ) . . . . . . . . . . . .
p = position( start->next, -1 ) . . . . . . . . . . . .
p = position( start, 6 ) . . . . . . . . . . . .
p = position( start, 7 ) . . . . . . . . . . . .
p = position( start, 8 ) . . . . . . . . . . . .
Schlussfolgerung:
Aufgabe 3: Positionierung: dritter Versuch
So richtig sch¨on waren die beiden vorherigen Versuche nicht. Versuchen wir es also noch- mals, diesmal mit einem etwas modifizierten Algorithmus sowie einer modifizierten Liste.
1 t y p e d e f s t r u c t u s e r { int i ; } D A T A ; 2
3 t y p e d e f s t r u c t _ e l e m e n t {
4 s t r u c t _ e l e m e n t * n e x t ;
5 D A T A d a t a ;
6 } ELEMENT , * EP ;
7
8 EP p o s i t i o n ( EP listp , int val )
9 {
10 w h i l e ( listp - > d a t a . i < val && listp - > n e x t != 0 ) 11 l i s t p = listp - > n e x t ;
12 r e t u r n l i s t p ;
13 }
start next:
i : 1
next:
i : 5
next:
i : 7
next:
i : BIG
Auf welche Elemente zeigen die Zeiger start und p nach folgenden Funktionsaufrufen:
Funktionsaufruf start p Anmerkung
p = position( start, 3 ) . . . . . . . . . . . .
p = position( start, -1 ) . . . . . . . . . . . .
p = position( start, 6 ) . . . . . . . . . . . .
p = position( start, 7 ) . . . . . . . . . . . .
p = position( start, 8 ) . . . . . . . . . . . .
Schlussfolgerung:
Position von p:
Aufgabe 4: Speicherallokation
Gegeben sei folgendes Programmst¨uck:
1 t y p e d e f s t r u c t u s e r { int i ; } D A T A ; 2
3 t y p e d e f s t r u c t _ e l e m e n t {
4 s t r u c t _ e l e m e n t * n e x t ;
5 D A T A d a t a ;
6 } ELEMENT , * EP ;
7
8 EP n e w E l e m e n t ( int val , EP n e x t )
9 {
10 EP new = m a l l o c ( s i z e o f ( E L E M E N T ) ) ;
11 if ( new )
12 {
13 new - > d a t a . i = val ; new - > n e x t = n e x t ;
14 }
15 r e t u r n new ;
16 }
Vervollst¨andige das Speicherbild f¨ur die folgenden beiden Aufrufe vonnewElement(). Wie bei fast allen ¨Ubungen gehen wir davon aus, dass sowohlint-Werte als auch Zeiger immer vier Bytes im Arbeitsspeicher belegen. Ferner nehmen wir an, dass der Aufruf der Funktion newElement() die Adresse 0xFA00 liefert.
1. EP p = newElement( 4711, 0 );
Segment: . . . . Adresse Variable Wert 0xFE7C EP p:
Segment: . . . . Adresse Wert
0xFA04 0xFA00 2. ELEMENT el; EP p = newElement( 815, & el );
Segment: . . . .
Adresse Variable Wert
0xFE7C el.data.i:
0xFE78 el.next :
0xFE74 EP p :
Segment: . . . . Adresse Wert
0xFA04 0xFA00
Vielen hilft es beim Verstehen, wenn sie zus¨atzlich noch die Zeiger in die Speicherbildchen einzeichnen.
Aufgabe 5: Elemente einf¨ ugen
F¨ur die beiden letzten Quizaufgaben haben wir den Positionierungsalgorithmus aus Auf- gabe 3 um ein paar Zeilen erweitert. Der erste Parameter dieser neuen Funktion ist die bisher im Arbeitsspeicher aufgebaute Liste. Der zweite Parameter ist ein Zeiger auf ein neues Element, wie wir es gerade eben in der vorherigen Aufgabe gesehen haben:
1 v o i d i n s e r t E l e m e n t ( EP list , EP new )
2 {
3 D A T A tmp ;
4 w h i l e ( list - > d a t a . i < new - > d a t a . i && list - > n e x t != 0 ) 5 l i s t = list - > n e x t ;
6 tmp = new - > d a t a ;
7 * new = * l i s t ;
8 list - > n e x t = new ; 9 list - > d a t a = tmp ;
10 }
In den beiden folgenden Aufgaben wird die Funktion insertElement() immer mit der selben Liste aufgerufen. Sie hat nur einDatenelement.Beim ersten Mal wird das Datenele- ment5, beim zweiten Mal 13 eingef¨ugt. Der in den Abbildungen verwendete start-Zeiger ist der im Hauptprogramm verwaltete Startzeiger der Liste. Er dient nur zur Illustrierung und wird nicht weiter verwendet. Bearbeite nun die beiden F¨alle:
1. Einf¨ugen des
”Datenelementes“ 5: Zeile 3:
next:
i : 9
next:
i : BIG next:
i : 5
new start
Ende Zeile 9:
next:
i :
next:
i :
next:
i :
new start list
2. Einf¨ugen des
"Datenelementes\ 13: Zeile 3:
next:
i : 9
next:
i : BIG next:
i : 13 new
start
Ende Zeile 9:
next:
i :
next:
i :
next:
i :
new start list
Damit w¨aren wir jetzt f¨ur den Anwendungsfall ger¨ustet.
Teil III: Fehlersuche
Aufgabe 1: Ausgabe von Listen
Die meisten Fehler- und Problemf¨alle haben wir bereits besprochen. Doch Dr. L. Ist- Wirth hat das Ausgeben Wirth’scher Listen noch nicht im Griff. Was lief hier schief. . . ?
1 v o i d p r t L i s t ( EP p )
2 {
3 p r i n t f ( " L i s t e : " ) ;
4 do {
5 p = p - > n e x t ;
6 p r i n t f ( " % i " , p - > d a t a . i ) ;
7 } w h i l e ( p != 0 ) ;
8 p r i n t f ( " \ n " ) ;
9 }
Teil IV: Anwendungen
Aufgabe 1: Definition einer geeigneten Datenstruktur
1. Aufgabenstellung
Definiere eine Datenstruktur f¨ur lineare Listen, in der jedes Element ein einzelnes Zeichen aufnehmen kann.
2. Kodierung
Aufgabe 2: Allokation eines neuen Elementes
1. Aufgabenstellung
Entwickle eine Funktion, die dynamisch ein neues Listenelement generiert.
2. Entwurf
Deklaration: neues Element alloziieren:
3. Kodierung
Aufgabe 3: Sortiertes Einf¨ ugen eines neuen Elementes
1. Aufgabenstellung
Nun wird’s ernst. Entwickle eine Funktion, die ein neues Element an der richtigen Stelle in die Liste einf¨ugt. Wir gehen wieder davon aus, dass die Elemente der Liste aufsteigend sortiert werden sollen.
2. Entwurf
Vervollst¨andige zun¨achst die Funktionsdeklaration:
Neues Element einf¨ugen:
3. Kodierung
Aufgabe 4: Ausgabe der Liste
1. Aufgabenstellung
Entwickle eine Funktion, die nacheinander alle Elemente einer ¨ubergebenen Liste ausgibt.
2. Entwurf
Vervollst¨andige zun¨achst die Funktionsdeklaration:
Liste ausgeben:
3. Kodierung
Aufgabe 5: Initialisierung der Liste
1. Aufgabenstellung
Jetzt fehlt noch die richtige Initialisierung einer neuen Liste. Im Skript haben wir gesehen, dass der konkrete Datenwert des Dummys unerheblich ist. Wichtig ist hin- gegen, dass der Next-Zeiger ein Null-Zeiger ist. Dies l¨asst sich einfach realisieren.
2. Kodierung
Aufgabe 6: Ein Hauptprogramm zum Testen
Den bereits erlernten argc/argv Mechanismus (siehe auch ¨Ubungspaket 25) k¨onnen wir hier sehr gut zum intelligenten Testen unseres Listen-Programms verwenden. Wenn wir auf diesen Mechanismus zur¨uckgreifen, brauchen wir nicht alles fest im Hauptprogramm zu kodieren und k¨onnen leicht und umfangreich testen.
1. Aufgabenstellung
Entwickle ein Hauptprogramm, mit dessen Hilfe wir unsere Listen-Funktionen in geeigneter Art und Weise testen k¨onnen. Mit Hilfe desargc/argv-Mechanismus soll das Hauptprogramm die folgenden Funktionalit¨aten anbieten:
1. Egal, wie wir unser Programm aufrufen, soll es die Liste direkt nach der Initia- lisierung sowie vor dem Programmende ausgeben.
2. Das erste Argument (argv[1]) soll alle Zeichen enthalten, die wir in die Liste aufnehmen. Beispiel: ./myListe vacation soll am Ende zu folgender Ausgabe f¨uhren: a a c i n o t v
3. Sollte noch ein zweites Argument (argv[2]) angegeben werden, dann soll die Liste nach jedem Einf¨ugen ausgegeben werden.
Mittels der bisherigen Vor¨ubungen, insbesondere ¨Ubungspaket 25, sollte die Umset- zung keine gr¨oßeren Schwierigkeiten bereiten.
2. Kodierung
Endlich fertig :-)
Aufgabe 7: Doppelt verkettete Listen
In dieser letzten ¨Ubungsaufgabe geht es um doppelt verkettete Listen. Die Bearbeitung ist vor allem theoretischer Natur, weshalb das Implementieren und Testen am Rechner freiwillig sind.
Wohin zeigen die Zeiger bei doppelt verketteten Listen?
Definiere eine Datenstruktur f¨ur ein Element einer doppelt verketteten Liste, die ein int und zwei double Variablen aufnehmen kann.
Skizziere an einem Beispiel, welche Zeiger beim Einf¨ugen eines neuen Elementes in eine bereits vorhandene Liste in welcher Form umgeh¨angt werden m¨ussen.