Tablice postrzępione [][]

Tablica dwuwymiarowa jest prostokątem — każdy wiersz ma tyle samo kolumn. Ale w prawdziwych danych rzadko tak bywa: jeden uczeń ma cztery oceny, inny dwanaście. Tablica postrzępiona to tablica tablic, w której każdy wiersz może mieć własną długość.

C# int[][] INF.03 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz tablicę, której wiersze mają różne długości
  • Odróżnisz zapis [w][k] od [w, k] i będziesz wiedział, kiedy który
  • Przejdziesz taką tablicę pętlą, w której warunek zależy od bieżącego wiersza
  • Użyjesz metod klasy Array na pojedynczym wierszu
  • Wybierzesz właściwy rodzaj tablicy do konkretnego zadania
TEORIA

Czym jest tablica postrzępiona

Nazwa („postrzępiona”, ang. jagged) bierze się z wyglądu: prawa krawędź jest nierówna, poszarpana.

tak to wygląda
tablica dwuwymiarowa int[3,4]     tablica postrzepiona int[3][]

  [ 1 ][ 2 ][ 3 ][ 4 ]              [ 1 ][ 2 ][ 3 ][ 4 ]
  [ 5 ][ 6 ][ 7 ][ 8 ]              [ 5 ][ 6 ]
  [ 9 ][10 ][11 ][12 ]              [ 7 ][ 8 ][ 9 ]

  prostokat - kazdy wiersz          kazdy wiersz ma wlasna
  ma tyle samo kolumn               dlugosc

Kluczowa różnica jest jednak głębsza niż wygląd. Tablica dwuwymiarowa to jeden obiekt w pamięci. Tablica postrzępiona to tablica, której elementami są inne tablice — każdy wiersz jest osobnym, niezależnym obiektem.

Stąd bierze się cała reszta

Skoro każdy wiersz jest zwykłą tablicą jednowymiarową, to ma własne Length, można go posortować przez Array.Sort, wypisać przez string.Join, a nawet podmienić na inny. Nic z tego nie działa na tablicy dwuwymiarowej. To największa praktyczna zaleta tablic postrzępionych.

Dlaczego to ważne na egzaminie

Tablice postrzępione pojawiają się w wymaganiach kwalifikacji INF.03. Zadania egzaminacyjne lubią dane w rodzaju „każdy uczeń ma inną liczbę ocen” albo „każdy dzień ma inną liczbę pomiarów” — a to jest dokładnie ten przypadek.

TEORIA

Tworzenie

Tworzenie odbywa się w dwóch krokach: najpierw powstaje tablica na wiersze, potem każdy wiersz z osobna.

Program.cs
// KROK 1: tablica na 3 wiersze - same wiersze jeszcze nie istnieja
int[][] dane = new int[3][];

// KROK 2: kazdy wiersz osobno, kazdy moze miec inna dlugosc
dane[0] = new int[4];
dane[1] = new int[2];
dane[2] = new int[3];

dane[0][0] = 1;
dane[1][0] = 5;
dane[2][2] = 9;

Console.WriteLine(dane[0][0]);      // 1
Console.WriteLine(dane[2][2]);      // 9

Bez kroku drugiego program przerwie działanie

Po new int[3][] masz trzy „puste miejsca na wiersze”, ale samych wierszy jeszcze nie ma. Odwołanie dane[0][0] przed przypisaniem wiersza kończy się błędem NullReferenceException. To najczęstsza pomyłka przy pierwszym użyciu tablic postrzępionych.

Zapis skrócony

Gdy znasz wartości od razu, wszystko mieści się w jednym wyrażeniu:

Program.cs
int[][] oceny =
{
    new int[] { 4, 5, 3, 4 },        // wiersz 0 - cztery elementy
    new int[] { 3, 3 },              // wiersz 1 - dwa
    new int[] { 5, 5, 5, 4, 6, 5 }   // wiersz 2 - szesc
};

// od C# 12 mozna jeszcze krocej:
int[][] krocej =
{
    [4, 5, 3, 4],
    [3, 3],
    [5, 5, 5, 4, 6, 5]
};

W przeciwieństwie do tablicy dwuwymiarowej każdy wiersz wymaga własnego new int[] (albo nawiasów kwadratowych w nowszym zapisie) — bo każdy jest osobną tablicą.

TEORIA

Rozmiary wierszy

Nie ma tu GetLength — jest po prostu Length, tylko trzeba wiedzieć, czyje:

