1. Czym tablice zębate różnią się od dwuwymiarowych
No i dotarliśmy do tematu, który wielu nazywa „tablicami tablic” albo „tablicami zębatymi” — po angielsku jagged arrays. W przeciwieństwie do tablic dwuwymiarowych, tablice zębate pozwalają przechowywać kolumny o różnej długości. To jakbyś miał kompleks budynków, gdzie każdy budynek ma inną liczbę mieszkań — w jednym 5, w drugim 20, a w trzecim tylko jedno.
Tablica zębata to tablica, której każdy element sam jest tablicą. Przy czym te wewnętrzne tablice (zwane też „podtablicami”) mogą mieć różną długość.
Główna różnica:
- W tablicy dwuwymiarowej każda „linia” (i każda „kolumna”) ma tyle samo elementów. Przykład: int[,] grid = new int[3, 5]; — zawsze mamy 3 linie po 5 elementów.
- W tablicy zębatej każda linia może mieć inną długość! Przykład: int[][] jagged = new int[3][]; — a potem każdą linię (podtablicę) inicjalizujemy osobno.
Tak to wygląda wizualnie:
| Tablica dwuwymiarowa | Tablica zębata | |
|---|---|---|
| Liczba elementów | Ściśle ustalona (np. 3x5) | Może się różnić między liniami |
| Indeksowanie | |
|
| Elastyczność | Niska | Wysoka |
| Zastosowanie | Tabele, matematyka | Nierówne dane: listy studentów z różną liczbą ocen, trójkąty |
Wizualizacja: porównanie tablicy dwuwymiarowej i zębatej
Tablica dwuwymiarowa (3x3):
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
├───┼───┼───┤
│ 7 │ 8 │ 9 │
└───┴───┴───┘
Tablica zębata (różne długości):
┌───┬───┐
│ 1 │ 2 │
├───┼───┼───┬───┐
│ 3 │ 4 │ 5 │ 6 │
├───┼───┴───┴───┘
│ 7 │
└───┘
2. Składnia deklaracji i inicjalizacji tablicy zębatej
Deklaracja tablicy zębatej nie jest straszniejsza niż poprzednich typów! Nie bój się podwójnych nawiasów kwadratowych:
int[][] jaggedArray = new int[3][];
To znaczy, że mamy tablicę z 3 elementami i każdy z nich to też tablica int-ów. Ale na razie te wewnętrzne tablice nie istnieją! Żeby to lepiej ogarnąć, rozbijmy to na kroki.
Krok po kroku: inicjalizacja tablicy zębatej
Krok 1 — tworzenie głównej (zewnętrznej) tablicy:
int[][] jaggedArray = new int[3][];
Teraz mamy 3 „linie”, ale wszystkie są na razie równe null.
Krok 2 — tworzenie i wypełnianie wewnętrznych tablic (podtablic):
Załóżmy, że pierwsza linia ma długość 2, druga — 4, trzecia — 3:
jaggedArray[0] = new int[2]; // 2 elementy w pierwszej linii
jaggedArray[1] = new int[4]; // 4 elementy w drugiej linii
jaggedArray[2] = new int[3]; // 3 elementy w trzeciej linii
Krok 3 — wypełnianie wartościami:
Wewnętrzne tablice to zwykłe tablice! Na przykład:
jaggedArray[0][0] = 1;
jaggedArray[0][1] = 2;
jaggedArray[1][0] = 3;
jaggedArray[1][1] = 4;
jaggedArray[1][2] = 5;
jaggedArray[1][3] = 6;
jaggedArray[2][0] = 7;
jaggedArray[2][1] = 8;
jaggedArray[2][2] = 9;
Skrócona inicjalizacja tablicy zębatej
Możesz od razu stworzyć i wypełnić tablicę zębatą, jeśli znasz wartości z góry:
int[][] jaggedArray = new int[][]
{
new int[] { 1, 2 },
new int[] { 3, 4, 5, 6 },
new int[] { 7, 8, 9 }
};
Albo jeszcze krócej, pomijając typ wewnętrznych tablic:
int[][] jaggedArray = {
new[] { 1, 2 },
new[] { 3, 4, 5, 6 },
new[] { 7, 8, 9 }
};
3. Iteracja i praca z tablicami zębatymi
Iterowanie po tablicy zębatej nie jest trudniejsze niż po dwuwymiarowej, ale teraz zewnętrzna pętla idzie po liniach, a wewnętrzna po elementach linii (które mogą mieć różną długość):
for (int i = 0; i < jaggedArray.Length; i++)
{
Console.WriteLine($"Linia {i}:");
for (int j = 0; j < jaggedArray[i].Length; j++)
{
Console.Write($"{jaggedArray[i][j]} ");
}
Console.WriteLine();
}
Efekt na ekranie:
Linia 0:
1 2
Linia 1:
3 4 5 6
Linia 2:
7 8 9
Możesz użyć foreach, żeby nie myśleć o indeksach:
foreach (int[] row in jaggedArray)
{
foreach (int value in row)
{
Console.Write($"{value} ");
}
Console.WriteLine();
}
4. Jak działa tablica tablic
A teraz zobaczysz, jak naprawdę działają tablice tablic. Gotowy?
Jeśli w przypadku zwykłej tablicy „zmienna-tablica przechowuje referencję do kontenera, który trzyma elementy tablicy”. To w przypadku tablic zębatych sytuacja jest trochę bardziej wybuchowa: zmienna-tablica-tablic przechowuje referencję do kontenera, który trzyma referencje do tablic jednowymiarowych. Lepiej to zobaczyć niż sto razy tłumaczyć:
Po lewej mamy „zmienną-tablicę-tablic”, która trzyma referencję do „obiektu-kontenera tablic”. W środku mamy „obiekt-kontener tablic”, w którego komórkach są referencje do tablic jednowymiarowych — linii tablicy zębatej. No i po prawej widzisz cztery tablice jednowymiarowe — linie naszej tablicy zębatej.
Tak właśnie naprawdę działają tablice zębate. I takie podejście daje C#-programiście kilka plusów:
Po pierwsze, ponieważ „kontener kontenerów” trzyma referencje do „tablic-linii”, możemy bardzo szybko i łatwo zamieniać linie miejscami. Żeby dostać się do „kontenera kontenerów”, wystarczy podać jeden indeks zamiast dwóch. Przykład:int[][] data = new int[2][];
data[0] = new int[5]; // pierwsza linia — tablica 5 elementów
data[1] = new int[5]; // druga linia — tablica 5 elementów
int[] row1 = data[0];
int[] row2 = data[1];
Takim kodem możesz zamienić linie miejscami:
// Ważna macierz z danymi
int[][] matrix = {
new int[] {1, 2, 3, 4, 5},
new int[] {5, 4, 3, 2, 1}
};
int[] tmp = matrix[0];
matrix[0] = matrix[1];
matrix[1] = tmp;
Jeśli odwołujesz się do komórki tablicy dwuwymiarowej, ale po nazwie tablicy podajesz tylko jeden indeks, to dostajesz się do kontenera kontenerów, w którego komórkach są referencje do zwykłych tablic jednowymiarowych.
5. Typowe scenariusze użycia tablic zębatych
Kiedy tablica zębata może być lepsza niż dwuwymiarowa?
- Gdy przechowujesz dla każdego użytkownika inną liczbę jakichś danych: oceny z przedmiotów, zakupy, komentarze itd.
- Gdy twoje dane mają trójkątną albo schodkową strukturę (np. do rysowania piramidek, trójkątów Pascala itd.).
- Gdy chcesz oszczędzać pamięć: w tablicy dwuwymiarowej wszystkie linie są stałe, a w zębatej — tylko tyle elementów, ile trzeba.
Przykład z życia: menedżer ocen studentów
Rozwińmy nasz projekt! Załóżmy, że każdy student może mieć inną liczbę ocen z każdego przedmiotu. Jeden robi więcej zadań, inny mniej. Do tego idealnie nada się tablica zębata.
Załóżmy, że mamy trzech studentów i oto ich oceny z różnych zadań z matematyki:
| Student | Oceny |
|---|---|
| 0 | 5, 4 |
| 1 | 3, 4, 4 |
| 2 | 5 |
Zadeklarujmy taką tablicę:
int[][] studentMarks = new int[3][];
studentMarks[0] = new int[] { 5, 4 }; // Pierwszy student - 2 oceny
studentMarks[1] = new int[] { 3, 4, 4 }; // Drugi student - 3 oceny
studentMarks[2] = new int[] { 5 }; // Trzeci student - 1 ocena
Wyświetlmy oceny każdego studenta:
for (int i = 0; i < studentMarks.Length; i++)
{
Console.Write($"Student {i}: ");
for (int j = 0; j < studentMarks[i].Length; j++)
{
Console.Write(studentMarks[i][j] + " ");
}
Console.WriteLine();
}
Tablice zębate z innymi typami
Tablica zębata może być tablicą czegokolwiek: stringów, tablic innych tablic (głębiej!), nawet twoich własnych obiektów.
Przykład: tablica stringów
string[][] groups = new string[][]
{
new string[] { "Ivan", "Piotr" },
new string[] { "Maria", "Aleksej", "Sergiej" },
new string[] { "Wasilisa" }
};
6. Cechy i możliwe błędy
Tablice zębate są elastyczne, ale pułapki czają się na każdym kroku.
- Jeśli nie zainicjalizujesz którejś z wewnętrznych tablic (jaggedArray[1] = ...), próba dostępu do niej skończy się NullReferenceException. Nie zapomnij zainicjalizować każdej wewnętrznej tablicy!
- Nie wszystkie linie (podtablice) mają tę samą długość. Jeśli użyjesz stałego indeksu w drugim wymiarze, możesz wyjść poza zakres.
- Nie myl z tablicą dwuwymiarową! Indeksowanie wygląda tak: array[i][j], a nie array[i, j].
GO TO FULL VERSION