Tablice dwuwymiarowe
Tablica jednowymiarowa to lista. Dwuwymiarowa to tabela: wiersze i kolumny, jak w arkuszu kalkulacyjnym albo w dzienniku ocen. Element wskazujemy dwoma numerami — wierszem i kolumną — a do przejścia całości potrzebne są dwie pętle, po jednej na każdy wymiar.
Czego się dziś nauczysz
- Utworzysz tablicę dwuwymiarową i wypełnisz ją danymi
- Odczytasz element po numerze wiersza i kolumny
- Odróżnisz
GetLength(0)odGetLength(1)i odLength - Przejdziesz całą tabelę dwiema pętlami i wypiszesz ją w równych kolumnach
- Policzysz sumy i średnie osobno dla wierszy i dla kolumn
Czym jest tablica dwuwymiarowa
Wróćmy do obrazka z szafką. Tablica jednowymiarowa to jeden rząd ponumerowanych szuflad. Dwuwymiarowa to regał: żeby wskazać szufladę, trzeba podać półkę i miejsce na niej.
| kol. 0 | kol. 1 | kol. 2 | kol. 3 | |
|---|---|---|---|---|
| wiersz 0 | [0,0] | [0,1] | [0,2] | [0,3] |
| wiersz 1 | [1,0] | [1,1] | [1,2] | [1,3] |
| wiersz 2 | [2,0] | [2,1] | [2,2] | [2,3] |
Kolejność jest zawsze taka sama: najpierw wiersz
oceny[2, 1] to trzeci wiersz, druga kolumna. To przyjęta w tej lekcji konwencja. C# numeruje wymiary, ale nie narzuca im znaczeń „wiersz” i „kolumna”. Zapamiętaj ją raz, bo zamiana indeksów miejscami to najczęstszy błąd w tej lekcji.
Kiedy warto sięgnąć po dwa wymiary:
| Zastosowanie | Co jest wierszem, co kolumną |
|---|---|
| dziennik ocen | wiersz — uczeń, kolumna — przedmiot |
| plansza do gry | wiersz i kolumna to współrzędne pola (szachy, saper, kółko i krzyżyk) |
| tabela sprzedaży | wiersz — produkt, kolumna — miesiąc |
| obraz | wiersz i kolumna to współrzędne piksela |
| macierz | to samo, co w matematyce |
Tworzenie i dostęp
// 1. pusta tablica: 3 wiersze na 4 kolumny
int[,] plansza = new int[3, 4];
// 2. z podanymi wartosciami - kazdy wiersz w osobnych klamrach
int[,] oceny =
{
{ 4, 5, 3 }, // wiersz 0
{ 3, 3, 4 }, // wiersz 1
{ 5, 5, 5 } // wiersz 2
};
// 3. zapis pelny
int[,] male = new int[,] { { 1, 2 }, { 3, 4 } };
Przecinek w nawiasach kwadratowych — int[,] — mówi „dwa wymiary”. Trzy wymiary to int[,,], i tak dalej.
int[,] oceny =
{
{ 4, 5, 3 },
{ 3, 3, 4 },
{ 5, 5, 5 }
};
Console.WriteLine(oceny[0, 0]); // 4 - pierwszy wiersz, pierwsza kolumna
Console.WriteLine(oceny[1, 2]); // 4 - drugi wiersz, trzecia kolumna
Console.WriteLine(oceny[2, 1]); // 5 - trzeci wiersz, druga kolumna
oceny[0, 2] = 6; // zmiana pojedynczego elementu
Console.WriteLine(oceny[0, 2]); // 6
4
4
5
6
Wszystkie wiersze muszą mieć tyle samo elementów
Tablica dwuwymiarowa to prostokąt. Zapis z wierszami różnej długości nie skompiluje się. Jeśli potrzebujesz „schodków” — na przykład różnej liczby ocen dla różnych uczniów — służą do tego tablice postrzępione z następnej lekcji.
Jak przy tablicach jednowymiarowych, świeżo utworzona tablica jest wypełniona wartościami domyślnymi — dla int samymi zerami.
GetLength kontra Length
Przy dwóch wymiarach jedna liczba nie wystarczy. Właściwość Length nadal istnieje, ale znaczy co innego, niż się wydaje:
int[,] oceny = new int[3, 4]; // 3 wiersze, 4 kolumny
Console.WriteLine(oceny.GetLength(0)); // 3 - liczba WIERSZY
Console.WriteLine(oceny.GetLength(1)); // 4 - liczba KOLUMN
Console.WriteLine(oceny.Length); // 12 - WSZYSTKIE elementy (3 * 4)
Console.WriteLine(oceny.Rank); // 2 - liczba wymiarow
3
4
12
2
| Zapis | Wynik | Znaczenie |
|---|---|---|
GetLength(0) | 3 | rozmiar pierwszego wymiaru — liczba wierszy |
GetLength(1) | 4 | rozmiar drugiego wymiaru — liczba kolumn |
Length | 12 | iloczyn obu — liczba wszystkich komórek |
Rank | 2 | ile wymiarów ma tablica |
Użycie Length jako liczby wierszy to gotowy błąd
Pętla for (int i = 0; i < oceny.Length; i++) na tablicy 3×4 wykona się dwanaście razy, choć wierszy jest trzy — i przerwie program przy czwartym obiegu. Do wymiarów służy wyłącznie GetLength.
Jak zapamiętać, który numer to co
Numer wymiaru odpowiada pozycji indeksu w nawiasach. W zapisie oceny[wiersz, kolumna] wiersz jest pierwszy, więc jego rozmiar to GetLength(0). Kolumna jest druga, więc GetLength(1). Ta sama kolejność, zawsze.
Przechodzenie tablicy
Dwa wymiary to dwie pętle zagnieżdżone — dokładnie ten mechanizm z lekcji 18, tylko teraz zamiast rysować tabelę, chodzimy po prawdziwych danych.
int[,] oceny =
{
{ 4, 5, 3 },
{ 3, 3, 4 },
{ 5, 5, 5 }
};
for (int w = 0; w < oceny.GetLength(0); w++) // wiersze
{
for (int k = 0; k < oceny.GetLength(1); k++) // kolumny
{
Console.Write($"{oceny[w, k],4}"); // Write - bez nowej linii
}
Console.WriteLine(); // koniec wiersza
}
4 5 3
3 3 4
5 5 5
Nazywaj liczniki po ludzku
w i k (wiersz, kolumna) albo wiersz i kolumna czyta się nieporównanie lepiej niż i i j. Przy dwóch wymiarach zamiana i z j to błąd, którego nie widać — a oceny[kolumna, wiersz] aż kłuje w oczy.
Pętla foreach
foreach też działa i przechodzi wszystkie elementy, wiersz po wierszu. Nie daje jednak dostępu do numerów, więc nadaje się tylko wtedy, gdy pozycja nie jest potrzebna:
int[,] oceny = { { 4, 5, 3 }, { 3, 3, 4 }, { 5, 5, 5 } };
int suma = 0;
int najwyzsza = oceny[0, 0];
foreach (int ocena in oceny) // idzie po wszystkich 9 elementach
{
suma += ocena;
if (ocena > najwyzsza)
{
najwyzsza = ocena;
}
}
Console.WriteLine($"Suma wszystkich: {suma}");
Console.WriteLine($"Najwyzsza: {najwyzsza}");
Console.WriteLine($"Srednia: {(double)suma / oceny.Length:F2}");
Suma wszystkich: 37
Najwyzsza: 5
Srednia: 4,11
Tu oceny.Length jest akurat tym, czego potrzeba — chodzi o liczbę wszystkich ocen, czyli 9.
Sumy wierszy i kolumn
To najważniejsza umiejętność w tej lekcji. Różnica między nimi sprowadza się do jednego: która pętla jest na zewnątrz.
for (int w = 0; w < oceny.GetLength(0); w++) // wiersz na zewnatrz
{
int suma = 0; // zerujemy dla KAZDEGO wiersza
for (int k = 0; k < oceny.GetLength(1); k++)
{
suma += oceny[w, k]; // wiersz staly, kolumna sie zmienia
}
Console.WriteLine($"Wiersz {w}: suma {suma}");
}
int[,] oceny = { { 4, 5, 3 }, { 3, 3, 4 }, { 5, 5, 5 } };
for (int w = 0; w < oceny.GetLength(0); w++) // wiersz na zewnatrz
{
int suma = 0; // zerujemy dla KAZDEGO wiersza
for (int k = 0; k < oceny.GetLength(1); k++)
{
suma += oceny[w, k]; // wiersz staly, kolumna sie zmienia
}
Console.WriteLine($"Wiersz {w}: suma {suma}");
}
for (int k = 0; k < oceny.GetLength(1); k++) // KOLUMNA na zewnatrz
{
int suma = 0;
for (int w = 0; w < oceny.GetLength(0); w++)
{
suma += oceny[w, k]; // kolumna stala, wiersz sie zmienia
}
Console.WriteLine($"Kolumna {k}: suma {suma}");
}
Wiersz 0: suma 12
Wiersz 1: suma 10
Wiersz 2: suma 15
Kolumna 0: suma 12
Kolumna 1: suma 13
Kolumna 2: suma 12
Zerowanie sumy musi być wewnątrz pętli zewnętrznej
Deklaracja int suma = 0; przed obiema pętlami dałaby jedną narastającą sumę wszystkiego. Suma dotyczy jednego wiersza, więc zeruje się przy każdym nowym wierszu — dokładnie jak flaga pierwsza w teście pierwszości z lekcji 18.
Wzrokowa kontrola
W zapisie oceny[w, k] ta zmienna, która stoi w wewnętrznej pętli, „przesuwa się” po tabeli. Przy sumie wiersza zmienia się k, czyli idziemy w poziomie. Przy sumie kolumny zmienia się w, czyli idziemy w pionie. Jeśli nie masz pewności, którą pętlę dać na zewnątrz — narysuj strzałkę.
Więcej wymiarów
Wymiarów może być więcej niż dwa. Zasada jest ta sama: każdy dodatkowy przecinek to jeden dodatkowy wymiar i jedna dodatkowa pętla.
// oceny[klasa, uczen, przedmiot]
int[,,] szkola = new int[4, 30, 12];
Console.WriteLine(szkola.GetLength(0)); // 4 - klasy
Console.WriteLine(szkola.GetLength(1)); // 30 - uczniowie
Console.WriteLine(szkola.GetLength(2)); // 12 - przedmioty
Console.WriteLine(szkola.Length); // 1440 - wszystkie komorki
Console.WriteLine(szkola.Rank); // 3
W praktyce rzadko przekracza się dwa wymiary
Trzy pętle zagnieżdżone są już trudne do ogarnięcia, a cztery — praktycznie nieczytelne. Zwykle lepszym rozwiązaniem jest tablica obiektów, o których dowiesz się w bloku o klasach. Trzeci wymiar bywa naturalny w grafice (szerokość, wysokość, kanał koloru) i w symulacjach przestrzennych.
Pamięć rośnie szybko
Tablica int[1000, 1000] to milion liczb, czyli około 4 MB. int[1000, 1000, 1000] to już miliard liczb, czyli około 4 GB — alokacja może się nie udać z powodu limitów środowiska lub dostępnej pamięci. Przy dużych rozmiarach zawsze pomnóż wymiary, zanim naciśniesz F5.
Użyte w tej lekcji elementy
| Nazwa | Zwraca | Działanie |
|---|---|---|
t.GetLength(n) | int |
Metoda (z nawiasami) — rozmiar wymiaru o numerze n. GetLength(0) to wiersze, GetLength(1) kolumny. |
t.Length | int |
Właściwość — liczba wszystkich komórek, czyli iloczyn wymiarów. Nie liczba wierszy! |
t.Rank | int |
Właściwość — liczba wymiarów tablicy (2 dla int[,]). |
t[w, k] | element | Odczyt lub zapis komórki. Najpierw wiersz, potem kolumna. |
Array.Clear(t) | nic | Przywraca wartości domyślne wszystkim komórkom — działa też na tablicach wielowymiarowych. |
Math.Max(a, b) | liczba | Większa z dwóch wartości — wygodne przy szukaniu maksimum bez pisania if. |
new string(z, n) | string |
Linie oddzielające w tabelach. |
Czego nie da się zrobić na tablicy dwuwymiarowej
Array.Sort, Array.Reverse, Array.IndexOf i string.Join działają tylko na tablicach jednowymiarowych. Na int[,] dostaniesz błąd przy kompilacji albo RankException przy uruchomieniu. Sortowanie wierszy tabeli trzeba napisać samemu albo przenieść dane do tablicy postrzępionej z następnej lekcji.
Dziennik klasy
Cztery osoby, pięć przedmiotów. Program wypisuje tabelę, średnią każdego ucznia (wiersz), średnią każdego przedmiotu (kolumna) i średnią całej klasy.
string[] uczniowie = { "Kowalski", "Nowak", "Wisniewska", "Zielinski" };
string[] przedmioty = { "Mat", "Pol", "Ang", "Inf", "WF" };
int[,] oceny =
{
{ 4, 5, 3, 4, 5 }, // Kowalski
{ 3, 3, 4, 2, 3 }, // Nowak
{ 5, 5, 5, 4, 6 }, // Wisniewska
{ 2, 4, 3, 3, 4 } // Zielinski
};
int wierszy = oceny.GetLength(0);
int kolumn = oceny.GetLength(1);
// --- naglowek ---
Console.Write($"{"Uczen",-12}");
foreach (string p in przedmioty)
{
Console.Write($"{p,5}");
}
Console.WriteLine($"{"Srednia",10}");
Console.WriteLine(new string('-', 47));
// --- wiersze: uczen po uczniu ---
int najlepszyUczen = 0;
double najlepszaSrednia = 0;
for (int w = 0; w < wierszy; w++)
{
Console.Write($"{uczniowie[w],-12}");
int sumaWiersza = 0;
for (int k = 0; k < kolumn; k++)
{
Console.Write($"{oceny[w, k],5}");
sumaWiersza += oceny[w, k];
}
double sredniaUcznia = (double)sumaWiersza / kolumn;
Console.WriteLine($"{sredniaUcznia,10:F2}");
if (sredniaUcznia > najlepszaSrednia)
{
najlepszaSrednia = sredniaUcznia;
najlepszyUczen = w;
}
}
// --- stopka: przedmiot po przedmiocie ---
Console.WriteLine(new string('-', 47));
Console.Write($"{"Srednia",-12}");
int najlepszyPrzedmiot = 0;
double najwyzszaSredniaPrzedmiotu = 0;
for (int k = 0; k < kolumn; k++)
{
int sumaKolumny = 0;
for (int w = 0; w < wierszy; w++)
{
sumaKolumny += oceny[w, k];
}
double sredniaPrzedmiotu = (double)sumaKolumny / wierszy;
Console.Write($"{sredniaPrzedmiotu,5:F2}");
if (sredniaPrzedmiotu > najwyzszaSredniaPrzedmiotu)
{
najwyzszaSredniaPrzedmiotu = sredniaPrzedmiotu;
najlepszyPrzedmiot = k;
}
}
Console.WriteLine();
Console.WriteLine();
// --- podsumowanie: jedno przejscie po wszystkim ---
int sumaWszystkich = 0;
foreach (int o in oceny)
{
sumaWszystkich += o;
}
Console.WriteLine($"Srednia klasy: {(double)sumaWszystkich / oceny.Length:F2}");
Console.WriteLine($"Najlepszy uczen: {uczniowie[najlepszyUczen]} ({najlepszaSrednia:F2})");
Console.WriteLine($"Najlepszy przedmiot: {przedmioty[najlepszyPrzedmiot]} ({najwyzszaSredniaPrzedmiotu:F2})");
Uczen Mat Pol Ang Inf WF Srednia
-----------------------------------------------
Kowalski 4 5 3 4 5 4,20
Nowak 3 3 4 2 3 3,00
Wisniewska 5 5 5 4 6 5,00
Zielinski 2 4 3 3 4 3,20
-----------------------------------------------
Srednia 3,50 4,25 3,75 3,25 4,50
Srednia klasy: 3,85
Najlepszy uczen: Wisniewska (5,00)
Najlepszy przedmiot: WF (4,50)
Omówienie
| Fragment | Co robi i dlaczego tak |
|---|---|
int wierszy = oceny.GetLength(0); |
Rozmiary zapisujemy raz, do zmiennych o mówiących nazwach. Dalej piszemy k < kolumn zamiast k < oceny.GetLength(1) — krócej i trudniej się pomylić o wymiar. |
| trzy tablice opisujące te same dane | uczniowie[w] to nazwisko z wiersza w, przedmioty[k] to nazwa kolumny k. Ten sam układ „równoległych tablic” co w lekcji 23, tylko rozciągnięty na dwa wymiary. |
| suma wiersza w trakcie wypisywania | Jedna pętla robi dwie rzeczy: wypisuje komórkę i dolicza ją do sumy. Drugie przejście byłoby zbędne — wszystko, czego potrzeba, jest już pod ręką. |
najlepszaSrednia = 0; |
Tutaj start od zera jest bezpieczny, bo oceny są dodatnie, więc pierwsza średnia na pewno go przebije. Przy danych mogących być ujemnymi (temperatury, salda) trzeba zacząć od pierwszego elementu — jak w lekcji 23. |
| pętla stopki ma kolumnę na zewnątrz | To jedyna różnica między sumą wiersza a sumą kolumny. Wewnątrz zmienia się w, więc schodzimy w dół po jednej kolumnie. |
foreach (int o in oceny) |
Do sumy wszystkiego numery są niepotrzebne, więc foreach jest krótszy i bezpieczniejszy. Przechodzi wszystkie 20 komórek. |
oceny.Length jako 20 |
Tu Length jest właściwym wyborem — chodzi o liczbę wszystkich ocen (4 × 5), a nie o wymiar. |
Sprawdź rachunek
Kowalski: 4 + 5 + 3 + 4 + 5 = 21, średnia 21 / 5 = 4,20. Kolumna „Mat”: 4 + 3 + 5 + 2 = 14, średnia 14 / 4 = 3,50. Wszystkich ocen jest 20, ich suma to 77, więc średnia klasy 77 / 20 = 3,85. Zwróć uwagę, że średnia klasy jest jednocześnie średnią ze średnich uczniów: (4,20 + 3,00 + 5,00 + 3,20) / 4 = 3,85 — bo każdy uczeń ma tyle samo ocen. Przy różnej liczbie ocen ta równość by nie zachodziła.
Szerokość linii oddzielającej
47 to suma kolumn: 12 (nazwisko) + 5 × 5 (przedmioty) + 10 (średnia). Gdy zmienisz którąkolwiek szerokość albo dodasz przedmiot, przelicz to na nowo — albo, elegancko, policz w kodzie: new string('-', 12 + 5 * kolumn + 10).
Na czym się najczęściej potykamy
| Błąd | Co się dzieje i jak poprawić |
|---|---|
int[][] t = new int[3][4]; |
To zapis tablicy postrzępionej, a nie dwuwymiarowej, i w tej postaci nie skompiluje się. Tablica prostokątna to int[,] t = new int[3, 4]; — jedne nawiasy, przecinek w środku. |
t[w][k] |
Ten zapis też należy do tablic postrzępionych. Dla int[,] jest to CS0021. Poprawnie: t[w, k]. |
Length zamiast GetLength(0) |
Pętla wykona zbyt wiele obiegów i przerwie program. Length to liczba wszystkich komórek. |
GetLength bez nawiasów |
GetLength to metoda, więc wymaga argumentu: GetLength(0). To odwrotnie niż Length, które jest właściwością. |
zamiana indeksów: t[k, w] |
Przy tablicy kwadratowej program zadziała i po cichu poda złe wyniki — najgorszy rodzaj błędu. Przy prostokątnej najczęściej przerwie działanie. Trzymaj się kolejności [wiersz, kolumna]. |
| zerowanie sumy przed obiema pętlami | Zamiast sum poszczególnych wierszy dostajesz jedną narastającą sumę. Zerowanie należy do pętli zewnętrznej. |
| wiersze różnej długości | Tablica dwuwymiarowa musi być prostokątna. Do „schodków” służą tablice postrzępione z lekcji 26. |
Array.Sort(t) na int[,] |
RankException — metody klasy Array działają na jednym wymiarze. |
WriteLine w pętli wewnętrznej |
Każda komórka w osobnej linii zamiast tabeli. Wewnątrz Write, WriteLine po pętli kolumn. |
Zadania
Utwórz tablicę 10×10, wypełnij ją iloczynami odpowiednich numerów wiersza i kolumny, a potem wypisz jej zawartość.
Dla tablicy 3×4 wypełnionej wybranymi liczbami policz sumę wszystkich elementów, ich średnią oraz wartość najmniejszą i największą.
Znajdź w tablicy dwuwymiarowej podaną wartość i podaj jej współrzędne (wiersz i kolumnę). Jeśli występuje wielokrotnie, wypisz wszystkie pozycje.
Tablica 4×3 zawiera sprzedaż czterech produktów w trzech miesiącach kwartału. Wypisz tabelę z sumami wierszy (produkt) i kolumn (miesiąc) oraz sumą całkowitą w prawym dolnym rogu.
Utwórz nową tablicę będącą transpozycją danej — zamień wiersze z kolumnami, tak że element z pozycji [w, k] trafia na [k, w].
Plansza 3×3 zawiera znaki 'X', 'O' i spacje. Sprawdź, czy któryś z graczy wygrał — trzy takie same znaki w wierszu, kolumnie lub na przekątnej.
Co trzeba zapamiętać
- int[,] oznacza tablicę dwuwymiarową. W kursie pierwszy indeks oznacza wiersz, drugi kolumnę; to konwencja modelu.
- GetLength(0) i GetLength(1) podają rozmiary wymiarów. Length liczy wszystkie elementy.
- Zagnieżdżone pętle pozwalają przechodzić wiersze i kolumny, a foreach wszystkie elementy.
- Zeruj sumę wewnątrz właściwej pętli zewnętrznej.
- Tablica prostokątna ma ten sam rozmiar drugiego wymiaru w każdym wierszu.
- Array.Sort i Array.IndexOf wymagają tablic jednowymiarowych; nie oznacza to, że wszystkie metody Array mają to ograniczenie.
- Przy transpozycji zamieniamy rozmiary wymiarów i indeksy.