Strukturen und verkettete Datenstrukturen in C

Lerne, zusammengehörige Daten mit Strukturen und 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

  1. Definiere Strukturtypen, gib ihnen mit typedef kurze 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 arbeiten
      Deklariere 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 übergeben
      Erstelle mit typedef Typaliasnamen, ü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 verarbeiten
      Speichere 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.
  2. 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 verwenden
      Definiere 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 ablegen
      Deklariere 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.
  3. 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 freigeben
      Definiere 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öschen
      Finde 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 erweitern
      Wä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.
  4. 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 anwenden
      Beschreibe 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 implementieren
      Beschreibe 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.
  5. 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 durchsuchen
      Verwende 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 freigeben
      Fü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.
  6. 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 zuordnen
      Erklä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 implementieren
      Baue 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.

Verkettete Datenstrukturen in C einfach erklärt

6 Kapitel

1

Zusammengehö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

  • typedef erzeugt 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->member ist 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].member und ü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 realloc in einem temporären Zeiger.
  • Eine kopierte Zeigerkomponente verweist weiterhin auf den ursprünglichen Speicherblock.
2

Auswahlmö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 break oder return, 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 switch wä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.
3

Verkettete 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 NULL zurü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
AufgabeArrayEinfach verkettete Liste
Position 500 erreichenDirekt a[500] verwendenVom Kopfzeiger aus Verweisen folgen
Nach einer bekannten Position einfügenNachfolgende Elemente verschiebenNach der Speicherallokation zwei Verweise ändern
Alle Werte durchlaufenZusammenhängende Elemente lesenMöglicherweise verstreuten Knoten folgen
Einen bekannten Knoten löschenNachfolgende Elemente verschiebenZusä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.
4

Stapel 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 top die 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.
5

Binä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
TraversierungBesuchsreihenfolgeTypischer Zweck
InorderLinks, Wurzel, rechtsSchlüssel eines binären Suchbaums sortiert ausgeben
PreorderWurzel, links, rechtsEltern vor Kindern besuchen
PostorderLinks, rechts, WurzelKinder 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.
6

Schlü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; strcmp bestä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

6 Interaktive Lektionen

1

Datensätze durch Kopien und Zeiger verfolgen

15 Schritte
Erstelle einen Messwertdatensatz, übergib ihn an Funktionen, durchlaufe ein Array und unterscheide eingebettete Daten von Zeigern auf Speicher im Besitz.
  1. 1
    Einfü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.

  2. 2
    BeispielDen 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 r aus, 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.

  3. 3
    KernaussageDie Komponentenwerte kopieren

    Erstelle nun mit struct reading s = r; einen zweiten Datensatz. Diese Zuweisung kopiert beide Komponentenwerte. Eine anschließende Änderung von s.id oder s.temp_c lässt r unverä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.

  4. 4
    HerausforderungEine ausgelassene Komponente lesen

    Welchen Wert weist struct reading a = {.id = 31}; der Komponente a.temp_c zu und darf Code diesen Wert lesen?

    ---reveal---

    Die ausgelassene Komponente wird mit null initialisiert, sodass die Temperatur 0.0 ist und gelesen werden kann. Ein Initialisierer ist vorhanden, obwohl er nur id nennt. Eine Deklaration einer lokalen Variablen a ohne Initialisierer würde dagegen Zuweisungen erfordern, bevor ihre Komponenten gelesen werden.

  5. 5
    ErklärungDem Typ einen kurzen Namen geben

    Ein typedef stellt 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.

  6. 6
    BeispielEine 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.

  7. 7
    TippSpeicher für einen Datensatz mit klarem Besitzer reservieren

    Binde <stdlib.h> ein und reserviere Speicher mit reading *p = malloc(sizeof *p);. Prüfe, bevor du über p schreibst, ob der Zeiger NULL ist, 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.

  8. 8
    HerausforderungVorhersagen, welche Temperatur sich ändert

    Ein Aufrufer beginnt mit reading r = {24, 17.0};, ruft warm_copy(r) und dann warm(&r) mit den beiden gezeigten Funktionen auf. Welche Temperatur enthält r?

    ---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.

  9. 9
    BeispielEine 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 best auf 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.

  10. 10
    TippEin 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 int zurückgegeben, kann eine kleine Differenz zu null werden. Eine Ganzzahlsubtraktion kann bei extremen Schlüsseln überlaufen. Ein auf Vergleichen beruhendes Vorzeichen vermeidet beide Fehler und qsort garantiert weiterhin nicht, die Reihenfolge gleicher Elemente zu erhalten.

  11. 11
    Erklä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 realloc bei 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.

  12. 12
    VerbindungErkennen, was eine Zeigerkopie gemeinsam nutzt

    Ein Personendatensatz mit char *name enthält eine Adresse, sodass nach person b = a; beide Namen auf eine Zeichenkette zeigen. Das ist eine flache Kopie.

    Wenn jeder Datensatz eine unabhängige Zeichenkette besitzen muss, gib b.name einen 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.

  13. 13
    HerausforderungDie Namenskopie reparieren

    Das Feld name eines 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.

  14. 14
    HerausforderungEinen 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.

  15. 15
    ZusammenfassungWerte 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.

2

Auswahl und Nutzdaten einer Union aufeinander abstimmen

11 Schritte
Verfolge Aufzählungswerte, weise Lücken zurück, erstelle eine gekennzeichnete Zelle und entscheide zwischen gleichzeitig benötigten und alternativen Feldern.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungDie Auswahl benennen und ihre Nutzdaten bestimmen
  2. 2
    BeispielAusdrückliche Aufzählungswerte nachvollziehen
  3. 3
    KernaussageDie erlaubte Menge prüfen
  4. 4
    TippDen Übergang in weitere Fälle verhindern
  5. 5
    HerausforderungEinen Stufenwert prüfen
  6. 6
    ErklärungDie alternativen Werte überlagern
  7. 7
    BeispielBeide Teile eines gekennzeichneten Werts setzen
  8. 8
    HerausforderungDie nicht passende Aktualisierung erkennen
  9. 9
    TippEinen kleinen Wertebereich für Flags speichern
  10. 10
    HerausforderungSpeicher für einen Datensatz wählen
  11. 11
    ZusammenfassungJede Beschreibung auf ihre Daten abstimmen
3

Erstellen, Einfügen und Löschen in Listen nachvollziehen

16 Schritte
Verwende eingehende Verweise, um eine Kette zu erhalten, und verwalte anschließend Enden, Anzahlen und Nutzdaten im Besitz.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungEine Kette Verweis für Verweis ändern
  2. 2
    ErklärungMit einem leeren Kopfzeiger beginnen
  3. 3
    BeispielAm Anfang einfügen, ohne die Kette zu verlieren
  4. 4
    KernaussageDen Aufrufer den neuen Kopfzeiger behalten lassen
  5. 5
    HerausforderungDie Reihenfolge beim Einfügen am Anfang vorhersagen
  6. 6
    ErklärungDer Adresse eines Verweises folgen
  7. 7
    BeispielEine sortierte Einfügeposition finden
  8. 8
    HerausforderungVor dem ersten Knoten einfügen
  9. 9
    BeispielEinen Knoten in der Mitte sicher entfernen
  10. 10
    TippBei einem fehlenden Wert sicher anhalten
  11. 11
    ErklärungTail-Zeiger und Anzahl hinzufügen
  12. 12
    VerbindungBei Bedarf einen Rückwärtsverweis zum Löschen verwenden
  13. 13
    HerausforderungSpeicher für wiederholte Lesezugriffe über Indizes wählen
  14. 14
    TippNutzdaten vor ihrem Knoten freigeben
  15. 15
    HerausforderungLöschen und Metadaten verbinden
  16. 16
    ZusammenfassungDen eingehenden Pfad gültig halten
4

Stapelarbeit und das Zurücksetzen in einer Warteschlange nachvollziehen

