JavaRush /Курсы /Kotlin SELF /Зубчатые массивы: Array<IntArray>

Зубчатые массивы: Array<IntArray>

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

1. Строки разной длинны

Когда мы доходим до таблиц, расписаний, матриц, «данных по дням», обычно хочется мыслить так: есть строки и есть столбцы. Но реальный мир редко идеален. У одного студента 5 оценок, у другого 3 (пропустил, болел, кот выдернул Wi‑Fi), в одной неделе 7 дней, а в последней неделе месяца — 23 дня. И вот тут «прямоугольная таблица» начинает страдать: приходится заполнять пустые клетки нулями или какими-то заглушками.

Зубчатый массив решает эту задачу: это буквально «полка с коробками», где каждая коробка может быть своей длины. Снаружи у нас один массив (допустим, недели), а внутри — отдельный массив для каждой недели (например, траты по дням). И у каждой недели может быть своё количество дней/записей.

Чтобы не путаться в терминах, будем договориться так: внешний массив — это таблица, его элементы — это строки, а каждый внутренний массив — это строка (row). Если вы привыкли к словам «двумерный массив», думайте именно так, но с важной оговоркой: «двумерность» здесь не обязана быть прямоугольной.

Создаём зубчатую структуру: строки разной длины

Чаще всего на первых шагах удобнее создавать зубчатый массив буквально «вручную» через arrayOf(...) и intArrayOf(...). Это самый читаемый способ, и он не требует никаких дополнительных знаний.

Представим, что мы делаем мини-приложение «Учебный журнал», где у каждой темы разное количество задач (то 2, то 5 — преподаватель тоже человек). Пусть каждая строка — это тема, а числа в строке — баллы за задачи.

fun main() {
    val scoresByTopic: Array<IntArray> = arrayOf(
        intArrayOf(10, 8, 9),        // тема 0: 3 задачи
        intArrayOf(7, 10),           // тема 1: 2 задачи
        intArrayOf(6, 6, 8, 10)      // тема 2: 4 задачи
    )

    println(scoresByTopic.size)      // 3
}

Здесь scoresByTopic.size — это количество тем (строк). А вот количество задач внутри темы — это уже scoresByTopic[topicIndex].size. И вот тут начинается первая сложность: у каждой строки свои границы.

Доступ table[row][col]: две проверки границ

Когда вы работаете с одномерным массивом, у вас одна проверка: индекс должен быть в a.indices. В зубчатом массиве проверок становится две: сначала проверяем, что существует строка, потом — что внутри строки существует столбец.

Самая частая ошибка новичка — «я проверил col по длине первой строки, значит, всё ок». Не ок: вторая строка может быть короче. В зубчатом массиве нет “общей ширины”.

Вот аккуратный шаблон безопасного доступа:

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(10, 20)
    )

    val row = 1
    val col = 1

    if (row in table.indices && col in table[row].indices) {
        println(table[row][col])     // 20
    } else {
        println("out of bounds")     // (не выведется)
    }
}

Обратите внимание на порядок: сначала row in table.indices, а только потом table[row]. Это всё не просто так — так вы не рискнёте обратиться к несуществующей строке.

2. Работа со строками разной длинны

Вложенные циклы: как обходить таблицу, если строки разной длины

Когда у нас есть «массив строк», логика обхода почти всегда такая: внешний цикл идёт по строкам, внутренний — по элементам строки. Но ключевая мысль немного другая: внутренний цикл должен опираться на текущую строку, а не на какую-то «воображаемую общую длину».

Самый простой и безопасный обход массива — получение всех значений:

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(10, 20)
    )

    for (row in table) {
        for (x in row) {
            print("$x ")
        }
        println()
    }
    // 1 2 3
    // 10 20
}

Этот вариант хорош тем, что вы вообще не думаете об индексах, а значит — почти исключаете выход за границы массива.

Но иногда индексы нужны: например, для красивого вывода с координатами ([row][col]) или для изменения массива. Тогда используем indices и помним, что они могут быть разные у каждой строки:

