1. Прохід масивом — ключова навичка
Якщо масив — це коробка з однаковими деталями, то прохід масивом — це момент, коли ви відкриваєте коробку й чесно рахуєте, що там є: скільки деталей, яка з них найбільша, чи трапляється брак, скільки все це коштує разом. Майже всі базові задачі на масиви зводяться до одного: пройтися елементами й акуратно щось підсумувати або перевірити.
У Kotlin цикл for уміє перебирати елементи масиву безпосередньо: for (x in a) { ... } — це природний і очікуваний стиль. Але іноді нам потрібен не лише елемент, а ще й його позиція (індекс). Тоді ми йдемо по indices: for (i in a.indices) { ... }.
Два стилі обходу: за елементами та за індексами
Зовні обидва стилі виглядають майже однаково, але відповідають на різні запитання. Обхід «за елементами» читається як «візьміть кожен елемент масиву й зробіть із ним щось». Це зручно для суми, підрахунку, перевірки умов і виводу. Обхід «за індексами» читається як «переберіть позиції масиву». Він потрібен, коли важливо памʼятати, де саме щось розташовано: індекс максимуму, позиція знайденого значення, заміна елементів на місці.
Почнімо з найпростішого: створімо масив кроків за тиждень і просто виведімо його, щоб побачити дані очима (програміст без виводу в консоль — як кухар без ложки).
fun main() {
val steps: IntArray = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
for (x in steps) {
print("$x ")
}
println() // 4200 5600 3100 8000 7600 9000 1200
}
Зверніть увагу: зараз ми йдемо за значеннями (x in steps). Індексів не бачимо, а x послідовно набуває всіх значень масиву (по одному).
2. Патерн №1: сума
Сума — найпопулярніший алгоритм у світі програмістів-початківців. І не просто так: це перший випадок, коли ви розумієте, що цикл — це не «сім разів вивести println», а «сім разів зробити щось важливе». Ми заводимо спеціальну змінну (зазвичай sum), даємо їй стартове значення (зазвичай 0) і на кожному кроці додаємо черговий елемент масиву.
На рівні логіки це виглядає як маленька блок-схема: ініціалізували суму, пройшли всі елементи, оновили суму, отримали результат. У реальній розробці такі схеми корисні, коли мозок намагається перескочити через частину кроків і випадково «викидає» з голови ініціалізацію або оновлення.
flowchart TD
A[sum = 0] --> B{є наступний елемент?}
B -->|так| C[sum = sum + x]
C --> B
B -->|ні| D[вивести sum]
Тепер реалізуймо це у вигляді невеликого проєкту StepStats.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var sum = 0
for (x in steps) {
sum = sum + x
}
println("Total steps = $sum") // Усього кроків = 38500
}
Важливий нюанс: sum — це var, адже ми її змінюємо. Якщо ви спробуєте написати val sum = 0, компілятор вас зупинить (і матиме рацію).
3. Середнє: сума й акуратне ділення
Середнє значення майже завжди обчислюють через суму. Спочатку ми рахуємо суму, потім ділимо на кількість елементів. І саме тут Kotlin, як добра мова програмування, нагадує: ділення Int / Int дасть Int, тобто дробова частина зникне. Це не баг, а сувора математика цілих чисел.
Тому, якщо ви хочете середнє як число з дробовою частиною, потрібно «перевести» принаймні одну частину виразу в Double. Найпростіший підхід: sum.toDouble().
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var sum = 0
for (x in steps) sum = sum + x
val avg = sum.toDouble() / steps.size
println("Average steps = $avg") // Середня кількість кроків = 5500.0
}
Зауважте: steps.size — це кількість елементів. Якщо масив на 7 днів, то й розмір буде 7.
І так, вийшло 5500.0, хоча наче «красивіше» було б 5500. Це нормально: Double зазвичай відображається як число з крапкою.
4. Патерн №2: підрахунок з умовою
Підрахунок з умовою дуже схожий на звичайне підсумовування, тільки замість «додаю значення елемента» ми «додаємо 1, якщо умова істинна». Цей патерн трапляється всюди: скільки днів було вище норми, скільки оцінок — пʼятірки, скільки елементів дорівнюють нулю, скільки символів — літери.
Зробімо в StepStats задачу: порахувати, скільки днів було не менше 7000 кроків (наша ціль).
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var goodDays = 0
for (x in steps) {
if (x >= 7000)
goodDays = goodDays + 1
}
println("Good days = $goodDays") // Хороших днів = 3
}
Тут ми знову використовуємо обхід за елементами, бо нас цікавить сам факт (значення), а не позиція.
Тепер зробімо трохи практичніший варіант: користувач вводить певне значення (поріг), а ми рахуємо, скільки днів було вище за поріг.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
print("Enter threshold: ")
val threshold = readln().toInt()
var count = 0
for (x in steps)
if (x >= threshold) count++
println("Days >= $threshold : $count")
}
Зверніть увагу: count++ — це коротка форма «збільш на 1». Дуже зручно, особливо для лічильників.
5. Патерн №3: пошук та індекс
Пошук — це коли ми хочемо зʼясувати: «чи є в масиві значення X?» або «на якій позиції розташоване перше значення X?». Тут майже завжди потрібні індекси: знайти значення часто недостатньо — важливо ще знати, де саме воно лежить.
А якщо нічого не знайшли — повертаємо -1. Чому -1? Тому що це значення точно не може бути коректним індексом масиву (коректні починаються з 0).
І ще одна важлива деталь: якщо ми шукаємо перший збіг, то після того, як знайшли, можна зупинитися за допомогою break.
Почнімо з пошуку значення в масиві кроків: зʼясуймо, чи був день рівно з 9000 кроків, і якщо був — який у нього індекс.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
val target = 9000
var index = -1
for (i in steps.indices) {
if (steps[i] == target)
{ index = i; break }
}
println("Index of $target = $index") // Індекс 9000 = 5
}
Ось тут саме час використовувати steps.indices — діапазон індексів масиву. Так не доводиться щоразу думати про «останній індекс = size - 1».
Зробімо ще один, практичніший приклад. Наприклад, знайдемо перший день, коли кроків було менше певного мінімуму (скажімо, менше 3000).
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var badIndex = -1
for (i in steps.indices) {
if (steps[i] < 3000)
{ badIndex = i; break }
}
println("First day with < 3000 steps: $badIndex") // Перший день з < 3000 кроків: 6
}
Якщо захочете, можна додати текст «не знайдено» (коли badIndex == -1). Але поки що нам важливий сам патерн.
6. Патерн №4: максимум
Максимум — теж доволі цікавий патерн. Початківці тут часто потрапляють у пастку: «а давайте присвоїмо max нулю». Іноді це справді працює, а іноді — ні.
Знайдемо максимальне значення в масиві.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var max = 0
for (x in steps) {
if (x > max)
max = x
}
println("Max steps = $max") // Максимум кроків = 9000
}
Усе працює. І працює правильно. От тільки тут захована логічна помилка.
Якщо масив міститиме відʼємні числа, то max = 0 ламає всю логіку: ви отримаєте максимум 0, навіть якщо його в масиві немає. Універсальне правило таке: якщо шукаємо максимум, то починати потрібно з першого елемента масиву.
Ось як правильно шукати максимальну кількість кроків.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var max = steps[0] // max дорівнює першому значенню масиву
for (x in steps) {
if (x > max)
max = x
}
println("Max steps = $max") // Максимум кроків = 9000
}
З цього випливає важливий висновок: щоб брати a[0], масив має бути непорожнім: size > 0. Тому або ми заздалегідь знаємо, що масив не порожній, або зобовʼязані написати перевірку if (a.size == 0).
А тепер спробуймо знайти не лише максимум, а й день, коли користувач поставив рекорд. Тут нам знову потрібні індекси.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var maxIndex = 0 // індекс стартового значення
var max = steps[maxIndex] // стартове значення `max`
for (i in steps.indices) {
if (steps[i] > max)
{ max = steps[i]; maxIndex = i }
}
println("Max = $max at index $maxIndex") // Max = 9000 at index 5
}
Зверніть увагу на стиль: ми оновлюємо і значення, і індекс одночасно. Якщо оновити лише одне — вийде «максимум від одного дня, а індекс від іншого». Так не можна.
7. Додаємо бізнес-логіку
Тепер зробимо найприємніший момент: візьмемо вже знайомі патерни й зберемо маленький «звіт за тиждень». Важливо не намагатися зробити «все в одному циклі» будь-якою ціною: початківцю простіше читати два-три зрозумілі цикли, ніж один цикл-монстр.
Зробімо так: порахуємо суму, середнє, максимум і кількість днів вище 7000 — і гарно все виведемо.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var sum = 0
var goodDays = 0
var max = steps[0]
for (x in steps) {
sum += x
if (x >= 7000) goodDays++
if (x > max) max = x
}
val avg = sum.toDouble() / steps.size
println("Total = $sum, avg = $avg, max = $max, goodDays = $goodDays")
// Total = 38500, avg = 5500.0, max = 9000, goodDays = 3
}
Нижче — невелика табличка, щоб зафіксувати, чим патерни схожі та чим відрізняються:
| Патерн | Що зберігає змінна-результат | Стартове значення | Що відбувається в циклі |
|---|---|---|---|
| Сума | сума елементів (sum) | 0 | sum += x |
| Підрахунок | кількість (count) | 0 | if (cond) count++ |
| Пошук | індекс (index) | -1 | за збігу index = i і часто break |
| Максимум | максимальне значення (max) | a[0] (лише якщо масив не порожній) | if (x > max) max = x |
Порожній масив: завжди є підступ
Порожній масив — це ситуація, коли size == 0. Іноді він зʼявляється спеціально (наприклад, «немає даних»), іноді випадково (наприклад, ви створили IntArray(n), а n виявилося 0).
Із порожнім масивом майже всі патерни працюють нормально, крім тих, де ви берете a[0] (максимум, мінімум, інколи — старт для чогось іще).
Тому правило просте: якщо ваш алгоритм потребує звернення до першого елемента, перед цим має бути перевірка, що масив не порожній.
Ось безпечний шаблон для пошуку максимуму:
fun main() {
val steps = intArrayOf() // порожньо
if (steps.size == 0) {
println("No data") // Немає даних
} else {
var max = steps[0]
for (x in steps) if (x > max) max = x
println("Max = $max")
}
}
Цей приклад не робить нічого «розумного» — він просто коректно обробляє випадок, коли даних немає.
8. Типові помилки під час проходів масивом
Помилка №1: рахувати середнє як sum / size і дивуватися, чому воно без дробової частини.
Якщо sum і size — це Int, то й результат буде Int. Тому під час обчислення середнього потрібно переводити суму (або розмір) у Double, наприклад sum.toDouble() / size.
Помилка №2: починати максимум з 0 «бо так простіше».
Іноді це випадково працює (якщо всі числа невідʼємні), але щойно зʼявляться відʼємні значення — логіка руйнується. Правильний старт для максимуму — перший елемент масиву a[0], але лише в гілці, де масив точно непорожній.
Помилка №3: забути про break у пошуку й отримати «останній знайдений індекс» замість «першого».
Найпоширеніший варіант: пошук написано, але break не використано. У результаті цикл продовжує працювати, і індекс перезаписується знову й знову.
Помилка №4: плутати обхід за елементами та за індексами, а потім не розуміти, чому «не виходить отримати позицію».
for (x in a) не дає вам індекс — лише значення. Якщо вам потрібен індекс, використовуйте for (i in a.indices) і беріть значення як a[i].
Помилка №5: використовувати неправильні межі індексів і випадково вийти за межі масиву.
Класика: написати for (i in 0..a.size) і отримати вихід за межі на останньому кроці. Найнадійніший варіант — a.indices, бо він охоплює рівно «коректні індекси».
Помилка №6: не ініціалізувати змінну-накопичувач або ініціалізувати її «не тим».
Для суми старт має бути 0. Для підрахунку також 0. Для пошуку індекс зручно стартувати з -1. Якщо переплутати стартові значення, алгоритм даватиме дивні результати — інколи навіть «майже правильні», а це особливо підступно.
Помилка №7: намагатися розвʼязати задачу «в один рядок» і втратити читабельність.
Початківці інколи роблять пошук, суму, максимум і друк в одній гігантській конструкції, а потім самі не можуть пояснити, що відбувається. На цьому етапі важливіше, щоб код читався лінійно: ініціалізація → цикл → оновлення → результат.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