14 Schritte
Werte verschachtelte Operationen aus, verwalte die Enden einer verketteten Warteschlange und verfolge die logische Reihenfolge einer ringförmigen Warteschlange.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungDas nächste wartende Element wählen
  2. 2
    Erklärungtop als Elementanzahl verwenden
  3. 3
    BeispielDie Stapelreihenfolge nachvollziehen
  4. 4
    BeispielDie schließende Klammer der obersten öffnenden zuordnen
  5. 5
    BeispielPostfixoperanden in der richtigen Reihenfolge halten
  6. 6
    HerausforderungEinen geänderten Postfixausdruck prüfen
  7. 7
    ErklärungVorderes und hinteres Ende unterscheiden
  8. 8
    TippDen Fall mit einem Knoten korrigieren
  9. 9
    ErklärungEinen Ring statt Verschieben verwenden
  10. 10
    BeispielEine zurückgesetzte Anordnung nachvollziehen
  11. 11
    HerausforderungDie nächste Einfügeposition finden
  12. 12
    KernaussageEine Regel für den vollen Zustand beibehalten
  13. 13
    HerausforderungDie Arbeitsreihenfolge wählen
  14. 14
    ZusammenfassungReihenfolge und Zustand aufeinander abstimmen
5

Einen binären Suchbaum aufbauen, durchsuchen und durchlaufen sowie daraus löschen

15 Schritte
Verfolge einen Baum bei der Wahl von Verzweigungen, beim Einfügen, bei Traversierungsreihenfolgen, beim Ersetzen durch einen Nachfolger und beim Aufräumen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungEinem Baum von der Wurzel zum Teilbaum folgen
  2. 2
    KernaussageDie Regel unter jedem Vorfahren anwenden
  3. 3
    BeispielDen Beispielbaum aufbauen
  4. 4
    BeispielJeweils eine Verzweigung durchsuchen
  5. 5
    HerausforderungEinen Verstoß bei einem Nachkommen erkennen
  6. 6
    VerbindungDie Form messen
  7. 7
    BeispielEine Einfügung an der Stelle verknüpfen, an der die Suche endet
  8. 8
    BeispielEine Besuchsreihenfolge wählen
  9. 9
    ErklärungVerzweigungen und Pfadlänge zählen
  10. 10
    ErklärungNull oder ein Kind behandeln
  11. 11
    BeispielEinen Schlüssel mit zwei Kindern ersetzen
  12. 12
    HerausforderungDas Kind des Nachfolgers erhalten
  13. 13
    TippNachkommen vor ihrem Elternknoten freigeben
  14. 14
    HerausforderungTraversierung und Höhe zusammen prüfen
  15. 15
    ZusammenfassungJeden geänderten Teilbaum wieder verbinden
6

Ein Programm zum Zählen von Wörtern mit einer Hashtabelle erstellen

14 Schritte
Verfolge Kollisionen, Gleichheit, kopierte Schlüssel, Wachstum, fehlgeschlagene Speicherallokationen und vollständiges Aufräumen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungDie Suche mit einem Schlüssel eingrenzen
  2. 2
    BeispielEinen Ausgangs-Bucket berechnen
  3. 3
    KernaussageGleichheit und Hashberechnung aufeinander abstimmen
  4. 4
    HerausforderungEine Kollision von einer Übereinstimmung unterscheiden
  5. 5
    ErklärungDen Belegungsfaktor unter Beachtung der Verteilung verwenden
  6. 6
    BeispielDie Platzierung nach dem Wachstum neu berechnen
  7. 7
    ErklärungText in einer Kette nachschlagen
  8. 8
    BeispielEinen neuen Schlüssel im Besitz einsetzen
  9. 9
    HerausforderungEine teilweise Speicherallokation behandeln
  10. 10
    TippEinen wiederverwendeten Eingabepuffer nicht behalten
  11. 11
    ErklärungSondierungspfade nach dem Löschen erhalten
  12. 12
    HerausforderungKollisionsbehandlung und Wortzählung verbinden
  13. 13
    VerbindungJeden Schlüssel und Knoten freigeben
  14. 14
    ZusammenfassungPosition, Gleichheit und Lebensdauer aufeinander abstimmen

Verkettete Datenstrukturen in C: Übungsaufgaben

50 Übungsfragen in 5 Sets

Set 1: Datensätze lesen und grundlegende Operationen nachvollziehen10 Übungsfragen

Frage 1 von 10

Welchen Wert hat r.temp_c nach struct reading { int id; double temp_c; }; struct reading r = {.id = 12};?
  1. Einen unbestimmten Wert
  2. 0.0
  3. 12.0
  4. Einen Initialisierungsfehler
Antwort zeigenAntwort ausblenden
Antwort: B
Der designierte Initialisierer setzt 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

Ein gültiger Zeiger reading *p zeigt auf einen Temperaturdatensatz. Welcher Ausdruck greift auf seine Komponente temp_c zu?
  1. p.temp_c
  2. *p.temp_c
  3. &p.temp_c
  4. p->temp_c