fun main() {
    val table = arrayOf(
        intArrayOf(1, 2, 3),
        intArrayOf(10, 20)
    )

    for (r in table.indices) {
        for (c in table[r].indices) {
            print("[$r,$c]=${table[r][c]} ")
        }
        println()
    }
    // [0,0]=1 [0,1]=2 [0,2]=3
    // [1,0]=10 [1,1]=20
}

Почему так правильно? Потому что table[r].indices описывает допустимые индексы столбцов именно для строки r, а не «для всей таблицы». Такой подход прямо следует идее indices как безопасного диапазона индексов.

Границы строк: почему нельзя «мерить всех по первой строке»

Сейчас будет важный момент, который спасает часы отладки.

Представьте таблицу:

  • строка 0: длина 3
  • строка 1: длина 2
  • строка 2: длина 4

Если вы сделаете внутренний цикл как for (c in 0..2) потому что в первой строке 3 элемента, то на второй строке вы рано или поздно полезете в table[1][2], которого не существует.

Именно поэтому нужно держать в голове мысль, что массив — это не всегда прямоугольник. Правильнее держать его в голове как набор строк, и где каждая строка — самостоятельный массив со своим size.

Давайте держать в голове маленькую табличку:

Модель Как выглядит в коде Идея ширины
«Прямоугольная» таблица условно Array<Array<Int>>, где все строки одинаковы ширина общая
Зубчатая таблица Array<IntArray> ширина у каждой строки своя

3. Практика: «Журнал задач по темам»

Давайте соберём маленький проект, который развивается по шагам. Идея такая: у нас есть темы, и в каждой теме — список баллов за задачи. Мы хотим вывести таблицу, посчитать сумму по каждой теме и найти «лучшую задачу» (максимум) внутри каждой темы.

Выводим таблицу

Сначала сделаем красивый вывод: номер темы и баллы.

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    for (t in scoresByTopic.indices) {
        print("Topic $t: ")
        for (score in scoresByTopic[t]) {
            print("$score ")
        }
        println()
    }
    // Topic 0: 10 8 9
    // Topic 1: 7 10
    // Topic 2: 6 6 8 10
}

Здесь внешний цикл идёт по темам (строкам), внутренний — по значениям в строке. Это максимально безопасная форма: вы не используете индексы там, где они не нужны.

Сумма по каждой строке: старый паттерн на новый лад

Теперь применим знакомый паттерн «сумма массива», но для каждой строки отдельно. Важно: переменная sum должна создаваться заново для каждой темы, иначе вы случайно получите «общую сумму всего массива».

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    for (row in scoresByTopic) {
        var sum = 0					// sum находится внутри внешнего цикла
        for (x in row) sum += x
        println("row sum = $sum")    // row sum = ...
    }
    // row sum = 27
    // row sum = 17
    // row sum = 30
}

Максимум в каждой строке: аккуратно стартуем с первого элемента

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

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    for (topic in scoresByTopic.indices) {
        val row = scoresByTopic[topic]
        var best = row[0]
        for (x in row) 
        	if (x > best) best = x
        println("topic $topic best = $best")
    }
    // topic 0 best = 10
    // topic 1 best = 10
    // topic 2 best = 10
}

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

Теперь представим, что преподаватель решил: «Если задача решена с подсказкой, минус 2 балла». Допустим, это тема 2, задача 1. То есть координаты (row=2, col=1).

Поменяем значение в массиве (с проверками границ):

fun main() {
    val scoresByTopic = arrayOf(
        intArrayOf(10, 8, 9),
        intArrayOf(7, 10),
        intArrayOf(6, 6, 8, 10)
    )

    val row = 2
    val col = 1

    if (row in scoresByTopic.indices && col in scoresByTopic[row].indices) {
        scoresByTopic[row][col] -= 2
    }

    println(scoresByTopic[2][1])    // 4
}

Ключевой момент: вторая проверка — именно col in scoresByTopic[row].indices. Не «в 0..something», не «как у первой строки», а конкретно по той строке, в которую вы лезете.