Program.cs
int[][] oceny =
{
    new int[] { 4, 5, 3, 4 },
    new int[] { 3, 3 },
    new int[] { 5, 5, 5, 4, 6, 5 }
};

Console.WriteLine(oceny.Length);            // 3  - liczba WIERSZY
Console.WriteLine(oceny[0].Length);         // 4  - dlugosc wiersza 0
Console.WriteLine(oceny[1].Length);         // 2  - dlugosc wiersza 1
Console.WriteLine(oceny[2].Length);         // 6  - dlugosc wiersza 2

// lacznej liczby elementow trzeba policzyc samemu
int razem = 0;

foreach (int[] wiersz in oceny)
{
    razem += wiersz.Length;
}

Console.WriteLine($"Wszystkich elementow: {razem}");   // 12
wynik w konsoli
3
4
2
6
Wszystkich elementow: 12
ZapisZnaczenieOdpowiednik przy int[,]
t.Lengthliczba wierszyt.GetLength(0)
t[w].Lengthdługość wiersza wt.GetLength(1) — ale ta sama dla wszystkich
trzeba policzyć pętląliczba wszystkich elementówt.Length

Uwaga na tę różnicę

Dla int[,] właściwość Length daje liczbę wszystkich komórek. Dla int[][] daje liczbę wierszy. Ta sama nazwa, dwa różne znaczenia — bo w drugim przypadku elementami naprawdę są wiersze.

TEORIA

Przechodzenie tablicy

Schemat jest ten sam co przy dwóch wymiarach, z jedną istotną zmianą: warunek wewnętrznej pętli zależy od bieżącego wiersza.

Program.cs
for (int w = 0; w < oceny.Length; w++)
{
    Console.Write($"Wiersz {w}: ");

    for (int k = 0; k < oceny[w].Length; k++)      // uwaga: oceny[w].Length
    {
        Console.Write($"{oceny[w][k]} ");
    }

    Console.WriteLine();
}
wynik w konsoli
Wiersz 0: 4 5 3 4
Wiersz 1: 3 3
Wiersz 2: 5 5 5 4 6 5 

Warunek k < oceny[0].Length to błąd

Użycie długości pierwszego wiersza dla wszystkich to najczęstsza pomyłka w tej lekcji. Przy wierszu krótszym program wyjdzie poza zakres, przy dłuższym po cichu pominie końcówkę danych. Zawsze oceny[w].Length — z tym samym w, po którym chodzi pętla zewnętrzna.

Wersja z foreach

Tu foreach jest wyraźnie ładniejszy niż przy tablicach dwuwymiarowych, bo zmienna pętli zewnętrznej to cała tablica-wiersz:

Program.cs
foreach (int[] wiersz in oceny)          // typ zmiennej to int[], nie int!
{
    int suma = 0;

    foreach (int x in wiersz)
    {
        suma += x;
    }

    Console.WriteLine($"{wiersz.Length} elementow, suma {suma}, " +
                      $"srednia {(double)suma / wiersz.Length:F2}");
}
wynik w konsoli
4 elementow, suma 16, srednia 4,00
2 elementow, suma 6, srednia 3,00
6 elementow, suma 30, srednia 5,00

Typ zmiennej pętli

W foreach (int[] wiersz in oceny) zmienna ma typ int[] — bo elementami tablicy postrzępionej są tablice. To najczęstsze miejsce, w którym warto użyć słowa var: foreach (var wiersz in oceny), i kompilator sam ustali typ.

TEORIA

Wiersz to zwykła tablica

To zdanie jest sednem całej lekcji. Skoro oceny[1] jest zwykłą tablicą int[], to wszystko, czego nauczyłeś się w lekcjach 17 i 18, działa na niej bez zmian:

Program.cs
int[][] dane =
{
    new int[] { 42, 17, 93, 8 },
    new int[] { 5, 3 },
    new int[] { 20, 10, 30 }
};

// sortowanie pojedynczego wiersza
Array.Sort(dane[0]);
Console.WriteLine(string.Join(", ", dane[0]));

// wypisanie wiersza jedna instrukcja
Console.WriteLine(string.Join(" | ", dane[2]));

// szukanie w wierszu
Console.WriteLine(Array.IndexOf(dane[2], 30));

// posortowanie WSZYSTKICH wierszy
foreach (int[] wiersz in dane)
{
    Array.Sort(wiersz);
}

