Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

Doppelt verkettete Liste in C++: Implementierung einer Queue und eines Stack mit Iteratoren

Lerne Schritt für Schritt, wie du eine doppelt verkettete Liste in C++ implementierst – inklusive Queue- und Stack-Operationen sowie einem Iterator. Perfekt für die Vorbereitung auf die COP3504C Lab 09.

doppelt verkettete Liste C++ LinkedList Implementierung Queue Stack C++ Iterator C++ COP3504C Lab 09 Lösung templatisierte Liste C++ C++ Datenstrukturen Tutorial verkettete Liste Anfänger C++ Speicherverwaltung Pointer C++ üben LinkedList Methoden enqueue dequeue pop C++ remove Methode doppelt verkettete Liste Iterator C++ linked list Beispiel ZyLabs C++ Abgabe C++ Programmierung Studium

Einführung in doppelt verkettete Listen

Stell dir vor, du organisierst eine Warteschlange für das neueste KI-Update deiner Lieblings-App – jeder Nutzer hat eine eindeutige ID und eine Position. Genau so funktioniert eine doppelt verkettete Liste: Jedes Element (Node) kennt seinen Vorgänger und Nachfolger. Das ermöglicht effizientes Einfügen und Löschen an beiden Enden – perfekt für Queue (FIFO) und Stack (LIFO). In diesem Tutorial implementierst du eine templatisierte, doppelt verkettete Liste in C++, wie sie in der COP3504C Lab 09 gefordert wird. Du wirst sehen, wie du die Liste als Queue oder Stack nutzen kannst und einen Iterator schreibst, der dir erlaubt, die Elemente zu durchlaufen.

Grundlagen: Node und LinkedList

Jeder Knoten in der Liste speichert Daten (z.B. Koordinatenpaare) und zwei Zeiger: prev und next. Der front-Zeiger zeigt auf den ersten, back auf den letzten Knoten. Leere Listen zeigen auf nullptr. Hier ein minimales Gerüst:

template <typename T>
struct Node {
    T data;
    Node* prev;
    Node* next;
    Node(const T& value) : data(value), prev(nullptr), next(nullptr) {}
};

template <typename T>
class LinkedList {
private:
    Node<T>* head;
    Node<T>* tail;
public:
    LinkedList() : head(nullptr), tail(nullptr) {}
    // ... Methoden
};

Queue-Operationen: enqueue und dequeue

Eine Queue funktioniert wie eine Ticket-Warteschlange beim Gaming-Event: Neue Spieler reihen sich hinten ein (enqueue), der vorderste wird als nächster bedient (dequeue). In der doppelt verketteten Liste fügst du am Ende (tail) ein und entfernst vom Anfang (head). Achte darauf, die Zeiger korrekt zu aktualisieren und Sonderfälle (leere Liste, ein Element) zu behandeln.

void enqueue(const T& element) {
    Node<T>* newNode = new Node<T>(element);
    if (isEmpty()) {
        head = tail = newNode;
    } else {
        tail->next = newNode;
        newNode->prev = tail;
        tail = newNode;
    }
}

void dequeue() {
    if (isEmpty()) return;
    Node<T>* temp = head;
    head = head->next;
    if (head) head->prev = nullptr;
    else tail = nullptr;
    delete temp;
}

Stack-Operationen: pop

Ein Stack verhält sich wie der Verlauf in deinem Browser – der letzte Eintrag wird zuerst angezeigt (LIFO). Für einen Stack fügst du am Ende ein (wie bei enqueue) und entfernst mit pop das letzte Element. Hier entfernst du vom tail:

void pop() {
    if (isEmpty()) return;
    Node<T>* temp = tail;
    tail = tail->prev;
    if (tail) tail->next = nullptr;
    else head = nullptr;
    delete temp;
}

Iterator-Klasse: Durch die Liste navigieren

Ein Iterator erlaubt es, die Liste wie einen Container der STL zu durchlaufen. Du implementierst begin(), end() und die Operatoren *, ++, --, ==, !=. Der end()-Iterator zeigt auf einen speziellen Zustand (z.B. nullptr). Hier ein Ausschnitt:

class Iterator {
private:
    Node<T>* current;
public:
    Iterator(Node<T>* node) : current(node) {}
    T operator*() const { return current->data; }
    Iterator& operator++() {
        if (current) current = current->next;
        return *this;
    }
    Iterator& operator--() {
        if (current) current = current->prev;
        return *this;
    }
    bool operator==(const Iterator& other) const { return current == other.current; }
    bool operator!=(const Iterator& other) const { return current != other.current; }
};

Iterator begin() const { return Iterator(head); }
Iterator end() const { return Iterator(nullptr); }

Weitere Methoden: contains, remove, clear

Mit contains(T element) prüfst du, ob ein bestimmtes Element in der Liste existiert – nützlich, wenn du z.B. in einer Finanz-App prüfen willst, ob eine Transaktions-ID bereits verarbeitet wurde. remove(T element) löscht den ersten Knoten mit diesem Wert. Hier musst du vier Fälle unterscheiden: Löschen des ersten, eines mittleren, des letzten oder des einzigen Knotens. clear() löscht alle Knoten – eine Schleife, die jeden Knoten einzeln freigibt.

Häufige Fehler und Tipps

Vergiss nicht, nach jeder Modifikation die Zeiger konsistent zu halten. Nutze Memory-Leak-Detection (z.B. nvwa) und teste mit dem mitgelieferten main.cpp. Achte auf exakte Ausgabe – ZyLabs vergleicht Zeichen für Zeichen. Verwende const wo sinnvoll, z.B. bei getFront() und getBack(). Der Iterator muss nach end() zeigen, wenn er über das Ende hinausläuft.

Fazit

Mit dieser Implementierung hast du eine flexible doppelt verkettete Liste, die du sowohl als Queue als auch als Stack nutzen kannst. Das Verständnis von Zeigern und dynamischem Speicher ist essenziell für fortgeschrittene C++-Programmierung – und ein echter Gamechanger, wenn du später eigene Datenstrukturen für KI-Algorithmen oder Echtzeitsysteme entwickelst. Viel Erfolg bei deiner Abgabe!