4. Как устроена зубчатая таблица в голове и в коде

Иногда полезно закрепить выученное не кодом, а картинкой. Вот как можно представлять Array<IntArray>:

flowchart TD
    T["table: Array⟨IntArray⟩"] --> R0["table[0]: IntArray (size=3)"]
    T --> R1["table[1]: IntArray (size=2)"]
    T --> R2["table[2]: IntArray (size=4)"]

    R0 --> A00["[0][0]"]
    R0 --> A01["[0][1]"]
    R0 --> A02["[0][2]"]

    R1 --> A10["[1][0]"]
    R1 --> A11["[1][1]"]

    R2 --> A20["[2][0]"]
    R2 --> A21["[2][1]"]
    R2 --> A22["[2][2]"]
    R2 --> A23["[2][3]"]

Обратите внимание: у table[1] нет [1][2]. Прямоугольные таблицы остались далеко в прошлом. Такова реальность программиста.

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

Ошибка №1: считать, что у таблицы есть “общая ширина”.
Это самая популярная ловушка: вы берёте table[0].size и используете её как ширину для всех строк. На коротких строках это заканчивается выходом за границы. В зубчатой структуре ширина — свойство каждой строки, поэтому во внутреннем цикле границы должны быть table[r].indices, а не “что-то одно на всех”.

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

Ошибка №3: проверять только один уровень границ.
Новички иногда проверяют row in table.indices, но забывают про col in table[row].indices. Это особенно коварно: кажется, что «я же проверил!», а падает всё равно. Для доступа table[row][col] нужны две проверки, потому что это два доступа [] подряд.

Ошибка №4: делать внутренний цикл по диапазону 0..row.size вместо корректных индексов.
Здесь классический off-by-one: диапазон 0..row.size включает row.size, а последний допустимый индекс — row.size - 1. Результат — выход за границы на последней итерации. Если хотите цикл по индексам, используйте row.indices — он уже правильный.

Ошибка №5: забывать, что “строка” — тоже массив со своей жизнью.
Иногда люди пишут код так, будто table[r] — это просто «часть таблицы», а не полноценный массив. Но у table[r] есть свой size, его можно обходить, в нём можно искать максимум, сумму и так далее. Как только вы начинаете воспринимать строку как обычный IntArray, вложенные циклы становятся логичными и предсказуемыми.

1
Задача
Kotlin SELF, 7 уровень, 5 лекция
Недоступна
Этажи лифта
Этажи лифта
1
Задача
Kotlin SELF, 7 уровень, 5 лекция
Недоступна
Чётный запуск
Чётный запуск
1
Задача
Kotlin SELF, 7 уровень, 5 лекция
Недоступна
Сумма по слотам
Сумма по слотам
1
Задача
Kotlin SELF, 7 уровень, 5 лекция
Недоступна
Логи наоборот
Логи наоборот
1
Опрос
Массивы в Kotlin, 7 уровень, 5 лекция
Недоступен
Массивы в Kotlin
Массивы в Kotlin
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
24 мая 2026
1) Строки разной длиНы правильно 2) "Чтобы не путаться в терминах, будем договориться так" - да вы издеваетесь? 😀
PROaction Уровень 10
28 марта 2026
1. Зачем эта лекция? Вся разница от прямоугольного двумерного массива заключается в том, что нужно во вложенном массиве получать индексы по текущей строке.

for (r in table.indices) {
    for (c in table[r].indices) {
    }
}
А не

for (r in table.indices) {
    for (c in table[0].indices) Х
    }
}
Хотя такая форма записи вообще не понятно зачем нужна. Надежнее всегда использовать индекс внешнего цикла. 2. Зачем путать подобными договоренностями?

внешний массив — это таблица, его элементы — это строки, а каждый внутренний массив — это строка (row).
Так же они:

val row = 1
val col = 1
3. Зачем после лекции про двумерные массивы давать задачи по одномерным массивам?