// podmiana calego wiersza
dane[1] = new int[] { 100, 200, 300, 400, 500 };
Console.WriteLine(string.Join(", ", dane[1]));
wynik w konsoli
8, 17, 42, 93
20 | 10 | 30
2
100, 200, 300, 400, 500

Dlaczego Array.Sort w foreach działa

Pamiętasz z lekcji 17, że w foreach nie wolno przypisywać do zmiennej pętli? To wciąż prawda — ale my niczego jej nie przypisujemy. Zmienna wiersz zawiera adres tablicy, a Array.Sort zmienia zawartość spod tego adresu. Zakaz dotyczy wiersz = coś, nie Array.Sort(wiersz). To ta sama zasada, którą poznałeś przy referencjach.

Podmiana wiersza zmienia długość

Instrukcja dane[1] = new int[5]; jest całkowicie legalna — po prostu w to miejsce trafia nowa, inna tablica. Tego przy int[,] zrobić się nie da, bo tam nie ma osobnych wierszy do podmiany.

TEORIA

Którą tablicę wybrać

Cechaint[,] — dwuwymiarowaint[][] — postrzępiona
Kształtprostokątdowolny, „schodkowy”
Zapis elementut[w, k]t[w][k]
Liczba wierszyt.GetLength(0)t.Length
Długość wierszat.GetLength(1)t[w].Length
Tworzeniejedna instrukcjatablica wierszy, potem każdy wiersz
Metody klasy Arraynie działajądziałają na wierszu
Podmiana wierszaniemożliwamożliwa
Pamięćjeden zwarty blokosobne obiekty, nieco więcej narzutu
Szybkość odczytutrochę wyższatrochę niższa (dwa skoki po adresie)

Prosta reguła wyboru

Dane naprawdę prostokątne — plansza do gry, macierz, obraz — to int[,]. Dane o wierszach różnej długości albo takie, na których chcesz używać Array.Sort i string.Join — to int[][]. W wątpliwych przypadkach wybierz postrzępioną: jest wygodniejsza w obsłudze, a różnica w szybkości przy szkolnych rozmiarach danych jest niemierzalna.

METODY WBUDOWANE

Użyte w tej lekcji elementy

NazwaZwracaDziałanie
t.Lengthint Liczba wierszy tablicy postrzępionej (a nie wszystkich elementów, jak przy int[,]).
t[w].Lengthint Długość konkretnego wiersza. To zwykłe Length zwykłej tablicy.
t[w][k]element Element: najpierw wybieramy wiersz, potem pozycję w nim. Dwie pary nawiasów.
Array.Sort(t[w])nic Sortuje jeden wiersz. Na tablicy dwuwymiarowej niemożliwe.
string.Join(sep, t[w])string Wypisuje cały wiersz jedną instrukcją.
Array.IndexOf(t[w], x)int Szuka wartości w jednym wierszu; -1 oznacza brak.
t[w] = new int[n] Podmiana całego wiersza na nowy, o innej długości.
var Słowo pozwalające pominąć nazwę typu tam, gdzie kompilator sam ją ustali — wygodne przy foreach (var wiersz in dane).
PRZYKŁAD Z OMÓWIENIEM

Oceny o różnej liczbie

Cztery osoby, każda z inną liczbą ocen — dokładnie ta sytuacja, w której tablica dwuwymiarowa się nie sprawdza.

Program.cs
string[] uczniowie = { "Kowalski", "Nowak", "Wisniewska", "Zielinski" };

int[][] oceny =
{
    new int[] { 4, 5, 3, 4 },
    new int[] { 3, 3 },
    new int[] { 5, 5, 5, 4, 6, 5 },
    new int[] { 2, 4, 3 }
};

// --- naglowek ---
Console.WriteLine($"{"Uczen",-12}{"Ocen",6}{"Srednia",10}  Oceny");
Console.WriteLine(new string('-', 40));

// --- wiersze ---
int wszystkichOcen = 0;
int sumaWszystkich = 0;

int najlepszy = 0;
double najlepszaSrednia = 0;

for (int w = 0; w < oceny.Length; w++)
{
    int suma = 0;

    foreach (int o in oceny[w])          // wiersz to zwykla tablica
    {
        suma += o;
    }

    int ile = oceny[w].Length;
    double srednia = (double)suma / ile;

    Console.WriteLine($"{uczniowie[w],-12}{ile,6}{srednia,10:F2}  " +
                      $"{string.Join(", ", oceny[w])}");

    wszystkichOcen += ile;
    sumaWszystkich += suma;

    if (srednia > najlepszaSrednia)
    {
        najlepszaSrednia = srednia;
        najlepszy = w;
    }
}

