JavaRush /Курсы /Kotlin SELF /Двумерные массивы

Двумерные массивы

Kotlin SELF
7 уровень , 4 лекция
Открыта

1. Введение

Пока мы работали с одномерными массивами, всё было довольно просто: данные шли в линию, одно значение за другим. Такой формат отлично подходит для списков — оценок одного студента, температур по дням, номеров задач или товаров в корзине.

Но на практике очень быстро возникает ситуация, когда одного измерения уже недостаточно. Данные начинают выглядеть как таблица: у нас есть строки и есть столбцы, и каждый элемент имеет не один индекс, а сразу два. Например, в журнале оценок строки соответствуют студентам, а столбцы — предметам. В игровом поле строки и столбцы образуют сетку клеток. В математике это матрицы, в играх — карты и уровни, в повседневных задачах — расписания и календари.

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

Такая структура и называется двумерным массивом.

2. Тип Array<IntArray> и смысл, который мы в него вкладываем

С точки зрения языка Kotlin, двумерный массив не является отдельным типом. Используется всё тот же знакомый нам тип Array<IntArray>. Формально это означает «массив, элементы которого являются массивами целых чисел». Kotlin не знает, хотим ли мы использовать эту структуру как прямоугольную таблицу или как набор строк разной длины.

Поэтому здесь важно различать техническую сторону и логическую. Технически Array<IntArray> — это просто вложенные массивы. Логически же, в рамках этой лекции, мы считаем, что каждый внутренний массив имеет одинаковый размер. Это не правило языка, а наше соглашение как программистов.

Другими словами, компилятор никак не помешает вам создать строки разной длины. Но если вы это сделаете, то нарушите модель «двумерной таблицы», и код, который рассчитывает на прямоугольную форму, начнёт работать неправильно. Поэтому важно заранее договориться: в этой лекции Array<IntArray> — это именно таблица, а не зубчатая структура.

3. Как правильно представлять двумерный массив в голове

Очень помогает, если с самого начала правильно выстроить образ двумерного массива. Это не некая волшебная матрица и не особый тип данных. Это обычный массив, внутри которого лежат другие массивы. Каждый внутренний массив — это одна строка таблицы.

Когда мы пишем table[0], мы не получаем «первый столбец» или «часть таблицы». Мы получаем полноценный одномерный массив — первую строку. У неё есть свой размер, свои индексы и свои элементы. И только после этого, обращаясь к table[0][2], мы берём конкретный элемент внутри этой строки.

Именно поэтому доступ к элементам выглядит как два шага подряд. Сначала выбирается строка, затем — элемент внутри строки. Такой порядок естественен и логичен, если воспринимать таблицу как набор строк.

Отсюда же появляется привычное обозначение координат: [row][col], где row — номер строки, а col — номер столбца. Индексация, как и во всех массивах Kotlin, начинается с нуля. Первая строка — это row = 0, первый столбец — col = 0. Если таблица имеет три строки и четыре столбца, то допустимые значения индексов — от 0 до 2 для строк и от 0 до 3 для столбцов.

4. Создание двумерного массива

Самый наглядный способ создать двумерный массив — задать его явно, перечислив строки одну за другой. Такой код легко читается и сразу показывает форму таблицы.

val table = arrayOf(
    intArrayOf(10, 20, 30),
    intArrayOf(40, 50, 60),
    intArrayOf(70, 80, 90)
)

Из этого фрагмента сразу видно, что таблица состоит из трёх строк, и в каждой строке по три элемента. Такой способ особенно удобен для примеров, тестовых данных и учебных задач.

Однако на практике часто бывает так, что размеры таблицы известны заранее, а значения будут вычисляться или заполняться позже. Например, нужно создать игровое поле определённого размера или таблицу для хранения промежуточных результатов. В этом случае используется конструктор Array, который позволяет задать количество строк, а затем создать для каждой строки массив нужной длины.

val rows = 3
val cols = 4
val table = Array(rows) { IntArray(cols) }

