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. Ta kolejność obowiązuje w całym C# — i w matematyce przy macierzach, i w większości języków. 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 16, 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 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
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 16.

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 — program po prostu się nie uruchomi. 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 17, 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 17.
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 20.
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ść.

pokaż rozwiązanie
int[,] tabliczka = new int[10, 10];

// wypelnienie
for (int w = 0; w < tabliczka.GetLength(0); w++)
{
    for (int k = 0; k < tabliczka.GetLength(1); k++)
    {
        tabliczka[w, k] = (w + 1) * (k + 1);      // indeks 0 to liczba 1
    }
}

// wypisanie
for (int w = 0; w < tabliczka.GetLength(0); w++)
{
    for (int k = 0; k < tabliczka.GetLength(1); k++)
    {
        Console.Write($"{tabliczka[w, k],5}");
    }

    Console.WriteLine();
}
wynik w konsoli
    1    2    3    4    5    6    7    8    9   10
    2    4    6    8   10   12   14   16   18   20
    3    6    9   12   15   18   21   24   27   30
    4    8   12   16   20   24   28   32   36   40
    5   10   15   20   25   30   35   40   45   50
    6   12   18   24   30   36   42   48   54   60
    7   14   21   28   35   42   49   56   63   70
    8   16   24   32   40   48   56   64   72   80
    9   18   27   36   45   54   63   72   81   90
   10   20   30   40   50   60   70   80   90  100

W lekcji 16 ta sama tabela powstawała „w locie”, bez zapamiętywania. Teraz najpierw ją wypełniamy, potem wypisujemy — i dzięki temu dane zostają w pamięci, więc można je jeszcze policzyć, posortować albo zapisać do pliku. To typowy podział pracy w programach na tablicach.

(w + 1) * (k + 1) — bo wiersz 0 ma zawierać wielokrotności jedynki.

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ą.

pokaż rozwiązanie
int[,] dane =
{
    { 12,  7, 20,  3 },
    {  8, 15,  6, 11 },
    { 19,  2, 14,  9 }
};

int suma = 0;
int najmniejsza = dane[0, 0];
int najwieksza = dane[0, 0];

foreach (int x in dane)
{
    suma += x;
    najmniejsza = Math.Min(najmniejsza, x);
    najwieksza = Math.Max(najwieksza, x);
}

Console.WriteLine($"Elementow:   {dane.Length}");
Console.WriteLine($"Suma:        {suma}");
Console.WriteLine($"Srednia:     {(double)suma / dane.Length:F2}");
Console.WriteLine($"Najmniejsza: {najmniejsza}");
Console.WriteLine($"Najwieksza:  {najwieksza}");
wynik w konsoli
Elementow:   12
Suma:        126
Srednia:     10,50
Najmniejsza: 2
Najwieksza:  20

Skoro numery nie są potrzebne, foreach wystarcza — i przy okazji nie da się w nim pomylić wymiarów.

Math.Min i Math.Max zastępują dwie instrukcje if. Zapis najwieksza = Math.Max(najwieksza, x) czyta się jako „zostaw większą z dwóch”.

Rachunek: 12+7+20+3 = 42, 8+15+6+11 = 40, 19+2+14+9 = 44, razem 126. Średnia 126 / 12 = 10,50.

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.

pokaż rozwiązanie
int[,] dane =
{
    { 12,  7, 20,  7 },
    {  8, 15,  6, 11 },
    {  7,  2, 14,  9 }
};

Console.Write("Czego szukamy? ");
bool ok = int.TryParse(Console.ReadLine(), out int szukana);

if (!ok)
{
    Console.WriteLine("Podaj liczbe calkowita.");
}
else
{
    int znalezione = 0;

    for (int w = 0; w < dane.GetLength(0); w++)
    {
        for (int k = 0; k < dane.GetLength(1); k++)
        {
            if (dane[w, k] == szukana)
            {
                Console.WriteLine($"  znaleziono w wierszu {w}, kolumnie {k}");
                znalezione++;
            }
        }
    }

    if (znalezione == 0)
    {
        Console.WriteLine($"Wartosci {szukana} nie ma w tabeli.");
    }
    else
    {
        Console.WriteLine($"Lacznie wystapien: {znalezione}");
    }
}
wynik w konsoli
Czego szukamy? 7
  znaleziono w wierszu 0, kolumnie 1
  znaleziono w wierszu 0, kolumnie 3
  znaleziono w wierszu 2, kolumnie 0
