Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

STL-Iteratoren und Algorithmen in C++: Eine praktische Einführung mit Beispielen aus Gaming und KI

Lerne, wie du mit STL-Iteratoren und Algorithmen in C++ effiziente, generische Funktionen schreibst – von Statistiken bis zur Tokenisierung. Mit aktuellen Beispielen aus Gaming und KI.

STL Iteratoren C++ C++ Algorithmen Tutorial range_sum Implementierung range_avg C++ print_range Funktion Histogramm C++ Tokenisierung C++ Forward-Iterator generische Programmierung C++ CSCI 340 Assignment 2a C++ für Spieleentwicklung KI-Programmierung C++ Datenanalyse mit C++ C++ Übungen Uni C++ Iteratoren verstehen C++ Algorithmen selbst schreiben

Einführung: Warum Iteratoren und Algorithmen?

In der modernen C++-Programmierung sind Iteratoren und Algorithmen aus der Standard Template Library (STL) unverzichtbar. Sie ermöglichen es, generische und wiederverwendbare Funktionen zu schreiben, die auf verschiedenen Containern wie std::vector, std::list oder std::string arbeiten. Im Rahmen der CSCI 340 Assignment 2a geht es darum, eigene Algorithmen zu implementieren, die mit Iteratoren arbeiten. Dieses Tutorial führt dich Schritt für Schritt durch die wichtigsten Konzepte und zeigt dir, wie du diese in der Praxis anwendest.

Stell dir vor, du entwickelst ein KI-Modell, das große Datenmengen analysiert – oder du programmierst ein Gaming-Scoreboard, das Spielerstatistiken berechnet. Genau hier kommen Iteratoren ins Spiel: Sie erlauben dir, flexibel über Daten zu iterieren, ohne dich auf einen bestimmten Containertyp festzulegen.

Grundlagen: Iteratoren verstehen

Ein Iterator ist ein Objekt, das auf ein Element in einem Container zeigt und sich bewegen lässt. Es gibt verschiedene Kategorien: Forward-Iteratoren, Bidirectional-Iteratoren und Random-Access-Iteratoren. In dieser Aufgabe arbeiten wir hauptsächlich mit Forward-Iteratoren, die nur vorwärts bewegt werden können. Das ist bewusst so gewählt, um die Algorithmen so generisch wie möglich zu halten.

Typische Operationen sind:

  • *it – Zugriff auf das aktuelle Element
  • ++it – Bewegung zum nächsten Element
  • begin und end – Start- und Enditerator eines Bereichs

Ein häufiger Fehler ist, Iteratoren zurückzusetzen oder mit -- zu bewegen. Da Forward-Iteratoren dies nicht unterstützen, musst du dich auf das Vorwärtsbewegen beschränken.

Implementierung der Statistikfunktionen

Die erste Gruppe von Funktionen berechnet einfache statistische Kennzahlen über einen Bereich. Alle Funktionen sind als Template-Funktionen definiert, sodass sie mit jedem Container und Elementtyp funktionieren, der die nötigen Operationen unterstützt.

range_sum

Diese Funktion summiert alle Elemente im Bereich [begin, end). Der Rückgabetyp sollte dem Elementtyp entsprechen. Ein Beispiel für die Implementierung:

template <typename Iterator>auto range_sum(Iterator begin, Iterator end) -> decltype(*begin) {    auto sum = *begin;    ++begin;    while (begin != end) {        sum += *begin;        ++begin;    }    return sum;}

Beachte, dass wir das erste Element vor der Schleife initialisieren, um den Typ korrekt abzuleiten. Alternativ könntest du std::iterator_traits verwenden, aber für Forward-Iteratoren reicht diese einfache Methode.

range_avg

Der Durchschnitt wird als double zurückgegeben. Du musst sowohl die Summe als auch die Anzahl der Elemente zählen. Achte darauf, dass du die Summe als double konvertierst, um eine Ganzzahldivision zu vermeiden.

template <typename Iterator>double range_avg(Iterator begin, Iterator end) {    double sum = 0.0;    size_t count = 0;    while (begin != end) {        sum += *begin;        ++count;        ++begin;    }    return sum / count;}

range_minval und range_maxval

Diese Funktionen finden das Minimum bzw. Maximum im Bereich. Initialisiere die Variable mit dem ersten Element und aktualisiere sie bei Bedarf.

template <typename Iterator>auto range_minval(Iterator begin, Iterator end) -> decltype(*begin) {    auto min = *begin;    ++begin;    while (begin != end) {        if (*begin < min) min = *begin;        ++begin;    }    return min;}

Analog für range_maxval mit >.

range_count

Zählt die Anzahl der Elemente im Bereich. Einfach eine Schleife mit Zähler.

template <typename Iterator>size_t range_count(Iterator begin, Iterator end) {    size_t count = 0;    while (begin != end) {        ++count;        ++begin;    }    return count;}

Formatierte Ausgabe mit print_range

