- Lernen
- Informatik & Informationswissenschaften
- Programmierung
- C-Programmierung – Zeiger, Speicherverwaltung und sicheres C
- Verkettete Datenstrukturen in C
Strukturen und verkettete Datenstrukturen in C
typedef zusammenzufassen, Wertemengen mit Aufzählungstypen zu benennen und alternative Daten in Unions zu speichern. Baue anschließend die klassischen verketteten Datenstrukturen aus Knoten auf, die mit malloc allokiert werden: verkettete Listen, Stapel, Warteschlangen, binäre Suchbäume und Hashtabellen. Bei jeder Struktur fügst du Elemente ein, suchst und entfernst sie und durchläufst die Struktur. Du behandelst leere Strukturen und Randfälle und gibst jeden Knoten ohne Speicherlecks frei.AnfängerEinheit 6 von 8 in Kurs: C-Programmierung – Zeiger, Speicherverwaltung und sicheres C
Was du lernst
- Definiere Strukturtypen, gib ihnen mit
typedefkurze Namen, greife über Variablen und Zeiger auf ihre Komponenten zu und speichere viele Datensätze in Arrays von Strukturen.- Eine Struktur definieren und mit ihren Komponenten arbeitenDeklariere einen Strukturtyp, erstelle und initialisiere Strukturvariablen, lies und ändere Komponenten und verstehe, wie Kopieren, Vergleichen und die Speicheranordnung funktionieren.
- Typen mit typedef benennen und Strukturen an Funktionen übergebenErstelle mit
typedefTypaliasnamen, übergib Strukturen als Wert oder über Zeiger, verwende den Pfeiloperator und reserviere Speicher für Strukturen auf dem Heap. - Datensätze in Arrays von Strukturen speichern und verarbeitenSpeichere viele Datensätze in einem Array von Strukturen, durchlaufe das Array, suche und sortiere Datensätze, vergrößere das Array auf dem Heap und verarbeite Strukturen, die weiteren Speicher besitzen.
- Eine Struktur definieren und mit ihren Komponenten arbeiten
- Ersetze magische Zahlen durch Aufzählungstypen und verwende Unions und diskriminierte Unions, um einen von mehreren Wertetypen im selben Speicherbereich abzulegen.
- Aufzählungstypen für benannte Wertemengen verwendenDefiniere Aufzählungstypen, lege ihre Werte fest, verwende sie in Bedingungen und
switch-Anweisungen und wisse, was C dabei prüft und was nicht. - Alternative Daten mit Unions und diskriminierten Unions im selben Speicherbereich ablegenDeklariere Unions, verstehe das Lesen und Schreiben ihrer Komponenten, kombiniere eine Union mit einer Kennzeichnung vom Aufzählungstyp, um Werte verschiedener Arten sicher zu speichern, und fasse kleine Flags in Bitfeldern zusammen.
- Aufzählungstypen für benannte Wertemengen verwenden
- Verknüpfe dynamisch allokierte Knoten zu Listen, durchlaufe sie, füge Knoten an beliebiger Stelle ein und lösche sie, gib alle Knoten frei und entscheide, wann eine Liste für eine Aufgabe besser geeignet ist als ein Array.
- Eine einfach verkettete Liste erstellen, durchlaufen und freigebenDefiniere einen Knotentyp, füge Knoten am Anfang und Ende einer Liste ein, besuche jeden Knoten, schreibe Funktionen, die den Kopfzeiger ändern, und gib alle Knoten frei.
- Knoten suchen, sortiert einfügen und löschenFinde einen Wert in einer Liste, füge einen Knoten an der richtigen Stelle in eine sortierte Liste ein und kette einen Knoten aus und gib ihn frei. Behandle dabei den ersten und letzten Knoten sowie fehlende Werte.
- Listen mit Arrays vergleichen und Listen erweiternWäge die Vor- und Nachteile verketteter Listen gegenüber Arrays ab, verwalte einen Tail-Zeiger und eine Anzahl, baue doppelt verkettete Listen auf und gib Knoten frei, die weiteren Speicher besitzen.
- Eine einfach verkettete Liste erstellen, durchlaufen und freigeben
- Implementiere Stapel nach dem Prinzip „zuletzt hinein, zuerst heraus“ und Warteschlangen nach dem Prinzip „zuerst hinein, zuerst heraus“ mit Arrays und mit verketteten Knoten, sichere leere und volle Strukturen ab und wende sie auf reale Aufgaben an.
- Einen Stapel implementieren und anwendenBeschreibe die Stapeloperationen, implementiere einen Stapel mit einem Array und mit verketteten Knoten, behandle leere und volle Stapel und prüfe mit Stapeln Klammern und werte Postfixausdrücke aus.
- Eine Warteschlange mit verketteten Knoten oder einem Ringpuffer implementierenBeschreibe die Warteschlangenoperationen, implementiere eine Warteschlange mit Kopf- und Tail-Zeiger sowie mit einem Ringpuffer fester Größe und unterscheide eine leere von einer vollen Warteschlange.
- Einen Stapel implementieren und anwenden
- Stelle Binärbäume mit verketteten Knoten dar, suche, füge Schlüssel in einen binären Suchbaum ein und lösche sie, besuche die Knoten in verschiedenen Reihenfolgen und gib einen ganzen Baum frei.
- Einen Binärbaum darstellen und einen binären Suchbaum durchsuchenVerwende die Fachbegriffe für Binärbäume, formuliere die Eigenschaft eines binären Suchbaums und durchsuche einen Baum rekursiv und mit einer Schleife.
- Baumknoten einfügen, durchlaufen, löschen und freigebenFüge Schlüssel in einen binären Suchbaum ein, besuche Knoten in Inorder, Preorder und Postorder, berechne Größe und Höhe, lösche Schlüssel und gib jeden Knoten frei.
- Einen Binärbaum darstellen und einen binären Suchbaum durchsuchen
- Wandle Schlüssel mit einer Hashfunktion in Bucket-Indizes um, behandle Kollisionen mit Knotenketten und implementiere, verwende und gib eine Hashtabelle in C frei.
- Schlüssel mit einer Hashfunktion Buckets zuordnenErkläre die Aufgabe einer Hashfunktion, berechne Bucket-Indizes für Zeichenkettenschlüssel, erkenne Kollisionen und ungleichmäßige Verteilungen und entscheide anhand des Belegungsfaktors, wann eine Tabelle vergrößert werden soll.
- Eine Hashtabelle mit separater Verkettung in C implementierenBaue eine Hashtabelle aus einem Array von Buckets mit verketteten Listen auf: Schlage Schlüssel nach, füge sie ein und zähle sie, kopiere Zeichenkettenschlüssel sicher und gib jeden allokierten Speicherblock frei.
- Schlüssel mit einer Hashfunktion Buckets zuordnen
Verkettete Datenstrukturen in C einfach erklärt
1Zusammengehörige Daten in Strukturen zusammenfassen
Zusammengehörige Werte lassen sich leichter verarbeiten, wenn sie als ein Datensatz weitergegeben werden. Strukturen geben diesen Werten Namen und Typaliasnamen machen die Deklarationen leichter lesbar. Bei Funktionsaufrufen stellt sich dann eine praktische Frage: Übergibst du eine Kopie oder eine Adresse, über die das Original erreichbar ist? Arrays erweitern diese Entscheidung auf viele Datensätze. Beim Sortieren, Ändern der Größe und Kopieren von Zeigerkomponenten muss klar sein, welche Objekte noch existieren und welcher Code sie besitzt.
1.1 Strukturkomponenten, Kopien und Speicheranordnung
Eine Struktur fasst zusammengehörige Werte zu einem Datensatz zusammen. Ihre Komponenten haben Namen und können unterschiedliche Typen haben. Ein Sensormesswert benötigt zum Beispiel sowohl einen Bezeichner als auch eine Temperatur.
Einen Datensatz definieren und initialisieren
struct reading {
int id;
double temp_c;
};
struct reading r = {.temp_c = 21.5, .id = 7};
r.temp_c += 0.5;
Die Definition führt den Typ struct reading ein; reading ist sein Struktur-Tag. Das abschließende Semikolon steht nach der schließenden geschweiften Klammer. Die Definition des Typs legt kein Objekt an. Die Deklaration von r erzeugt ein Objekt und . wählt dessen Komponenten aus. Hier wird die Temperatur zu 22,0 °C. Wenn nur ein Struktur-Tag deklariert ist, enthält der Typname struct: reading r; benötigt ein separates typedef.
Ein Initialisierer wie {7, 21.5} folgt der Reihenfolge der Deklaration. Komponentenbezeichnungen wie .id = 7 erlauben es dir, Komponenten in einer anderen Reihenfolge zu benennen. In einem Initialisierer ausgelassene Komponenten erhalten Nullwerte, sodass {0} den ganzen Datensatz initialisiert. Eine lokale Deklaration struct reading r; ohne Initialisierer lässt ihre Komponenten unbestimmt, bis ihnen Werte zugewiesen werden.
Eine Komponente hat ihren eigenen Typ und ihre eigene Adresse. Um eine Temperatur mit scanf einzulesen, verwende scanf("%lf", &r.temp_c) und prüfe, ob der Rückgabewert 1 ist. Der Punkt bindet vor &, sodass die Adresse der double-Komponente übergeben wird.
Werte kopieren und vergleichen
struct reading s = r;
s.temp_c = -4.0;
int same = r.id == s.id && r.temp_c == s.temp_c;
Die Zuweisung kopiert alle Komponenten. Eine Änderung von s.temp_c lässt r.temp_c bei 22,0. Feste Arrays innerhalb von Strukturen werden ebenfalls kopiert: Eine Komponente char name[32] gehört zum Datensatz. Das funktioniert, obwohl eigenständige C-Arrays nicht mit = zugewiesen werden können. Zeigerkomponenten erfordern besondere Sorgfalt, weil der kopierte Zeiger weiterhin auf dasselbe Objekt zeigt.
C definiert == nicht für ganze Strukturen. Vergleiche die Komponenten, die für deine Aufgabe Gleichheit bestimmen. memcmp vergleicht rohe Bytes einschließlich Füllbytes und kann einen Unterschied zwischen Datensätzen melden, deren Komponentenwerte gleich sind.
Die Speicheranordnung nachvollziehen
Komponenten stehen in der Reihenfolge ihrer Deklaration, ohne Füllbytes vor der ersten Komponente. Der Compiler kann ungenutzte Bytes zwischen Komponenten und am Ende hinzufügen, um die Anforderungen an die Speicherausrichtung zu erfüllen. Auf einer gängigen 64-Bit-Plattform mit einem int von 4 Byte und einer Ausrichtung von 4 Byte belegt struct { char c; int i; char d; } 12 Byte. Seine Komponenten beginnen bei den Offsets 0, 4 und 8. Eine Umordnung zu int i; char c; char d; verringert die Größe üblicherweise auf 8 Byte. Verwende sizeof für die tatsächliche Größe und offsetof aus <stddef.h> für einen Offset.
Strukturen können verschachtelt sein. Bei struct point { double x_m, y_m; }; und struct rect { struct point corner; double width_m, height_m; }; greift box.corner.x_m auf die x-Koordinate der Ecke in Metern zu. Eine Struktur kann keine vollständige Komponente ihres eigenen Typs enthalten: Das würde ein unendlich großes Objekt erfordern. Ein Zeiger auf ihren eigenen Typ hat eine feste Größe und ermöglicht verkettete Knoten.
Wichtige Erkenntnisse
- Verwende
., um eine Komponente eines Strukturobjekts auszuwählen. - Ein Initialisierer setzt ausgelassene Komponenten auf null; eine nicht initialisierte lokale Struktur hat unbestimmte Komponenten.
- Die Zuweisung einer ganzen Struktur kopiert ihre Komponenten einschließlich eingebetteter Arrays.
- Vergleiche Komponentenwerte und verwende
sizeof, um Füllbytes einzubeziehen. - Eine Struktur kann einen Zeiger auf ihren eigenen Typ enthalten.
1.2 Typnamen, Funktionsaufrufe und Strukturzeiger
Ein typedef gibt einem bestehenden Typ einen weiteren Namen. Es ändert, wie du den Typ schreibst, nicht aber, wie sich seine Objekte verhalten. typedef double meters; macht meters zu einem weiteren Namen für double.
Eine Struktur benennen und als Wert übergeben
typedef struct {
int id;
double temp_c;
} reading;
reading make_reading(int id, double t) {
reading r = {id, t};
return r;
}
void warm_copy(reading r) {
r.temp_c += 1.0;
}
Du kannst nun reading r = make_reading(7, 21.5); deklarieren. Die Rückgabe einer Struktur gibt ihren Wert zurück. Der Aufruf von warm_copy(r) gibt der Funktion eine Kopie, sodass die Temperatur des Aufrufers bei 21,5 °C bleibt. Eine Kopie eines kleinen Datensatzes ist oft die klarste Schnittstelle, wenn das Objekt des Aufrufers unverändert bleiben soll.
Ein selbstreferenzieller Typ benötigt sein Struktur-Tag innerhalb der Definition:
typedef struct node {
int number;
struct node *next;
} node;
Der Aliasname node ist nach der Deklaration verfügbar. Innerhalb dieser geschweiften Klammern bezeichnet struct node bereits den unvollständigen Typ und kann für einen Zeiger verwendet werden. Das Struktur-Tag und der Aliasname können gleich geschrieben werden, weil Tags einen eigenen Namensraum verwenden.
Über einen Zeiger auf das Objekt des Aufrufers zugreifen
void warm(reading *p) {
p->temp_c += 1.0;
}
reading r = {7, 21.5};
reading *p = &r;
p->temp_c -= 1.5;
warm(&r);
Die Subtraktion ändert r.temp_c auf 20,0; warm ändert den Wert anschließend auf 21,0. Die Funktion erhält eine Kopie der Adresse und beide Zeiger greifen weiterhin auf denselben Datensatz zu. Verwende -> mit einem Zeiger und . mit einem Strukturobjekt. p->temp_c bedeutet (*p).temp_c. Die Klammern sind nötig, weil der Komponentenzugriff stärker bindet als die Dereferenzierung: *p.temp_c versucht, eine Komponente von p auszuwählen, bevor * angewendet wird.
Für lesenden Zugriff ohne Strukturkopie nimm const reading *p entgegen. Eine Zuweisung an p->temp_c über diesen Zeiger wird zurückgewiesen. Die Zeigervariable kann sich weiterhin ändern, zum Beispiel wenn sie durch ein Array läuft. Dagegen macht reading *const p den Zeiger selbst unveränderlich. Bei Strukturen mit Zeigerkomponenten macht ein Zeiger auf konstante Daten separat allokierte Daten, auf die eine Komponente zeigt, nicht schreibgeschützt.
Einem Datensatz auf dem Heap einen Besitzer zuweisen
Binde <stdlib.h> für die Speicherallokation und <stdbool.h> für dieses Erfolgsergebnis ein:
bool make_heap_reading(reading **out, int id, double t) {
reading *p = malloc(sizeof *p);
if (p == NULL) return false;
*p = (reading){.id = id, .temp_c = t};
*out = p;
return true;
}
Die Funktion schreibt *out erst nach erfolgreicher Speicherallokation. Der Aufrufer kann reading *item = NULL; deklarieren, make_heap_reading(&item, 8, -2.0) aufrufen, ein falsches Ergebnis behandeln und später free(item) aufrufen. sizeof *p bestimmt die Größe des Typs, auf den gezeigt wird, noch bevor p auf einen initialisierten Datensatz zeigt; dieser Ausdruck liest den Datensatz nicht.
Ein Zeiger auf eine automatische lokale Struktur wird beim Zurückkehren der Funktion zu einem hängenden Zeiger. Gib den Strukturwert oder einen allokierten Speicherblock auf dem Heap mit einem klaren Besitzer zurück. Die Rückgabe von &r aus einer Funktion mit der lokalen Variablen r kann deren Lebensdauer nicht verlängern.
Wichtige Erkenntnisse
typedeferzeugt einen Aliasnamen, und eine selbstreferenzielle Definition verwendet ihr Struktur-Tag.- Übergib eine Struktur als Wert für eine Kopie oder übergib ihre Adresse, um das Original zu ändern.
p->memberist gleichbedeutend mit(*p).member.- Ein Zeiger auf konstante Daten verhindert Änderungen an Komponenten über diesen Zeiger.
- Prüfe die Speicherallokation vor der Initialisierung und gib jedem Datensatz auf dem Heap einen Besitzer.
- Eine lokale Struktur als Wert zurückzugeben ist zulässig; die Rückgabe ihrer Adresse hinterlässt einen hängenden Zeiger.
1.3 Arrays von Datensätzen und Zeichenketten in ihrem Besitz
Ein Array von Strukturen hält Datensätze nebeneinander. Verwende einen Index, um einen Datensatz auszuwählen, und anschließend einen Punkt, um eine Komponente auszuwählen. So funktionieren Durchlaufen, Suchen und Sortieren ähnlich wie bei einem Array von Zahlen.
Datensätze mit einer ausdrücklichen Anzahl durchlaufen
reading readings[4] = {
{1, 21.5}, {2, -3.0}, {3, 18.25}, {4, 30.0}
};
size_t n = sizeof readings / sizeof readings[0];
size_t best = 0;
for (size_t i = 1; i < n; ++i) {
if (readings[i].temp_c > readings[best].temp_c)
best = i;
}
Hier ist reading der Aliasname für die Struktur mit Bezeichner und Temperatur. Der Index des besten Werts wandert von 0 zu 3, sodass der wärmste Datensatz die ID 4 und die Temperatur 30,0 °C hat. Mit einem vorhandenen Element zu beginnen funktioniert auch, wenn jede Temperatur negativ ist; ein vermeintliches Maximum von null als Ausgangswert kann fehlschlagen. Dieser Durchlauf benötigt mindestens ein Element. Eine wiederverwendbare Suche muss ein leeres Array melden, bevor sie Element 0 liest.
Die Berechnung der Anzahl mit sizeof funktioniert dort, wo readings ein Array ist. Bei einem Funktionsparameter wird eine Arraydeklaration zu einem Zeiger, übergib daher n separat. (items + i)->temp_c und items[i].temp_c greifen auf dieselbe Komponente zu.
Eine Vergleichsfunktion für qsort bereitstellen
Binde <stdlib.h> ein. Für endliche Temperaturwerte gibt diese Vergleichsfunktion ein Vorzeichen zurück, ohne eine kleine Temperaturdifferenz in eine Ganzzahl umzuwandeln:
int cmp_temp(const void *a, const void *b) {
const reading *x = a;
const reading *y = b;
return (x->temp_c > y->temp_c)
- (x->temp_c < y->temp_c);
}
/* With the array above: */
qsort(readings, n, sizeof readings[0], cmp_temp);
Die Vergleichsfunktion erhält Adressen von Elementen. Negativ bedeutet, dass das erste vorher kommt, null bedeutet Gleichstand und positiv bedeutet danach. Die sortierte ID-Reihenfolge ist 2, 3, 1, 4. Die Rückgabe der double-Differenz schneidet eine Differenz wie −0,25 zu null ab. Das Subtrahieren von Ganzzahlschlüsseln kann überlaufen. Zuerst zu vergleichen vermeidet diese Fehler. qsort erhält die ursprüngliche Reihenfolge gleich eingestufter Elemente nicht.
Ein Array vergrößern, ohne seinen Speicher zu verlieren
Halte die Anzahl belegter Elemente und die Kapazität getrennt fest. Verwende für ein allokiertes Array mit positivem cap beim Verdoppeln der Kapazität einen temporären Zeiger. Binde <stdint.h> für SIZE_MAX ein:
if (cap > SIZE_MAX / 2 / sizeof *items) return false;
size_t new_cap = 2 * cap;
reading *tmp = realloc(items, new_cap * sizeof *items);
if (tmp == NULL) return false;
items = tmp;
cap = new_cap;
Diese Anweisungen gehören in eine Funktion, die bool zurückgibt; items ist ihr Arrayzeiger und cap ihre positive Kapazität. Bei einem Fehlschlag bleibt der alte Speicherblock verfügbar. Bei Erfolg kann der Speicherblock verschoben werden, sodass alte Zeiger auf seine Elemente ersetzt werden müssen. Eine Anfangskapazität von null benötigt eine eigene erste Speicherallokation.
Entscheiden, was eine Kopie besitzt
Bei typedef struct { char *name; int age; } person; kopiert person b = a; die Adresse in name. Beide Datensätze greifen auf dieselbe Zeichenkette zu. Würden beide als Besitzer die Zeichenkette freigeben, würde derselbe Speicherblock zweimal freigegeben. Für eine unabhängige Kopie reserviere strlen(a.name) + 1 Byte, prüfe den Erfolg und verwende strcpy aus <string.h>. Eine feste Komponente char name[32] wird mit der Struktur kopiert. strdup übernimmt Speicherallokation und Kopieren, gehört aber zu POSIX und C23, nicht zur C17-Standardbibliothek.
Wichtige Erkenntnisse
- Verwende
items[i].memberund übergib eine Anzahl an Funktionen, die Arrays entgegennehmen. - Initialisiere eine Maximumsuche mit einem vorhandenen Element, nachdem du auf ein leeres Array geprüft hast.
- Eine
qsort-Vergleichsfunktion gibt ein Vorzeichen zurück; das Subtrahieren von Schlüsseln kann Werte abschneiden oder überlaufen. - Prüfe die Berechnung der Allokationsgröße und speichere das Ergebnis von
reallocin einem temporären Zeiger. - Eine kopierte Zeigerkomponente verweist weiterhin auf den ursprünglichen Speicherblock.
2Auswahlmöglichkeiten mit Aufzählungstypen benennen und Speicher mit Unions gemeinsam nutzen
Ein Programm benötigt oft eine Auswahl mit einem lesbaren Namen oder einen Wert, der mehrere verschiedene Formen annehmen kann. Aufzählungstypen benennen die Auswahlmöglichkeiten; Unions nutzen Speicher für Alternativen gemeinsam. Kombiniert beschreiben sie die Art jedes gespeicherten Werts. Entscheidend ist, Name und Nutzdaten aufeinander abzustimmen und zu beachten, dass C die Eingabeprüfung und die Wahl der Darstellung dem Programm überlässt.
2.1 Aufzählungswerte und geprüfte Auswahlmöglichkeiten
Ein Aufzählungstyp gibt ganzzahligen Auswahlmöglichkeiten lesbare Namen. Namen wie HIGH machen den Zweck einer Bedingung sichtbar, ohne dass sich Lesende merken müssen, wofür die Zahl 10 steht.
Die Nummerierung nachvollziehen
enum weekday { MON, TUE, WED, THU, FRI, SAT, SUN };
enum level { LOW = 1, MEDIUM, HIGH = 10, CRITICAL };
enum level alert = HIGH;
Ein ausgelassener erster Wert ist null. Spätere ausgelassene Werte sind jeweils um eins größer als der vorherige Wert. Somit ist MON gleich 0 und SUN gleich 6; die Stufen sind 1, 2, 10 und 11. Ein ausdrücklicher Wert setzt die Zählung an dieser Stelle neu an. Verschiedene Konstanten können denselben Wert haben, aber gleiche Werte können nicht als separate case-Marken im selben switch auftreten.
Ein Aliasname kann eine Deklaration verkürzen: typedef enum { RED, YELLOW, GREEN } light; erlaubt light signal = RED;. Aufzählungskonstanten teilen sich in ihrem Gültigkeitsbereich den gewöhnlichen Namensraum der Bezeichner. Zwei verschiedene Aufzählungstypen können nicht beide RED im selben Gültigkeitsbereich deklarieren. Präfixe wie LIGHT_RED und PAINT_RED halten die Namen unterscheidbar.
Jede Auswahlmöglichkeit behandeln
const char *level_name(enum level value) {
switch (value) {
case LOW: return "low";
case MEDIUM: return "medium";
case HIGH: return "high";
case CRITICAL: return "critical";
}
return "unknown";
}
Jedes return beendet seinen Fall. Wenn ein Fall stattdessen eine Aktion ausführt und dann enden soll, gib ihm ein break; wird die nächste Marke erreicht, setzt sich die Ausführung dort fort. Bei GCC aktiviert -Wall Warnungen für fehlende Fälle eines Aufzählungstyps, wenn kein default vorhanden ist; Clang meldet solche Auslassungen ebenfalls standardmäßig. Eine Warnung kann einen fehlenden Fall aufdecken, wenn du später eine Konstante hinzufügst.
Eingaben als Auswahlmenge prüfen
Der Aufzählungstyp erzwingt nicht die Zugehörigkeit zur benannten Menge. C17 erlaubt die Zuweisung einer Ganzzahl, sodass enum level x = 42; kompiliert werden kann. Eine Prüfung, ob die Zahl zwischen 1 und 11 einschließlich liegt, lässt weiterhin unbenannte Werte wie 6 zu. Prüfe die tatsächlich erlaubten Werte:
#include <stdbool.h>
bool valid_level(int value) {
return value == LOW || value == MEDIUM
|| value == HIGH || value == CRITICAL;
}
Erst nach dieser Prüfung sollte eine Eingabe zu einer Stufe werden. Für die aufeinanderfolgenden Wochentage genügt eine Wertebereichsprüfung von MON bis SUN. In einem gewöhnlichen C17-Aufzählungstyp haben die Aufzählungskonstanten den Typ int. C23 unterstützt zusätzlich einen festgelegten zugrunde liegenden Typ wie enum color : unsigned char; diese Syntax gehört nicht zu C17.
Namen ausgeben und Anzahlen verwenden
printf("%d\n", SUN) gibt „6“ aus, nicht „SUN“. Für aufeinanderfolgende Werte, die bei null beginnen, liefert ein Namensarray den Text: const char *day_names[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};. Nach der Prüfung eines Index ist day_names[SUN] gleich „Sun“. Für den Zugriff auf ein solches Array mit einem Stufenwert wie HIGH wäre eine andere Zuordnung nötig, weil die Stufenwerte Lücken haben.
Wird DAY_COUNT unmittelbar nach SUN hinzugefügt, erhält es den Wert 7, der als Schleifenschranke oder Arraygröße nützlich ist. Es zählt die Tage, ist aber selbst kein gültiger Tag. Namen von Aufzählungskonstanten folgen den Gültigkeitsregeln von C und können von Debuggern angezeigt werden; #define führt dagegen eine Textersetzung durch den Präprozessor aus.
Wichtige Erkenntnisse
- Die Nummerierung eines Aufzählungstyps beginnt bei null, sofern kein Wert angegeben wird, und läuft nach jedem ausdrücklichen Wert weiter.
- Eine Variable eines Aufzählungstyps kann einen unbenannten Wert enthalten, prüfe daher externe Eingaben.
- Verwende
breakoderreturn, um einen Fall zu beenden, wenn ein Fall-through unerwünscht ist. - Ein Array zur Zuordnung von Indizes zu Namen benötigt geprüfte, aufeinanderfolgende Indizes, die bei null beginnen.
2.2 Unions mit passender Kennzeichnung
Eine Union gibt mehreren Komponenten denselben Speicherbereich. Sie eignet sich für einen Wert, der eine von mehreren Arten haben kann. Eine Struktur gibt jeder Komponente einen eigenen Speicherbereich und eignet sich für einen Datensatz, der alle diese Werte zusammen benötigt.
Eine Alternative speichern
union value {
int i;
double d;
char c;
};
union value v = {.d = 2.5};
Alle Komponenten beginnen an derselben Adresse. Die Union muss ihre größte Komponente aufnehmen und die Anforderungen an die Speicherausrichtung erfüllen; mit einem double von 8 Byte belegt diese Union auf gängigen Plattformen 8 Byte. Ein einfacher Initialisierer {42} initialisiert ihre erste Komponente, i; .d = 2.5 wählt d ausdrücklich aus.
Das Schreiben von v.i = 42 nach dem Initialisierer ersetzt die gespeicherte Alternative. Das anschließende Lesen von v.d wandelt 42 nicht in einen double um: Es interpretiert die gespeicherten Bytes anhand einer anderen Darstellung. Diese Darstellung kann ungültig sein. Lies die Komponente, die zu dem gespeicherten Wert gehört.
Festhalten, welche Komponente gültig ist
Kombiniere die Union mit einem Aufzählungstyp. Der Aufzählungswert ist die Kennzeichnung und die Union enthält die Nutzdaten:
enum kind { K_INT, K_DOUBLE, K_CHAR };
struct tagged {
enum kind kind;
union value as;
};
struct tagged make_double(double d) {
return (struct tagged){.kind = K_DOUBLE, .as = {.d = d}};
}
Eine Hilfsfunktion setzt beide Teile gemeinsam. Schreibender Code hält die beiden Felder aufeinander abgestimmt. Wenn Code t.as.i schreibt, aber t.kind auf K_DOUBLE belässt, wählt später lesender Code die falsche Darstellung.
#include <stdio.h>
void print_cell(struct tagged t) {
switch (t.kind) {
case K_INT: printf("%d", t.as.i); break;
case K_DOUBLE: printf("%.1f", t.as.d); break;
case K_CHAR: printf("%c", t.as.c); break;
}
}
Wird diese Funktion für Zellen mit 42, 2,5 und 'x' aufgerufen und werden zwischen den Aufrufen Leerzeichen ausgegeben, entsteht „42 2.5 x“. Jeder Fall wählt sowohl die passende Komponente als auch das passende Format. Ein Token in einem Taschenrechner kann ebenso entweder eine Zahl oder einen Operator enthalten.
Auf gängigen 64-Bit-Plattformen belegt struct tagged mit einem Aufzählungswert von 4 Byte, Füllbytes und dieser Union von 8 Byte insgesamt 16 Byte. Eine Struktur, die den Aufzählungswert und alle drei Alternativen getrennt speichert, benötigt üblicherweise 24 Byte. Dies sind Beispiele für die Speicheranordnung; frage die tatsächlichen Typgrößen mit sizeof ab.
Kleine Flags in Bitfeldern zusammenfassen
struct flags {
unsigned int ready : 1;
unsigned int mode : 3;
};
Die Breite steht nach dem Doppelpunkt. Ein vorzeichenloses Feld mit 1 Bit enthält 0 oder 1; ein Feld mit 3 Bit enthält 0 bis 7. Wenn du 9 in mode speicherst, bleiben die drei niederwertigen Bits erhalten und 1 wird gespeichert. Dabei gehen Informationen verloren. Prüfe daher einen Modus vor dem Speichern, wenn größere Werte einen Fehler anzeigen.
Ein Bitfeld hat keine separat adressierbare Stelle, daher ist &flags.mode für eine Variable namens flags ungültig. Seine Allokationsreihenfolge und Anordnung hängen von der Implementierung ab. Verwende eine Bitfelddeklaration nicht als portabel gültige Zusage für eine exakte Datei- oder Geräteanordnung. Eine angegebene Breite legt den Wertebereich des Feldes fest, nicht die Anordnung jedes umgebenden Bytes.
Wichtige Erkenntnisse
- Komponenten einer Union überlappen sich, während Strukturkomponenten getrennte Speicherbereiche haben.
- Eine Kennzeichnung muss zu der Union-Komponente passen, die den gespeicherten Wert enthält.
- Ein
switchwählt die passende Komponente und das passende Ausgabeformat. - Die Breite vorzeichenloser Bitfelder begrenzt die gespeicherten Werte; überschüssige höherwertige Bits gehen verloren.
- Die Anordnung von Bitfeldern ist implementierungsdefiniert und die Felder haben keine eigene Adresse.
3Verkettete Listen aufbauen und verwalten
Verkettete Strukturen wachsen, indem separat allokierte Speicherblöcke verbunden werden. Eine Liste macht diese Verbindungen sichtbar: Ein Zeiger beginnt die Kette und jeder Knoten führt zum nächsten. Besonders wichtig wird es, wenn sich Verweise ändern, vor allem am ersten oder letzten Knoten. Mit dem richtigen eingehenden Verweis kann dieselbe Operation mehrere Positionen behandeln, während Tail-Zeiger und Verweise in beide Richtungen verändern, für welche Operationen eine Suche nötig ist.
3.1 Eine Liste erstellen und jeden Knoten erreichbar halten
Eine einfach verkettete Liste speichert jedes Element in einem separat allokierten Knoten. Jeder Knoten enthält seinen Wert und die Adresse des nächsten Knotens. Der Kopfzeiger zeigt auf den ersten Knoten; NULL kennzeichnet sowohl eine leere Liste als auch das Ende einer Kette.
Den Verweis definieren und am Anfang einfügen
#include <stdbool.h>
#include <stdlib.h>
typedef struct node {
int number;
struct node *next;
} node;
bool prepend(node **head, int value) {
node *n = malloc(sizeof *n);
if (n == NULL) return false;
n->number = value;
n->next = *head;
*head = n;
return true;
}
Beginne mit node *list = NULL;. Rufe prepend(&list, value) auf und behandle ein falsches Ergebnis. Die Funktion nimmt die Adresse des Kopfzeigers des Aufrufers entgegen. *head bezeichnet daher den Zeiger, der geändert werden muss. Würde die Funktion nur node *head entgegennehmen und diesem lokalen Zeiger etwas zuweisen, bliebe der Kopfzeiger des Aufrufers unverändert.
Die Reihenfolge der letzten Zuweisungen erhält die alte Liste. Verbinde zuerst den neuen Knoten mit dem alten ersten Knoten und ersetze dann den Kopfzeiger. Wenn du diese Anweisungen umkehrst, wird n->next auf n selbst gesetzt und die alte Kette geht verloren. Für die eintreffenden Werte 1, 2 und 3 ergibt das wiederholte Einfügen am Anfang erst 1, dann 2 → 1 und dann 3 → 2 → 1 → NULL. Das Einfügen am Anfang kehrt die Reihenfolge des Eintreffens um.
Durchlaufen, ohne den besitzenden Zeiger zu bewegen
#include <stdio.h>
void print_list(const node *head) {
for (const node *p = head; p != NULL; p = p->next)
printf("%d ", p->number);
}
Der Zeiger p für den Durchlauf bewegt sich, während head verfügbar bleibt. Das Beispiel gibt „3 2 1 “ mit einem abschließenden Leerzeichen aus. Bei einer leeren Liste wird der Schleifenrumpf nullmal ausgeführt. Es gibt keinen Arrayindex: Jeder Schritt liest einen Verweis, um den nächsten Knoten zu finden. Die allokierten Speicherblöcke können über den Speicher verstreut sein und das Einfügen eines Knotens bewegt vorhandene Knoten nicht.
Am Ende hinzufügen
bool append(node **head, int value) {
node *n = malloc(sizeof *n);
if (n == NULL) return false;
*n = (node){.number = value, .next = NULL};
node **link = head;
while (*link != NULL) link = &(*link)->next;
*link = n;
return true;
}
link beginnt bei der Adresse des Kopfzeigers des Aufrufers. Danach wird der Zeiger zur Adresse jeder next-Komponente weiterbewegt, bis der abschließende Nullverweis erreicht ist. Eine Zuweisung an dieser Stelle funktioniert auch für eine leere Liste. Ohne gespeicherten Tail-Zeiger muss bei jedem Anhängen zunächst der vorhandenen Kette bis zum Ende gefolgt werden. Eine Funktion kann auch einen neuen Kopfzeiger zurückgeben, sofern der Aufrufer ihn speichert und eine fehlgeschlagene Speicherallokation behandelt, ohne die alte Liste zu verlieren.
Knoten in sicherer Reihenfolge freigeben
void clear_list(node **head) {
while (*head != NULL) {
node *old = *head;
*head = old->next;
free(old);
}
}
Lies den Nachfolger, solange old noch existiert, verschiebe den Kopfzeiger und gib dann den alten Knoten frei. Nach der Schleife ist der Kopfzeiger des Aufrufers NULL. Das Lesen von old->next nach der Freigabe von old ist ein Zugriff nach der Speicherfreigabe, selbst wenn der Speicher unverändert erscheint. Ein Speicherprüfprogramm kann helfen, ungültige Lesezugriffe und vergessene Speicherblöcke aufzudecken; das Aufräumen muss auch nach dem Fehlschlag einer späteren Operation erfolgen.
Wichtige Erkenntnisse
- Eine leere Liste hat einen Nullzeiger als Kopfzeiger und der letzte Knoten hat einen Nullzeiger als nächsten Verweis.
- Reserviere Speicher für einen neuen Knoten und initialisiere ihn, bevor du die bestehende Kette änderst.
- Übergib Funktionen, die den Kopfzeiger ändern, dessen Adresse oder speichere den zurückgegebenen Kopfzeiger.
- Durchlaufe die Liste mit einem separaten Zeiger, damit sie erreichbar bleibt.
- Ohne Tail-Zeiger muss zum Anhängen die Liste bis zum Ende durchlaufen werden.
- Lies den Nachfolger, bevor du einen Knoten freigibst.
3.2 Den eingehenden Verweis beim Einfügen und Löschen ändern
Die Suche in einer Liste folgt jeweils einem Verweis. Zum Einfügen und Löschen wird mehr benötigt: die Adresse des Verweises, der geändert werden muss. Dieser Verweis kann der Kopfzeiger oder eine next-Komponente in einem vorherigen Knoten sein.
Nach einem Wert suchen
Mit dem Typ node mit den Komponenten number und next:
node *find(node *head, int value) {
for (node *p = head; p != NULL; p = p->next)
if (p->number == value) return p;
return NULL;
}
Bei einer Übereinstimmung wird die Adresse des tatsächlichen Knotens zurückgegeben. Bei einem fehlenden Wert wird NULL zurückgegeben. Um den 1.000. Knoten zu erreichen, musst du vom Kopfzeiger aus 999 Verweisen folgen, selbst wenn du seine Position im Voraus kennst.
Einen Zeiger auf den zu ändernden Verweis halten
Ein node ** kann entweder auf den Kopfzeiger oder auf das next-Feld eines Knotens zeigen. Nenne ihn link: *link ist der Knoten, der aktuell über diesen Verweis erreicht wird. Das Weitergehen mit link = &(*link)->next ändert, welchen Verweis du untersuchst; die Liste selbst wird dadurch nicht geändert.
bool insert_sorted(node **head, int value) {
node **link = head;
while (*link != NULL && (*link)->number < value)
link = &(*link)->next;
node *n = malloc(sizeof *n);
if (n == NULL) return false;
n->number = value;
n->next = *link;
*link = n;
return true;
}
Binde <stdbool.h> und <stdlib.h> ein. Die Eingabeliste muss bereits aufsteigend sortiert sein. Halte vor dem ersten Wert an, der nicht kleiner als der neue Wert ist, sodass Duplikate vor gleichen Werten eingefügt werden. Die linke Seite von && prüft auf NULL, bevor die rechte Seite den Knoten dereferenziert. Würdest du die Tests vertauschen, würde am Ende ein Nullzeiger dereferenziert.
Nach dem Einfügen von 5, 2, 8 und 4 lautet die Liste 2 → 4 → 5 → 8. Beim Einfügen von 4 in 2 → 5 → 8 bewegt sich link vom Kopfzeiger zu 2.next. Der neue Knoten zeigt auf 5 und 2.next wird auf den neuen Knoten umgelenkt. Ist das neue Element das kleinste, bezeichnet link noch den Kopfzeiger. Am Ende bezeichnet der Zeiger den abschließenden Nullverweis.
Vor der Freigabe ausketten
bool remove_first(node **head, int value) {
node **link = head;
while (*link != NULL && (*link)->number != value)
link = &(*link)->next;
if (*link == NULL) return false;
node *old = *link;
*link = old->next;
free(old);
return true;
}
Die Schleife findet den eingehenden Verweis. Indem sein Ziel als old gespeichert wird, bleibt der Knoten verfügbar, während *link auf seinen Nachfolger geändert wird. Erst danach wird der entfernte Knoten freigegeben. Das Entfernen von 5 aus 2 → 4 → 5 → 8 ergibt 2 → 4 → 8. Wird anschließend 2 entfernt, ändert sich der Kopfzeiger und es bleibt 4 → 8. Fehlt der Wert, gibt die Funktion false zurück, ohne die Liste zu ändern.
Eine Version mit zwei Zeigern verwaltet prev und cur. Ist prev null, ist der gefundene Knoten der erste und der Kopfzeiger muss geändert werden; andernfalls aktualisiere prev->next. Wird der Fall des ersten Knotens vergessen, bleibt ein Zeiger auf freigegebenen Speicher zurück. Die Version mit der Adresse des Verweises behandelt beide Fälle durch dieselbe Zuweisung.
Prüfe eine leere Eingabe, eine Liste mit einem Knoten, Übereinstimmungen am Anfang und Ende sowie einen fehlenden Wert. Bestimme bei jeder Änderung den eingehenden Verweis, den betroffenen Knoten und seinen Nachfolger, bevor du eine Zuweisung schreibst. Kein verbleibender Knoten darf unerreichbar werden und kein Verweis darf auf einen freigegebenen Knoten zeigen.
Wichtige Erkenntnisse
- Die Suche gibt einen passenden Knoten oder nach Erreichen des Endes
NULLzurück. - Ein Zeiger auf einen Verweis kann entweder den Kopfzeiger oder ein next-Feld bezeichnen.
- Beim sortierten Einfügen wird der neue Knoten mit seinem Nachfolger verknüpft, bevor der eingehende Verweis umgelenkt wird.
- Beim Löschen wird der eingehende Verweis umgelenkt, bevor der entfernte Knoten freigegeben wird.
- Teste den ersten und letzten Knoten, eine leere Liste, eine Liste mit einem Knoten und einen fehlenden Wert.
3.3 Ein Array, einen Tail-Zeiger oder Verweise in beide Richtungen wählen
Entscheidend ist, welche Operationen dein Programm ausführt. Arrays und Listen können dieselben Werte enthalten, greifen aber unterschiedlich darauf zu und verschieben sie unterschiedlich.
Die Operationen vergleichen
| Aufgabe | Array | Einfach verkettete Liste |
|---|---|---|
| Position 500 erreichen | Direkt a[500] verwenden | Vom Kopfzeiger aus Verweisen folgen |
| Nach einer bekannten Position einfügen | Nachfolgende Elemente verschieben | Nach der Speicherallokation zwei Verweise ändern |
| Alle Werte durchlaufen | Zusammenhängende Elemente lesen | Möglicherweise verstreuten Knoten folgen |
| Einen bekannten Knoten löschen | Nachfolgende Elemente verschieben | Zusätzlich seinen eingehenden Verweis ermitteln |
Bei einer Liste ist das Finden einer Position nicht kostenlos. Das Einfügen nach einem bereits bekannten Knoten erfolgt direkt, aber das Löschen benötigt außerdem den Vorgänger oder den Verweis auf den Knoten. Wenn nur die Adresse des Knotens vorliegt, muss eine einfach verkettete Liste normalerweise vom Kopfzeiger aus durchlaufen werden, um diesen eingehenden Verweis zu finden.
Auch der Speicherbedarf ist wichtig. Auf einer typischen 64-Bit-Plattform belegt ein Knoten mit einem int von 4 Byte und einem Zeiger von 8 Byte nach dem Einfügen von Füllbytes 16 Byte. Für 1.000 Ganzzahlen benötigen Knoten 16.000 Byte zuzüglich Verwaltungsdaten der Speicherallokation; ein zusammenhängendes Ganzzahlarray benötigt 4.000 Byte. Das Durchlaufen des Arrays ist normalerweise schneller, weil benachbarte Werte zusammen gespeichert sind.
Metadaten der Liste zusammenhalten
typedef struct {
node *head;
node *tail;
size_t count;
} list_t;
Initialisiere eine umschließende Struktur mit list_t list = {0};. Für den leeren Zustand gelten head == NULL, tail == NULL und count == 0. Jedes erfolgreiche Einfügen oder Löschen muss neben den Verweisen auch die Anzahl aktualisieren. Eine fehlgeschlagene Speicherallokation ändert keinen dieser Werte.
Mit einem Tail-Zeiger sind nach einer geprüften Speicherallokation zum Anhängen diese Zuweisungen nötig:
n->next = NULL;
if (list.tail == NULL) list.head = n;
else list.tail->next = n;
list.tail = n;
++list.count;
Hier ist n ein initialisierter Knoten. Beim ersten Knoten werden Kopf- und Tail-Zeiger gleich. Beim Entfernen des letzten Knotens muss der Tail-Zeiger zurückgesetzt werden; bei einer einfach verketteten Liste wird dafür der Vorgänger benötigt. Das Entfernen des einzigen Knotens setzt beide Enden auf NULL und die Anzahl auf null.
In beide Richtungen verknüpfen
typedef struct dnode {
int value;
struct dnode *prev, *next;
} dnode;
typedef struct {
dnode *head, *tail;
} dlist;
void unlink_node(dlist *list, dnode *d) {
if (d->prev != NULL) d->prev->next = d->next;
else list->head = d->next;
if (d->next != NULL) d->next->prev = d->prev;
else list->tail = d->prev;
free(d);
}
Diese Funktion erhält einen noch existierenden Knoten, der zur Liste gehört. Beide Nachbarn werden angepasst, bevor er freigegeben wird. An den Enden wird statt eines fehlenden Nachbarn der Kopf- oder Tail-Zeiger geändert. Eine doppelt verkettete Liste kann rückwärts durchlaufen werden und einen bekannten Knoten löschen, ohne nach seinem Vorgänger zu suchen. Dafür sind ein weiterer Zeiger pro Knoten und mehr zu verwaltende Verweise nötig.
Daten im Besitz vor ihrem Knoten freigeben
Ein word_node kann sowohl einen Speicherblock für den Knoten als auch über word einen Speicherblock für eine Zeichenkette besitzen. Sichere next, rufe free(n->word) und dann free(n) auf und gehe zum gesicherten Nachfolger. Wird nur der Knoten freigegeben, entsteht für die Zeichenkette ein Speicherleck. Das Lesen von n->word nach der Freigabe des Knotens ist ungültig. Der Besitz beschreibt, welcher Code jeden Speicherblock freigeben muss, selbst wenn mehrere Zeiger darauf zugreifen können.
Wichtige Erkenntnisse
- Arrays bieten direkten Zugriff über Indizes und eine kompakte zusammenhängende Speicherung.
- Listen erlauben lokale Änderungen an Verweisen, sobald die benötigte Position oder der eingehende Verweis bekannt ist.
- Ein Tail-Zeiger ermöglicht direktes Anhängen, aber jede Operation muss beide Enden korrekt verwalten.
- Doppelt verkettete Knoten speichern den Vorgänger und den Nachfolger.
- Gib Nutzdaten im Besitz vor dem Knoten frei, der ihre Adresse speichert.
- Eine gespeicherte Anzahl muss bei jedem erfolgreichen Einfügen und Löschen geändert werden.
4Stapel und Warteschlangen implementieren
Stapel und Warteschlangen beschreiben die Reihenfolge, in der Aufgaben bearbeitet werden. Ein Stapel hält das neueste unerledigte Element bereit, was sich für Verschachtelungen und die Auswertung von Ausdrücken eignet. Eine Warteschlange hält das am längsten wartende Element bereit, was zur Reihenfolge des Eintreffens passt. Beide können Arrays oder verkettete Knoten verwenden. Ihre Implementierungen müssen leere und volle Zustände ausdrücklich behandeln und nach einer Änderung jede Anzahl, jedes Ende und jeden Index korrekt halten.
4.1 Stapeloperationen und verschachtelte Aufgaben
Ein Stapel entfernt das zuletzt hinzugefügte Element zuerst: zuletzt hinein, zuerst heraus (LIFO). Seine Operationen definieren einen abstrakten Datentyp, unabhängig davon, ob ein Array oder verkettete Knoten die Werte enthalten. push fügt oben hinzu, pop entfernt das oberste Element und peek liest das oberste Element, ohne es zu entfernen.
Ein Array und eine Anzahl verwenden
#include <stdbool.h>
#define STACK_CAP 100
typedef struct {
int items[STACK_CAP];
int top;
} stack;
bool push(stack *s, int value) {
if (s->top == STACK_CAP) return false;
s->items[s->top++] = value;
return true;
}
bool pop(stack *s, int *out) {
if (s->top == 0) return false;
*out = s->items[--s->top];
return true;
}
Initialisiere mit stack s = {0};. Hier ist top die Anzahl der gespeicherten Elemente, sodass der oberste belegte Index top - 1 ist. Ein erfolgreiches Ablegen schreibt auf den nächsten freien Platz und erhöht die Anzahl. Ein erfolgreiches Entnehmen verringert zuerst die Anzahl und liest dann diesen Index. Der Aufrufer stellt einen gültigen out-Zeiger bereit und prüft das Ergebnis, bevor er dessen Wert verwendet.
Wenn du 4, 7 und 1 ablegst und dann zweimal entnimmst, erhältst du 1 und 7; 4 bleibt übrig. Ohne Kapazitätsprüfung schreibt das Ablegen auf einem vollen Stapel nach items[STACK_CAP] hinter das Ende. Ohne Prüfung auf einen leeren Stapel fällt das vorzeichenbehaftete top von 0 auf −1 und das Entnehmen liest items[-1]. Beide Zugriffe führen zu undefiniertem Verhalten.
Den ersten Listenknoten als oberstes Element verwenden
Ein verketteter Stapel legt ab, indem er nach geprüfter Speicherallokation am Anfang einfügt. Das Entnehmen entfernt den ersten Knoten:
bool pop_linked(node **top, int *out) {
if (*top == NULL) return false;
node *old = *top;
*out = old->number;
*top = old->next;
free(old);
return true;
}
node hat eine Ganzzahl number und einen next-Zeiger. Ein verketteter Stapel hat keine feste Arraykapazität, aber das Ablegen kann bei fehlgeschlagener Speicherallokation fehlschlagen. Er benötigt weiterhin Prüfungen auf einen leeren Zustand und die Freigabe der am Ende verbleibenden Knoten.
Verschachtelte Klammern zuordnen
Durchlaufe die Eingabe von links nach rechts. Lege jede öffnende runde, eckige oder geschweifte Klammer auf den Stapel. Entnimm bei jeder schließenden Klammer eine Klammer und prüfe, ob die Typen übereinstimmen. Eine schließende Klammer ohne öffnende Klammer führt sofort zum Fehlschlag. Prüfe am Ende, ob der Stapel leer ist: Übrig gebliebene öffnende Klammern bedeuten ebenfalls eine fehlende Übereinstimmung.
Der Text „( [ ] { } )“ ist ausgeglichen. Bei „( ]“ passt die schließende Klammer nicht zur entnommenen öffnenden Klammer. Bei „( ( )“ bleibt nach dem Durchlauf eine öffnende Klammer übrig. Ein einfaches Zählen öffnender und schließender Klammern würde eine falsche Verschachtelung wie „( [ ) ]“ übersehen.
Postfixausdrücke auswerten
Lege Zahlen beim Eintreffen auf den Stapel. Entnimm bei einem binären Operator zuerst den rechten, dann den linken Operanden, berechne left operator right und lege das Ergebnis ab. Bei „3 4 + 2 *“ verbindet + die 3 und 4 zu 7, dann verbindet * die 7 und 2 zu 14. Bei „9 4 -“ entnimm 4 als rechten und 9 als linken Operanden; das ergibt 5. Ihre Umkehrung ergibt −5. Weise zu wenige Operanden zurück und verlange genau ein Endergebnis; die Division benötigt außerdem einen rechten Operanden ungleich null.
Funktionsaufrufe haben stapelförmige Lebensdauern: Eine aufgerufene Funktion kehrt zurück, bevor ihr Aufrufer fortfährt. Jeder aktive Aufruf benötigt einen Stackframe, sodass sehr tiefe Rekursion den Aufrufstapel erschöpfen kann, selbst wenn Speicherallokationen auf dem Heap gelingen.
Wichtige Erkenntnisse
- LIFO bedeutet, dass das neueste Element zuerst entfernt wird.
- In der Arrayimplementierung zählt
topdie Elemente; prüfe vor dem Zugriff auf einen leeren oder vollen Stapel. - Ein verketteter Stapel legt Elemente am Anfang der Liste ab und entnimmt sie dort.
- Beim Zuordnen von Klammern müssen die Typen übereinstimmen und der Stapel muss am Ende leer sein.
- Bei einer Subtraktion oder Division in Postfixschreibweise ist der zuerst entnommene Wert der rechte Operand.
4.2 Enden einer Warteschlange und ringförmige Speicherung
Eine Warteschlange entfernt das älteste Element zuerst: zuerst hinein, zuerst heraus (FIFO). Einreihen fügt hinten hinzu; Entnehmen entfernt vorn. Nachdem A, B und C eingereiht wurden, geben die nächsten beiden Entnahmen A und B zurück. Das eignet sich für Aufgaben, die in der Reihenfolge ihres Eintreffens bearbeitet werden müssen.
Beide Enden einer verketteten Warteschlange verwalten
Mit Knoten mit den Komponenten number und next und nach dem Einbinden von <stdbool.h> und <stdlib.h>:
typedef struct { node *head, *tail; } queue;
bool enqueue(queue *q, int value) {
node *n = malloc(sizeof *n);
if (n == NULL) return false;
*n = (node){.number = value, .next = NULL};
if (q->tail == NULL) q->head = n;
else q->tail->next = n;
q->tail = n;
return true;
}
bool dequeue(queue *q, int *out) {
if (q->head == NULL) return false;
node *old = q->head;
*out = old->number;
q->head = old->next;
if (q->head == NULL) q->tail = NULL;
free(old);
return true;
}
Initialisiere mit queue q = {0};. Das Einreihen setzt den Verweis des bisherigen letzten Knotens, bevor der Tail-Zeiger verschoben wird. Beim ersten Knoten setzt es auch den Kopfzeiger. Das Entnehmen verschiebt den Kopfzeiger nach vorn. Wird dabei der einzige Knoten entfernt, muss auch der Tail-Zeiger NULL werden. Andernfalls schreibt das nächste Einreihen über einen hängenden Tail-Zeiger. Keine der beiden Operationen muss die Liste durchlaufen.
Ein festes Array als Ring wiederverwenden
Eine Warteschlange in einem einfachen Array, die immer Index 0 entnimmt, muss ihre verbleibenden Werte verschieben. Ein Ringpuffer verwaltet stattdessen einen Index für das vorderste Element und eine Anzahl der Elemente:
#define CAP 4
typedef struct {
char items[CAP];
unsigned head, count;
} ring;
bool ring_put(ring *q, char value) {
if (q->count == CAP) return false;
q->items[(q->head + q->count) % CAP] = value;
++q->count;
return true;
}
bool ring_get(ring *q, char *out) {
if (q->count == 0) return false;
*out = q->items[q->head];
q->head = (q->head + 1) % CAP;
--q->count;
return true;
}
Initialisiere einen Ring mit {0}. head bezeichnet immer das vorderste Element, wenn die Warteschlange nicht leer ist. Der nächste Einfügeindex ist der vordere Index zuzüglich der Anzahl der Elemente, wobei % CAP ihn zurücksetzt. Lies das vorderste Element, bevor du head verschiebst. Die Anzahl unterscheidet leer von voll, ohne einen Platz aufzugeben.
Das Zurücksetzen nachvollziehen
Reihe A, B und C auf den Plätzen 0, 1 und 2 ein. Entnimm A: head wird 1 und count wird 2. D kommt auf Platz 3; E kommt auf Platz 0. Das physische Array enthält nun E, B, C, D, aber die logische Entnahmereihenfolge ist B, C, D, E. Der Ring ist voll, weil count 4 ist, obwohl das vorderste Element bei Index 1 liegt.
Ein anderer Entwurf verwaltet head und den nächsten freien Index tail ohne eine Anzahl. Dabei könnten gleiche Indizes leer oder voll bedeuten, sodass häufig ein Platz ungenutzt bleibt. Leer ist head == tail; voll ist (tail + 1) % CAP == head. Vermische diese Regel nicht mit den Kapazitätsregeln der Version mit Anzahl. Warteschlangen unterstützen auch den Besuch von Bäumen Ebene für Ebene, indem sie Knoten aufnehmen, die auf ihren Besuch warten.
Wichtige Erkenntnisse
- FIFO entfernt den ältesten eingereihten Wert.
- Eine verkettete Warteschlange hängt am Ende an und entnimmt am Anfang.
- Das Entfernen des einzigen Knotens muss sowohl Kopf- als auch Tail-Zeiger zurücksetzen.
- Eine ringförmige Warteschlange setzt Indizes zurück und speichert die logische Reihenfolge unabhängig von der physischen Reihenfolge.
- Verwende eine Anzahl oder einen ungenutzten Platz, um leer und voll zu unterscheiden.
- Lies das vorderste Element, bevor du den Kopfzeiger weiterbewegst.
5Binärbäume aufbauen und durchlaufen
Bäume ersetzen eine einzelne Kette durch Verzweigungen. Ein Suchbaum nutzt die Reihenfolge der Schlüssel, um eine Verzweigung zu wählen, aber seine Form bestimmt, wie viele Knoten die Suche erreicht. Die Vorstellung von Teilbäumen hilft auch beim Einfügen, Durchlaufen, Zählen und Löschen. Jede Operation benötigt einen Fall für den leeren Baum, eine Regel für den Umgang mit dem aktuellen Knoten und eine klare Entscheidung, welcher Elternzeiger eine geänderte Teilbaumwurzel erhalten muss.
5.1 Einen Baum anhand der Ordnungsregel durchsuchen
Ein Binärbaum gibt jedem Knoten höchstens zwei Kinder, links und rechts genannt. Die Wurzel ist der oberste Knoten. Ein Blatt hat keine Kinder und ein Knoten bildet zusammen mit allen seinen Nachkommen einen Teilbaum. Ein fehlendes Kind und ein leerer Baum werden durch NULL dargestellt.
Die Verzweigungen darstellen
typedef struct tnode {
int key;
struct tnode *left;
struct tnode *right;
} tnode;
Jeder Zeiger auf ein Kind führt zur Wurzel eines anderen Baums. Dieselbe Definition beschreibt daher ein Blatt, einen großen verzweigten Baum oder eine lange Kette. Die Höhe zählt die Kanten auf dem längsten Weg von der Wurzel zu einem Blatt: Eine Wurzel allein hat die Höhe 0. Verwende −1 für einen leeren Baum, sodass die beiden leeren Teilbäume eines Blattes unmittelbar zur Höhe 0 führen. Das Zählen von Ebenen ergäbe stattdessen eins mehr.
Die Regel auf ganze Teilbäume anwenden
Ein binärer Suchbaum ordnet die Schlüssel. Für jeden Knoten sind alle Schlüssel in seinem linken Teilbaum kleiner und alle Schlüssel in seinem rechten Teilbaum größer als der Schlüssel des Knotens. Speichere jeden Schlüssel einmal. Diese Bedingung schließt entfernte Nachkommen ein. Eine Wurzel 50 mit dem linken Kind 30 und einem rechten Kind 60 unter dieser 30 ist ungültig: 60 liegt weiterhin im linken Teilbaum von 50.
Füge 50, 30, 70, 20, 40, 60 und 80 in einen leeren binären Suchbaum ein. Die Wurzel ist 50; 30 und 70 sind ihre Kinder. Die Kinder von 30 sind 20 und 40, die von 70 sind 60 und 80. Jede Wahl einer Verzweigung erhält die von den Vorfahren übernommenen Schranken.
In jedem Schritt eine Verzweigung wählen
#include <stddef.h>
tnode *search(tnode *t, int key) {
while (t != NULL) {
if (key == t->key) return t;
if (key < t->key) t = t->left;
else t = t->right;
}
return NULL;
}
Die Suche nach 60 besucht im Beispiel 50, geht nach rechts zu 70 und dann nach links zu 60: drei Knoten. Die Suche nach 65 nimmt denselben Weg und versucht dann das rechte Kind von 60. Dieses Kind ist NULL, sodass 65 fehlt. Du musst den linken Teilbaum von 50 nicht untersuchen, weil dort jeder Schlüssel kleiner als 50 ist.
Die rekursive Form behandelt zuerst t == NULL und einen passenden Schlüssel und durchsucht dann das ausgewählte Kind. Die Prüfung auf einen leeren Teilbaum ist der Basisfall. Ohne sie dereferenziert eine erfolglose Suche irgendwann einen Nullzeiger. Die Schleifenversion hält an derselben Stelle an, ohne rekursive Aufrufe hinzuzufügen.
Den Aufwand durch die Form erklären
Ein perfekt balancierter Baum mit 20 Ebenen enthält 1.048.575 Schlüssel und eine Suche besucht höchstens 20 Knoten. Wenn du stattdessen bereits sortierte Schlüssel 10, 20, 30, 40, 50 einfügst, entsteht eine Kette nach rechts. Ihre Höhe ist 4 und die Suche nach 50 besucht alle fünf Knoten. Die Ordnungsregel eines binären Suchbaums allein hält den Baum nicht niedrig. Selbstbalancierende Bäume wie AVL-Bäume ordnen Verzweigungen um, um die Höhe zu begrenzen.
Um ein Minimum zu finden, folge linken Kindern, bis der nächste linke Verweis NULL ist. Für ein Maximum folge rechten Kindern. Im Beispiel mit sieben Knoten sind diese Schlüssel 20 und 80. Dieselbe Richtungswahl funktioniert bei jeder Teilbaumwurzel, weil die Ordnungsregel auch dort gilt.
Wichtige Erkenntnisse
- Ein Knoten eines Binärbaums hat höchstens zwei Kinder; fehlende Kinder sind Nullzeiger.
- Die Ordnung eines binären Suchbaums gilt für jeden Nachkommen in jedem Teilbaum.
- Die Suche folgt bei einem kleineren Schlüssel nach links und bei einem größeren nach rechts.
- Die Höhe zählt hier Kanten; ein leerer Baum hat die Höhe −1.
- Ein niedriger Baum verringert die Anzahl besuchter Knoten; ein binärer Suchbaum kann dennoch zu einer Kette werden.
5.2 Einen Suchbaum aktualisieren, durchlaufen und freigeben
Baumfunktionen arbeiten auf einem Teilbaum und verbinden ihr Ergebnis mit dessen Elternknoten. Behandle den Fall des leeren Teilbaums ausdrücklich und unterscheide Operationen, die Verweise ändern, von solchen, die Knoten nur besuchen.
Über den Teilbaumverweis einfügen
Für tnode mit key, left und right binde <stdbool.h> und <stdlib.h> ein:
bool insert(tnode **link, int key) {
if (*link == NULL) {
tnode *n = malloc(sizeof *n);
if (n == NULL) return false;
*n = (tnode){.key = key, .left = NULL, .right = NULL};
*link = n;
return true;
}
if (key < (*link)->key) return insert(&(*link)->left, key);
if (key > (*link)->key) return insert(&(*link)->right, key);
return true;
}
Rufe die Funktion mit insert(&root, key) auf und prüfe ihr Ergebnis. Ein vorhandener Schlüssel lässt den Baum unverändert und gilt als Erfolg. Die Adresse des Verweises ermöglicht es dem Einfügen, entweder die Wurzel des Aufrufers oder einen Zeiger auf ein Kind zu aktualisieren. Bei einer Schnittstelle, die die Wurzel zurückgibt, speichere jedes Ergebnis: t->left = insert(t->left, key) und root = insert(root, key). Wird ein zurückgegebener allokierter Knoten ignoriert, wenn der ursprüngliche Teilbaum leer war, bleibt dieser Knoten unverknüpft und es entsteht ein Speicherleck.
Wählen, wann die Wurzel besucht wird
| Traversierung | Besuchsreihenfolge | Typischer Zweck |
|---|---|---|
| Inorder | Links, Wurzel, rechts | Schlüssel eines binären Suchbaums sortiert ausgeben |
| Preorder | Wurzel, links, rechts | Eltern vor Kindern besuchen |
| Postorder | Links, rechts, Wurzel | Kinder vor der Freigabe eines Elternknotens abschließen |
Beim Einfügen von 50, 30, 70, 20, 40, 60, 80 lautet die Inorder-Ausgabe „20 30 40 50 60 70 80“. Preorder ergibt „50 30 20 40 70 60 80“; Postorder ergibt „20 40 30 60 80 70 50“.
void free_tree(tnode *t) {
if (t == NULL) return;
free_tree(t->left);
free_tree(t->right);
free(t);
}
Setze nach free_tree(root) beim Besitzer root = NULL. Die Funktion gibt zuerst die Kinder frei, damit sie deren Adressen aus dem noch existierenden Elternknoten lesen kann. Die Größe ist bei einem leeren Teilbaum null, sonst eins plus die Größen beider Kinder. Die Höhe ist bei einem leeren Teilbaum −1, sonst eins plus die größere der beiden Kinderhöhen. Das Beispiel hat die Größe 7 und die Höhe 2.
Löschen, ohne die Ordnung zu verletzen
Ein Blatt wird zu NULL. Ein Knoten mit einem Kind wird durch dieses Kind ersetzt. Kopiere bei zwei Kindern den kleinsten Schlüssel aus dem rechten Teilbaum in den Knoten und lösche dann diesen Nachfolger von seiner ursprünglichen Position:
tnode *erase(tnode *t, int key) {
if (t == NULL) return NULL;
if (key < t->key) t->left = erase(t->left, key);
else if (key > t->key) t->right = erase(t->right, key);
else {
if (t->left == NULL || t->right == NULL) {
tnode *child = t->left != NULL ? t->left : t->right;
free(t);
return child;
}
tnode *s = t->right;
while (s->left != NULL) s = s->left;
t->key = s->key;
t->right = erase(t->right, s->key);
}
return t;
}
Rufe die Funktion als root = erase(root, key) auf. Das Löschen von 50 aus dem Beispiel setzt 60 in die Wurzel und entfernt den alten Knoten mit 60. Die Inorder-Ausgabe wird zu „20 30 40 60 70 80“; Preorder wird zu „60 30 20 40 70 80“. Ein Nachfolger kann ein rechtes Kind haben, daher muss seine Entfernung dieses Kind wieder verknüpfen.
Verwende für Wortschlüssel das Vorzeichen von strcmp, nicht Zeigergleichheit, und gib jedem Wort im Besitz einen eigenen Speicherblock. Gib das Wort vor seinem Knoten frei. Das Löschen von Datensätzen mit zwei Kindern benötigt auch einen Plan für den Besitz des verschobenen Wortes und eines zugehörigen Werts; würde ein besitzender Zeiger kopiert und dann das Wort seines ursprünglichen Knotens freigegeben, bliebe ein hängender Schlüssel zurück.
Wichtige Erkenntnisse
- Das Einfügen muss eine neue Teilbaumwurzel einsetzen und eine fehlgeschlagene Speicherallokation melden.
- Inorder gibt die Schlüssel eines binären Suchbaums sortiert aus; Preorder beginnt beim Elternknoten.
- Das Aufräumen in Postorder gibt beide Kinder vor ihrem Elternknoten frei.
- Die Größe addiert die Größen beider Kinder; die Höhe nimmt die größere Kinderhöhe.
- Das Löschen verknüpft null oder ein Kind neu oder ersetzt bei zwei Kindern den Schlüssel durch seinen Nachfolger.
- Speichere nach dem Löschen die zurückgegebene Wurzel, auch beim Löschen der ursprünglichen Wurzel.
- Bäume mit Zeichenkettenschlüsseln vergleichen Text und verwalten jeden Speicherblock für Zeichenketten in ihrem Besitz.
6Schlüssel in Hashtabellen speichern und nachschlagen
Hashtabellen wandeln einen Schlüssel in eine Ausgangsposition um und behandeln dort auftretende Kollisionen. Die Hashfunktion und die Gleichheitsprüfung müssen darin übereinstimmen, welche Schlüssel als gleich gelten. Eine Implementierung mit separater Verkettung verbindet Arrays, das Einfügen in Listen, Zeichenkettenkopien und das Aufräumen. Ihr Nutzen hängt davon ab, dass Schlüssel über Buckets verteilt und ihre Positionen bei einer Änderung der Bucket-Anzahl neu berechnet werden.
6.1 Schlüssel auf Buckets abbilden und Kollisionen behandeln
Eine Hashtabelle wählt anhand eines Schlüssels eine Ausgangsposition. Eine Hashfunktion erzeugt eine Zahl; ihr Rest modulo einer positiven Bucket-Anzahl ergibt einen Index. Separate Verkettung hält an jedem Bucket eine verkettete Liste, sodass die Suche die ausgewählte Kette durchsucht.
Hashberechnung und Gleichheit unterscheiden
Eine brauchbare Hashfunktion ist deterministisch, schnell zu berechnen und verteilt die erwarteten Schlüssel über die Buckets. Gleiche Schlüssel müssen gleiche Hashwerte ergeben. Verschiedene Schlüssel können dennoch denselben Index haben: Das ist eine Kollision, und jede Tabelle muss sie behandeln. Gibt es mehr mögliche Schlüssel als Buckets, sind einige Kollisionen unvermeidbar.
Für mit Buchstaben beginnende Wörter auf einem ASCII-kompatiblen System weist toupper((unsigned char)word[0]) - 'A' 26 Buchstaben-Buckets zu; „Luigi“ beginnt in Bucket 11. Viele englische Wörter beginnen mit „s“ und wenige mit „x“, sodass diese Regel sehr ungleichmäßige Ketten hinterlassen kann. Die explizite Typumwandlung ist nötig, weil Funktionen aus <ctype.h> einen Wert eines vorzeichenlosen Zeichentyps oder EOF benötigen. Prüfe den Anfangsbuchstaben und die Annahme über den Zeichensatz, bevor du diese Indexregel verwendest.
Auch eine Hashfunktion mit Zeichensummen hat Schwächen. Mit ASCII-Codes und zehn Buckets ergibt „cat“ die Summe 99 + 97 + 116 = 312 und erreicht Bucket 2; „dog“ ergibt die Summe 314 und erreicht Bucket 4. „act“ ergibt ebenfalls die Summe 312. Die Addition verliert die Buchstabenreihenfolge, sodass jede Umstellung kollidiert.
Die Zeichenreihenfolge in den Hashwert einbeziehen
unsigned long hash_word(const char *word) {
unsigned long h = 5381;
const unsigned char *p = (const unsigned char *)word;
while (*p != '\0') {
h = h * 33 + *p;
++p;
}
return h;
}
Dieses djb2-Muster verwendet vorzeichenlose Arithmetik, deren Überlauf auf definierte Weise zu einem Wraparound führt. Ein Überlauf bei vorzeichenbehafteten Ganzzahlen wäre undefiniertes Verhalten. Für die ASCII-Zeichenketten „cat“ und „act“ sind die Werte 193.488.125 und 193.486.013. Reduziere mit hash_word(word) % N, wobei N die positive Bucket-Anzahl ist. Die Buchstabenreihenfolge verändert diese Hashwerte, obwohl ihre Reduktion weiterhin eine Kollision ergeben kann.
Wenn eine Tabelle „Cat“ und „cat“ als gleich behandelt, müssen sowohl die Hashberechnung als auch der Vergleich dieselbe Regel für Groß- und Kleinschreibung verwenden. Wird die Groß- und Kleinschreibung nur beim Vergleich ignoriert, können gleiche Schlüssel in verschiedene Buckets gelangen, wo die Suche sie nicht miteinander vergleicht. Ein negativer Divisionsrest eines vorzeichenbehafteten Hashwerts eignet sich ebenfalls nicht als Arrayindex.
Eine Kollisionsstrategie wählen
Separate Verkettung speichert jede Kollision in der verketteten Liste eines Buckets. Die Suche vergleicht Schlüssel, bis sie eine Übereinstimmung findet oder das Ende erreicht. Offene Adressierung speichert Einträge im Array selbst. Beim linearen Sondieren wird bei einer Kollision der nächste Platz versucht und am Ende wieder von vorn begonnen. Die Suche folgt derselben Sondierungsfolge und kann daher mehrere Arrayplätze untersuchen.
Indizes beim Vergrößern der Tabelle neu berechnen
Der Belegungsfaktor ist die Anzahl der gespeicherten Schlüssel geteilt durch die Bucket-Anzahl. Zwölf Schlüssel in acht Buckets ergeben durchschnittlich 1,5 Einträge pro Bucket. Sechzehn Buckets verringern diesen Durchschnitt auf 0,75. Dieser Durchschnitt garantiert nicht, dass jede Kette kurz ist; eine schlechte Hashfunktion kann alle Schlüssel zusammen ablegen.
Ein Bucket-Index hängt von der Bucket-Anzahl ab. Berechne nach dem Reservieren und Initialisieren eines größeren Bucket-Arrays den Index jedes gespeicherten Schlüssels für die neue Anzahl neu und verknüpfe ihn dort. Würdest du die alten Kopfzeiger in die gleich nummerierten neuen Buckets kopieren, blieben Schlüssel an Stellen, an denen die neue Suche nicht nachschlägt. Eine Tabelle mit separater Verkettung kann mehr Einträge als Buckets enthalten; offene Adressierung benötigt ungenutzte Plätze für neue Einträge und zuverlässiges Sondieren.
Wichtige Erkenntnisse
- Reduziere einen deterministischen Hashwert modulo einer positiven Bucket-Anzahl, um einen Index zu erhalten.
- Gleiche Schlüssel benötigen gleiche Hashwerte; verschiedene Schlüssel können kollidieren.
- Vorzeichenlose Arithmetik vermeidet undefinierten Überlauf bei vorzeichenbehafteten Ganzzahlen in der Hashberechnung.
- Separate Verkettung durchläuft eine Bucket-Liste; offene Adressierung folgt einer Sondierungsfolge.
- Der Belegungsfaktor misst die Einträge pro Bucket, nicht die längste Kette.
- Berechne für jeden Schlüssel den Hashwert erneut, wenn sich die Bucket-Anzahl ändert.
6.2 Eine Tabelle zum Zählen von Wörtern erstellen, die ihre Schlüssel besitzt
Eine Tabelle mit separater Verkettung verbindet ein Array, Listenverweise, Zeichenkettenvergleich und Besitz. Jeder Eintrag besitzt seinen kopierten Schlüssel. Das Bucket-Array besitzt die Ketten und jede Kette endet bei NULL.
Buckets initialisieren und Text nachschlagen
#include <stdbool.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#define N 26
typedef struct entry {
char *key;
int count;
struct entry *next;
} entry;
static entry *table[N];
Bei statischer Speicherdauer enthält anfangs jeder Bucket einen Nullzeiger. Ein lokales Array benötigt dagegen einen Initialisierer wie entry *table[N] = {NULL};. Verwende die vorzeichenlose Funktion hash_word, die bei 5381 beginnt und jedes Byte mit dem Multiplikator 33 einbezieht.
entry *find_word(const char *key) {
size_t i = hash_word(key) % N;
for (entry *e = table[i]; e != NULL; e = e->next)
if (strcmp(e->key, key) == 0) return e;
return NULL;
}
strcmp prüft Text. e->key == key würde prüfen, ob die Zeiger dieselbe Stelle bezeichnen. Zwei getrennte Puffer können gleiche Wörter enthalten, daher ist Zeigergleichheit keine Wortsuche.
Einen vorhandenen Schlüssel zählen oder einen neuen einsetzen
bool add_word(const char *key) {
entry *e = find_word(key);
if (e != NULL) {
if (e->count == INT_MAX) return false;
++e->count;
return true;
}
e = malloc(sizeof *e);
if (e == NULL) return false;
e->key = malloc(strlen(key) + 1);
if (e->key == NULL) {
free(e);
return false;
}
strcpy(e->key, key);
e->count = 1;
size_t i = hash_word(key) % N;
e->next = table[i];
table[i] = e;
return true;
}
Der Aufrufer stellt eine nullterminierte Zeichenkette bereit. Die Prüfung auf INT_MAX meldet einen Fehlschlag, bevor die Anzahl ihren int-Speicherbereich überlaufen würde. Das zusätzliche Byte im Speicherblock der Zeichenkette enthält '\0'. Wenn die zweite Speicherallokation fehlschlägt, gib den noch nicht verknüpften Eintrag frei. Der Kopfzeiger des Buckets ändert sich erst, nachdem beide Speicherallokationen und die Initialisierung gelungen sind.
Würde stattdessen e->key = key gespeichert, bliebe der Speicher des Aufrufers eingebunden. Wenn die Eingabe einen Puffer wiederholt verwendet, könnte jeder gespeicherte Zeiger das neueste Wort anzeigen. Das Kopieren des Textes gibt jedem Eintrag eine von diesem Puffer unabhängige Lebensdauer. strdup stellt diese Operation in POSIX und C23 bereit; ausdrückliche Speicherallokation und strcpy funktionieren in C17.
In einem Beispiel mit zehn Buckets und Zeichensummen ergibt das Einfügen von „cat“, „dog“, „act“ die Kette „act“ → „cat“ in Bucket 2 und „dog“ in Bucket 4. Die Suche nach „tac“ durchsucht Bucket 2, vergleicht beide Wörter und erreicht NULL. Ein passender Bucket ist nur eine mögliche Fundstelle, kein Beleg für einen gleichen Schlüssel.
Nutzdaten, Knoten und Bucket-Kopfzeiger freigeben
void clear_table(void) {
for (size_t i = 0; i < N; ++i) {
entry *e = table[i];
while (e != NULL) {
entry *next = e->next;
free(e->key);
free(e);
e = next;
}
table[i] = NULL;
}
}
Das Sichern von next und die Freigabe des Schlüssels erfolgen, während der Eintrag noch existiert. Das Zurücksetzen jedes Buckets ermöglicht die Wiederverwendung. Speicherprüfprogramme helfen, durchlaufene Pfade zu prüfen; auch bei fehlgeschlagener Speicherallokation muss aufgeräumt werden. Die Hashreihenfolge ist keine sortierte Reihenfolge. Für eine alphabetische Ausgabe sammle Schlüssel zum Sortieren oder speichere sie in einem binären Suchbaum.
Bei offener Adressierung benötigt das Löschen eine Löschmarkierung, damit die Suche über einen entfernten Platz hinweg zu späteren kollidierenden Schlüsseln fortgesetzt wird. Ein tatsächlich ungenutzter Platz kann eine erfolglose Suche beenden. Sondierungsfolgen müssen begrenzt sein und die Tabelle muss vergrößert werden, bevor jeder Platz belegt ist.
Wichtige Erkenntnisse
- Initialisiere jeden Bucket-Kopfzeiger mit
NULL. - Der Hashwert wählt eine Kette;
strcmpbestätigt, ob ein Schlüssel übereinstimmt. - Erhöhe die Anzahl eines vorhandenen Schlüssels, statt einen doppelten Eintrag hinzuzufügen.
- Kopiere Eingabezeichenketten und gib den Eintrag frei, wenn die Speicherallokation für den Schlüssel fehlschlägt.
- Gib den Schlüssel vor seinem Eintrag frei, sichere den Nachfolger und setze jeden Bucket zurück.
- Beim Löschen mit offener Adressierung erhalten Löschmarkierungen die Suchpfade.
Verkettete Datenstrukturen in C: Beispiele
1Datensätze durch Kopien und Zeiger verfolgen
15 Schritte
- 1EinführungEinen Messwert zusammenhalten
Ein Messwert verbindet einen Bezeichner und eine Temperatur. Wenn sie in einem Datensatz, einer C-Struktur, zusammengehalten werden, kann Code das Paar als einen Wert übergeben oder speichern.
Verfolge diesen Datensatz von seiner Definition über einen Funktionsaufruf bis in ein Array. Unterscheide bei jedem Schritt das Objekt von den Zeigern, über die es erreicht wird: Diese Unterscheidung bestimmt, ob eine Änderung eine Kopie oder das Original betrifft und ob eine flache Kopie einer Zeigerkomponente Speicher gemeinsam nutzt.
- 2BeispielDen Datensatz definieren
Die Definition führt einen Typ ein; die nächste Deklaration erzeugt ein Objekt dieses Typs.
struct reading { int id; double temp_c; }; struct reading r = {.id = 24, .temp_c = 16.5}; r.temp_c += 0.5;Der Punkt wählt eine Komponente von
raus, sodass die Temperatur nun 17,0 °C beträgt. Jede Komponente hat ihren eigenen Typ und das Semikolon nach der schließenden geschweiften Klammer beendet die Typdefinition. - 3KernaussageDie Komponentenwerte kopieren
Erstelle nun mit
struct reading s = r;einen zweiten Datensatz. Diese Zuweisung kopiert beide Komponentenwerte. Eine anschließende Änderung vons.idoders.temp_clässtrunverändert.Auch ein Array innerhalb einer Struktur wird kopiert. Füllbytes können sich zwischen Objekten dennoch unterscheiden, daher vergleichst du für die Gleichheit die für die Aufgabe relevanten Komponenten. C bietet keinen
==-Operator für ganze Strukturen. - 4HerausforderungEine ausgelassene Komponente lesen
Welchen Wert weist
struct reading a = {.id = 31};der Komponentea.temp_czu und darf Code diesen Wert lesen?---reveal---
Die ausgelassene Komponente wird mit null initialisiert, sodass die Temperatur
0.0ist und gelesen werden kann. Ein Initialisierer ist vorhanden, obwohl er nuridnennt. Eine Deklaration einer lokalen Variablenaohne Initialisierer würde dagegen Zuweisungen erfordern, bevor ihre Komponenten gelesen werden. - 5ErklärungDem Typ einen kurzen Namen geben
Ein
typedefstellt einen Aliasnamen bereit, ohne das Verhalten von Objekten zu ändern.typedef struct { int id; double temp_c; } reading; reading make(int id, double t) { return (reading){.id = id, .temp_c = t}; }Ein Aufruf kann den vollständigen Datensatz als Wert zurückgeben. Wenn ein Knoten innerhalb einer Definition auf seinen eigenen Typ verweisen muss, gib ihm ein Struktur-Tag und schreibe diesen Selbstverweis mit
struct node *next. - 6BeispielEine Kopie oder eine Adresse wählen
Vergleiche zwei Funktionsschnittstellen:
void warm_copy(reading x) { x.temp_c += 1.0; } void warm(reading *p) { p->temp_c += 1.0; }warm_copy(r)ändert eine lokale Kopie.warm(&r)greift auf den Datensatz des Aufrufers zu und ändert dessen Komponente. Der Pfeil bedeutet(*p).temp_c; die Klammern sorgen dafür, dass die Dereferenzierung erfolgt, bevor der Punkt die Komponente auswählt. - 7TippSpeicher für einen Datensatz mit klarem Besitzer reservieren
Binde
<stdlib.h>ein und reserviere Speicher mitreading *p = malloc(sizeof *p);. Prüfe, bevor du überpschreibst, ob der ZeigerNULList, und behandle einen Fehlschlag.Initialisiere nach Erfolg die Felder und bestimme einen Besitzer, der für
free(p)verantwortlich ist. Einen Strukturwert zurückzugeben ist sicher; die Rückgabe der Adresse einer lokalen Struktur hinterlässt einen hängenden Zeiger, weil das lokale Objekt beim Zurückkehren seiner Funktion aufhört zu existieren. - 8HerausforderungVorhersagen, welche Temperatur sich ändert
Ein Aufrufer beginnt mit
reading r = {24, 17.0};, ruftwarm_copy(r)und dannwarm(&r)mit den beiden gezeigten Funktionen auf. Welche Temperatur enthältr?---reveal---
Der Datensatz enthält 18,0 °C. Der erste Aufruf ändert nur seine eigene Kopie, sodass der Aufrufer beim Beginn des zweiten Aufrufs noch 17,0 hat. Der zweite erhält die Adresse des Datensatzes des Aufrufers und addiert über diesen Zeiger eins.
- 9BeispielEine Reihe von Datensätzen durchlaufen
Angenommen, ein Array enthält die Temperaturen −5,5, −1,0 und −7,5 °C. Nachdem du geprüft hast, dass die Anzahl nicht null ist, setze
bestauf Index 0 und vergleiche jeden späteren Datensatz mit dem aktuell besten.Index 1 ersetzt Index 0, weil −1,0 wärmer als −5,5 ist. Index 2 ersetzt ihn nicht. Mit einem vorhandenen Element zu beginnen funktioniert, wenn alle Werte unter null liegen; ein erfundenes Anfangsmaximum von null würde sie übersehen.
- 10TippEin Vorzeichen aus der Vergleichsfunktion zurückgeben
Für endliche Temperaturen kann eine Vergleichsfunktion
(x->temp_c > y->temp_c) - (x->temp_c < y->temp_c)zurückgeben. Das Ergebnis ist negativ, null oder positiv.Wird die Temperaturdifferenz als
intzurückgegeben, kann eine kleine Differenz zu null werden. Eine Ganzzahlsubtraktion kann bei extremen Schlüsseln überlaufen. Ein auf Vergleichen beruhendes Vorzeichen vermeidet beide Fehler undqsortgarantiert weiterhin nicht, die Reihenfolge gleicher Elemente zu erhalten. - 11ErklärungBei fehlgeschlagener Größenänderung den Zugriff erhalten
Halte bei einem Array auf dem Heap die Anzahl belegter Elemente getrennt von der Kapazität fest. Prüfe, ob die angeforderte Byteanzahl darstellbar ist, und weise dann das Ergebnis von
reallocbei einer Größe ungleich null einem temporären Zeiger zu.Bei einem Fehlschlag bleibt das ursprüngliche Array verfügbar. Übernimm bei Erfolg die neue Basisadresse und die Kapazität zusammen. Erzeuge Elementzeiger anhand ihrer gespeicherten Indizes neu, weil der Speicherblock möglicherweise verschoben oder sein bisheriger Speicher ersetzt wurde.
- 12VerbindungErkennen, was eine Zeigerkopie gemeinsam nutzt
Ein Personendatensatz mit
char *nameenthält eine Adresse, sodass nachperson b = a;beide Namen auf eine Zeichenkette zeigen. Das ist eine flache Kopie.Wenn jeder Datensatz eine unabhängige Zeichenkette besitzen muss, gib
b.nameeinen eigenen geprüften Speicherblock und kopiere den Text einschließlich des abschließenden Nullbytes. Jeder Besitzer kann seine eigene Zeichenkette dann einmal freigeben. Das Kopieren eines eingebetteten Zeichenarrays erzeugt bereits getrennte Zeichen. - 13HerausforderungDie Namenskopie reparieren
Das Feld
nameeines Datensatzes ist eine Zeichenkette auf dem Heap, die der Datensatz besitzt. Code kopiert den Datensatz und plant dann, beide Namen unabhängig voneinander freizugeben. Was muss vor diesem Aufräumen geschehen?---reveal---
Reserviere für die Kopie einen separaten Namen, prüfe den Erfolg und kopiere den ursprünglichen Text, bevor eine der beiden Zeichenketten freigegeben wird. Die beiden Datensätze teilen sich derzeit eine Adresse. Würden beide gemeinsamen Zeiger freigegeben, würde derselbe Speicherblock zweimal freigegeben; den Zeiger erneut zu kopieren würde nicht helfen.
- 14HerausforderungEinen Datensatz mit eingebetteter Beschriftung zurückgeben
Eine Funktion gibt eine Struktur mit
char label[12]als Wert zurück. Wird die Beschriftung ungültig, weil die ursprüngliche Struktur lokal war? Erkläre dies anhand von Lebensdauer und Kopieren.---reveal---
Nein. Die Rückgabe als Wert kopiert die Komponentenwerte der Struktur einschließlich des eingebetteten Arrays in den Rückgabewert. Der Aufrufer besitzt den resultierenden Datensatz. Die Rückgabe eines Zeigers auf die lokale Struktur würde den Aufrufer dagegen auf ein Objekt zugreifen lassen, dessen Lebensdauer beendet ist.
- 15ZusammenfassungWerte und Besitzer verfolgen
Ein Strukturobjekt speichert Komponentenwerte. Ein Zeiger bezeichnet ein Objekt und ein kopierter Zeiger bezeichnet weiterhin dasselbe Ziel. Funktionsparameter und Rückgabewerte folgen dieser Unterscheidung.
Halte bei Arrays die Anzahlen ausdrücklich fest und erhalte den Besitzer bei einer Größenänderung. Entscheide bei Datensätzen mit Zeigern auf Speicher im Besitz, ob eine Kopie den Speicher gemeinsam nutzt oder unabhängige Speicherblöcke erhält. Diese Entscheidungen bestimmen sowohl Aktualisierungen als auch das Aufräumen.
2Auswahl und Nutzdaten einer Union aufeinander abstimmen
11 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDie Auswahl benennen und ihre Nutzdaten bestimmen
- 2BeispielAusdrückliche Aufzählungswerte nachvollziehen
- 3KernaussageDie erlaubte Menge prüfen
- 4TippDen Übergang in weitere Fälle verhindern
- 5HerausforderungEinen Stufenwert prüfen
- 6ErklärungDie alternativen Werte überlagern
- 7BeispielBeide Teile eines gekennzeichneten Werts setzen
- 8HerausforderungDie nicht passende Aktualisierung erkennen
- 9TippEinen kleinen Wertebereich für Flags speichern
- 10HerausforderungSpeicher für einen Datensatz wählen
- 11ZusammenfassungJede Beschreibung auf ihre Daten abstimmen
3Erstellen, Einfügen und Löschen in Listen nachvollziehen
16 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungEine Kette Verweis für Verweis ändern
- 2ErklärungMit einem leeren Kopfzeiger beginnen
- 3BeispielAm Anfang einfügen, ohne die Kette zu verlieren
- 4KernaussageDen Aufrufer den neuen Kopfzeiger behalten lassen
- 5HerausforderungDie Reihenfolge beim Einfügen am Anfang vorhersagen
- 6ErklärungDer Adresse eines Verweises folgen
- 7BeispielEine sortierte Einfügeposition finden
- 8HerausforderungVor dem ersten Knoten einfügen
- 9BeispielEinen Knoten in der Mitte sicher entfernen
- 10TippBei einem fehlenden Wert sicher anhalten
- 11ErklärungTail-Zeiger und Anzahl hinzufügen
- 12VerbindungBei Bedarf einen Rückwärtsverweis zum Löschen verwenden
- 13HerausforderungSpeicher für wiederholte Lesezugriffe über Indizes wählen
- 14TippNutzdaten vor ihrem Knoten freigeben
- 15HerausforderungLöschen und Metadaten verbinden
- 16ZusammenfassungDen eingehenden Pfad gültig halten
4Stapelarbeit und das Zurücksetzen in einer Warteschlange nachvollziehen
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDas nächste wartende Element wählen
- 2Erklärungtop als Elementanzahl verwenden
- 3BeispielDie Stapelreihenfolge nachvollziehen
- 4BeispielDie schließende Klammer der obersten öffnenden zuordnen
- 5BeispielPostfixoperanden in der richtigen Reihenfolge halten
- 6HerausforderungEinen geänderten Postfixausdruck prüfen
- 7ErklärungVorderes und hinteres Ende unterscheiden
- 8TippDen Fall mit einem Knoten korrigieren
- 9ErklärungEinen Ring statt Verschieben verwenden
- 10BeispielEine zurückgesetzte Anordnung nachvollziehen
- 11HerausforderungDie nächste Einfügeposition finden
- 12KernaussageEine Regel für den vollen Zustand beibehalten
- 13HerausforderungDie Arbeitsreihenfolge wählen
- 14ZusammenfassungReihenfolge und Zustand aufeinander abstimmen
5Einen binären Suchbaum aufbauen, durchsuchen und durchlaufen sowie daraus löschen
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungEinem Baum von der Wurzel zum Teilbaum folgen
- 2KernaussageDie Regel unter jedem Vorfahren anwenden
- 3BeispielDen Beispielbaum aufbauen
- 4BeispielJeweils eine Verzweigung durchsuchen
- 5HerausforderungEinen Verstoß bei einem Nachkommen erkennen
- 6VerbindungDie Form messen
- 7BeispielEine Einfügung an der Stelle verknüpfen, an der die Suche endet
- 8BeispielEine Besuchsreihenfolge wählen
- 9ErklärungVerzweigungen und Pfadlänge zählen
- 10ErklärungNull oder ein Kind behandeln
- 11BeispielEinen Schlüssel mit zwei Kindern ersetzen
- 12HerausforderungDas Kind des Nachfolgers erhalten
- 13TippNachkommen vor ihrem Elternknoten freigeben
- 14HerausforderungTraversierung und Höhe zusammen prüfen
- 15ZusammenfassungJeden geänderten Teilbaum wieder verbinden
6Ein Programm zum Zählen von Wörtern mit einer Hashtabelle erstellen
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDie Suche mit einem Schlüssel eingrenzen
- 2BeispielEinen Ausgangs-Bucket berechnen
- 3KernaussageGleichheit und Hashberechnung aufeinander abstimmen
- 4HerausforderungEine Kollision von einer Übereinstimmung unterscheiden
- 5ErklärungDen Belegungsfaktor unter Beachtung der Verteilung verwenden
- 6BeispielDie Platzierung nach dem Wachstum neu berechnen
- 7ErklärungText in einer Kette nachschlagen
- 8BeispielEinen neuen Schlüssel im Besitz einsetzen
- 9HerausforderungEine teilweise Speicherallokation behandeln
- 10TippEinen wiederverwendeten Eingabepuffer nicht behalten
- 11ErklärungSondierungspfade nach dem Löschen erhalten
- 12HerausforderungKollisionsbehandlung und Wortzählung verbinden
- 13VerbindungJeden Schlüssel und Knoten freigeben
- 14ZusammenfassungPosition, Gleichheit und Lebensdauer aufeinander abstimmen
Verkettete Datenstrukturen in C: Übungsaufgaben
Set 1: Datensätze lesen und grundlegende Operationen nachvollziehen10 Übungsfragen
Frage 1 von 10
r.temp_c nach struct reading { int id; double temp_c; }; struct reading r = {.id = 12};?Antwort zeigenAntwort ausblenden
id und initialisiert die ausgelassene Komponente temp_c mit null, sodass ihr Wert 0.0 ist. Das unterscheidet sich von der Deklaration einer lokalen Struktur ganz ohne Initialisierer. Der vorhandene Initialisierer macht die ausgelassene Komponente ohne spätere Zuweisung verwendbar.Frage 2 von 10
reading *p zeigt auf einen Temperaturdatensatz. Welcher Ausdruck greift auf seine Komponente temp_c zu?Antwort zeigenAntwort ausblenden
(*p).temp_c. Ein Punkt benötigt links ein Strukturobjekt, daher ist p.temp_c hier ungültig. Die Schreibweise *p.temp_c behebt das Problem nicht, weil der Punkt vor der Dereferenzierung angewendet wird.Frage 3 von 10
best = 0 und aktualisiert den Index bei einem echt wärmeren Datensatz. Welcher Index bleibt?Antwort zeigenAntwort ausblenden
Frage 4 von 10
PAUSED in enum state { IDLE = 3, RUNNING, FAILED = 9, PAUSED };?Antwort zeigenAntwort ausblenden
FAILED setzt die Folge ausdrücklich auf 9, sodass PAUSED 10 ist. Würde für PAUSED ab dem Anfang der Deklaration gezählt, bliebe der durch den ausdrücklichen Wert eingeführte Neustart unberücksichtigt.Frage 5 von 10
kind vom Aufzählungstyp und die Union as mit den Komponenten int i und double d. Ein vorhandener Wert ist mit K_DOUBLE gekennzeichnet. Welche Aktualisierung speichert die Ganzzahl 18 konsistent?Antwort zeigenAntwort ausblenden
K_INT und das Schreiben von as.i lassen beide Teile die Ganzzahl 18 beschreiben. Würde die Ganzzahlkomponente beschrieben und die double-Kennzeichnung beibehalten, würde späterer Code die falsche Darstellung lesen; die Kennzeichnung ändert sich nicht automatisch.Frage 6 von 10
Antwort zeigenAntwort ausblenden
Frage 7 von 10
Antwort zeigenAntwort ausblenden
next-Komponente des Knotens mit 7. Ein Anhalten beim Verweis nach 12 würde 9 anhängen und die aufsteigende Ordnung der Liste verletzen.Frage 8 von 10
Antwort zeigenAntwort ausblenden
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Set 2: Regeln für Zustand und Speicherung anwenden10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
struct tag { char name[8]; int id; }; wird für a und b verwendet. Was geschieht nach b = a; b.name[0] = 'Z'; mit a.name[0]?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
void cool(reading r) { r.temp_c -= 2.0; }. Ein Aufrufer setzt reading a = {9, 14.0}; und ruft cool(a); auf. Welchen Wert hat a.temp_c des Aufrufers danach?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
printf("%d", WED); für enum day { MON, TUE, WED, THU }; aus?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
double unter der Kennzeichnung K_DOUBLE. Später ändert Code nur die Kennzeichnung zu K_INT und eine Ausgabefunktion liest as.i. Was ist der Fehler?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
head = 4 und count = 2. Auf welchen Platz schreibt das nächste Einreihen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
key = buffer. Es liest zuerst „ash“, dann „elm“. Was bleibt vor dem Aufräumen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 3: Verweise, Zurücksetzen und Kollisionen nachvollziehen10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
x->temp_c - y->temp_c als int zurück. Zwei verschiedene Datensätze haben die Temperaturen 6,20 und 6,45 °C. Warum kann die Sortierung sie als gleich behandeln?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
unsigned int mode : 4?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
n mit head = n; n->next = head; am Anfang eingefügt. Worauf zeigt das next des neuen Knotens?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 4: Kopierfehler und Fehler an Grenzen untersuchen10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
const reading *p. Welche Zuweisung kann den lokalen Zeiger ändern, ohne einen Datensatz zu ändern?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
a.name besitzt eine Zeichenkette auf dem Heap. Nach person b = a; reserviert Code erfolgreich Speicher für eine temporäre Zeichenkette copy, hat sie aber noch nicht gefüllt und keinen der beiden Datensätze geändert. Beide Datensätze müssen am Ende unabhängige Besitzer sein. Welche Folge erreicht das, ohne Daten oder einen Speicherblock zu verlieren?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
enum mode { M_IDLE = 0, M_READ = 4, M_WRITE = 5 }; vorgesehen. Welcher Test akzeptiert genau dessen benannte Werte?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
p noch und der Code führt free(p); p = p->next; aus. Welche Änderung erhält den beabsichtigten Durchlauf sicher?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
number < value gilt. Eine neue 6 wird eingefügt. Wohin kommt sie?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
top = 0 und das Entnehmen liest ungeprüft items[--top]. Welchen Index versucht es zu lesen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
head = 5, count = 3 und den Werten P, Q, R in Warteschlangenreihenfolge. Sie entnimmt zweimal und reiht dann S und T ein. Wie lauten am Ende Kopfindex, Anzahl und vorderster Wert?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 5: Regeln in unbekannten Fällen kombinieren10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Eine Schleife versucht, jede 5 aus 5 → 5 → 9 zu löschen:
node **link = &head;
while (*link != NULL) {
if ((*link)->number == 5) {
node *old = *link;
*link = old->next;
free(old);
}
if (*link != NULL) link = &(*link)->next;
}
Warum bleibt eine 5 und welche Änderung korrigiert die Schleife?
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
node **link = &head->next;, entfernt den ersten Knoten 4 und gibt ihn frei und verwendet dann *link, um 9 zu löschen. Warum ist diese zweite Operation ungültig und welchen eingehenden Verweis sollte sie nun verwenden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
insert(root->left, 20) auf und ignoriert den zurückgegebenen allokierten Knoten, speichert dann root = insert(root, 60) und schließlich root = erase(root, 40). Erfolgreiches Einfügen verknüpft intern die Ergebnisse für Kinder; das Löschen verbindet verbleibende Kinder wieder. Was bleibt?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
abstrakter Datentyp
am Anfang einfügen
Array von Strukturen
readings[3].temp_c greift auf die Temperatur im vierten Datensatz des Arrays zu.Aufzählungskonstante
enum state { IDLE = 3, RUNNING }; erhält die Aufzählungskonstante RUNNING den Wert 4.Aufzählungstyp
Ausketten
next eines Vorgängers auf den Nachfolger des entfernten Knotens hält die verbleibende Kette erreichbar.B
Basisfall
Baumgröße
Belegungsfaktor
Besitz
Binärbaum
binärer Suchbaum
Bitfeld
Blatt
Bucket
D
designierter Initialisierer
id vor temp_c deklariert ist, benennt der Initialisierer {.temp_c = 21.5, .id = 7} ihre Komponenten in umgekehrter Deklarationsreihenfolge.diskriminierte Union
K_CHAR gekennzeichneter Wert wird mit der Zeichenkomponente und einer Formatkonvertierung für Zeichen ausgegeben.doppelt verkettete Liste
E
einfach verkettete Liste
eingehender Verweis
Einreihen
(head + count) % CAP und erhöht dann seine Anzahl.Eintrag
Entnehmen
F
Fall-through
break auslässt, kann auch die für den folgenden Fall geschriebene Aktion ausführen.FIFO
flache Kopie
Füllbytes
H
hängender Zeiger
Hashfunktion
Höhe
I
implementierungsdefiniertes Verhalten
Inorder-Nachfolger
Inorder-Traversierung
K
Kapazität
Knoten
Kollision
Komponente
r.temp_c wählt die Temperaturkomponente des in r gespeicherten Messwerts aus.Kopfzeiger
L
LIFO
lineares Sondieren
Löschmarkierung
N
Nutzdaten
O
offene Adressierung
P
Pfeiloperator
p auf einen noch existierenden Datensatz erreicht p->temp_c dieselbe Komponente wie (*p).temp_c.Postfixausdruck
Postorder-Traversierung
Preorder-Traversierung
R
Reallokation
Rehashing
Ringpuffer
S
separate Verkettung
sortiertes Einfügen
Speicherausrichtung
Stapel
Struktur
Struktur-Tag
struct und kann den Typ innerhalb seiner eigenen Definition bezeichnen.struct node erlaubt es der Definition, einen Selbstverweis struct node *next zu deklarieren.switch-Anweisung
T
Tail-Zeiger
Teilbaum
tiefe Kopie
Traversierung
next-Verweise, während der Besitzer den ursprünglichen Kopfzeiger behält.typedef
typedef struct reading reading; kann eine Deklaration reading *p für einen Zeiger auf diese Struktur verwenden.U
Union
Unterlauf
V
Vergleichsfunktion
W
Warteschlange
Wertübergabe
Wurzel
Z
Zeichenkettengleichheit
strcmp(e->key, key) == 0, um gleichen Text in separaten Speicherblöcken zu erkennen.Zeiger auf einen Zeiger
node **link kann sich von der Adresse des Kopfzeigers zur Adresse des next-Feldes eines Knotens bewegen.Zeiger auf konstante Daten
const reading *p lesen, aber p->id keinen Wert zuweisen.zusammenhängende Speicherung
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.