1. Чем зубчатые массивы отличаются от двумерных
Вот мы дошли до темы, которую многие называют «массивами массивов» или «зубчатыми массивами» — по-английски jagged arrays. В отличие от двумерных массивов, зубчатые массивы позволяют хранить колонки разной длины. Это как если бы у вас был комплекс зданий, где у каждого здания свое количество квартир — в одном здании 5 квартир, в другом 20, а в третьем — всего одна.
Зубчатый массив — это массив, каждый элемент которого сам по себе является массивом. При этом вложенные массивы (их ещё называют «подмассивы») могут иметь разную длину.
Главное отличие:
- В двумерном массиве у каждой «строки» (и у каждого «столбца») одинаковое количество элементов. Пример: int[,] grid = new int[3, 5]; — у нас всегда 3 строки по 5 элементов.
- В зубчатом массиве каждая строка может быть разной длины! Пример: int[][] jagged = new int[3][]; — и только потом мы каждую строку (подмассив) инициализируем по-своему.
Вот как это выглядит визуально:
| Двумерный массив | Зубчатый массив | |
|---|---|---|
| Количество элементов | Строго фиксированное (например, 3х5) | Может отличаться между разными строками |
| Индексация | |
|
| Гибкость | Низкая | Высокая |
| Применение | Таблицы, математика | Неравномерные данные: списки студентов с разным числом оценок, треугольники |
Визуализация: сравним двумерный и зубчатый массив
Двумерный массив (3x3):
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
├───┼───┼───┤
│ 7 │ 8 │ 9 │
└───┴───┴───┘
Зубчатый массив (разные длины):
┌───┬───┐
│ 1 │ 2 │
├───┼───┼───┬───┐
│ 3 │ 4 │ 5 │ 6 │
├───┼───┴───┴───┘
│ 7 │
└───┘
2. Синтаксис объявления и инициализации зубчатого массива
Объявление зубчатого массива не страшнее, чем объявления предыдущих типов! Пугаться двойных квадратных скобок не стоит:
int[][] jaggedArray = new int[3][];
Это значит, что у нас есть массив из 3 элементов, и каждый из них — это тоже массив int-ов. Но пока что внутренние массивы не созданы! Для лучшего понимания давайте разберём это подробнее.
Пошаговая инициализация зубчатого массива
Шаг 1 — создание основного (внешнего) массива:
int[][] jaggedArray = new int[3][];
Теперь у нас есть 3 «строки», но все они пока равны null.
Шаг 2 — создание и заполнение внутренних массивов (подмассивов):
Например, пусть первая строка будет длиной 2, вторая — 4, третья — 3:
jaggedArray[0] = new int[2]; // 2 элемента в первой строке
jaggedArray[1] = new int[4]; // 4 элемента во второй строке
jaggedArray[2] = new int[3]; // 3 элемента в третьей строке
Шаг 3 — заполнение значениями:
Внутренние массивы — это обычные массивы! Например:
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;
Краткая инициализация зубчатого массива
Можно создать и заполнить зубчатый массив сразу, если вы знаете значения заранее:
int[][] jaggedArray = new int[][]
{
new int[] { 1, 2 },
new int[] { 3, 4, 5, 6 },
new int[] { 7, 8, 9 }
};
Или чуть короче, опуская тип внутренних массивов:
int[][] jaggedArray = {
new[] { 1, 2 },
new[] { 3, 4, 5, 6 },
new[] { 7, 8, 9 }
};
3. Перебор и работа с зубчатыми массивами
Перебирать зубчатый массив — не сложнее, чем двумерный, но теперь внешний цикл идёт по строкам, а внутренний — по элементам строки (которые могут иметь разную длину):
for (int i = 0; i < jaggedArray.Length; i++)
{
Console.WriteLine($"Строка {i}:");
for (int j = 0; j < jaggedArray[i].Length; j++)
{
Console.Write($"{jaggedArray[i][j]} ");
}
Console.WriteLine();
}
Итог на экране:
Строка 0:
1 2
Строка 1:
3 4 5 6
Строка 2:
7 8 9
Можно использовать foreach, чтобы не думать об индексах:
foreach (int[] row in jaggedArray)
{
foreach (int value in row)
{
Console.Write($"{value} ");
}
Console.WriteLine();
}
4. Устройство массива массивов
А сейчас вы узнаете, как на самом деле устроены массивы массивы. Готовы?
Если в случае с обычным массивом «переменная-массив хранит ссылку на контейнер, который хранит элементы массива». То в случае с зубчатыми массивами у нас ситуация немного взрывоопаснее: переменная-массив-массивов хранит ссылку на контейнер, который хранит ссылки на одномерные массивы. Это лучше один раз увидеть, чем сто раз попробовать объяснить:
Слева у нас «переменная-массив-массивов», которая хранит ссылку на «объект-контейнер массивов». В середине у нас «объект-контейнер массивов», в ячейках которого хранятся ссылки на одномерные массивы — строки зубчатого массива. Ну и справа вы видите четыре одномерных массива — строки нашего зубчатого массива.
Это то, как на самом деле устроены зубчатые массивы. И такой подход дает C#-программисту несколько преимуществ:
Во-первых, т.к. «контейнер контейнеров» хранит ссылки на «массивы-строки», мы можем очень быстро и просто менять строки местами. Чтобы получить доступ к «контейнеру контейнеров», нужно просто указать один индекс вместо двух. Пример:int[][] data = new int[2][];
data[0] = new int[5]; // первая строка — массив из 5 элементов
data[1] = new int[5]; // вторая строка — массив из 5 элементов
int[] row1 = data[0];
int[] row2 = data[1];
Вот с помощью такого кода можно поменять строки местами:
// Важная матрица с данными
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;
Если вы обращаетесь к ячейке двумерного массива, но после имени массива указываете только один индекс, вы таким образом обратитесь к контейнеру контейнеров, в ячейках которого хранятся ссылки на обычные одномерные массивы.
5. Типичные сценарии применения зубчатых массивов
Когда зубчатый массив может быть полезнее двумерного?
- Если вы храните для каждого пользователя разное количество каких-то данных: оценки по предметам, покупки, комментарии и так далее.
- Если ваши данные имеют треугольную или ступенчатую структуру (например, для вывода пирамидок, треугольников Паскаля, и др.).
- Если хотите экономить память: в двумерном массиве все строки фиксированы, а в зубчатом — только нужное количество элементов.
Пример из жизни: менеджер оценок студентов
Давайте расширим наш учебный проект! Пусть у каждого студента может быть разное количество оценок по каждому предмету. Например, кто-то сдаёт больше работ, кто-то меньше. Для этого идеально подойдёт зубчатый массив.
Предположим, у нас есть три студента, и вот их оценки за разные задания по математике:
| Студент | Оценки |
|---|---|
| 0 | 5, 4 |
| 1 | 3, 4, 4 |
| 2 | 5 |
Объявим такой массив:
int[][] studentMarks = new int[3][];
studentMarks[0] = new int[] { 5, 4 }; // Первый студент - 2 оценки
studentMarks[1] = new int[] { 3, 4, 4 }; // Второй студент - 3 оценки
studentMarks[2] = new int[] { 5 }; // Третий студент - 1 оценка
Выведем оценки каждого студента:
for (int i = 0; i < studentMarks.Length; i++)
{
Console.Write($"Студент {i}: ");
for (int j = 0; j < studentMarks[i].Length; j++)
{
Console.Write(studentMarks[i][j] + " ");
}
Console.WriteLine();
}
Использование зубчатых массивов с другими типами
Зубчатым может быть массив чего угодно: строк, массивов других массивов (глубже!), даже ваших собственных объектов.
Пример: массив строк
string[][] groups = new string[][]
{
new string[] { "Иван", "Пётр" },
new string[] { "Мария", "Алексей", "Сергей" },
new string[] { "Василиса" }
};
6. Особенности и возможные ошибки
Зубчатые массивы — штука гибкая, но и ловушки расставлены на каждом шагу.
- Если вы не инициализировали какой-то из внутренних массивов (jaggedArray[1] = ...), попытка обратиться к нему приведёт к NullReferenceException. Не забывайте инициализировать каждый внутренний массив!
- Не все строки (подмассивы) одинаковой длины. Если использовать фиксированный индекс во втором измерении, можно выйти за его пределы.
- Не путайте с двумерным массивом! Индексация выглядит так: array[i][j], а не array[i, j].
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