Lacznie wystapien: 3

Szukamy wszystkich wystąpień, więc nie ma tu break. Gdyby wystarczyło pierwsze, trzeba by wyjść z dwóch pętli naraz — czyli sięgnąć po flagę z lekcji 16.

Zwróć uwagę na kolejność wypisywania: pętla po wierszach jest zewnętrzna, więc trafienia pojawiają się wierszami, od lewej do prawej. To najbardziej naturalny porządek czytania tabeli.

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.

pokaż rozwiązanie
string[] produkty = { "Zeszyty", "Dlugopisy", "Bloki", "Linijki" };
string[] miesiace = { "Sty", "Lut", "Mar" };

int[,] sprzedaz =
{
    { 120, 140, 110 },
    { 340, 300, 380 },
    {  85,  90,  75 },
    {  45,  50,  40 }
};

int wierszy = sprzedaz.GetLength(0);
int kolumn = sprzedaz.GetLength(1);

// naglowek
Console.Write($"{"Produkt",-12}");
foreach (string m in miesiace)
{
    Console.Write($"{m,7}");
}
Console.WriteLine($"{"RAZEM",9}");
Console.WriteLine(new string('-', 42));

// wiersze
for (int w = 0; w < wierszy; w++)
{
    Console.Write($"{produkty[w],-12}");

    int sumaWiersza = 0;

    for (int k = 0; k < kolumn; k++)
    {
        Console.Write($"{sprzedaz[w, k],7}");
        sumaWiersza += sprzedaz[w, k];
    }

    Console.WriteLine($"{sumaWiersza,9}");
}

// stopka
Console.WriteLine(new string('-', 42));
Console.Write($"{"RAZEM",-12}");

int sumaCalkowita = 0;

for (int k = 0; k < kolumn; k++)
{
    int sumaKolumny = 0;

    for (int w = 0; w < wierszy; w++)
    {
        sumaKolumny += sprzedaz[w, k];
    }

    Console.Write($"{sumaKolumny,7}");
    sumaCalkowita += sumaKolumny;
}

Console.WriteLine($"{sumaCalkowita,9}");
wynik w konsoli
Produkt         Sty    Lut    Mar    RAZEM
------------------------------------------
Zeszyty         120    140    110      370
Dlugopisy       340    300    380     1020
Bloki            85     90     75      250
Linijki          45     50     40      135
------------------------------------------
RAZEM           590    580    605     1775

Suma całkowita powstaje z doliczania sum kolumn — nie trzeba trzeciego przejścia. Można ją też policzyć z sum wierszy: 370 + 1020 + 250 + 135 = 1775. Obie drogi muszą dać ten sam wynik i to jest doskonały sposób na sprawdzenie, czy w programie nie pomyliłeś wymiarów.

Szerokość linii: 12 + 3 × 7 + 9 = 42.

Kolumny sum: 590 = 120 + 340 + 85 + 45, 580 = 140 + 300 + 90 + 50, 605 = 110 + 380 + 75 + 40.

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].

pokaż rozwiązanie
int[,] a =
{
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 }
};

int wierszy = a.GetLength(0);      // 2
int kolumn = a.GetLength(1);       // 4

// UWAGA: wymiary sie zamieniaja!
int[,] t = new int[kolumn, wierszy];

for (int w = 0; w < wierszy; w++)
{
    for (int k = 0; k < kolumn; k++)
    {
        t[k, w] = a[w, k];         // to jest cala transpozycja
    }
}

Console.WriteLine($"Oryginal ({wierszy}x{kolumn}):");
Wypisz(a);

Console.WriteLine();
Console.WriteLine($"Transpozycja ({kolumn}x{wierszy}):");
Wypisz(t);

static void Wypisz(int[,] m)
{
    for (int w = 0; w < m.GetLength(0); w++)
    {
        for (int k = 0; k < m.GetLength(1); k++)
        {
            Console.Write($"{m[w, k],4}");
        }

        Console.WriteLine();
    }
}
wynik w konsoli
Oryginal (2x4):
   1   2   3   4
   5   6   7   8

Transpozycja (4x2):
   1   5
   2   6
   3   7
   4   8

Najważniejsza linia to new int[kolumn, wierszy]. Tablica 2×4 po transpozycji ma wymiary 4×2. Utworzenie jej o tych samych wymiarach co oryginał skończyłoby się wyjściem poza zakres — chyba że macierz jest kwadratowa, i wtedy błąd zostałby niezauważony aż do pierwszej macierzy prostokątnej.