Antwort zeigenAntwort ausblenden
Antwort: D
Der Pfeiloperator wählt eine Komponente über einen Zeiger aus und ist gleichbedeutend mit (*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

Datensätze haben an den Indizes 0, 1 und 2 die endlichen Temperaturen −8,0, −2,5 und −6,0 °C. Eine Maximumsuche beginnt mit best = 0 und aktualisiert den Index bei einem echt wärmeren Datensatz. Welcher Index bleibt?
  1. 1
  2. 0
  3. 2
  4. Kein Index; das Maximum bleibt null
Antwort zeigenAntwort ausblenden
Antwort: A
Die Suche beginnt mit dem Datensatz bei Index 0 und ersetzt ihn dann durch Index 1, weil −2,5 wärmer als −8,0 ist. Der letzte Wert −6,0 ist nicht wärmer als −2,5, sodass Index 1 bleibt. Negative Temperaturen verursachen kein besonderes Problem, wenn der anfänglich beste Wert ein tatsächlicher Datensatz ist.

Frage 4 von 10

Welchen Wert hat PAUSED in enum state { IDLE = 3, RUNNING, FAILED = 9, PAUSED };?
  1. 5
  2. 4
  3. 10
  4. 9
Antwort zeigenAntwort ausblenden
Antwort: C
Ein impliziter Aufzählungswert ist um eins größer als die unmittelbar vorherige Aufzählungskonstante. 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

Eine diskriminierte Union hat die Kennzeichnung 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?
  1. t.kind = K_DOUBLE; t.as.i = 18;
  2. t.kind = K_INT; t.as.d = 18.0;
  3. t.kind = K_INT; t.as.i = 18;
  4. t.kind = K_DOUBLE; t.as.d = 18.0;
Antwort zeigenAntwort ausblenden
Antwort: C
Die Kennzeichnung muss angeben, welche Komponente den gespeicherten Wert enthält. Die Zuweisung von 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

In eine leere Liste werden 6, 11 und dann 3 jeweils am Anfang eingefügt. Welche Werte erscheinen beim Durchlaufen vom Kopfzeiger aus?
  1. 6 → 11 → 3
  2. 3 → 11 → 6
  3. 11 → 6 → 3
  4. 3 → 6 → 11
Antwort zeigenAntwort ausblenden
Antwort: B
Jedes Einfügen am Anfang setzt den neuen Knoten vor den aktuellen ersten Knoten. Die ersten Knoten sind daher 6, dann 11 und dann 3, wobei frühere Knoten dahinter verknüpft bleiben. Die Reihenfolge des Eintreffens 6 → 11 → 3 ergäbe sich durch Anhängen am Ende, nicht durch das angegebene Einfügen am Anfang.

Frage 7 von 10

Eine sortierte Liste enthält 1 → 7 → 12. Eine Einfügeschleife geht weiter, solange der aktuelle Wert kleiner als 9 ist. Bei welchem eingehenden Verweis hält sie an?
  1. Beim Kopfzeiger auf den Knoten mit 1
  2. Beim next-Feld des Knotens mit 1
  3. Beim next-Feld des Knotens mit 12
  4. Beim next-Feld des Knotens mit 7
Antwort zeigenAntwort ausblenden
Antwort: D
Die Schleife geht an 1 und 7 vorbei und hält dann vor 12 an, weil 12 nicht kleiner als 9 ist. Der Verweis, der derzeit 12 erreicht, ist die 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

Ein leerer Stapel führt aus: 8 ablegen, 2 ablegen, entnehmen, 5 ablegen, entnehmen, entnehmen. Welche Werte geben die drei Entnahmen zurück?
  1. 2, 5, 8
  2. 8, 2, 5
  3. 2, 8, 5
  4. 5, 2, 8
Antwort zeigenAntwort ausblenden
Antwort: A
Die erste Entnahme entfernt 2, den zu diesem Zeitpunkt neuesten Wert. Das Ablegen von 5 setzt diesen Wert über die verbleibende 8, sodass die nächsten Entnahmen 5 und 8 zurückgeben. Zuerst 8 zurückzugeben würde der Reihenfolge einer Warteschlange folgen und nicht dem Verhalten des Stapels, bei dem das zuletzt abgelegte Element zuerst entnommen wird.

Frage 9 von 10

Ein binärer Suchbaum wird durch Einfügen von 42, 18, 63, 27 und 21 aufgebaut. Welche Knoten besucht eine Suche nach 21?
  1. 42 → 18 → 21
  2. 42 → 63 → 27 → 21
  3. 18 → 27 → 21
  4. 42 → 18 → 27 → 21
Antwort zeigenAntwort ausblenden
Antwort: D
Bei 42 ist 21 kleiner, sodass die Suche nach links zu 18 geht; dann geht sie nach rechts zu 27 und nach links zu 21. Das Einfügen von 21 platzierte den Knoten unter 27, nicht direkt unter 18. Eine Suche folgt vorhandenen Kindverweisen und kann nicht zu einem kleineren Schlüssel springen, nur weil bekannt ist, dass dieser vorhanden ist.

Frage 10 von 10

Eine befüllte Tabelle behandelt ASCII-Wörter unabhängig von Groß- und Kleinschreibung, berechnet aber Hashwerte aus ihren ursprünglichen Bytes. Eine Suche nach „mint“ verfehlt einen vorhandenen Eintrag „Mint“, ohne ihn zu besuchen. Welche vollständige Korrektur unterstützt vorhandene und zukünftige Einträge?
  1. Rohe Bytes für Hashwerte behalten, Gleichheit vereinheitlichen und dieselben Platzierungen neu aufbauen.
  2. Groß- und Kleinschreibung bei Hashwerten vereinheitlichen, den Vergleich beibehalten und Platzierungen neu aufbauen.
  3. Groß- und Kleinschreibung bei Hashwerten vereinheitlichen, den Vergleich und alte Platzierungen beibehalten.
  4. Rohe Bytes für Hashwerte behalten, Adressen vergleichen und das Bucket-Array vergrößern.
Antwort zeigenAntwort ausblenden
Antwort: B
Die Hashberechnung muss gleiche Schreibweisen gleich behandeln. Vereinheitliche daher vor der Hashberechnung die Groß- und Kleinschreibung und behalte den Vergleich ohne Unterscheidung der Groß- und Kleinschreibung bei. Vorhandene Einträge müssen ebenfalls nach der korrigierten Regel platziert werden. Nur zukünftige Hashwerte zu ändern lässt alte Einträge an Positionen zurück, die ihre korrigierten Suchen möglicherweise nie untersuchen.
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]?
  1. Es ändert sich zum Zeichen Z.
  2. Es wird zu einem unbestimmten Wert.
  3. Es behält sein ursprüngliches Zeichen.
  4. Die Strukturzuweisung lässt sich nicht kompilieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Gegeben ist 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?
  1. 14.0
  2. 12.0
  3. 16.0
  4. Einen unbestimmten Wert
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Welchen Text gibt printf("%d", WED); für enum day { MON, TUE, WED, THU }; aus?
  1. „WED“
  2. „3“
  3. „Wednesday“
  4. „2“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Eine diskriminierte Union speichert einen 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?
  1. Die Änderung der Kennzeichnung wandelt den gespeicherten Wert in eine Ganzzahl um.
  2. Die neue Kennzeichnung wählt eine Komponente, die nicht gespeichert wurde.
  3. Die Union behält beide typisierten Werte, bis sie freigegeben wird.
  4. Die Ausgabefunktion muss die alte Kennzeichnung behalten und ihr Format ändern.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine einfach verkettete Liste speichert Kopfzeiger, Tail-Zeiger und Anzahl. Ihr einziger Knoten wird entfernt und freigegeben. Welcher Zustand muss bleiben?
  1. head = NULL; tail = NULL; count = 0;
  2. head = NULL; tail = old; count = 0;
  3. head = old; tail = NULL; count = 0;
  4. head = NULL; tail = NULL; count = 1;
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Welches Ergebnis liefert der Postfixausdruck „15 6 - 3 /“ mit ganzzahligen Operanden?
  1. -3
  2. 7
  3. -7
  4. 3
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Eine ringförmige Warteschlange mit Anzahl hat die Kapazität 5, head = 4 und count = 2. Auf welchen Platz schreibt das nächste Einreihen?
  1. Platz 0
  2. Platz 2
  3. Platz 1
  4. Platz 4
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Welche Traversierung gibt jeden Kindteilbaum frei, bevor sie seinen Elternknoten freigibt?
  1. Preorder
  2. Postorder
  3. Inorder
  4. Nur die Wurzel
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine Tabelle mit separater Verkettung für Ganzzahlschlüssel verwendet den Schlüssel modulo der Bucket-Anzahl. Sie fügt 13, 25 und 23 jeweils am Anfang in 6 leere Buckets ein. Dann wächst sie auf 10 Buckets: Sie durchläuft die alten Buckets in aufsteigender Indexreihenfolge und jede Kette vom ersten Knoten aus und fügt jeden Eintrag am Anfang seines neuen Buckets ein. Welche Ketten entstehen?
  1. Bucket 3: 13 → 23; Bucket 5: 25
  2. Bucket 1: 25 → 13; Bucket 5: 23
  3. Bucket 3: 23 → 13; Bucket 5: 25
  4. Bucket 3: 23; Bucket 5: 25 → 13
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein zunächst leeres Programm zum Zählen von Wörtern mit separater Verkettung verwendet eine Hashfunktion, die „ash“ und „elm“ demselben Bucket zuordnet. Es liest in einen wiederverwendeten Puffer ein, sucht dann diesen Text und erhöht bei einer Übereinstimmung die Anzahl oder reserviert Speicher für einen Knoten mit der Anzahl 1. Neue Knoten speichern fälschlich key = buffer. Es liest zuerst „ash“, dann „elm“. Was bleibt vor dem Aufräumen?
  1. Ein Eintrag zeigt „elm“ mit der Anzahl 2
  2. Zwei Einträge, „ash“ und „elm“, mit je der Anzahl 1
  3. Ein Eintrag zeigt „ash“ mit der Anzahl 2
  4. Zwei Einträge zeigen jeweils „elm“ mit der Anzahl 1
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

Eine Vergleichsfunktion für endliche Temperaturen gibt 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?
  1. Die const void *-Argumente erzwingen für beide Eingaben Ganzzahlen.
  2. Die Vergleichsfunktion muss genau ihre Temperaturdifferenz zurückgeben.
  3. Die beiden endlichen Werte sind nach dem Kopieren durch qsort gleich.
  4. Die gebrochene Differenz wird bei der Umwandlung zu null.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welcher Wertebereich passt in unsigned int mode : 4?
  1. 0 bis 4
  2. 0 bis 15
  3. 1 bis 16
  4. −8 bis 7
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

