Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 25 z 58

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.

C# int[,] GetLength 50 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz tablicę dwuwymiarową i wypełnisz ją danymi
  • Odczytasz element po numerze wiersza i kolumny
  • Odróżnisz GetLength(0) od GetLength(1) i od Length
  • Przejdziesz całą tabelę dwiema pętlami i wypiszesz ją w równych kolumnach
  • Policzysz sumy i średnie osobno dla wierszy i dla kolumn
TEORIA

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. 0kol. 1kol. 2kol. 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:

ZastosowanieCo jest wierszem, co kolumną
dziennik ocenwiersz — uczeń, kolumna — przedmiot
plansza do grywiersz i kolumna to współrzędne pola (szachy, saper, kółko i krzyżyk)
tabela sprzedażywiersz — produkt, kolumna — miesiąc
obrazwiersz i kolumna to współrzędne piksela
macierzto samo, co w matematyce
TEORIA

Tworzenie i dostęp

Program.cs
// 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.

Program.cs
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
wynik w konsoli
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.

TEORIA

GetLength kontra Length

Przy dwóch wymiarach jedna liczba nie wystarczy. Właściwość Length nadal istnieje, ale znaczy co innego, niż się wydaje:

Program.cs
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
wynik w konsoli
3
4
12
2
ZapisWynikZnaczenie
GetLength(0)3rozmiar pierwszego wymiaru — liczba wierszy
GetLength(1)4rozmiar drugiego wymiaru — liczba kolumn
Length12iloczyn obu — liczba wszystkich komórek
Rank2ile 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.

TEORIA

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.

Program.cs
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
}
wynik w konsoli
   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:

Program.cs
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}");
wynik w konsoli
Suma wszystkich: 37
Najwyzsza: 5
Srednia: 4,11

Tu oceny.Length jest akurat tym, czego potrzeba — chodzi o liczbę wszystkich ocen, czyli 9.

TEORIA

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.

suma każdego wiersza
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}");
}
suma każdej kolumny
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}");
}
wynik dla tablicy 4/5/3, 3/3/4, 5/5/5
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ę.

TEORIA

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.

Program.cs
// 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.

METODY WBUDOWANE

Użyte w tej lekcji elementy

NazwaZwracaDziałanie
t.GetLength(n)int Metoda (z nawiasami) — rozmiar wymiaru o numerze n. GetLength(0) to wiersze, GetLength(1) kolumny.
t.Lengthint Właściwość — liczba wszystkich komórek, czyli iloczyn wymiarów. Nie liczba wierszy!
t.Rankint 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.

PRZYKŁAD Z OMÓWIENIEM

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.

Program.cs
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})");
wynik w konsoli
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

FragmentCo 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).

CZĘSTE BŁĘDY

Na czym się najczęściej potykamy

BłądCo 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

Zadania

ZAD 1Tabliczka mnożenia w tablicy★☆☆

Utwórz tablicę 10×10, wypełnij ją iloczynami odpowiednich numerów wiersza i kolumny, a potem wypisz jej zawartość.

ZAD 2Suma i średnia całej tabeli★☆☆

Dla tablicy 3×4 wypełnionej wybranymi liczbami policz sumę wszystkich elementów, ich średnią oraz wartość najmniejszą i największą.

ZAD 3Wyszukiwanie w tabeli★★☆

Znajdź w tablicy dwuwymiarowej podaną wartość i podaj jej współrzędne (wiersz i kolumnę). Jeśli występuje wielokrotnie, wypisz wszystkie pozycje.

ZAD 4Sprzedaż kwartalna★★☆

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.

ZAD 5Transpozycja macierzy★★☆

Utwórz nową tablicę będącą transpozycją danej — zamień wiersze z kolumnami, tak że element z pozycji [w, k] trafia na [k, w].

ZAD 6Kółko i krzyżyk — sprawdzenie wygranej★★★

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.

PODSUMOWANIE

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.
Postęp zapisuje się w tej przeglądarce.