Вам дана таблица температур за неделю для трёх городов: Москва, Казань, Сочи. 7 дней, 3 города.
Задача: Объявите двумерный массив int[3, 7], заполните произвольными значениями и найдите город с максимальной средней температурой за неделю.
Подсказка: Внешний цикл по городам (строкам), внутренний по дням (столбцам).
В прошлом уроке мы разобрали одномерные массивы и List. Элементы в ряд — всё понятно. Но реальные данные часто устроены иначе: таблица с оценками студентов, игровое поле крестиков-ноликов, карта уровня в игре, пиксели изображения. Это уже не ряд, а сетка — строки и столбцы. Для таких случаев в C# есть двумерные массивы.
Двумерный массив — не какая-то экзотика. Любая таблица в Excel, любое игровое поле, любое изображение — это матрица. Программисты работают с ними постоянно, и понимание их устройства сэкономит вам немало времени. Представьте таблицу оценок: 3 студента, 4 предмета. Нарисуем её:
Матем Физика История Англ
Алиса: 5 4 3 5
Борис: 3 5 4 4
Вера: 4 3 5 3
Это сетка 3 на 4: три строки, четыре столбца. Каждый элемент адресуется двумя числами — номером строки и номером столбца. Алиса по физике — это строка 0, столбец 1.
В C# такая структура называется прямоугольным двумерным массивом и создаётся так:
int[,] grades = new int[3, 4];
// Или сразу с данными:
int[,] grades = {
{ 5, 4, 3, 5 }, // Алиса
{ 3, 5, 4, 4 }, // Борис
{ 4, 3, 5, 3 } // Вера
};
Синтаксис [,] — квадратные скобки с запятой — сигнализирует C#: это двумерный массив. Два числа в new int[3, 4] — три строки и четыре столбца. Обращение к элементу тоже через запятую: grades[0, 1] — оценка Алисы по физике.
Обход двумерного массива — вложенные циклы:
for (int row = 0; row < grades.GetLength(0); row++)
{
for (int col = 0; col < grades.GetLength(1); col++)
{
Console.Write(grades[row, col] + " ");
}
Console.WriteLine();
}
GetLength(0) возвращает количество строк (первое измерение), GetLength(1) — столбцов (второе). Внешний цикл идёт по строкам, внутренний — по столбцам каждой строки.
Почему не grades.Length? Потому что Length у двумерного массива возвращает общее количество элементов (строки × столбцы = 12). Нам же нужны размеры по каждому измерению отдельно — для этого GetLength.
Обратиться к несуществующему индексу — классическая ловушка. Если у массива 3 строки (индексы 0, 1, 2) и вы пишете grades[3, 0] — получите IndexOutOfRangeException. C# не выйдет за границы тихо, как это делают некоторые другие языки. Это хорошо: ошибка сразу видна, а не прячется в некорректных данных.
В C# есть два принципиально разных способа создать двумерный массив: прямоугольный (int[,]) и зубчатый (int[][]). Они выглядят похоже, но устроены совершенно по-разному.
Прямоугольный массив (int[,]) — это единый блок памяти. Все строки одинаковой длины, данные лежат подряд. Именно это мы использовали выше.
Зубчатый массив (int[][]) — это массив массивов. Каждый элемент — отдельный одномерный массив, который может быть любой длины. Строки не обязаны совпадать по размеру.
// Зубчатый массив — строки разной длины
int[][] triangle = new int[4][];
triangle[0] = new int[] { 1 };
triangle[1] = new int[] { 1, 2 };
triangle[2] = new int[] { 1, 2, 3 };
triangle[3] = new int[] { 1, 2, 3, 4 };
Это треугольник: первая строка — один элемент, вторая — два, и так далее. Прямоугольным массивом такое не выразить — у него все строки одной длины.
Синтаксис обращения тоже разный: у прямоугольного — arr[i, j], у зубчатого — arr[i][j] (две пары скобок). Перепутать легко — компилятор сразу укажет на ошибку, но лучше запомнить сразу: запятая — прямоугольный, две пары скобок — зубчатый.
У зубчатого массива каждую строку нужно инициализировать отдельно — объявление new int[4][] создаёт только массив из четырёх пустых ссылок. Если обратиться к элементу до инициализации строки — NullReferenceException. Прямоугольный массив лишён этой проблемы: new int[3, 4] сразу выделяет всю память и заполняет нулями.
Посмотрим на классический пример — крестики-нолики. Поле 3×3, все строки одинаковой длины — прямоугольный массив:
char[,] board = {
{ '.', '.', '.' },
{ '.', '.', '.' },
{ '.', '.', '.' }
};
// Ход X в центр
board[1, 1] = 'X';
// Вывод поля
for (int r = 0; r < 3; r++)
{
for (int c = 0; c < 3; c++)
Console.Write(board[r, c] + " ");
Console.WriteLine();
}
Результат:
. . .
. X .
. . .
Точка — пустая клетка, X — ход игрока. Состояние всей игры хранится в одной переменной board, доступ к любой клетке — O(1).
Теперь та же логика с реальной операцией. Подсчитаем среднюю оценку по каждому предмету:
int[,] grades = {
{ 5, 4, 3, 5 },
{ 3, 5, 4, 4 },
{ 4, 3, 5, 3 }
};
int subjects = grades.GetLength(1);
int students = grades.GetLength(0);
for (int col = 0; col < subjects; col++)
{
int sum = 0;
for (int row = 0; row < students; row++)
sum += grades[row, col];
Console.WriteLine($"Предмет {col}: средняя {(double)sum / students:F1}");
}
Здесь внешний цикл идёт по столбцам (предметам), внутренний — по строкам (студентам). Порядок циклов зависит от задачи: нужна средняя по строке — внешний по строкам, нужна по столбцу — внешний по столбцам. Это частая точка путаницы у новичков — всегда рисуйте таблицу на бумаге перед тем, как писать вложенные циклы.
Заметьте: (double)sum / students — явное приведение к double перед делением. Без него C# выполнит целочисленное деление и округлит вниз. 5 / 3 == 1, а не 1.67. Для средних арифметических это критично.
Двумерные массивы встречаются везде, где данные имеют табличную структуру: расписание занятий, карта тайлов в игре, матрицы в линейной алгебре, попиксельная обработка изображений. Как только увидите задачу со словами «строки и столбцы» или «сетка» — скорее всего, нужен двумерный массив. Разберём несколько типичных операций с матрицами, которые часто встречаются на практике.
Поиск максимума. Найти максимальный элемент во всей матрице:
int[,] prices = {
{ 120, 85, 200 },
{ 95, 310, 150 },
{ 175, 60, 280 }
};
int max = int.MinValue;
for (int r = 0; r < prices.GetLength(0); r++)
for (int c = 0; c < prices.GetLength(1); c++)
if (prices[r, c] > max)
max = prices[r, c];
Console.WriteLine(max); // 310
Стандартный паттерн: заводим переменную с минимально возможным значением (int.MinValue) и обновляем её при обходе. Работает для любого размера матрицы.
Подсчёт элементов по условию. Сколько оценок ниже 4 в таблице успеваемости:
int[,] grades = {
{ 5, 4, 3, 5 },
{ 3, 5, 4, 4 },
{ 4, 3, 5, 3 }
};
int countLow = 0;
foreach (int grade in grades) // foreach работает с прямоугольными массивами
if (grade < 4) countLow++;
Console.WriteLine(countLow); // 4
Обратите внимание: foreach на прямоугольном массиве обходит все элементы подряд — строка за строкой. Если нужен просто перебор без индексов — foreach короче вложенных циклов. На зубчатом массиве foreach ведёт себя иначе: он возвращает каждую строку как int[], а не отдельные элементы — нужен второй уровень цикла.
Заполнение по формуле. Создадим таблицу умножения:
int[,] table = new int[10, 10];
for (int i = 0; i < 10; i++)
for (int j = 0; j < 10; j++)
table[i, j] = (i + 1) * (j + 1);
// table[2, 3] == 12, table[6, 7] == 56
Здесь мы не инициализируем массив вручную — заполняем программно по формуле. Для больших матриц это единственный разумный подход.
Трёхмерные массивы. C# поддерживает и более высокие измерения. Например, данные по городам, месяцам и часам суток — это трёхмерный массив:
// [город, месяц, час] — температура
float[,,] weather = new float[5, 12, 24];
weather[0, 6, 14] = 28.5f; // Москва, июль, 14:00
На практике трёхмерные массивы встречаются реже — чаще их заменяют классами или вложенными коллекциями. Но знать, что синтаксис [,,] существует, полезно.
Ключевое правило работы с многомерными массивами: размерность фиксируется при создании и не меняется. Нельзя добавить строку в int[3, 4] — только создать новый массив большего размера и скопировать данные. Если размер заранее неизвестен — используйте List<int[]> или List<List<int>>.
Ещё один момент, который экономит время: метод Array.Copy и класс Buffer.BlockCopy умеют копировать данные между одномерными массивами быстро, но для двумерных — только с помощью вложенных циклов или LINQ. Нет встроенного CopyTo для int[,]. Если нужно часто копировать или изменять размер матрицы — рассмотрите сторонние библиотеки вроде MathNet.Numerics, особенно для задач линейной алгебры.
На практике двумерные массивы чаще всего встречаются в двух областях: алгоритмы на матрицах (умножение матриц, поворот изображения, поиск в 2D-сетке) и динамическое программирование (таблицы DP, расстояние Левенштейна, задача о рюкзаке). В следующих блоках курса мы будем возвращаться к двумерным массивам — они фундаментальный инструмент.
Прямоугольный массив (int[,]) — единый блок памяти, все строки одинаковой длины. Объявляется через new int[rows, cols], элемент — arr[i, j]. Количество строк и столбцов — через GetLength(0) и GetLength(1).
Зубчатый массив (int[][]) — массив массивов, строки могут быть разной длины. Элемент — arr[i][j]. Подходит для данных с переменным числом элементов в строке.
Обход двумерного массива — вложенные циклы: внешний по строкам, внутренний по столбцам (или наоборот — зависит от задачи).
Используйте двумерные массивы, когда данные имеют табличную структуру: игровые поля, таблицы оценок, карты тайлов, матрицы.