Console.WriteLine(new string('-', 40));
Console.WriteLine();
Console.WriteLine($"Wszystkich ocen:  {wszystkichOcen}");
Console.WriteLine($"Srednia klasy:    {(double)sumaWszystkich / wszystkichOcen:F2}");
Console.WriteLine($"Najlepszy:        {uczniowie[najlepszy]} ({najlepszaSrednia:F2})");
Console.WriteLine();

// --- oceny kazdego posortowane rosnaco ---
Console.WriteLine("Oceny posortowane:");

for (int w = 0; w < oceny.Length; w++)
{
    int[] kopia = oceny[w][..];          // kopia, zeby nie zepsuc kolejnosci
    Array.Sort(kopia);

    Console.WriteLine($"  {uczniowie[w],-12}{string.Join(", ", kopia)}");
}
wynik w konsoli
Uczen         Ocen   Srednia  Oceny
----------------------------------------
Kowalski         4      4,00  4, 5, 3, 4
Nowak            2      3,00  3, 3
Wisniewska       6      5,00  5, 5, 5, 4, 6, 5
Zielinski        3      3,00  2, 4, 3
----------------------------------------

Wszystkich ocen:  15
Srednia klasy:    4,07
Najlepszy:        Wisniewska (5,00)

Oceny posortowane:
  Kowalski    3, 4, 4, 5
  Nowak       3, 3
  Wisniewska  4, 5, 5, 5, 5, 6
  Zielinski   2, 3, 4

Omówienie

FragmentCo robi i dlaczego tak
foreach (int o in oceny[w]) oceny[w] to zwykła tablica int[], więc foreach po niej wygląda tak samo jak w lekcji 17. Nie trzeba znać jej długości — pętla sama wie, kiedy skończyć.
int ile = oceny[w].Length; Długość tego wiersza. Przy tablicy dwuwymiarowej byłaby jedna liczba dla wszystkich; tutaj każdy uczeń ma swoją.
string.Join(", ", oceny[w]) Cały wiersz jedną instrukcją. Na int[,] to nie zadziała — trzeba by pisać pętlę.
wszystkichOcen += ile; Liczby wszystkich ocen nie da się odczytać z żadnej właściwości — sumujemy ją po drodze. To jedyna rzecz, którą tablica dwuwymiarowa dałaby za darmo (przez Length).
sumaWszystkich / wszystkichOcen Uwaga: to nie to samo, co średnia ze średnich uczniów. Wisniewska ma sześć ocen, Nowak dwie, więc jej wyniki ważą więcej. Średnia ze średnich dałaby (4,00 + 3,00 + 5,00 + 3,00) / 4 = 3,75, a średnia ze wszystkich ocen to 4,07. Obie liczby są poprawne — odpowiadają na różne pytania.
int[] kopia = oceny[w][..]; Zakres z lekcji 17 zastosowany do wiersza. Sortujemy kopię, bo pierwotna kolejność ocen (chronologiczna) ma swoją wartość.
Array.Sort(kopia) Metoda klasy Array na jednym wierszu — to właśnie ta możliwość, której nie ma przy int[,].

Sprawdź rachunek

Kowalski: 4 + 5 + 3 + 4 = 16, średnia 16 / 4 = 4,00. Wisniewska: 5+5+5+4+6+5 = 30, średnia 30 / 6 = 5,00. Wszystkich ocen: 4 + 2 + 6 + 3 = 15, ich suma 16 + 6 + 30 + 9 = 61, więc średnia klasy 61 / 15 = 4,0666… → 4,07.

CZĘSTE BŁĘDY

Na czym się najczęściej potykamy

