17.12 — Wielowymiarowe tablice w stylu C

Rozważ grę przypominającą Kółko i krzyżyk. Standardową planszą w tej grze jest siatka 3×3, na której gracze na zmianę umieszczają symbole „X” i „O”. Wygrywa ten, kto pierwszy zdobędzie trzy symbole z rzędu.

Chociaż możesz przechowywać dane tablicy jako 9 pojedynczych zmiennych, wiemy, że jeśli masz wiele wystąpień elementu, lepiej jest użyć tablicy:

int ttt[9]; // a C-style array of ints (value 0 = empty, 1 = player 1, 2 = player 2)

To definiuje tablicę w stylu C z 9 elementami ułożonymi sekwencyjnie w pamięci. Możemy sobie wyobrazić te elementy ułożone jako pojedynczy wiersz wartości, w następujący sposób:

// ttt[0] ttt[1] ttt[2] ttt[3] ttt[4] ttt[5] ttt[6] ttt[7] ttt[8]

Klasa wymiar tablicy to liczba indeksów potrzebnych do wybrania elementu. Tablica zawierająca tylko jeden wymiar nazywana jest tablicą jednowymiarową lub a tablicą jednowymiarową (czasami w skrócie 1d tablica). ttt powyżej jest przykładem tablicy jednowymiarowej, ponieważ elementy można wybierać za pomocą jednego indeksu (np. ttt[2]).

Ale zauważ, że nasza jednowymiarowa tablica nie przypomina naszej planszy do gry w kółko i krzyżyk, która istnieje w dwóch wymiarach. Można to zrobić lepiej.

Tablice dwuwymiarowe

W poprzednim z lekcji zauważyliśmy, że elementy tablicy mogą być dowolnego typu obiektowego. Oznacza to, że typem elementu tablicy może być inna tablica! Zdefiniowanie takiej tablicy jest proste:

int a[3][5]; // a 3-element array of 5-element arrays of int

Tablica tablic nazywana jest a tablicą dwuwymiarową (czasami w skrócie tablicą 2d), ponieważ ma dwa indeksy dolne.

W przypadku tablicy dwuwymiarowej wygodnie jest myśleć o pierwszym (lewym) indeksie jako wyborze wiersza, a drugim (prawym) jako indeksie. wybranie kolumny. Koncepcyjnie możemy wyobrazić sobie tę dwuwymiarową tablicę ułożoną w następujący sposób:

// col 0    col 1    col 2    col 3    col 4
// a[0][0]  a[0][1]  a[0][2]  a[0][3]  a[0][4]  row 0
// a[1][0]  a[1][1]  a[1][2]  a[1][3]  a[1][4]  row 1
// a[2][0]  a[2][1]  a[2][2]  a[2][3]  a[2][4]  row 2

Aby uzyskać dostęp do elementów dwuwymiarowej tablicy, po prostu używamy dwóch indeksów dolnych:

a[2][3] = 7; // a[row][col], where row = 2 and col = 3

W ten sposób w przypadku planszy kółko i krzyżyk możemy zdefiniować tablicę 2d w następujący sposób:

int ttt[3][3];

A teraz mamy siatkę elementów 3×3, którą możemy łatwo manipulować za pomocą wierszy i kolumn indeksy!

Tablice wielowymiarowe

Tablice z więcej niż jednym wymiarem nazywane są tablicami wielowymiarowymi.

C++ obsługuje nawet tablice wielowymiarowe z więcej niż 2 wymiarami:

int threedee[4][4][4]; // a 4x4x4 array (an array of 4 arrays of 4 arrays of 4 ints)

Na przykład teren w Minecrafcie jest podzielony na bloki o wymiarach 16x16x16 (tzw. sekcje fragmentów).

Tablice o wymiarach większych niż 3 są obsługiwane, ale rzadkie.

Jak tablice 2d są ułożone w pamięci

Pamięć jest liniowa (1-wymiarowa), więc tablice wielowymiarowe są w rzeczywistości przechowywane jako sekwencyjna lista elementów.

Istnieją dwa możliwe sposoby przechowywania następującej tablicy w pamięci:

// col 0   col 1   col 2   col 3   col 4
// [0][0]  [0][1]  [0][2]  [0][3]  [0][4]  row 0
// [1][0]  [1][1]  [1][2]  [1][3]  [1][4]  row 1
// [2][0]  [2][1]  [2][2]  [2][3]  [2][4]  row 2

