Пропустити до головного контенту

Цикли та Рекурсії

Тепер же, перейдемо до досить цікавої, але, знову ж, трішки непростої теми — цикли. Щоб більше зрозуміти, що таке цикли, створім якесь завдання. Наприклад, візьмемо завдання, яке ми вирішували у минулій темі. Для того, щоб розв'язати рівняння з використанням введення, ми щоразу запускаємо нашу програму. А що, якщо зробити в нашій програмі нескінченне введення, щоб щоразу не перезапускати нашу програму?

Взагалі, без нашої теми циклів це цілком можна було вирішити наступним чином:

fun main() {
println("Введіть число:")
val input: Double = readln().toDouble()
println("Результат: " + input.toString())
return main() // в кінці функції просто викликаємо її ще раз
}

І ось, рішення знайдено!

Подібне називають рекурсією. Простими словами - це поняття оголошення (напису, опису) коду функції через саму себе. Це як матрьошка, яка в нашому випадку не має кінця.

Що ж, а як тепер завершити нашу програму? Можна, звичайно, це зробити, закривши примусово процес програми через інструменти системи або IDE, але давайте будемо людьми й зробимо якийсь механізм виходу.

Щоб сильно не морочитися, введемо умову, що для виходу з програми нам потрібно написати «:q».

fun main() {
println("Введіть число: (або скористуйтесь :q для виходу):")
val input: String = readln() // створюємо змінну з текстом, тому що нам потрібно перевіряти введення користувача
if(input != ":q") {
val input: Double = input.toDouble() // сила областей видимості!
println("Результат: " + f(input).toString())
main()
}
}