Sama zamiana to jedna linia: t[k, w] = a[w, k];. Warto ją prześledzić na kartce dla elementu 7: stoi na a[1, 2], trafia na t[2, 1].

Metoda Wypisz jest tu tylko po to, żeby nie powtarzać tych samych ośmiu linii dwa razy. To zapowiedź bloku o funkcjach.

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.

pokaż rozwiązanie
char[,] plansza =
{
    { 'X', 'O', 'O' },
    { 'O', 'X', 'O' },
    { 'X', 'O', 'X' }
};

// --- wypisanie planszy ---
for (int w = 0; w < 3; w++)
{
    Console.WriteLine($" {plansza[w, 0]} | {plansza[w, 1]} | {plansza[w, 2]} ");

    if (w < 2)
    {
        Console.WriteLine("---+---+---");
    }
}

Console.WriteLine();

char zwyciezca = ' ';

// --- wiersze ---
for (int w = 0; w < 3; w++)
{
    if (plansza[w, 0] != ' ' &&
        plansza[w, 0] == plansza[w, 1] &&
        plansza[w, 1] == plansza[w, 2])
    {
        zwyciezca = plansza[w, 0];
    }
}

// --- kolumny ---
for (int k = 0; k < 3; k++)
{
    if (plansza[0, k] != ' ' &&
        plansza[0, k] == plansza[1, k] &&
        plansza[1, k] == plansza[2, k])
    {
        zwyciezca = plansza[0, k];
    }
}

// --- przekatna glowna: [0,0] [1,1] [2,2] ---
if (plansza[0, 0] != ' ' &&
    plansza[0, 0] == plansza[1, 1] &&
    plansza[1, 1] == plansza[2, 2])
{
    zwyciezca = plansza[0, 0];
}

// --- przekatna druga: [0,2] [1,1] [2,0] ---
if (plansza[0, 2] != ' ' &&
    plansza[0, 2] == plansza[1, 1] &&
    plansza[1, 1] == plansza[2, 0])
{
    zwyciezca = plansza[0, 2];
}

if (zwyciezca == ' ')
{
    Console.WriteLine("Brak zwyciezcy.");
}
else
{
    Console.WriteLine($"Wygral gracz {zwyciezca}!");
}
wynik w konsoli
 X | O | O
---+---+---
 O | X | O
---+---+---
 X | O | X

Wygral gracz X!

Sprawdzenie != ' ' jest konieczne. Bez niego trzy puste pola w wierszu zostałyby uznane za wygraną spacji. To najczęstszy błąd w tym zadaniu i objawia się dopiero przy pustej planszy.

Przekątne mają stałe współrzędne, więc nie potrzebują pętli — wystarczą dwa warunki. Główna to indeksy równe ([0,0], [1,1], [2,2]), druga to takie, w których suma indeksów wynosi 2.

W przykładzie wygrywa X na przekątnej głównej: [0,0], [1,1] i [2,2] to trzy razy X. Sprawdź, że żaden wiersz ani kolumna nie daje trójki.

Wersja z pętlą po wierszach i kolumnach zamiast czterech osobnych bloków też jest możliwa i krótsza, ale wymaga sprytnego indeksowania. Przy planszy 3×3 czytelność wygrywa z pomysłowością.

PODSUMOWANIE

Co trzeba zapamiętać

  • Tablica dwuwymiarowa to int[,] — jedne nawiasy, przecinek w środku.
  • Element wskazujemy dwoma numerami: t[wiersz, kolumna]zawsze w tej kolejności.
  • GetLength(0) to liczba wierszy, GetLength(1) — kolumn. To metoda, z nawiasami.
  • Length to liczba wszystkich komórek, czyli iloczyn wymiarów.
  • Przejście całości to dwie pętle zagnieżdżone; wewnątrz Write, po pętli kolumn — WriteLine.
  • Suma wiersza: wiersz na zewnątrz. Suma kolumny: kolumna na zewnątrz.
  • Zmienną na sumę zeruj wewnątrz pętli zewnętrznej.
  • Wszystkie wiersze mają tyle samo kolumn — tablica jest prostokątna.
  • Metody klasy Array (Sort, IndexOf) oraz string.Join nie działają na dwóch wymiarach.
  • Przy transpozycji nowa tablica ma zamienione wymiary.