Здесь важно понимать, что каждая строка создаётся отдельно. В результате мы получаем таблицу, в которой все строки имеют одинаковую длину, а значит, структура остаётся прямоугольной. Это поведение критично: если бы строки ссылались на один и тот же массив, любое изменение отражалось бы сразу во всех строках. Kotlin делает всё правильно, но полезно осознавать, что происходит за кулисами.

5. Доступ к элементам и размеры таблицы

Доступ к элементам двумерного массива всегда осуществляется через двойную индексацию. Сначала указывается номер строки, затем номер столбца. Например, выражение table[1][2] означает: взять вторую строку таблицы и внутри неё элемент с индексом два.

Чтение и запись элементов выглядят одинаково просто:

val value = table[0][1]
table[2][0] = 42

Поскольку table[row] — это обычный одномерный массив, с ним можно работать точно так же, как и раньше: узнавать его размер, перебирать элементы, изменять значения.

Для прямоугольной таблицы удобно заранее выделять её размеры. Количество строк определяется как table.size, а количество столбцов — как table[0].size. Это безопасно именно потому, что мы договорились о равной длине всех строк. В зубчатых массивах такой приём уже не работает, и это станет важной темой следующей лекции.

При работе с индексами всегда стоит помнить о границах массива. Для корректного доступа нужно, чтобы индекс строки находился в пределах table.indices, а индекс столбца — в пределах допустимых индексов строки. В случае прямоугольной таблицы для проверки столбца достаточно ориентироваться на первую строку, так как все остальные имеют такую же длину.

Очень частая ошибка — путать размеры строк и столбцов. table.size — это количество строк, а не «размер всей таблицы». Если перепутать эти понятия, вложенные циклы начинают работать некорректно, и программа либо падает с ошибкой, либо выдаёт странные результаты. Поэтому важно с самого начала чётко разделять эти два измерения в голове.

6. Обход двумерного массива: как читать таблицу последовательно

Работа с двумерным массивом почти всегда означает использование вложенных циклов. Но прежде чем писать код, полезно зафиксировать модель мышления. Двумерный массив читается так же, как таблица на бумаге: сначала мы берём строку целиком, затем двигаемся по элементам внутри этой строки. Поэтому внешний цикл отвечает за строки, а внутренний — за столбцы.

Когда нам важно знать координаты элемента, мы работаем с индексами. Внешний цикл перебирает допустимые индексы строк, а внутренний — допустимые индексы столбцов. Поскольку в этой лекции мы рассматриваем прямоугольную таблицу, количество столбцов одинаково для всех строк, и мы можем безопасно брать диапазон столбцов по первой строке.

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(4, 5, 6)
    )

    for (r in table.indices) {
        for (c in table[0].indices) {
            print("[$r,$c]=${table[r][c]} ")
        }
        println()
    }
}

Такой способ обхода даёт полный контроль: мы всегда знаем, с какой строкой и каким столбцом работаем. Это особенно важно, когда нужно менять значения, проверять условия или строить вывод с координатами.

Однако далеко не всегда координаты действительно нужны. Во многих задачах нас интересуют только сами значения. В этом случае код становится заметно проще, если отказаться от индексов и работать напрямую со строками и элементами внутри них.

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(4, 5, 6)
    )

    for (row in table) {
        for (x in row) {
            print("$x ")
        }
        println()
    }
}

Этот вариант легче читается и практически исключает ошибки выхода за границы. Хорошее правило: если индекс не нужен для решения задачи, лучше его не использовать.

7. Базовые операции с двумерным массивом

Многие операции, знакомые по одномерным массивам, в двумерном случае просто «разворачиваются» в два уровня. Например, чтобы посчитать сумму всех элементов таблицы, мы сначала берём одну строку, затем перебираем все значения внутри неё и накапливаем сумму. После этого переходим к следующей строке.

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(4, 5, 6)
    )

    var sum = 0
    for (row in table) {
        for (x in row) {
            sum += x
        }
    }

    println(sum)   // 21
}

Если же задача формулируется как «найти сумму в каждой строке», важно помнить, что каждая строка — это отдельный массив. Поэтому переменная для суммы должна создаваться заново для каждой строки, иначе вы получите одну общую сумму для всей таблицы.

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(4, 5, 6)
    )

    for (r in table.indices) {
        var rowSum = 0
        for (x in table[r]) {
            rowSum += x
        }
        println("row $r sum = $rowSum")
    }
}