In eine nicht leere Liste wird nach der Allokation von n mit head = n; n->next = head; am Anfang eingefügt. Worauf zeigt das next des neuen Knotens?
  1. Auf den bisherigen ersten Knoten
  2. Auf den bisherigen letzten Knoten
  3. Auf den neuen Knoten selbst
  4. Auf den Nullzeiger am Ende
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Eine aufsteigend sortierte Liste beginnt mit 3 → 8 → 12. Füge 6 ein, lösche den ersten Knoten und füge dann 8 mit einer Schleife ein, die weitergeht, solange der aktuelle Wert echt kleiner als der eingefügte Wert ist. Wo wird die neue 8 verknüpft?
  1. Nach 6 und vor der vorhandenen 8
  2. Nach der vorhandenen 8 und vor 12
  3. Vor 6 und nach der entfernten 3
  4. Nach 12 und vor dem Nullverweis
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Programm liest wiederholt Datensätze über einen numerischen Index und durchläuft alle Datensätze, fügt aber nur selten welche ein. Welche Speicherung passt am besten zu diesen Operationen?
  1. Eine einfach verkettete Liste von Datensätzen
  2. Eine doppelt verkettete Liste von Datensätzen
  3. Eine verkettete Warteschlange von Datensatzknoten
  4. Ein zusammenhängendes Array von Datensätzen
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

In eine leere verkettete Warteschlange werden 12 und 30 erfolgreich eingereiht, einmal wird entnommen, beim Einreihen von 41 schlägt die Speicherallokation fehl, es wird erneut entnommen und dann 44 erfolgreich eingereiht. Korrekte Operationen erhalten den Zustand bei einem Fehlschlag. Wie lauten die Endzeiger und wartenden Werte am Schluss?
  1. Kopfzeiger auf 30, Tail-Zeiger auf 44; Werte 30 → 44.
  2. Kopfzeiger auf 44, während der Tail-Zeiger null ist.
  3. Beide Endzeiger zeigen auf den einzigen Knoten 44.
  4. Kopfzeiger auf 12, Tail-Zeiger auf 44; Werte 12 → 44.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Ein Baum hat die Wurzel 36, das linke Kind 17 und 41 als rechtes Kind von 17. Warum erfüllt er die Regel für binäre Suchbäume nicht?
  1. 17 ist kleiner als sein Elternknoten und liegt links.
  2. 41 liegt im linken Teilbaum von 36, ist aber größer als 36.
  3. 41 ist größer als sein Elternknoten und liegt rechts.
  4. 36 hat nur ein direktes Kind statt zwei Kindern.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein binärer Suchbaum wird aus 48, 24, 75, 62, 68, 90 aufgebaut. Lösche 48 mithilfe seines Inorder-Nachfolgers. Was sind die neue Wurzel und das verbleibende linke Kind von 75?
  1. Wurzel 62; linkes Kind 68
  2. Wurzel 62; linkes Kind NULL
  3. Wurzel 75; linkes Kind 62
  4. Wurzel 68; linkes Kind 62
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine Tabelle mit separater Verkettung hat 21 gespeicherte Schlüssel und 14 Buckets. Wie groß ist ihr Belegungsfaktor?
  1. 1.5
  2. 0.67
  3. 7
  4. 35
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Hashsuche erreicht bei der Suche nach „oak“ eine Kette mit „elm“ und „ash“. Beide Textvergleiche schlagen fehl. Was muss sie nach dem Ende der Kette zurückgeben?
  1. Den ersten Eintrag für „elm“
  2. Den letzten Eintrag für „ash“
  3. NULL
  4. Einen neuen Eintrag für „oak“
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

Auf einer Plattform mit char von 1 Byte, int von 4 Byte mit einer Ausrichtung auf 4 Byte und auf ein Vielfaches von 4 aufgerundeten Strukturgrößen hat A die Komponenten char, int, char in dieser Reihenfolge; B hat int, char, char. Wie viel Speicher belegt ein Array aus fünf A-Datensätzen insgesamt im Vergleich zu fünf B-Datensätzen?
  1. A belegt 60 Byte; B belegt 40 Byte.
  2. A belegt 30 Byte; B belegt 30 Byte.
  3. A belegt 45 Byte; B belegt 30 Byte.
  4. A belegt 60 Byte; B belegt 30 Byte.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein Funktionsparameter ist const reading *p. Welche Zuweisung kann den lokalen Zeiger ändern, ohne einen Datensatz zu ändern?
  1. p->id = 19;
  2. (*p).temp_c = 8.0;
  3. p = another_reading_pointer;
  4. p->temp_c += 0.5;
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?
  1. Setze a.name = copy; kopiere b.name nach a.name; gib a.name zweimal frei.
  2. Kopiere a.name nach copy; setze b.name = copy; gib jeden Namen einmal frei.
  3. Gib a.name frei; kopiere b.name nach copy; setze b.name = copy.
  4. Kopiere a.name nach copy; lasse b.name geteilt; gib beide Namen frei.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Eine Eingabe ist für enum mode { M_IDLE = 0, M_READ = 4, M_WRITE = 5 }; vorgesehen. Welcher Test akzeptiert genau dessen benannte Werte?
  1. x >= 0 && x <= 5
  2. x >= 4 && x <= 5
  3. x == 0 || x == 4 || x == 6
  4. x == 0 || x == 4 || x == 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Beim Aufräumen einer Liste existiert p noch und der Code führt free(p); p = p->next; aus. Welche Änderung erhält den beabsichtigten Durchlauf sicher?
  1. Weise zuerst p = p->next zu und gib dann das neue p frei.
  2. Sichere zuerst next, gib p frei und weise dann p = next zu.
  3. Gib zuerst p->next frei und weise dann p = p->next zu.
  4. Setze zuerst p->next = NULL und fahre nach der Freigabe von p fort.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine sortierte Liste 2 → 6 → 6 → 14 verwendet eine Schleife, die weitergeht, solange number < value gilt. Eine neue 6 wird eingefügt. Wohin kommt sie?
  1. Zwischen 2 und die erste vorhandene 6
  2. Zwischen die beiden vorhandenen 6er-Knoten
  3. Zwischen die zweite 6 und 14
  4. Nach 14 ans Ende der Liste
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Ein Stapel auf Arraybasis hat ein vorzeichenbehaftetes top = 0 und das Entnehmen liest ungeprüft items[--top]. Welchen Index versucht es zu lesen?
  1. 0
  2. 1
  3. -1
  4. Den letzten gültigen Arrayindex
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine ringförmige Warteschlange mit Kapazität 6 verwendet eine Anzahl. Sie beginnt mit 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?
  1. Kopfindex 0; Anzahl 3; vorn Q
  2. Kopfindex 1; Anzahl 5; vorn R
  3. Kopfindex 3; Anzahl 3; vorn T
  4. Kopfindex 1; Anzahl 3; vorn R
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein binärer Suchbaum enthält die Wurzel 31, das linke Kind 12 mit dem rechten Kind 19 und das rechte Kind 54. Welche Preorder-Ausgabe bleibt nach dem Löschen von 12?
  1. 19, 31, 54
  2. 31, 54, 19
  3. 31, 19, 54
  4. 31, 12, 19, 54
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Für einen neuen Tabelleneintrag wird Speicher reserviert, aber die Speicherallokation für seine Schlüsselzeichenkette schlägt fehl, bevor er verknüpft wird. Welches Aufräumen erhält die Tabelle und vermeidet ein Speicherleck?
  1. Den Schlüssel des Aufrufers freigeben und Erfolg zurückgeben.
  2. Den neuen Eintrag freigeben und Fehlschlag zurückgeben.
  3. Ihn mit einem Nullzeiger als Schlüssel verknüpfen und Erfolg zurückgeben.
  4. Fehlschlag zurückgeben, aber den neuen Eintrag behalten.
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?

  1. Sie überspringt einen freigegebenen Knoten; gib vor dem Weitergehen den Nachfolger frei.
  2. Sie überspringt eine verbleibende Übereinstimmung; gehe nur bei fehlender Übereinstimmung weiter.
  3. Sie verliert den Kopfverweis; starte die Schleife nach jeder fehlenden Übereinstimmung neu.
  4. Sie löscht den falschen Knoten; gib ihn vor dem Lesen des Nachfolgers frei.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein Array auf dem Heap hat drei belegte Datensätze, die jeweils eine eigene Namenszeichenkette besitzen, sowie einen gespeicherten Zeiger auf Datensatz 1. Eine Größenänderung auf Kapazität 6 gelingt. Das Programm plant, Datensatz 0 zu entfernen, indem es die anderen beiden Datensätze nach links verschiebt und die Anzahl belegter Elemente auf 2 verringert. Welcher Plan erhält den Zugriff auf den ursprünglichen Datensatz 1 und gibt allen Speicher im Besitz frei?
  1. Entfernten Namen freigeben; verschieben; Zeiger bei Index 0 neu bilden; später aktive Namen und Array freigeben.
  2. Entfernten Namen freigeben; verschieben; alten Zeiger behalten; später alle sechs Kapazitätsplätze und das Array freigeben.
  3. Verschieben; Zeiger bei Index 1 neu bilden; später nur die zwei aktiven Namen und das Array freigeben.
  4. Verschieben; Zeiger bei Index 0 neu bilden; später Namen aus allen drei alten Plätzen und das Array freigeben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Einem vorzeichenlosen 3-Bit-Feld wird zur Laufzeit der Wert 13 zugewiesen. Welcher Wert wird gespeichert?
  1. 13
  2. 7
  3. 1
  4. 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Eine Liste lautet 4 → 9 → 16. Code speichert 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?
  1. link zeigt auf den existierenden Knoten 9; verwende die Adresse des nächsten Feldes von 9.
  2. link zeigt in den existierenden Knoten 16; verwende die Adresse des nächsten Feldes von 16.
  3. link zeigt in den freigegebenen Knoten 4; verwende die Adresse des aktuellen Kopfzeigers.
  4. link ist die Adresse des aktuellen Kopfzeigers; verwende die Adresse des alten Knotens 4.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine doppelt verkettete Liste lautet A ↔ B ↔ C, wobei A der erste und C der letzte Knoten ist. Entferne B, hänge D an und entferne dann C, wobei beide Richtungen erhalten bleiben. Was sind der letzte Knoten und sein Vorgänger am Schluss?
  1. Letzter Knoten D; sein Vorgänger A
  2. Letzter Knoten C; sein Vorgänger A
  3. Letzter Knoten D; sein Vorgänger B
  4. Letzter Knoten A; sein Vorgänger D
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Ein Programm zur Postfixauswertung behandelt Divisionen korrekt, vertauscht aber bei jeder Subtraktion die beiden Operanden. Wie lauten für „20 8 - 3 / 2 -“ das korrekte Ergebnis und das Ergebnis des fehlerhaften Programms? Alle Divisionen hier sind exakte Ganzzahldivisionen.
  1. Korrektes Ergebnis 6; fehlerhaftes Ergebnis 2
  2. Korrektes Ergebnis 2; fehlerhaftes Ergebnis −6
  3. Korrektes Ergebnis 2; fehlerhaftes Ergebnis 6
  4. Korrektes Ergebnis −2; fehlerhaftes Ergebnis 6
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