Die Funktion print_range gibt die Elemente eines Bereichs formatiert aus. Sie benötigt Parameter für den Ausgabestream, die Iteratoren sowie Präfix, Trennzeichen, Suffix und eine optionale Breite. Mit std::setw kannst du die Ausgabe in Spalten ausrichten.

template <typename Iterator>void print_range(std::ostream& ost, Iterator begin, Iterator end,                 const std::string& pre, const std::string& sep,                 const std::string& post, int width) {    ost << pre;    bool first = true;    while (begin != end) {        if (!first) ost << sep;        if (width > 0) ost << std::setw(width);        ost << *begin;        first = false;        ++begin;    }    ost << post;}

Diese Funktion ist extrem nützlich, um z.B. Spielergebnisse in einer Tabelle auszugeben – ähnlich wie ein E-Sport-Scoreboard, das aktuelle Punktzahlen anzeigt.

Histogramm-Funktion

Ein Histogramm verteilt Daten in Bins (Kategorien). Du erhältst einen Bereich, ein Array/Vector für die Zähler, die Anzahl der Bins und einen Divisor. Der Index eines Elements ergibt sich aus element / divisor. Liegt der Index außerhalb von [0, N-1], wird er ignoriert.

template <typename Iterator, typename Container>void histogram(Iterator begin, Iterator end, Container& nums,               size_t N, int divisor) {    // Zähler zurücksetzen    for (size_t i = 0; i < N; ++i) nums[i] = 0;    while (begin != end) {        int index = *begin / divisor;        if (index >= 0 && index < static_cast<int>(N)) {            ++nums[index];        }        ++begin;    }}

Ein praktisches Beispiel: Stell dir vor, du analysierst die Ladezeiten einer KI-gestützten App in Millisekunden. Mit einem Histogramm kannst du sehen, wie viele Ladevorgänge in bestimmte Zeitfenster fallen – z.B. 0-100ms, 100-200ms usw.

Tokenisierung: Sequenzen aufteilen

Die zweite Gruppe von Algorithmen dient dazu, eine lange Sequenz in kleinere Tokens zu zerlegen. Dies wird oft in der Textverarbeitung verwendet, z.B. um einen Satz in Wörter zu teilen. Auch hier arbeiten wir nur mit Forward-Iteratoren.

Ein typischer Algorithmus ist split, der eine Sequenz anhand eines Trennzeichens in Teilbereiche aufteilt. Da die genaue Signatur nicht im Auszug steht, nehmen wir an, dass du eine Funktion schreibst, die einen Bereich und ein Trennzeichen akzeptiert und die Token in einen Ausgabecontainer einfügt.

template <typename InputIt, typename OutputIt, typename T>void split(InputIt begin, InputIt end, OutputIt out, T delimiter) {    InputIt tokenStart = begin;    while (begin != end) {        if (*begin == delimiter) {            *out = std::make_pair(tokenStart, begin);            ++out;            ++begin;            tokenStart = begin;        } else {            ++begin;        }    }    // Letztes Token    if (tokenStart != begin) {        *out = std::make_pair(tokenStart, begin);    }}

Diese Art von Algorithmus wird z.B. in Chatbots verwendet, um Benutzereingaben in Befehle zu zerlegen – ein Thema, das durch den aktuellen KI-Hype besonders relevant ist.

Praktische Tipps und häufige Fehler

  • Iteratoren nicht zurücksetzen: Da Forward-Iteratoren keine -- Operation unterstützen, musst du mit einer einzigen Vorwärtsschleife auskommen. Speichere dir ggf. den Startiterator in einer Kopie, falls du später nochmal darauf zugreifen musst.
  • Typkorrektheit: Verwende auto und decltype, um den Rückgabetyp flexibel zu halten. Für numerische Typen ist auto ausreichend, aber bei range_avg musst du explizit double verwenden.
  • Keine Container-spezifischen Annahmen: Deine Algorithmen sollten nur über Iteratoren auf die Daten zugreifen. Verwende keine Funktionen wie .size() oder .at(), da diese nicht für alle Container gleich sind.
  • Testen mit verschiedenen Containern: Probiere deine Funktionen mit std::vector, std::list und std::string aus. Das hilft, Fehler zu finden, die nur bei bestimmten Iteratorkategorien auftreten.

Zusammenfassung und Ausblick

Mit diesen Grundlagen bist du in der Lage, die geforderten Funktionen für die CSCI 340 Assignment 2a zu implementieren. Du hast gelernt, wie man mit Forward-Iteratoren arbeitet, statistische Kennzahlen berechnet, formatierte Ausgaben erzeugt und Sequenzen tokenisiert. Diese Konzepte sind nicht nur für die Uni wichtig, sondern auch in der Praxis – sei es in der Spieleentwicklung, bei KI-Anwendungen oder in der Finanzanalyse.

Denk daran, deine Änderungen in einem separaten development-Branch zu committen und den main-Branch unberührt zu lassen. Am Ende reichst du einen Pull-Request ein, um deine Arbeit abzugeben. Viel Erfolg!