Сумма по столбцам требует немного другого взгляда на таблицу. Столбцы не представлены в виде отдельных массивов, поэтому для каждого столбца мы фиксируем его индекс и затем проходим по всем строкам, извлекая элемент с этим индексом.

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(4, 5, 6)
    )

    val cols = table[0].size

    for (c in 0 until cols) {
        var colSum = 0
        for (r in table.indices) {
            colSum += table[r][c]
        }
        println("col $c sum = $colSum")
    }
}

Этот код работает корректно именно потому, что таблица прямоугольная. Мы уверены, что у каждой строки есть элемент с индексом c.

8. Изменение элементов по координатам

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

fun main() {
    val table = Array(3) { IntArray(3) }

    table[0][0] = 1
    table[1][1] = 5
    table[2][2] = 9

    for (row in table) {
        println(row.joinToString())
    }
}

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

if (row in table.indices && col in table[0].indices) {
    table[row][col] += 1
}

9. Типичные ошибки при работе с двумерными массивами

Ошибка №1: путать строки и столбцы в индексации.
Одна из самых распространённых проблем при работе с двумерными массивами — перепутанный порядок индексов. В записи table[row][col] первый индекс всегда указывает на строку, а второй — на столбец. Если написать table[col][row], код может скомпилироваться и даже какое-то время работать, но логика программы будет нарушена. Особенно опасно то, что такая ошибка не всегда приводит к аварийному завершению программы, а значит, может долго оставаться незамеченной. Хорошая привычка — использовать имена row и col (или r и c) и читать выражение вслух: «строка r, столбец c».

Ошибка №2: использовать table.indices для обхода столбцов.
table.indices описывает допустимые индексы строк, а не столбцов. Новички часто используют этот диапазон и во внешнем, и во внутреннем цикле, считая, что таблица «симметрична». В прямоугольной таблице количество строк и столбцов может отличаться, поэтому для столбцов нужно использовать table[0].indices или table[0].size. Этот код работает именно потому, что в рамках этой лекции мы предполагаем одинаковую длину всех строк.

Ошибка №3: выход за границы из-за неправильного диапазона.
Классическая ошибка «на единицу» выглядит так: for (c in 0..table[0].size). Такой цикл обязательно попробует обратиться к элементу с индексом size, которого не существует. Последний допустимый индекс всегда равен size - 1. Чтобы не держать это в голове постоянно, лучше использовать indices: этот диапазон уже корректный и безопасный.

Ошибка №4: забывать, что столбцы не являются массивами.
В отличие от строк, которые реально существуют как массивы (IntArray), столбцы — это логическое представление. Их нельзя взять напрямую, у них нет собственного size и indices. Чтобы обработать столбец, нужно зафиксировать его индекс и пройтись по всем строкам. Попытки «обращаться к столбцу как к массиву» почти всегда приводят к путанице и ошибкам в логике.

Ошибка №5: считать, что любой Array<IntArray> — прямоугольный.
Это самая важная ошибка в контексте всего блока лекций. В этой лекции мы сознательно работаем с Array<IntArray> как с прямоугольной таблицей и используем это предположение в коде. Но сам тип этого не гарантирует. Уже в следующей лекции мы увидим массивы, где строки имеют разную длину, и код, который здесь выглядит естественно, перестанет быть безопасным. Именно поэтому важно понимать не только синтаксис, но и модель данных, с которой вы работаете.

1
Задача
Kotlin SELF, 7 уровень, 4 лекция
Недоступна
Координаты панели
Координаты панели
1
Задача
Kotlin SELF, 7 уровень, 4 лекция
Недоступна
Таблица умножения
Таблица умножения
1
Задача
Kotlin SELF, 7 уровень, 4 лекция
Недоступна
Энергия по строкам
Энергия по строкам
1
Задача
Kotlin SELF, 7 уровень, 4 лекция
Недоступна
Тяжёлый столбец
Тяжёлый столбец
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