In eine leere Warteschlange werden zuerst 22, dann 7 und dann 15 eingereiht. Was gibt die erste Entnahme zurück?
  1. 15
  2. 7
  3. Einen Fehler wegen leerer Warteschlange
  4. 22
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Füge die verschiedenen Schlüssel 9, 16, 23, 30 in dieser Reihenfolge in einen leeren binären Suchbaum ein. Wie groß ist seine Höhe, wenn sie in Kanten gemessen wird?
  1. 4
  2. 3
  3. 2
  4. 1
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein binärer Suchbaum hat zunächst nur die Wurzel 40. Bei einer Schnittstelle, die die Wurzel zurückgibt, ruft Code 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?
  1. Wurzel 60 mit linkem Kind 20; kein Speicherblock geht verloren.
  2. Wurzel 20 mit rechtem Kind 60; kein Speicherblock geht verloren.
  3. Wurzel 40 mit rechtem Kind 60; Knoten 20 geht verloren.
  4. Nur Wurzel 60; der allokierte Knoten 20 geht verloren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Tabelle der Größe 7 mit offener Adressierung verwendet lineares Sondieren. Drei Schlüssel, die Platz 5 zugeordnet werden, belegen die Plätze 5, 6 und 0. Der Schlüssel auf Platz 6 wird gelöscht. Warum muss Platz 6 eine Löschmarkierung behalten, statt ungenutzt zu werden?
  1. Eine Suche nach dem Schlüssel auf Platz 0 muss an Platz 6 vorbeigehen.
  2. Eine Suche nach dem Schlüssel auf Platz 5 muss ihn auf Platz 6 verschieben.
  3. Eine Suche muss den gelöschten Schlüssel als weiterhin vorhanden behandeln.
  4. Eine Suche muss ihre Sondierung jedes Mal bei Platz 0 neu beginnen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

72 Schlüsselbegriffe, jeweils mit Definition

A

abstrakter Datentyp
Ein abstrakter Datentyp beschreibt eine Sammlung durch ihre erlaubten Operationen und ihr Verhalten, unabhängig von der für ihre Implementierung gewählten Speicherung.
Ein Stapel behält dasselbe Verhalten beim Ablegen und Entnehmen, unabhängig davon, ob seine Werte in einem Array oder in verketteten Knoten liegen.
am Anfang einfügen
Am Anfang einfügen bedeutet, ein Element vor dem aktuellen ersten Element einer Folge einzufügen und damit zu ändern, welches Element über ihren Kopfzeiger erreicht wird.
Wenn du zuerst 1, dann 2 und dann 3 am Anfang einfügst, entsteht eine Liste, deren Durchlauf 3 vor 2 und dann 1 besucht.
Array von Strukturen
Ein Array von Strukturen speichert Datensätze eines Strukturtyps zusammenhängend und ermöglicht die direkte Auswahl eines Datensatzes über seinen numerischen Index.
Der Ausdruck readings[3].temp_c greift auf die Temperatur im vierten Datensatz des Arrays zu.
Aufzählungskonstante
Eine Aufzählungskonstante ist eine benannte Ganzzahlkonstante in der Definition eines Aufzählungstyps, deren Wert ausdrücklich angegeben oder aus ihrem Vorgänger bestimmt wird.
In enum state { IDLE = 3, RUNNING }; erhält die Aufzählungskonstante RUNNING den Wert 4.
Aufzählungstyp
Ein Aufzählungstyp definiert benannte Ganzzahlkonstanten und einen Typ für Werte dieser Auswahlmöglichkeiten, ohne die Zugehörigkeit zur benannten Menge zu erzwingen.
Ein Aufzählungstyp für Warnungen kann die Stufen niedrig, mittel, hoch und kritisch benennen, während Eingaben weiterhin geprüft werden müssen.
Ausketten
Ausketten entfernt einen Knoten aus einer Struktur, indem die Verweise um ihn herum neu verbunden werden, bevor Speicher im Besitz des Knotens freigegeben wird.
Das Umlenken des next eines Vorgängers auf den Nachfolger des entfernten Knotens hält die verbleibende Kette erreichbar.

B