BłądCo się dzieje i jak poprawić
new int[3][4] Nie skompiluje się. Tablicę postrzępioną tworzy się w dwóch krokach: new int[3][], a potem każdy wiersz osobno.
brak tworzenia wierszy NullReferenceException — po new int[3][] wierszy jeszcze nie ma. Każdy trzeba utworzyć: dane[0] = new int[4];.
t[w, k] na tablicy postrzępionej CS0022 — zła liczba indeksów. Poprawnie: t[w][k], dwie pary nawiasów.
k < t[0].Length w pętli wewnętrznej Długość pierwszego wiersza użyta dla wszystkich. Krótszy wiersz → wyjście poza zakres, dłuższy → ciche pominięcie danych. Zawsze t[w].Length.
t.Length jako liczba wszystkich elementów To liczba wierszy. Wszystkie elementy trzeba policzyć pętlą.
t.GetLength(1) Nie ma sensu — tablica postrzępiona ma jeden wymiar tablic. GetLength(0) zadziała (to liczba wierszy), ale przyjęło się pisać Length.
foreach (int x in dane) CS0030 — elementami są tablice, nie liczby. Poprawnie: foreach (int[] wiersz in dane) albo foreach (var wiersz in dane).
brak new int[] przy każdym wierszu W zapisie skróconym każdy wiersz potrzebuje własnego new int[] { ... } — albo, od C# 12, samych nawiasów kwadratowych.
sortowanie oryginału zamiast kopii Array.Sort(dane[w]) nieodwracalnie zmienia kolejność w tym wierszu. Gdy jest jeszcze potrzebna, sortuj dane[w][..].
ZADANIA

Zadania

ZAD 1Trójkąt liczb★☆☆

Utwórz tablicę postrzępioną o pięciu wierszach, w której wiersz numer w ma w + 1 elementów, wypełnij ją kolejnymi liczbami naturalnymi i wypisz.

pokaż rozwiązanie
int[][] trojkat = new int[5][];
int licznik = 1;

for (int w = 0; w < trojkat.Length; w++)
{
    trojkat[w] = new int[w + 1];        // wiersz 0 ma 1 element, wiersz 4 ma 5

    for (int k = 0; k < trojkat[w].Length; k++)
    {
        trojkat[w][k] = licznik;
        licznik++;
    }
}

foreach (int[] wiersz in trojkat)
{
    Console.WriteLine(string.Join(" ", wiersz));
}
wynik w konsoli
1
2 3
4 5 6
7 8 9 10
11 12 13 14 15

Tworzenie wiersza (new int[w + 1]) odbywa się wewnątrz pętli zewnętrznej, tuż przed jego wypełnieniem. Tego kroku nie ma przy tablicy dwuwymiarowej i właśnie o nim najłatwiej zapomnieć.

licznik jest zadeklarowany przed obiema pętlami, bo ma rosnąć nieprzerwanie przez cały trójkąt. Zerowany w pętli zewnętrznej dałby w każdym wierszu 1, 2, 3…

ZAD 2Sumy wierszy★☆☆

Dla tablicy postrzępionej wypisz każdy wiersz wraz z jego długością, sumą i średnią. Na końcu podaj wiersz o największej sumie.

pokaż rozwiązanie
int[][] dane =
{
    new int[] { 5, 8, 2 },
    new int[] { 10, 10 },
    new int[] { 1, 2, 3, 4, 5, 6 },
    new int[] { 40 }
};

int najlepszyWiersz = 0;
int najwiekszaSuma = 0;

for (int w = 0; w < dane.Length; w++)
{
    int suma = 0;

    foreach (int x in dane[w])
    {
        suma += x;
    }

    double srednia = (double)suma / dane[w].Length;

    Console.WriteLine($"Wiersz {w}: {dane[w].Length} el., suma {suma,3}, " +
                      $"srednia {srednia,6:F2}");

    if (suma > najwiekszaSuma)
    {
        najwiekszaSuma = suma;
        najlepszyWiersz = w;
    }
}

Console.WriteLine();
Console.WriteLine($"Najwieksza suma: wiersz {najlepszyWiersz} ({najwiekszaSuma})");
wynik w konsoli
Wiersz 0: 3 el., suma  15, srednia   5,00
Wiersz 1: 2 el., suma  20, srednia  10,00
Wiersz 2: 6 el., suma  21, srednia   3,50
Wiersz 3: 1 el., suma  40, srednia  40,00

Najwieksza suma: wiersz 3 (40)

Zwróć uwagę, że wiersz o największej sumie (wiersz 3, jeden element o wartości 40) to co innego niż wiersz o największej średniej — akurat tutaj to ten sam wiersz, ale przy innych danych już nie musi tak być. Zawsze dopytaj, o którą wielkość chodzi.

Suma zeruje się na początku każdego obiegu pętli zewnętrznej — dokładnie ta sama zasada co przy sumach wierszy w lekcji 19.

ZAD 3Wczytanie danych od użytkownika★★☆

Zapytaj o liczbę uczniów, a potem dla każdego — o liczbę jego ocen i o same oceny. Na koniec wypisz zestawienie.

pokaż rozwiązanie
int ilu;
bool ok;

