Zum Inhalt springen
Topos Uranos

Zusammenfassung

Viele Daten bilden keine Reihe, sondern ein Raster: die Noten eines Kurses nach Studierenden und nach Prüfungen, ein Spielbrett, eine Matrix. Diese Lektion stellt die Tabelle vor, die zweidimensionale Struktur, in ihren beiden Formen in C++: den Vektor von Vektoren, dessen Größe zur Laufzeit festgelegt wird, und das Array von Arrays, dessen Größe zur Kompilierzeit bekannt ist. Es folgen die Durchgänge nach Zeilen und nach Spalten; die Transponierte und das Matrizenprodukt, mit kubischen Kosten; die Nachbarn einer Zelle, die dazu zwingen, auf die Ränder zu achten; und die Tabelle, die mit dem Index i * columns + j in einem einzigen Vektor gespeichert wird, so wie der Speicher sie sieht.

Lernziele

  1. Eine Tabelle als std::vector<std::vector<T>> oder als std::array von std::array darstellen und zwischen beiden danach wählen, ob die Größe zur Kompilierzeit bekannt ist oder nicht.
  2. Eine Tabelle nach Zeilen und nach Spalten durchlaufen und die Summen und die Mittelwerte jeder Zeile und jeder Spalte berechnen.
  3. Operationen auf Tabellen implementieren (die Transponierte, das Matrizenprodukt, die Zählung der Nachbarn einer Zelle) und dabei darauf achten, dass kein Index über die Ränder hinausgeht.
  4. Eine Tabelle mit dem Index i * columns + j in einem flachen Vektor darstellen und an einem Beispiel die Entsprechung zwischen der Zelle und ihrer Position erklären.

Tabellen

Zeilen und Spalten

Eine Tabelle mit mm Zeilen und nn Spalten ist eine Sammlung von m⋅nm \cdot n Daten desselben Typs, von denen jedes durch zwei Indizes bezeichnet wird: den seiner Zeile, ii, mit 0≤i<m0 \leq i < m, und den seiner Spalte, jj, mit 0≤j<n0 \leq j < n. Die Mathematik nennt sie Matrix und schreibt aija_{ij}; der Kurs Algebra und Geometrie II untersucht die Matrizen und ihre Operationen, und diese Lektion schreibt einige davon als Programme. Die Reihenfolge der Indizes lässt keine Ausnahme zu: zuerst die Zeile, dann die Spalte. Sie zu verwechseln ist der häufigste Fehler bei Tabellen, und der Compiler kann davor nicht warnen, weil beide Indizes denselben Typ haben.

Die Lektion Vektoren hat die Folge variabler Größe vorgestellt. Wenn dem so ist, dann ist eine Tabelle eine Folge von Zeilen, und jede Zeile ist ihrerseits eine Folge: ein Vektor, dessen Elemente Vektoren sind, std::vector<std::vector<int>>.

C++-Programm
#include <print>#include <vector>​int main(){    std::vector<std::vector<int>> table{{1, 2, 3}, {4, 5, 6}};    std::println("{}", table);    std::println("Zeilen {}, Spalten {}", table.size(), table[0].size());    std::println("Zeile 1: {}; Zeile 1, Spalte 2: {}", table[1], table[1][2]);    table[0][1] = 20;    for (const std::vector<int>& row : table) {        for (const int value : row) {            std::print("{:>4}", value);        }        std::println("");    }    const std::vector<std::vector<int>> zeros(3, std::vector<int>(4));    std::println("{}", zeros);}
Ausgabe
[[1, 2, 3], [4, 5, 6]]Zeilen 2, Spalten 3Zeile 1: [4, 5, 6]; Zeile 1, Spalte 2: 6   1  20   3   4   5   6[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

Programmzeile 6 schreibt die Tabelle als Liste von Zeilen, jede in ihren geschweiften Klammern, und die Formatierung von Ranges gibt sie ebenso aus, mit eckigen Klammern. table.size() ist die Zahl der Zeilen, und table[0].size() die Länge der ersten, die in einer wohlgeformten Tabelle die Zahl der Spalten ist. table[1] ist die ganze Zeile 1, ein std::vector<int>, und table[1][2] das Element 2 dieser Zeile: Der doppelte Index ist keine neue Operation, sondern zwei aufeinanderfolgende Zugriffe; der erste wählt die Zeile und der zweite, innerhalb von ihr, die Spalte. Die Programmzeilen 11 bis 16 schreiben die Tabelle als Raster mit zwei verschachtelten bereichsbasierten for-Schleifen; die Tabellenzeile wird per konstanter Referenz genommen, damit sie nicht in jedem Durchlauf kopiert wird. Programmzeile 17 legt eine Tabelle von 3 mal 4 voller Nullen an: Mit runden Klammern werden, wie in der Lektion Vektoren, 3 Kopien einer Zeile aus vier Nullen angefordert.

BeispielNach Zeilen und nach Spalten durchlaufen

Eine Tabelle von 3 mal 4, Zeile für Zeile durchlaufen (i in der äußeren Schleife) und danach Spalte für Spalte (die Schleifen vertauscht). Jede Zelle zeigt die Nummer, mit der sie an die Reihe kommt.

Beweis

  1. Die Tabelle, mit den Indizes der Zeilen links und denen der Spalten oben.

  2. Nach Zeilen: Bei i=0i = 0 durchläuft die innere Schleife j=0,1,2,3j = 0, 1, 2, 3.

  3. Bei i=1i = 1 beginnt die innere Schleife wieder bei j=0j = 0.

  4. Bei i=2i = 2: Die Tabelle wurde durchlaufen, wie man einen Text liest, Zeile um Zeile.

  5. Nach Spalten: Bei j=0j = 0 durchläuft die innere Schleife i=0,1,2i = 0, 1, 2.

  6. Bei j=1j = 1 die zweite Spalte.

  7. Bei j=2j = 2.

  8. Bei j=3j = 3. Jede Zelle wurde wieder genau einmal besucht: Das Vertauschen der Schleifen ändert nicht, was besucht wird, sondern wann.

Zeilen verschiedener Länge

Nichts zwingt die Zeilen eines Vektors von Vektoren, gleich lang zu sein: Jede ist ein unabhängiger Vektor. Manchmal ist das gewollt, wie beim Pascalschen Dreieck; aber in einer Tabelle, die per Definition rechteckig ist, ist eine kürzere Zeile ein Fehler in den Daten, und das Programm, das table[0].size() als Zahl der Spalten benutzt, liest über sie hinaus. Eine Tabelle, die von außen kommt, muss daher geprüft werden.

C++-Programm
#include <print>#include <vector>​// Vorbedingung: keine.// Nachbedingung: gibt true zurück, wenn table mindestens eine Zeile hat und// alle ihre Zeilen die Länge der ersten haben.bool isRectangular(const std::vector<std::vector<int>>& table){    if (table.empty()) {        return false;    }    for (const std::vector<int>& row : table) {        if (row.size() != table[0].size()) {            return false;        }    }    return true;}​int main(){    const std::vector<std::vector<int>> pascal{{1}, {1, 1}, {1, 2, 1}, {1, 3, 3, 1}};    const std::vector<std::vector<int>> grid{{1, 2, 3}, {4, 5, 6}};    std::println("{} {}", pascal, isRectangular(pascal));    std::println("{} {}", grid, isRectangular(grid));}
Ausgabe
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]] false[[1, 2, 3], [4, 5, 6]] true

