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] означає: взяти другий рядок таблиці й усередині нього — елемент з індексом 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("рядок $r: сума = $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("стовпець $c: сума = $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> як із прямокутною таблицею й використовуємо це припущення в коді. Але сам тип цього не гарантує. Уже в наступній лекції ми побачимо масиви, де рядки мають різну довжину. І код, який тут виглядає природно, перестане бути безпечним. Саме тому важливо розуміти не лише синтаксис, а й модель даних, з якою ви працюєте.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