do
{
    Console.Write("Ilu uczniow (1-10)? ");
    ok = int.TryParse(Console.ReadLine(), out ilu) && ilu >= 1 && ilu <= 10;

    if (!ok)
    {
        Console.WriteLine("  Podaj liczbe od 1 do 10.");
    }
}
while (!ok);

string[] imiona = new string[ilu];
int[][] oceny = new int[ilu][];

for (int w = 0; w < ilu; w++)
{
    Console.WriteLine();
    Console.Write($"Imie ucznia {w + 1}: ");
    imiona[w] = Console.ReadLine() ?? $"Uczen {w + 1}";

    int ile;

    do
    {
        Console.Write("  Ile ocen (1-20)? ");
        ok = int.TryParse(Console.ReadLine(), out ile) && ile >= 1 && ile <= 20;

        if (!ok)
        {
            Console.WriteLine("  Podaj liczbe od 1 do 20.");
        }
    }
    while (!ok);

    oceny[w] = new int[ile];            // TERAZ tworzymy wiersz

    for (int k = 0; k < ile; k++)
    {
        int ocena;

        do
        {
            Console.Write($"  Ocena {k + 1}: ");
            ok = int.TryParse(Console.ReadLine(), out ocena) && ocena >= 1 && ocena <= 6;

            if (!ok)
            {
                Console.WriteLine("  Ocena musi byc z zakresu 1-6.");
            }
        }
        while (!ok);

        oceny[w][k] = ocena;
    }
}

Console.WriteLine();
Console.WriteLine("=== ZESTAWIENIE ===");

for (int w = 0; w < ilu; w++)
{
    int suma = 0;

    foreach (int o in oceny[w])
    {
        suma += o;
    }

    Console.WriteLine($"{imiona[w],-14}{string.Join(", ", oceny[w]),-24}" +
                      $"srednia {(double)suma / oceny[w].Length:F2}");
}
wynik w konsoli (skrócony)
Ilu uczniow (1-10)? 2

Imie ucznia 1: Ala
  Ile ocen (1-20)? 3
  Ocena 1: 4
  Ocena 2: 5
  Ocena 3: 3

Imie ucznia 2: Bartek
  Ile ocen (1-20)? 2
  Ocena 1: 2
  Ocena 2: 4

=== ZESTAWIENIE ===
Ala           4, 5, 3                 srednia 4,00
Bartek        2, 4                    srednia 3,00

Kolejność jest tu wszystkim. Wiersz można utworzyć dopiero wtedy, gdy znamy jego długość — czyli po pytaniu „ile ocen”. To właśnie ta elastyczność, której tablica dwuwymiarowa nie daje: tam rozmiar wszystkich wierszy trzeba znać z góry.

Trzy zagnieżdżone pętle do-while to trzy niezależne walidacje. Każda pilnuje swojego pytania i każda używa tego samego wzorca z lekcji 14.

Zmienna ok jest zadeklarowana raz, na samej górze, i używana ponownie w każdej pętli. To dopuszczalne, bo w każdej chwili interesuje nas tylko ostatnie sprawdzenie.

Zapis {string.Join(", ", oceny[w]),-24} pokazuje ciekawą rzecz: wyrównanie stosuje się do wyniku wyrażenia, więc można wyrównać cały sklejony napis.

ZAD 4Trójkąt Pascala★★☆

Zbuduj trójkąt Pascala o zadanej liczbie wierszy: pierwszy i ostatni element każdego wiersza to 1, a każdy element w środku jest sumą dwóch elementów stojących nad nim.

pokaż rozwiązanie
const int Wierszy = 6;

int[][] pascal = new int[Wierszy][];

for (int w = 0; w < Wierszy; w++)
{
    pascal[w] = new int[w + 1];

    pascal[w][0] = 1;                    // pierwszy zawsze 1
    pascal[w][w] = 1;                    // ostatni zawsze 1

    for (int k = 1; k < w; k++)          // tylko srodek - stad start od 1
    {
        pascal[w][k] = pascal[w - 1][k - 1] + pascal[w - 1][k];
    }
}

foreach (int[] wiersz in pascal)
{
    foreach (int x in wiersz)
    {
        Console.Write($"{x,4}");
    }

    Console.WriteLine();
}
wynik w konsoli
   1
   1   1
   1   2   1
   1   3   3   1
   1   4   6   4   1
   1   5  10  10   5   1

Wewnętrzna pętla idzie od 1 do w (bez w). Skrajne elementy są już ustawione, więc liczymy tylko środek. Dla wierszy 0 i 1 pętla nie wykona się ani razu — to poprawne, bo tam nie ma żadnego środka.

