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

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[][] ćwiczenia 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.

Kiedy przydaje się ten model

Tablica tablic pasuje do danych, w których każdy uczeń ma inną liczbę ocen albo każdy dzień inną liczbę pomiarów. Wymagania konkretnego egzaminu sprawdzaj osobno w jego aktualnej dokumentacji.

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

GetLength(0) istnieje i zwraca długość tablicy zewnętrznej. Wygodniej użyć 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
int[][] oceny = { new int[] { 4, 5, 3, 4 }, new int[] { 3, 3 }, new int[] { 5, 5, 5, 4, 6, 5 } };

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
int[][] oceny = { new int[] { 4, 5, 3, 4 }, new int[] { 3, 3 }, new int[] { 5, 5, 5, 4, 6, 5 } };

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 23 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 23, ż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
Array.Sort i Array.Reversenie działajądziałają na wierszu
Podmiana wierszaniemożliwamożliwa
Pamięćjeden zwarty blokosobne obiekty, nieco więcej narzutu
Szybkość odczytuzależy od programu i środowiskazależy od programu i środowiska; wydajność sprawdzamy pomiarem

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

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.

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.

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.

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.

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.

PODSUMOWANIE

Co trzeba zapamiętać

  • int[][] to tablica, której elementami są referencje do tablic int[].
  • Nowa tablica zewnętrzna ma początkowo wiersze null. Przed dostępem do elementów utwórz wiersze.
  • t.Length liczy wiersze, a t[w].Length elementy wiersza.
  • GetLength(0) na tablicy zewnętrznej też działa, ale Length jest czytelniejsze.
  • Warunek pętli wewnętrznej powinien używać długości bieżącego wiersza.
  • Pojedynczy wiersz można sortować, kopiować lub podmienić jak zwykłą tablicę.
  • Aby policzyć wszystkie elementy, sumujemy długości wierszy. Wiersze puste i null wymagają świadomej obsługi.
  • Wybierz reprezentację według modelu danych, nie założenia, że jeden rodzaj tablic zawsze jest szybszy.
Postęp zapisuje się w tej przeglądarce.