JavaRush /Курси /Kotlin SELF /Прохід масивом: сума, пошук, максимум і підрахунок

Прохід масивом: сума, пошук, максимум і підрахунок

Kotlin SELF
Рівень 7 , Лекція 3
Відкрита

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: намагатися розвʼязати задачу «в один рядок» і втратити читабельність.
Початківці інколи роблять пошук, суму, максимум і друк в одній гігантській конструкції, а потім самі не можуть пояснити, що відбувається. На цьому етапі важливіше, щоб код читався лінійно: ініціалізація → цикл → оновлення → результат.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