pascal[w][0] = 1; pascal[w][w] = 1; — dla wiersza 0 obie linie zapisują ten sam element. Nie szkodzi, wartość jest ta sama.

Wzór pascal[w-1][k-1] + pascal[w-1][k] sięga do poprzedniego wiersza, który jest już policzony. To dlatego wiersze muszą powstawać po kolei, od góry.

Sprawdź na wierszu 4: 4 = 1 + 3 (z wiersza 3), 6 = 3 + 3, 4 = 3 + 1.

To zadanie idealnie pokazuje sens tablicy postrzępionej — wiersz w ma dokładnie w + 1 elementów, więc prostokąt marnowałby połowę miejsca i wymagał pilnowania zer.

ZAD 5Pomiary temperatury★★☆

Dla siedmiu dni tygodnia zapisano różną liczbę pomiarów temperatury. Dla każdego dnia podaj liczbę pomiarów, średnią oraz rozstęp (różnicę między największym a najmniejszym). Wskaż dzień o największym rozstępie.

pokaż rozwiązanie
string[] dni = { "Pn", "Wt", "Sr", "Cz", "Pt", "So", "Nd" };

double[][] pomiary =
{
    new double[] { 12.5, 15.0, 18.5, 14.0 },
    new double[] { 10.0, 11.5 },
    new double[] { 16.0, 19.5, 22.0, 20.5, 17.0 },
    new double[] { 21.0 },
    new double[] { 8.5, 12.0, 15.5 },
    new double[] { 14.0, 16.5, 15.0, 13.5, 12.0, 11.0 },
    new double[] { 19.0, 20.0 }
};

Console.WriteLine($"{"Dzien",-7}{"Pomiarow",10}{"Srednia",10}{"Min",8}{"Max",8}{"Rozstep",10}");
Console.WriteLine(new string('-', 53));

int dzienMaxRozstepu = 0;
double maxRozstep = -1;

for (int d = 0; d < pomiary.Length; d++)
{
    double[] dzien = pomiary[d];         // krotsza nazwa dla wiersza

    double suma = 0;
    double min = dzien[0];
    double max = dzien[0];

    foreach (double t in dzien)
    {
        suma += t;
        min = Math.Min(min, t);
        max = Math.Max(max, t);
    }

    double srednia = suma / dzien.Length;
    double rozstep = max - min;

    Console.WriteLine($"{dni[d],-7}{dzien.Length,10}{srednia,10:F2}" +
                      $"{min,8:F1}{max,8:F1}{rozstep,10:F1}");

    if (rozstep > maxRozstep)
    {
        maxRozstep = rozstep;
        dzienMaxRozstepu = d;
    }
}

Console.WriteLine(new string('-', 53));
Console.WriteLine();
Console.WriteLine($"Najwiekszy rozstep: {dni[dzienMaxRozstepu]} ({maxRozstep:F1} stopnia)");
wynik w konsoli
Dzien    Pomiarow   Srednia     Min     Max   Rozstep
-----------------------------------------------------
Pn              4     15,00    12,5    18,5       6,0
Wt              2     10,75    10,0    11,5       1,5
Sr              5     19,00    16,0    22,0       6,0
Cz              1     21,00    21,0    21,0       0,0
Pt              3     12,00     8,5    15,5       7,0
So              6     13,67    11,0    16,5       5,5
Nd              2     19,50    19,0    20,0       1,0
-----------------------------------------------------

Najwiekszy rozstep: Pt (7,0 stopnia)

double[] dzien = pomiary[d]; to nie kopia, tylko drugi klucz do tej samej tablicy — zmienna referencyjna z lekcji 17. Robimy to wyłącznie dla skrócenia zapisu: dzien[0] czyta się lepiej niż pomiary[d][0]. Gdybyśmy chcieli tę tablicę zmieniać bez wpływu na oryginał, potrzebna byłaby kopia pomiary[d][..].

min i max startują od dzien[0] — i tu jest to szczególnie ważne, bo temperatury mogą być ujemne. Start od zera dałby dla mroźnego dnia maksimum równe 0.

Czwartek ma tylko jeden pomiar, więc rozstęp wynosi 0, a średnia równa się temu jedynemu pomiarowi. Program radzi sobie z tym bez żadnego dodatkowego warunku — to zasługa tego, że min i max zaczynają od pierwszego elementu.