Basisfall
Ein Basisfall behandelt die kleinste Eingabe einer rekursiven Funktion ohne einen weiteren rekursiven Aufruf und beendet so den Abstieg zu kleineren Teilproblemen.
Das Aufräumen eines Baums kehrt bei einem Teilbaum mit dem Wert null sofort zurück, bevor versucht wird, Kindzeiger zu lesen.
Baumgröße
Die Baumgröße ist die Anzahl seiner Knoten. Sie wird berechnet, indem eins für seine Wurzel zu den Größen beider Kindteilbäume addiert wird; für einen leeren Baum ist sie null.
Eine Wurzel mit zwei Kindern, die Blätter sind, hat die Größe 3, obwohl ihre in Kanten gemessene Höhe nur 1 ist.
Belegungsfaktor
Der Belegungsfaktor ist die Anzahl gespeicherter Schlüssel geteilt durch die Anzahl der Buckets. Er misst die durchschnittliche Belegung, ohne die Länge jeder Kette festzulegen.
Zwölf Schlüssel in acht Buckets ergeben den Belegungsfaktor 1,5, selbst wenn manche Buckets leer bleiben.
Besitz
Besitz weist die Verantwortung für die Freigabe eines Speicherblocks zu, selbst wenn während seiner Lebensdauer mehrere Zeiger dasselbe Objekt erreichen können.
Ein Wortknoten besitzt seine Zeichenkette, sodass beim Aufräumen diese Zeichenkette vor dem Knoten freigegeben wird, der ihre Adresse enthält.
Binärbaum
Ein Binärbaum ist eine verkettete Struktur, in der jeder Knoten höchstens zwei Kinder hat, die als sein linkes und rechtes Kind unterschieden werden.
Ein Knoten kann keine Kinder, nur ein Kind oder sowohl ein linkes als auch ein rechtes Kind haben.
binärer Suchbaum
Ein binärer Suchbaum ordnet verschiedene Schlüssel so, dass jeder Knoten in seinem gesamten linken Teilbaum kleinere und in seinem gesamten rechten Teilbaum größere Schlüssel hat.
Der Vergleich des gesuchten Werts mit dem aktuellen Schlüssel wählt den einzigen Teilbaum, der eine Übereinstimmung enthalten kann.
Bitfeld
Ein Bitfeld ist eine Strukturkomponente, die mit einer Breite in Bit deklariert wird. Diese begrenzt ihre gespeicherten Ganzzahlwerte, während ihre Anordnung implementierungsdefiniert bleibt.
Ein vorzeichenloses 3-Bit-Modusfeld speichert Werte von 0 bis 7; seine Adresse kann nicht ermittelt werden.
Blatt
Ein Blatt ist ein Baumknoten ohne Kinder, sodass beide Kindzeiger null sind und seine Höhe null ist, wenn die Höhe Kanten zählt.
Das Löschen eines Blattes gibt seinen Speicherblock frei und ersetzt seinen eingehenden Kindverweis durch einen Nullzeiger.
Bucket
Ein Bucket ist eine durch einen Hashtabellenindex ausgewählte Stelle; bei separater Verkettung speichert er den Kopfzeiger einer Liste der dort zugeordneten Einträge.
Eine Tabelle mit zehn Buckets und ASCII-Zeichensummen platziert „dog“ in Bucket 4, weil seine Summe 314 ist.

D

designierter Initialisierer
Ein designierter Initialisierer benennt die Komponente oder das Element, das einen Wert erhält, und erlaubt es, Strukturkomponenten unabhängig von der Deklarationsreihenfolge zu initialisieren.
Für eine Struktur, in der id vor temp_c deklariert ist, benennt der Initialisierer {.temp_c = 21.5, .id = 7} ihre Komponenten in umgekehrter Deklarationsreihenfolge.
diskriminierte Union
Eine diskriminierte Union kombiniert gemeinsam genutzten Speicher für Nutzdaten mit einer separaten Kennzeichnung, die angibt, auf welche Union-Komponente lesender Code zugreifen soll.
Ein mit K_CHAR gekennzeichneter Wert wird mit der Zeichenkomponente und einer Formatkonvertierung für Zeichen ausgegeben.
doppelt verkettete Liste
Eine doppelt verkettete Liste speichert in jedem Knoten Verweise auf Nachfolger und Vorgänger und erlaubt so das Rückwärtsdurchlaufen und das Anpassen über beide Nachbarn.
Das Entfernen eines inneren Knotens aktualisiert den next-Verweis des vorherigen Knotens und den prev-Verweis des folgenden Knotens.

E

einfach verkettete Liste
Eine einfach verkettete Liste ist eine Folge von Knoten, die durch next-Zeiger verbunden sind. Am Anfang steht ein Kopfzeiger, am Ende ein Nullverweis.
Das Einfügen eines Knotens am Anfang gibt ihm den bisherigen ersten Knoten als Nachfolger und macht den neuen Knoten zum ersten Knoten.
eingehender Verweis
Ein eingehender Verweis ist der Zeiger, der einen Knoten erreicht: entweder der Listenkopfzeiger oder das next-Feld eines Vorgängers. Er muss geändert werden, wenn dieser Knoten umgangen wird.
Das Löschen des ersten Knotens ändert den Kopfzeiger, weil kein Vorgängerknoten seinen eingehenden Verweis bereitstellt.
Einreihen
Einreihen bedeutet, ein Element hinten an einer Warteschlange hinzuzufügen und dabei die vordere Position bereits wartender Werte zu erhalten.
Ein Ring mit Anzahl schreibt das neue Element an (head + count) % CAP und erhöht dann seine Anzahl.
Eintrag
Ein Eintrag ist ein gespeicherter Hashtabellendatensatz mit einem Schlüssel und zugehörigen Daten sowie einem next-Zeiger, wenn separate Verkettung Einträge verbindet.
Ein Eintrag zur Wortzählung besitzt eine Schlüsselzeichenkette, speichert die Anzahl des Wortes und verweist auf den nächsten Eintrag in seinem Bucket.
Entnehmen
Entnehmen bedeutet, das vorderste Element einer Warteschlange zu entfernen und eine leere Eingabe zu melden, bevor der Zustand geändert oder ein Wert gelesen wird.
Eine Entnahme aus einem Ring liest das vorderste Element, bewegt den Kopfindex mit Zurücksetzen weiter und verringert die Anzahl.

F

Fall-through
Fall-through bezeichnet die Fortsetzung der Ausführung von einem switch-Fall in den nächsten, weil kein break, return oder anderer Kontrolltransfer sie beendet.
Ein Fall, der einen Zustand ausgibt, aber break auslässt, kann auch die für den folgenden Fall geschriebene Aktion ausführen.
FIFO
FIFO bedeutet „zuerst hinein, zuerst heraus“ und beschreibt die Reihenfolge, bei der das früheste gespeicherte Element vor späteren noch wartenden Elementen behandelt wird.
Nach dem Einreihen von A, B und C gibt eine FIFO-Entnahme A vor beiden späteren Buchstaben zurück.
flache Kopie
Eine flache Kopie vervielfältigt die Komponentenwerte eines Datensatzes einschließlich der Zeigeradressen, während separat allokierte Daten, auf die gezeigt wird, gemeinsam genutzt bleiben.
Nach dem Kopieren eines Personendatensatzes erreichen beide Namenszeiger weiterhin dieselbe Zeichenkette, sofern kein Speicher für eine neue Kopie reserviert wird.
Füllbytes
Füllbytes sind ungenutzte Bytes, die eine Implementierung zwischen Strukturkomponenten oder nach der letzten Komponente einfügen kann, um Anforderungen an die Speicheranordnung zu erfüllen.
Ein byteweiser Vergleich kann unterschiedliche Füllbytes erkennen, selbst wenn zwei Datensätze gleiche Komponentenwerte enthalten.

H

hängender Zeiger
Ein hängender Zeiger verweist auf ein Objekt, dessen Lebensdauer beendet ist, sodass seine Dereferenzierung keinen gültigen Zugriff auf dieses Objekt mehr bietet.
Die Rückgabe der Adresse eines lokalen Datensatzes hinterlässt dem Aufrufer einen Zeiger auf ein Objekt, das nicht mehr existiert.
Hashfunktion
Eine Hashfunktion wandelt einen Schlüssel deterministisch in eine Zahl um, die auf einen Bucket-Index reduziert wird. Dabei soll sie die erwarteten Schlüssel gleichmäßig verteilen.
Eine Hashfunktion mit Zeichensummen sendet Umstellungen wie „cat“ und „act“ an denselben Bucket.
Höhe
Die Höhe eines nicht leeren Baums ist die Anzahl der Kanten auf seinem längsten Weg von der Wurzel zu einem Blatt; die hier verwendete Höhe eines leeren Baums ist −1.
Eine Kette aus fünf Knoten hat auf ihrem längsten Weg vier Kanten, sodass ihre Höhe 4 ist.