Це так само залишиться рекурсією, тільки вже не нескінченною (у нас з'явилася умова).

Що ж, розглянувши досить простий приклад рекурсії, до якого можна було прийти самому під час спроби розв'язати завдання з перезапуском рішення рівнянь.

Що тоді таке цикли?

Визначення

Цикли - це засоби мови, які відтворюють рекурсію. Їх також відносять до операторів, називаючи циклічними операторами.

Тож тепер розглянемо, як це можна вирішити іншими засобами мови. Не завжди ж ви створюватимете окремо функції для 'повторення чогось', так?

While

Для полегшення вам життя вигадали досить корисну конструкцію - while. Записується так:

while(boolean) {
// тут дія, що повторюється
}

Подібна конструкція виконує свій вміст у {}, але перед кожним виконанням дивиться в умову (aka boolean-вираз) і якщо там true, то зміст виконується, а якщо false - ні.

Наш попередній код можна виразити через while наступним чином:

fun main() {
var shouldRun: Boolean = true
while(shouldRun) {
println("Введіть число (або скористайтесь :q для виходу):")
val input: String = readln()
if(input == ":q") {
shouldRun = false // при наступному виконанні цикл побачить, що умова `false`
} else {
val input: Double = input.toDouble()
println("Результат: " + f(input).toString())
}
}
}

Ось і наш перший цикл! Але якийсь він складний, вам не здається? Все це можна спростити скориставшись спеціальними додатковими операторами: break та continue.

Що роблять ці два оператори? Розберімось.

  • break (можна перекласти як розірвати, обірвати) – примусово закінчує цикл. Тобто навіть якщо умова буде true цикл все одно закінчиться.
  • continue (перекладається як продовжити) - закінчує виконання поточного повторення. На відміну від break, continue, грубо кажучи, виходить з коду (код після нього не виконується) і переходить відразу до наступного повторення (до перевірки умови та подальшого повторення у разі, якщо там true).

Давайте перепишемо наш код:

fun main() {
while(true) { // умова нам не потрібна
println("Введите число (або скористайтесь :q для виходу):")
val input: String = readln()
if(input == ":q") {
break // виходимо з цикло
} else {
val input: Double = input.toDouble()
println("Результат: " + f(input).toString())
continue // взагалі, він необов'язковий у нашому випадку, але для наочності додамо
println("Я не надрукуюсь!") // IDE нам підкаже, що до цієї ділянки коду ми ніколи не дійдемо через continue
}
}
}

Через непотрібність ми викинули змінну shouldRun, тому що є куди зручніший спосіб з break.

Do-while

Одним із підвидів циклу while є do-while. Крім назви, він відрізняється тим, що в do while спочатку виконується тіло циклу, а потім перевіряється умова продовження циклу. Через таку особливість do while називають циклом з постумовою, а звичайний while називають циклом з передумовою.

Записується так:

do {
// щось
} while(bool)

У такому циклі також існує break та continue, які ніяк не відрізняються. Однак, наше завдання можна вирішити через do-while і без них:

// створимо змінну з повідомленням, щоб потім її перевикористовувати
val numberInputMessage = "Введите число (или :q для выхода):"

// створимо окрему функцію для зручності
private fun requestInput(message: String): String {
println(message)
return readln()
}

fun main() {
var input = requestInput(numberInputMessage)
do {
println("Результат: " + f(input.toDouble()).toString())
input = requestInput(numberInputMessage) // записуємо наступне введення, щоб перевірити після повторення, що було введено
} while(input != ":q") // якщо введення не ":q" програма продовжуватиме працювати
}

Ми створили для зручності функцію та змінну, що поєднувала схожий код. Знову зробили змінну поза циклом і запис її в кінці циклу (для того, щоб перевіряти після повторення введення користувача).

Це альтернативне рішення, хоч і не найкраще.

For

І тепер перейдемо до не менш важливого виду циклів - for. Відмінність цього виду циклів у тому, що він будується не за умови, а на ітераторі. Що таке ітератор? Ітератор - це вбудована утиліта в мову, яка проходить між якоюсь сумою елементів. Тобто кожне повторення буде відповідати одному елементу у цій сумі. У нашому випадку, ця сума елементів буде відповідати діапазону, а елемент — одиниці рахування цього діапазону.

Що таке діапазон? Простими словами - інтервал значень будь-якої величини. Прикладом діапазону може бути [0; 5] (описує інтервал чисел від 0 до 5, включно). Бувають різні види діапазонів, але поки що ми розглянемо найпростіший варіант із діапазоном цілих чисел.

Як створити такий цикл? Для початку розглянемо прогресію з цілими числами:

for(i in 0..5) {
println(i)
}

Тут ми бачимо оператор in, який працює з ітератором (у нашому випадку, з тим, що його виражає - діапазоном).

Цей код надрукує наступне:

0
1
2
3
4
5

Досить очевидно працює, чи не так?

Вирішім наступне завдання:

Завдання

Відтворіть функцію степеня для позитивних чисел. Еквівалентно функції Int.pow(x: Double).

fun pow(number: Int, times: Int): Int {
var output = number // створюємо змінну, де зберігається помножене значення
for(i in 1..times) { // через діапазон вказуємо, скільки разів має цикл повторитись
output *= number // множимо те, що вже є, на параметр number
}
return output // повертаємо число в степені
}

Тут нам IDE підкаже, що ідентифікатор не використовується і його бажано замінити на _. Річ у тому, що в котліні за код-стилем прийнято, що ідентифікатори, що не використовуються, називають саме так.

Що ж до завдання, тут нескладний імперативний варіант рішення.

Вирішім ще одне завдання:

Завдання

Напишіть програму, де користувач вводить будь-яке ціле позитивне число. А програма підсумовує всі числа від 1 до введеного користувачем числа. Тобто, якщо введуть число 4, ми маємо підсумовувати такі числа: 1+2+3+4.

У цьому нам дуже допоможуть діапазони!

fun sum(input: Int): Int {
var output: Int = 0 // створюємо тимчасову змінну, що буде зберігати значення, що змінюється в циклі

for(i in 1..input)
output += i // можно прибрати `{}`, бо тут одна послідовність дій

return output
}

Ми створили тимчасову змінну і знову використали діапазони зі змінною i, що містить елемент інтервалу цього діапазону на кожну ітерацію (повторення) циклу (який і відповідає тому, що ми по суті й робимо).

І наостанок, вирішимо ще одне завдання:

Завдання

Дані натуральні числа від 1 до 50. Знайти суму з них, які діляться на 5 чи 7.

Згадайте

Перед розв'язанням цієї задачі, згадаємо один з арифметичних операторів - % (залишок від поділу).

fun main() {
println(22 % 4)
println(4 % 2)
}

Надрукує 2 і 0, оскільки буде такий залишок після поділу (у першому не ділиться націло, у другому - ділиться).

Наше завдання полягає в тому, щоб знайти числа, що діляться націло на 5 та 7. Це буде еквівалентно наступному:

number % 5 == 0 || number % 7 == 0 

Ця умова нам підходитиме. Тепер же залишається тільки зробити цикл та тимчасову змінну в яку ми додаватимемо результат.

fun main() {
var temp: Int = 0
for(i in 1..50)
if(i % 5 == 0 || i % 7 == 0)
temp += i
println("Сума: " + temp)
}

Відповіддю у нас має вийти: 436.