maxRozstep = -1 na starcie, bo rozstęp nie może być ujemny — dowolny prawdziwy wynik go przebije. Start od zera byłby tu ryzykowny: gdyby wszystkie dni miały po jednym pomiarze, żaden nie przebiłby zera i program wskazałby dzień 0 bez uzasadnienia.

Sprawdź środę: 16,0 + 19,5 + 22,0 + 20,5 + 17,0 = 95,0, średnia 95 / 5 = 19,00, rozstęp 22,0 − 16,0 = 6,0.

ZAD 6Sortowanie wierszy według sumy★★★

Posortuj wiersze tablicy postrzępionej malejąco według ich sumy — tak, żeby wiersz o największej sumie znalazł się na górze. Wykorzystaj Array.Sort z dwiema tablicami.

pokaż rozwiązanie
int[][] dane =
{
    new int[] { 5, 8, 2 },          // suma 15
    new int[] { 10, 10 },           // suma 20
    new int[] { 1, 2, 3, 4, 5, 6 }, // suma 21
    new int[] { 40 },               // suma 40
    new int[] { 7, 7, 7 }           // suma 21
};

// --- klucz sortowania: suma kazdego wiersza ---
int[] sumy = new int[dane.Length];

for (int w = 0; w < dane.Length; w++)
{
    foreach (int x in dane[w])
    {
        sumy[w] += x;
    }
}

Console.WriteLine("Przed sortowaniem:");
Wypisz(dane, sumy);

// --- sortujemy: sumy jako klucz, wiersze jada za nimi ---
Array.Sort(sumy, dane);
Array.Reverse(sumy);
Array.Reverse(dane);                // OBIE tablice!

Console.WriteLine();
Console.WriteLine("Po sortowaniu malejaco:");
Wypisz(dane, sumy);

static void Wypisz(int[][] t, int[] s)
{
    for (int w = 0; w < t.Length; w++)
    {
        Console.WriteLine($"  suma {s[w],3}:  {string.Join(", ", t[w])}");
    }
}
wynik w konsoli
Przed sortowaniem:
  suma  15:  5, 8, 2
  suma  20:  10, 10
  suma  21:  1, 2, 3, 4, 5, 6
  suma  40:  40
  suma  21:  7, 7, 7

Po sortowaniu malejaco:
  suma  40:  40
  suma  21:  7, 7, 7
  suma  21:  1, 2, 3, 4, 5, 6
  suma  20:  10, 10
  suma  15:  5, 8, 2

To jest coś, czego nie da się zrobić na tablicy dwuwymiarowej. Array.Sort(sumy, dane) traktuje dane jak zwykłą tablicę pięciu elementów — a że jej elementami są tablice, przestawia całe wiersze naraz. Przy int[,] wierszy nie ma czego przestawiać, bo to jeden zwarty obiekt.

sumy[w] += x; działa bez wcześniejszego zerowania, bo świeżo utworzona tablica int[] jest wypełniona zerami. To ta wartość domyślna z lekcji 17.

Pamiętaj o odwróceniu obu tablic. Odwrócenie samych sum rozjechałoby je z wierszami — i program pokazałby złe liczby, nie zgłaszając żadnego błędu. Ten sam problem był w lekcji 18 przy rankingu uczniów.

Zwróć uwagę na dwa wiersze o sumie 21: po sortowaniu stanęły obok siebie, ale ich wzajemna kolejność mogła się zmienić. Array.Sort nie gwarantuje stabilności — przy równych kluczach nie wiadomo, który element trafi wyżej.

PODSUMOWANIE

Co trzeba zapamiętać

  • Tablica postrzępiona to tablica tablic: int[][], dwie pary nawiasów.
  • Tworzenie odbywa się w dwóch krokach: najpierw new int[n][], potem każdy wiersz osobno.
  • Bez utworzenia wiersza odwołanie do niego kończy się NullReferenceException.
  • Element to t[w][k], nie t[w, k].
  • t.Length to liczba wierszy, t[w].Length — długość konkretnego wiersza.
  • W pętli wewnętrznej warunek to k < t[w].Length — z bieżącym w.
  • Każdy wiersz jest zwykłą tablicą, więc działają na nim Array.Sort, Array.IndexOf i string.Join.
  • Cały wiersz można podmienić na inny, także innej długości.
  • Liczby wszystkich elementów nie da się odczytać — trzeba ją policzyć pętlą.
  • Dane prostokątne → int[,]. Wiersze różnej długości albo potrzeba metod Arrayint[][].