I

implementierungsdefiniertes Verhalten
Implementierungsdefiniertes Verhalten ist eine Entscheidung, die die C-Implementierung dokumentieren muss. Portabler Code kann daher nicht annehmen, dass jede Implementierung dieselbe Entscheidung trifft.
Die dokumentierte Reihenfolge für die Anordnung von Bitfeldern kann sich zwischen Implementierungen derselben C-Deklaration unterscheiden.
Inorder-Nachfolger
Der Inorder-Nachfolger eines Knotens mit rechtem Teilbaum ist der kleinste Schlüssel in diesem Teilbaum. Er wird beim Löschen verwendet, um den Schlüssel eines Knotens mit zwei Kindern zu ersetzen.
Folge dem rechten Kind und dann linken Verweisen, um den Ersatzschlüssel für das Löschen eines Knotens mit zwei Kindern zu finden.
Inorder-Traversierung
Eine Inorder-Traversierung besucht jeden linken Teilbaum vor seiner Wurzel und danach deren rechten Teilbaum. Bei einem binären Suchbaum ergibt sie sortierte Schlüssel.
Die Inorder-Ausgabe eines binären Suchbaums zum Zählen von Wörtern erzeugt seine gespeicherten Wörter in der Reihenfolge seiner Textvergleiche.

K

Kapazität
Die Kapazität ist die Anzahl der Elemente, die ein allokiertes Array aufnehmen kann; sie kann größer als die Anzahl der derzeit belegten Elemente sein.
Ein Array mit Kapazität 16 und 10 belegten Datensätzen kann vor einem weiteren Wachstumsschritt sechs weitere Datensätze aufnehmen.
Knoten
Ein Knoten ist ein Datensatz, der Daten und die Zeiger enthält, die diesen Datensatz mit anderen Teilen einer verketteten Struktur verbinden.
Ein Listenknoten enthält eine Ganzzahl und seinen Zeiger auf den Nachfolger; ein Baumknoten hat dagegen zwei Zeiger auf Kinder.
Kollision
Eine Kollision tritt auf, wenn verschiedene Schlüssel denselben Bucket-Index erhalten. Die Tabelle muss ihre separaten Einträge daher erhalten und unterscheiden.
Eine Suche muss die Schlüssel innerhalb eines gemeinsam genutzten Buckets vergleichen, statt anzunehmen, dass sein erster Eintrag übereinstimmt.
Komponente
Eine Komponente ist ein benannter Bestandteil einer Struktur oder Union, der aus einem Objekt mit einem Punkt oder über einen Zeiger mit einem Pfeil ausgewählt wird.
Der Ausdruck r.temp_c wählt die Temperaturkomponente des in r gespeicherten Messwerts aus.
Kopfzeiger
Der Kopfzeiger bezeichnet den ersten Knoten einer verketteten Folge. Ein Kopfzeiger mit dem Wert null stellt eine Folge ohne Knoten dar.
Beim Entfernen des ersten Listenknotens muss der Kopfzeiger des Besitzers aktualisiert werden, bevor der Speicherblock des alten Knotens freigegeben wird.

L

LIFO
LIFO bedeutet „zuletzt hinein, zuerst heraus“ und beschreibt die Reihenfolge, bei der das neueste gespeicherte Element vor allen noch vorhandenen früheren Elementen behandelt wird.
Nachdem ein Stapel 4, 7 und 1 erhalten hat, geben seine ersten beiden Entnahmen 1 und 7 zurück und lassen 4 übrig.
lineares Sondieren
Lineares Sondieren löst eine Kollision, indem aufeinanderfolgende Arrayplätze mit Zurückspringen am Ende versucht werden. Einfügen und Suchen verwenden dieselbe Folge.
Eine Sondierung, die beim letzten Platz beginnt, wird bei Platz null fortgesetzt, wenn eine weitere belegte Position geprüft werden muss.
Löschmarkierung
Eine Löschmarkierung markiert einen gelöschten Platz bei offener Adressierung, ohne ihn als nie verwendet erscheinen zu lassen. Dadurch kann die Suche zu späteren kollidierenden Einträgen weitergehen.
Eine Suche geht an einer Löschmarkierung vorbei, statt vor einem passenden Schlüssel weiter hinten im Sondierungspfad anzuhalten.

N

Nutzdaten
Nutzdaten sind die gespeicherten Daten, die durch die Art eines gekennzeichneten Werts ausgewählt werden. Sie werden getrennt von der Kennzeichnung gehalten, die ihre Interpretation beschreibt.
In einem Token eines Taschenrechners enthalten die Nutzdaten entweder den numerischen Operanden oder den durch die Kennzeichnung angegebenen Operator.

O

offene Adressierung
Offene Adressierung speichert Einträge direkt in Arrayplätzen und löst Kollisionen, indem sie eine festgelegte Folge alternativer Plätze durchsucht.
Eine Suche kann mehrere Plätze untersuchen, obwohl sie bei einem aus dem Schlüssel abgeleiteten Hashindex beginnt.

P

Pfeiloperator
Der Pfeiloperator wählt eine Struktur- oder Union-Komponente über einen Zeiger aus und verbindet Dereferenzierung mit Komponentenauswahl.
Für einen Zeiger p auf einen noch existierenden Datensatz erreicht p->temp_c dieselbe Komponente wie (*p).temp_c.
Postfixausdruck
Ein Postfixausdruck schreibt jeden Operator nach seinen Operanden, sodass ein Stapel Zwischenwerte halten kann, bis der Operator sie kombiniert.
Bei „9 4 -“ ist der zuerst entnommene Wert der rechte Operand 4 und das Ergebnis ist 5.
Postorder-Traversierung
Eine Postorder-Traversierung besucht den linken und den rechten Teilbaum vor ihrer Wurzel und ermöglicht es beim Aufräumen, Kinder vor ihrem Elternknoten freizugeben.
Eine Funktion zur Freigabe eines Baums liest Kindverweise, solange der Elternknoten existiert, und gibt diesen Elternknoten zuletzt frei.
Preorder-Traversierung
Eine Preorder-Traversierung besucht die Wurzel eines Teilbaums vor dessen linkem und rechtem Teilbaum und setzt jeden Elternknoten vor seine Nachkommen.
Eine Preorder-Ausgabe beginnt mit dem Wurzelschlüssel, selbst wenn dieser weder der kleinste noch der größte ist.

R

Reallokation
Eine Reallokation ändert die Größe eines allokierten Speicherblocks und kann ihn verschieben. Nach Erfolg muss der Besitzer die zurückgegebene Basisadresse verwenden.
Ein temporärer Ergebniszeiger erhält den Zugriff auf die alten Datensätze, wenn eine Größenänderung des Arrays auf eine Größe ungleich null fehlschlägt.
Rehashing
Rehashing berechnet die Platzierung gespeicherter Schlüssel für eine neue Bucket-Anzahl neu und verschiebt oder verknüpft Einträge, sodass spätere Suchen die richtigen Stellen verwenden.
Das Verdoppeln des Bucket-Arrays erfordert, den Hashwert jedes Schlüssels vor seiner Platzierung mit der neuen Anzahl zu reduzieren.
Ringpuffer
Ein Ringpuffer verwendet ein festes Array wieder, indem seine Positionen vom letzten Platz zum ersten zurückspringen, statt verbleibende Elemente zu verschieben.
Nach einer Entnahme vorn kann ein späteres Einfügen den freigewordenen Platz wiederverwenden, sobald sein Index zurückspringt.

S