Die Funktion schließt die Tabelle ohne Zeilen aus, in der table[0] nicht existiert. Die Funktionen dieser Lektion, die table[0].size() lesen, verlangen in ihrer Vorbedingung eine rechteckige, nicht leere Tabelle, und isRectangular erlaubt es, sie sicherzustellen.

Das Array von Arrays

Wenn die Dimensionen zur Kompilierzeit bekannt sind (ein Schachbrett, ein magisches Quadrat), kann die Tabelle ein std::array von std::array sein: std::array<std::array<int, 3>, 3> liest man von innen nach außen, „3 Zeilen zu 3 ganzen Zahlen“. Als Beispiel dient das magische Quadrat der Ordnung 3, dessen Zeilen, Spalten und Diagonalen die Summe 15 haben.

C++-Programm
#include <array>#include <print>​int main(){    constexpr std::array<std::array<int, 3>, 3> magic{{        {2, 7, 6},        {9, 5, 1},        {4, 3, 8},    }};    std::println("{}", magic);    std::println("Mitte {}, {} Bytes", magic[1][1], sizeof(magic));}
Ausgabe
[[2, 7, 6], [9, 5, 1], [4, 3, 8]]Mitte 5, 36 Bytes

Die geschweiften Klammern sind an jedem Ende drei. std::array hat ein einziges Member, ein C-Array, sodass die äußere Klammer die von std::array ist, die zweite die dieses inneren Arrays und erst die dritte die jeder Zeile. Mit zwei Klammern wird die erste Zeile als Initialisierer des inneren Arrays genommen, und die zweite hat nichts mehr zu initialisieren.

C++-Programm
#include <array>#include <print>​int main(){    const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}};    std::println("{}", table);}
Fehlermeldungen des Compilers
main.cpp: In function ‘int main()’:main.cpp:6:71: error: too many initializers for ‘const std::array<std::array<int, 3>, 2>’    6 |     const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}};      |                                                                       ^

Too many initializers: Die Liste {4, 5, 6} ist überzählig. Die flache Liste, {1, 2, 3, 4, 5, 6}, kompiliert ebenfalls und füllt die Zeilen der Reihe nach, verbirgt aber die Form der Tabelle; der Kurs schreibt die drei Klammern.

Die 36 Bytes des sizeof sind die von neun int zu 4 Bytes (die übliche Größe, die der Standard nicht garantiert), ohne irgendetwas sonst: Das Array von Arrays speichert die Zeilen hintereinander in einem einzigen Block. Der Vektor von Vektoren dagegen fordert einen Block pro Zeile an, zusätzlich zu dem der Zeilen, und diese Blöcke müssen nicht zusammenhängen. Die Wahl folgt daher der Regel der Lektionen Arrays und Vektoren.

  • Sind beide Dimensionen zur Kompilierzeit bekannt, das Array von Arrays: ein einziger Block, die durch den Typ garantierte Rechteckform und die Möglichkeit von constexpr.
  • Ist eine davon erst zur Laufzeit bekannt, der Vektor von Vektoren, mit der Pflicht zu prüfen, dass die Zeilen gleich lang sind, oder der flache Vektor aus dem letzten Teil der Lektion.