C++ używa wiersz-główny porządek, gdzie elementy są sekwencyjnie umieszczane w pamięci wiersz po rzędzie, od lewej do prawej, od góry do dołu:

[0][0] [0][1] [0][2] [0][3] [0][4] [1][0] [1][1] [1][2] [1][3] [1][4] [2][0] [2][1] [2][2] [2][3] [2][4]

Niektóre inne języki (np. Fortran) używają kolejności głównej kolumny, elementy są sekwencyjnie umieszczane w pamięci kolumna po kolumnie, od góry do dołu, od lewej do po prawej:

[0][0] [1][0] [2][0] [0][1] [1][1] [2][1] [0][2] [1][2] [2][2] [0][3] [1][3] [2][3] [0][4] [1][4] [2][4]

W C++ podczas inicjowania tablicy elementy są inicjowane w kolejności od głównego wiersza. A podczas przechodzenia przez tablicę najskuteczniejszy jest dostęp do elementów w kolejności, w jakiej są ułożone w pamięci.

Inicjowanie tablic dwuwymiarowych

Aby zainicjować tablicę dwuwymiarową, najłatwiej jest użyć zagnieżdżonych nawiasów klamrowych, przy czym każdy zestaw liczb reprezentuje wiersz:

int array[3][5]
{
  { 1, 2, 3, 4, 5 },     // row 0
  { 6, 7, 8, 9, 10 },    // row 1
  { 11, 12, 13, 14, 15 } // row 2
};

Chociaż niektóre kompilatory pozwalają pominąć wewnętrzne nawiasy klamrowe, zdecydowanie zalecamy ich mimo to dołączenie dla czytelności

W przypadku użycia nawiasów wewnętrznych brakujące inicjatory zostaną zainicjowane wartością:

int array[3][5]
{
  { 1, 2 },          // row 0 = 1, 2, 0, 0, 0
  { 6, 7, 8 },       // row 1 = 6, 7, 8, 0, 0
  { 11, 12, 13, 14 } // row 2 = 11, 12, 13, 14, 0
};

Zainicjowana tablica wielowymiarowa może pominąć (tylko) specyfikację długości znajdującą się najbardziej po lewej stronie:

int array[][5]
{
  { 1, 2, 3, 4, 5 },
  { 6, 7, 8, 9, 10 },
  { 11, 12, 13, 14, 15 }
};

W takich przypadkach kompilator może wykonać obliczenia matematyczne, aby dowiedzieć się, jaka jest długość znajdująca się najbardziej po lewej stronie z liczby inicjatorów.

Pomijanie wymiarów innych niż lewe nie jest dozwolone:

int array[][] 
{
  { 1, 2, 3, 4 },
  { 5, 6, 7, 8 }
};

Podobnie jak zwykłe tablice, tablice wielowymiarowe można nadal inicjalizować do 0 w następujący sposób:

int array[3][5] {};

Dwuwymiarowe tablice i pętle

W przypadku jednowymiarowej tablicy możemy użyć pojedynczej pętli do iteracji po wszystkich elementach tablicy:

#include <iostream>

int main()
{
    int arr[] { 1, 2, 3, 4, 5 };

    // for-loop with index
    for (std::size_t i{0}; i < std::size(arr); ++i)
        std::cout << arr[i] << ' ';

    std::cout << '\n';

    // range-based for-loop
    for (auto e: arr)
        std::cout << e << ' ';

    std::cout << '\n';

    return 0;
}

W przypadku dwuwymiarowej tablicy potrzebujemy dwóch pętli: jednej do wybrania wiersza, a drugiej do wybrania kolumny.

W przypadku dwóch pętli musimy również określić, która pętla będzie zewnętrzną pętlą, a która będzie być pętlą wewnętrzną. Najbardziej efektywny jest dostęp do elementów w kolejności, w jakiej są ułożone w pamięci. Ponieważ C++ używa kolejności głównych wierszy, selektor wierszy powinien być pętlą zewnętrzną, a selektor kolumn powinien być pętlą wewnętrzną.

#include <iostream>

int main()
{
    int arr[3][4] { 
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }};

    // double for-loop with indices
    for (std::size_t row{0}; row < std::size(arr); ++row) // std::size(arr) returns the number of rows
    {
        for (std::size_t col{0}; col < std::size(arr[0]); ++col) // std::size(arr[0]) returns the number of columns
            std::cout << arr[row][col] << ' ';

        std::cout << '\n';
    }

    // double range-based for-loop
    for (const auto& arow: arr)   // get each array row
    {
        for (const auto& e: arow) // get each element of the row
            std::cout << e << ' ';

        std::cout << '\n';
    }

    return 0;
}

Przykład dwuwymiarowej tablicy

Przyjrzyjmy się praktycznemu przykładowi dwuwymiarowej tablicy:

#include <iostream>

int main()
{
    constexpr int numRows{ 10 };
    constexpr int numCols{ 10 };

    // Declare a 10x10 array
    int product[numRows][numCols]{};

    // Calculate a multiplication table
    // We don't need to calc row and col 0 since mult by 0 always is 0
    for (std::size_t row{ 1 }; row < numRows; ++row)
    {
        for (std::size_t col{ 1 }; col < numCols; ++col)
        {
            product[row][col] = static_cast<int>(row * col);
        }
     }

    for (std::size_t row{ 1 }; row < numRows; ++row)
    {
        for (std::size_t col{ 1 }; col < numCols; ++col)
        {
            std::cout << product[row][col] << '\t';
        }

        std::cout << '\n';
     }


    return 0;
}

Ten program oblicza i drukuje tabliczkę mnożenia dla wszystkich wartości od 1 do 9 (włącznie). Zauważ, że podczas drukowania tabeli pętle for zaczynają się od 1 zamiast od 0. Ma to na celu pominięcie drukowania kolumny 0 i wiersza 0, co byłoby po prostu pęczkiem zer! Oto wynik:

1    2    3    4    5    6    7    8    9
2    4    6    8    10   12   14   16   18
3    6    9    12   15   18   21   24   27
4    8    12   16   20   24   28   32   36
5    10   15   20   25   30   35   40   45
6    12   18   24   30   36   42   48   54
7    14   21   28   35   42   49   56   63
8    16   24   32   40   48   56   64   72
9    18   27   36   45   54   63   72   81

Współrzędne kartezjańskie a indeksy tablicy

W geometrii Kartezjański układ współrzędnych jest często używany do opisu położenia obiektów. W dwóch wymiarach mamy dwie osie współrzędnych, umownie nazywane „x” i „y”. „x” to oś pozioma, a „y” to oś pionowa.

Kartezjański Schemat układu współrzędnych

W dwóch wymiarach kartezjańskie położenie obiektu można opisać jako parę {x, y}, gdzie współrzędna x i współrzędna y to wartości wskazujące, jak daleko na prawo od osi x i jak daleko nad osią y znajduje się obiekt. Czasami oś Y jest odwracana (tak, że współrzędna Y opisuje, jak daleko poniżej osi Y coś się znajduje).

Przyjrzyjmy się teraz naszemu układowi tablicy 2D w C++:

// col 0   col 1   col 2   col 3   col 4
// [0][0]  [0][1]  [0][2]  [0][3]  [0][4]  row 0
// [1][0]  [1][1]  [1][2]  [1][3]  [1][4]  row 1
// [2][0]  [2][1]  [2][2]  [2][3]  [2][4]  row 2

Jest to również dwuwymiarowy układ współrzędnych, w którym położenie elementu można opisać jako [wiersz][col] (gdzie odwrócona jest oś col).

Podczas gdy każdy z nich układy współrzędnych są dość łatwe do zrozumienia niezależnie, konwersja z kartezjańskiego { x, y } na indeksy tablicy [wiersz][kol] jest nieco sprzeczna z intuicją.

Kluczowym spostrzeżeniem jest to, że współrzędna x w układzie kartezjańskim opisuje, która kolumny jest wybierana w systemie indeksowania tablic. I odwrotnie, współrzędna y opisuje, który wiersz jest wybierany. Dlatego współrzędne kartezjańskie { x, y } przekładają się na współrzędne tablicy [y][x], czyli odwrotnie niż moglibyśmy się spodziewać!

Prowadzi to do pętli 2d, które wyglądają tak:

    for (std::size_t y{0}; y < std::size(arr); ++y) // outer loop is rows / y
    {
        for (std::size_t x{0}; x < std::size(arr[0]); ++x) // inner loop is columns / x
            std::cout << arr[y][x] << ' '; // index with y (row) first, then x (col)

Zauważ, że w tym przypadku indeksujemy tablicę jako a[y][x], co prawdopodobnie jest odwrotne od oczekiwanego porządku alfabetycznego.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
200 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze