Tablice dwuwymiarowe
Tablice pozwalają przechowywać uporządkowane dane w pamięci w sposób ciągły i indeksowany. W przypadku danych tabelarycznych, macierzy, plansz czy siatek obliczeniowych pojedynczy indeks przestaje wystarczać. Wtedy stosuje się strukturę, w której element identyfikowany jest przez dwa indeksy – wiersz i kolumnę. W językach niskopoziomowych oznacza to specyficzny sposób organizacji pamięci oraz indeksowania. W dalszej części tablice dwuwymiarowe będą analizowane głównie w kontekście języka C.
Spis treści
Tablice dwuwymiarowe jako liniowy obszar pamięci interpretowany przez dwa indeksy
Tablica dwuwymiarowa w C nie jest „tablicą tablic” w sensie logicznym, lecz jednym ciągłym blokiem pamięci. Kompilator przelicza dwa indeksy na jedno przesunięcie względem początku struktury.
Deklaracja:
int macierz[3][4];
oznacza tablicę o 3 wierszach i 4 kolumnach.
Jeżeli int zajmuje 4 bajty, to cała struktura zajmuje:
3 * 4 * 4 = 48 bajtów
W pamięci dane ułożone są w kolejności wierszami (row-major order):
macierz[0][0]
macierz[0][1]
macierz[0][2]
macierz[0][3]
macierz[1][0]
...
Dostęp:
macierz[i][j]
jest tłumaczony przez kompilator na:
*( *(macierz + i) + j )
Indeksowanie działa według wzoru:
adres = baza + (i * liczba_kolumn + j) * sizeof(int)
Liczba kolumn musi być znana kompilatorowi, aby mógł obliczyć przesunięcie.
Tablice dwuwymiarowe – deklaracja, tworzenie tablic dwuwymiarowych i inicjalizacja
Tworzenie tablic dwuwymiarowych może odbywać się statycznie lub dynamicznie.
Deklaracja statyczna
int macierz[2][3];
Inicjalizacja przy deklaracji:
int macierz[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
Możliwe jest również zapisanie liniowe:
int macierz[2][3] = {1, 2, 3, 4, 5, 6};
Kompilator przypisze wartości kolejno wierszami.
Jeżeli liczba elementów jest mniejsza niż rozmiar, pozostałe zostaną wyzerowane.
Można pominąć pierwszy wymiar:
int macierz[][3] = {
{1, 2, 3},
{4, 5, 6}
};
Nie można pominąć drugiego wymiaru – jest potrzebny do obliczeń adresów.
Uzupełnianie tablic dwuwymiarowych przy użyciu pętli zagnieżdżonych
Uzupełnianie tablic dwuwymiarowych odbywa się zwykle za pomocą dwóch pętli.
Przykład:
#include <stdio.h>
int main() {
int macierz[2][3];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
scanf("%d", &macierz[i][j]);
}
}
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", macierz[i][j]);
}
printf("\n");
}
return 0;
}
Zewnętrzna pętla iteruje po wierszach, wewnętrzna po kolumnach.
Zagnieżdżenie pętli jest naturalną konsekwencją dwóch wymiarów. Każdy indeks odpowiada jednej pętli.
Tablice dwuwymiarowe przekazywane do funkcji oraz zależność od znanego rozmiaru kolumn
Przy przekazywaniu do funkcji należy określić drugi wymiar:
void wypisz(int m[][3], int wiersze) {
for (int i = 0; i < wiersze; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", m[i][j]);
}
printf("\n");
}
}
Alternatywnie:
void wypisz(int (*m)[3], int wiersze);
Dlaczego drugi wymiar musi być znany? Ponieważ kompilator musi wiedzieć, jak daleko przesunąć wskaźnik przy przejściu do kolejnego wiersza.
Jeżeli drugi wymiar nie jest znany, kompilator nie jest w stanie obliczyć poprawnego adresu elementu.
Tablice wielowymiarowe jako uogólnienie konstrukcji dwuwymiarowej
Tablice wielowymiarowe to rozszerzenie idei na więcej niż dwa wymiary.
Przykład:
int t[2][3][4];
Rozmiar:
2 * 3 * 4 elementów
Indeksowanie:
t[i][j][k]
Obliczenie przesunięcia:
((i * 3 + j) * 4 + k)
Każdy kolejny wymiar zwiększa złożoność indeksowania, ale w pamięci dane nadal są liniowe.
W praktyce najczęściej stosowane są dwa lub trzy wymiary (np. grafika 3D, przetwarzanie obrazów).
Tablica dwuwymiarowa w C++ oraz różnice względem C
W C++ klasyczna składnia jest identyczna:
int macierz[2][3];
Dodatkowo można użyć kontenera std::vector:
#include <vector>
std::vector<std::vector<int>> macierz(2, std::vector<int>(3));
Tu każdy wiersz jest osobnym wektorem. Pamięć nie musi być ciągła w całej strukturze.
Różnica jest istotna przy operacjach wymagających jednego bloku pamięci.
tablice w pythonie i tablica w pythonie jako struktura list zagnieżdżonych
W Python nie istnieje klasyczna tablica dwuwymiarowa w sensie C. Stosuje się listy zagnieżdżone.
Przykład:
macierz = [
[1, 2, 3],
[4, 5, 6]
]
Dostęp:
macierz[0][1]
Jednak tablica w pythonie jest strukturą dynamiczną. Wiersze mogą mieć różne długości:
macierz = [
[1, 2],
[3, 4, 5]
]
W C taka konstrukcja nie byłaby poprawną klasyczną tablicą dwuwymiarową.
W Pythonie pamięć nie musi być ciągła, a zarządzanie pamięcią odbywa się automatycznie.
Typowe błędy przy pracy z tablicami dwuwymiarowymi
- Zamiana liczby wierszy i kolumn.
- Wyjście poza zakres indeksu.
- Niezgodność deklaracji przy przekazywaniu do funkcji.
- Błędne obliczenie rozmiaru przy dynamicznej alokacji.
- Próba użycia niezainicjalizowanej macierzy.
Wyjście poza zakres w C skutkuje niezdefiniowanym zachowaniem. Może nadpisać sąsiednie dane w pamięci.
Przy dynamicznej alokacji często popełnia się błąd polegający na alokacji wskaźników bez alokacji właściwych wierszy.
Tablice dwuwymiarowe są logicznym rozszerzeniem jednowymiarowych struktur danych i wymagają zrozumienia sposobu liniowego przechowywania danych w pamięci. Każdy dodatkowy wymiar oznacza dodatkowy poziom przeliczania indeksów na przesunięcia adresowe. Precyzyjne rozumienie tego mechanizmu jest niezbędne przy pracy z algorytmami macierzowymi, przetwarzaniem obrazów oraz strukturami danych o charakterze tabelarycznym.