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") // Total steps = 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") // Average steps = 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") // Good days = 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") // Index of 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") // First day with < 3000 steps: 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") // Max steps = 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") // Max steps = 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") // 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: пытаться решить задачу «в одну строку» и потерять читаемость.
Новички иногда делают поиск, сумму, максимум и печать в одной гигантской конструкции, и потом сами не могут объяснить, что происходит. На этом этапе важнее, чтобы код читался линейно: инициализация → цикл → обновления → результат.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