separate Verkettung
Separate Verkettung behandelt Hashkollisionen, indem sie an jedem Bucket eine verkettete Liste von Einträgen hält und Schlüssel innerhalb der gewählten Liste vergleicht.
Neue Worteinträge können am Anfang eingefügt werden, weil ihre Reihenfolge innerhalb eines Buckets die exakte Suche nicht beeinflusst.
sortiertes Einfügen
Sortiertes Einfügen platziert einen neuen Wert so, dass die bestehende Ordnung erhalten bleibt, und verbindet den neuen Knoten zwischen seinem Vorgänger und seinem Nachfolger.
Eine Schleife für eine aufsteigende Liste hält vor dem ersten Wert an, der nicht kleiner als der neue Wert ist.
Speicherausrichtung
Speicherausrichtung ist eine Anforderung des Objekttyps an die Platzierung seiner Adresse, die Lücken zwischen Komponenten oder am Ende einer Struktur verursachen kann.
Eine Ganzzahlkomponente kann nach einer Lücke hinter einer Zeichenkomponente beginnen, damit ihre Adresse passend ausgerichtet ist.
Stapel
Ein Stapel ist eine Sammlung, bei der Einfügen und Entfernen oben erfolgen, sodass der zuletzt eingefügte Wert als Nächstes entfernt wird.
Ein Programm zur Klammerprüfung speichert die zuletzt geöffnete, noch nicht zugeordnete Klammer oben auf seinem Stapel.
Struktur
Eine Struktur ist ein C-Typ, der benannte Komponenten, die unterschiedliche Typen haben können, zu einem Datensatz mit separatem Speicher für jede Komponente zusammenfasst.
Ein Temperaturdatensatz kombiniert einen ganzzahligen Sensorbezeichner und eine Temperatur in einem Strukturobjekt.
Struktur-Tag
Ein Struktur-Tag benennt einen Strukturtyp nach dem Schlüsselwort struct und kann den Typ innerhalb seiner eigenen Definition bezeichnen.
Das Tag in struct node erlaubt es der Definition, einen Selbstverweis struct node *next zu deklarieren.
switch-Anweisung
Eine switch-Anweisung wählt anhand eines ganzzahligen Ausdrucks einen Ausgangsfall und führt anschließend weiter aus, bis die Steuerung die Anweisung oder Funktion verlässt.
Ein switch für einen gekennzeichneten Wert wählt die Ausgabeoperation für die Art der aktuell gespeicherten Nutzdaten.

T

Tail-Zeiger
Der Tail-Zeiger bezeichnet den letzten Knoten einer verketteten Folge. Er wird verwaltet, damit beim Einfügen am Ende ein vollständiger Durchlauf vermieden werden kann.
Eine verkettete Warteschlange setzt sowohl Kopf- als auch Tail-Zeiger auf null, wenn ihr einziger Knoten entfernt und freigegeben wird.
Teilbaum
Ein Teilbaum besteht aus einem Knoten und allen seinen Nachkommen und bildet einen kleineren Baum, auf den dieselben Such- und Aktualisierungsregeln angewendet werden können.
Die Einschränkung für den linken Teilbaum eines binären Suchbaums gilt sowohl für entfernte Nachkommen als auch für das direkte linke Kind.
tiefe Kopie
Eine tiefe Kopie erstellt neben dem äußeren Datensatz unabhängige Kopien der Daten im Besitz, auf die gezeigt wird. Für die Freigabe der neuen Speicherblöcke muss die Verantwortung festgelegt werden.
Das Kopieren eines Namens in einen neuen Speicherblock erlaubt es jedem Personendatensatz, seinen eigenen Namen ohne doppelte Speicherfreigabe freizugeben.
Traversierung
Eine Traversierung besucht Knoten, indem sie den Verweisen einer Struktur in einer festgelegten Reihenfolge folgt und am entsprechenden Ende oder beim Fall eines leeren Teilbaums anhält.
Ein separater Zeiger für den Listendurchlauf bewegt sich über next-Verweise, während der Besitzer den ursprünglichen Kopfzeiger behält.
typedef
Eine typedef-Deklaration führt einen Aliasnamen für einen bestehenden Typ ein, sodass Code einen kürzeren Namen verwenden kann, ohne einen eigenständigen Typ zu erzeugen.
Nach typedef struct reading reading; kann eine Deklaration reading *p für einen Zeiger auf diese Struktur verwenden.

U

Union
Eine Union ist ein C-Typ, dessen Komponenten Speicher gemeinsam nutzen, sodass dieser Bereich eine gespeicherte Alternative aus verschiedenen Komponententypen enthalten kann.
In einer Union mit einer Ganzzahl- und einer double-Komponente ersetzt das Schreiben des double-Werts die vorherigen ganzzahligen Nutzdaten, statt zwei Werte zu erhalten.
Unterlauf
Ein Unterlauf tritt auf, wenn ein Programm versucht, ein Element aus einer leeren Sammlung zu entfernen. Vor einem ungültigen Zugriff muss ein Fehlerergebnis zurückgegeben werden.
Das Entnehmen aus einem Stapel auf Arraybasis muss eine Elementanzahl von null prüfen, bevor sie verringert und das Array gelesen wird.

V

Vergleichsfunktion
Eine Vergleichsfunktion meldet die relative Reihenfolge zweier Werte durch ein negatives Ergebnis, null für Gleichstand oder ein positives Ergebnis.
Eine Temperaturvergleichsfunktion gibt nach dem Vergleich der Werte ein Vorzeichen zurück und vermeidet so das Abschneiden einer kleinen Differenz.

W

Warteschlange
Eine Warteschlange ist eine Sammlung, die hinten hinzufügt und vorn entfernt und so die Reihenfolge des Eintreffens unter den noch wartenden Werten erhält.
Eine verkettete Warteschlange fügt einen neuen Knoten hinter ihrem Tail-Zeiger hinzu und entfernt den Knoten, der aktuell über ihren Kopfzeiger erreicht wird.
Wertübergabe
Wertübergabe bedeutet, dass ein Funktionsparameter eine Kopie des übergebenen Werts erhält, sodass eine Änderung dieses Parameters den Wert des Aufrufers nicht ersetzt.
Eine Temperaturänderung an einem als Wert übergebenen Datensatzparameter lässt die Temperaturkomponente des Aufrufers unverändert.
Wurzel
Die Wurzel ist der Knoten, bei dem ein Baum oder Teilbaum beginnt. Sie wird über den Wurzelzeiger des Besitzers oder einen Kindverweis eines Elternknotens erreicht.
Beim Ersetzen einer gelöschten Wurzel mit einem Kind muss das verbleibende Kind als neue Wurzel des Baums gespeichert werden.

Z

Zeichenkettengleichheit
Zeichenkettengleichheit bedeutet, dass zwei nullterminierte Zeichenketten übereinstimmende Zeichenfolgen enthalten, unabhängig davon, ob sich ihre Speicheradressen unterscheiden.
Eine Suche in einer Worttabelle verwendet strcmp(e->key, key) == 0, um gleichen Text in separaten Speicherblöcken zu erkennen.
Zeiger auf einen Zeiger
Ein Zeiger auf einen Zeiger enthält die Adresse eines Zeigerobjekts und erlaubt einer Funktion, diesen Zeiger über die zusätzliche Ebene zu lesen oder zu ersetzen.
Ein node **link kann sich von der Adresse des Kopfzeigers zur Adresse des next-Feldes eines Knotens bewegen.
Zeiger auf konstante Daten
Ein Zeiger auf konstante Daten bietet Zugriff auf ein Objekt, das über diesen Zeiger nicht geändert werden kann, während der Zeiger selbst weiterhin veränderbar sein kann.
Eine Ausgabefunktion kann über const reading *p lesen, aber p->id keinen Wert zuweisen.
zusammenhängende Speicherung
Zusammenhängende Speicherung legt Arrayelemente in einer Folge im Speicher nebeneinander ab und unterstützt direkten Zugriff über Indizes sowie Durchläufe benachbarter Speicherstellen.
Ein Ganzzahlarray vermeidet die Verweiszeiger pro Knoten und den Aufwand separater Speicherallokationen einer Ganzzahlliste.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten