Вам дана таблица температур за неделю для трёх городов: Москва, Казань, Сочи. 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]. Подходит для данных с переменным числом элементов в строке.

Обход двумерного массива — вложенные циклы: внешний по строкам, внутренний по столбцам (или наоборот — зависит от задачи).

Используйте двумерные массивы, когда данные имеют табличную структуру: игровые поля, таблицы оценок, карты тайлов, матрицы.

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.