1. Вступ
Коли ви лише починаєте програмувати, масив здається просто коробкою з числами: поклали — дістали. Але на практиці він дуже швидко перетворюється на «дані», з якими постійно треба виконувати одні й ті самі дії: знайти мінімум, знайти максимум, перевірити, чи є потрібне значення, змінити порядок елементів. Саме такі повторювані прийоми — майже як кулінарні рецепти — і називають алгоритмічними шаблонами.
Головна ідея проста: масив фіксованого розміру найчастіше обробляють одним лінійним проходом за індексами 0..N-1. Під час цього проходу ви або оновлюєте поточну відповідь, як у min/max, або шукаєте збіг, як під час пошуку, або міняєте елементи місцями, як під час розвороту.
Закріпімо це у вигляді невеликої таблиці — не для іспиту, а щоб вам було легше зорієнтуватися:
| Операція | Що робимо в циклі | Скільки проходів потрібно | Ідея результату |
|---|---|---|---|
|
порівнюємо поточний елемент із найкращим на цей момент | 1 | «найкраще значення» |
| лінійний пошук | перевіряємо a[i] == target | 1 | «позиція» або «не знайдено» |
| розворот | міняємо місцями пари елементів | |
масив змінено «на місці» |
2. Мінімум і максимум за один прохід
Мінімум і максимум — ідеальний приклад того, як цикл стає «машиною оновлення результату». Спочатку відповіді ще немає, тож треба взяти якесь стартове значення. Саме тут новачки часто кажуть: «давайте почнемо з 0» — і отримують сюрпризи, якщо всі числа відʼємні або якщо нуль узагалі не трапляється. Правильніше й надійніше почати з першого елемента масиву.
Логіка така: «поки що мінімум — це a[0]. Тепер подивімося, чи не знайдеться щось менше». Тому цикл зазвичай починається з i = 1, бо a[0] ми вже використали як стартове значення.
Приклад 1: min/max для C‑масиву — міні‑застосунок «Температури»
Уявімо, що ми пишемо невелику програму «Температури за тиждень»: вводимо 7 цілих значень температури й хочемо знайти мінімум та максимум.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 7;
int t[N] = {}; // температури
for (std::size_t i = 0; i < N; ++i) std::cin >> t[i];
int mn = t[0], mx = t[0];
for (std::size_t i = 1; i < N; ++i) { if (t[i] < mn) mn = t[i]; if (t[i] > mx) mx = t[i]; }
std::cout << "min=" << mn << " max=" << mx << '\n'; // наприклад: min=-3 max=8
}
Зверніть увагу на два моменти, які роблять цей код надійним. Ми взяли стартове значення з t[0], тому не залежимо від того, які саме числа лежать у масиві. Цикл починається з i = 1, щоб не порівнювати t[0] із самим собою. Це не помилка, але зайва робота й зайвий шум у голові.
Важлива передумова
Такий шаблон працює за умови, що масив не порожній. У нашому випадку це забезпечується тим, що N фіксований і більший за нуль. Якби розмір міг бути 0, довелося б окремо вирішувати, що робити далі, але це вже інша історія.
3. Лінійний пошук і маркер «не знайдено»
Лінійний пошук — це коли ми послідовно йдемо зліва направо й ніби запитуємо в кожного елемента: «ти той самий?». Компʼютер на такі запитання не ображається — для нього це цілком нормально. Це один із найкорисніших шаблонів для початківця, бо він тренує дисципліну: коректні межі, коректний тип індексу, зрозумілий результат.
Найважливіше питання тут навіть не «як порівняти», а «як повернути результат». Часто ми хочемо повернути позицію (індекс), але позиція — це число від 0 до N-1. Тоді як позначити стан «не знайдено»?
Один зі зручних способів, особливо якщо індекс зберігається у std::size_t, — використати значення N. Це безпечно, бо N не може бути коректним індексом: останній індекс — N-1.
Приклад 2: пошук першого входження
Додамо це до нашого застосунку: користувач вводить число target, а ми шукаємо, у який день тижня воно траплялося.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 7;
int t[N] = {};
for (std::size_t i = 0; i < N; ++i) std::cin >> t[i];
int target = 0;
std::cin >> target;
std::size_t pos = N; // маркер "не знайдено"
for (std::size_t i = 0; i < N; ++i)
if (t[i] == target)
{ pos = i; break; }
if (pos != N)
std::cout << "знайдено в день " << pos << '\n'; // наприклад: знайдено в день 3
else
std::cout << "не знайдено\n";
}
Тут break означає: «нам достатньо першого збігу». Це цілком нормальна стратегія, і часто очікують саме її. Але важливо розуміти: якщо прибрати break, ви отримаєте останній збіг, бо pos перезаписуватиметься щоразу.
Один каркас циклу для різних завдань
Якщо придивитися, min/max і пошук схожі більше, ніж здається. В обох випадках у нас є цикл for, і в ньому є if. Відмінність лише в тому, що саме ми робимо всередині if.
Для min/max ми не виходимо з циклу, а оновлюємо змінну відповіді й продовжуємо прохід. Для пошуку ми або теж оновлюємо «відповідь» — наприклад, позицію, — або зупиняємося, якщо вибрали стратегію «перше входження».
Щоб це закріпилося не як магія, а як звичка, корисно подумки тримати таку міні‑схему:
Ідемо по i від 0 до N-1
якщо елемент підходить під умову:
або покращуємо відповідь і йдемо далі
або фіксуємо відповідь і зупиняємося
Це і є один із найкорисніших «каркасів» у програмуванні.
4. Розворот масиву «на місці»
Розворот масиву — операція, у якій легко припуститися класичної помилки з індексом: переплутати N - i і N - 1 - i. Тут важливо памʼятати просте правило: останній індекс — N - 1.
Суть розвороту така: перший елемент міняється місцями з останнім, другий — із передостаннім, і так далі. Якщо пройти так до кінця масиву, частину пар ви обміняєте двічі й у підсумку повернете елементи назад. Тому обміни треба виконувати лише до середини: i < N / 2.
Невелика схема пар під час розвороту
Для масиву з 7 елементів індекси мають такий вигляд:
0 1 2 3 4 5 6
| | | | | | |
6 5 4 3 2 1 0
Тобто індекс елемента в парі для i — це N - 1 - i.
Приклад 3: розворот C‑масиву з тимчасовою змінною
Додамо до нашого застосунку розворот температур, щоб вивести їх від кінця тижня до початку.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 7;
int t[N] = {};
for (std::size_t i = 0; i < N; ++i) std::cin >> t[i];
for (std::size_t i = 0; i < N / 2; ++i) {
int tmp = t[i];
t[i] = t[N - 1 - i];
t[N - 1 - i] = tmp;
}
for (std::size_t i = 0; i < N; ++i) std::cout << t[i] << ' ';
std::cout << '\n'; // наприклад: 8 6 4 1 0 -2 -3
}
Чому тут не використовується std::swap? Тому що зараз нам важливіше побачити сам механізм: тимчасово зберегти один елемент у tmp, а потім виконати два присвоєння. Це прозоро: видно кожен крок, і помилки легше помітити.
5. Ті самі шаблони на std::array
std::array зручний тим, що сам уміє повідомляти свій розмір: a.size(). Це суттєво зменшує кількість «магічних чисел» і ризик розсинхронізації, коли ви змінили розмір масиву, а межу циклу — ні.
На практиці це теж допомагає: ви менше думаєте «а я точно всюди використовую 7?», і більше — про саму задачу.
Приклад 4: min/max для std::array
#include <array>
#include <cstddef>
#include <iostream>
int main() {
std::array<int, 7> t{}; // 7 температур
for (std::size_t i = 0; i < t.size(); ++i) std::cin >> t[i];
int mn = t[0], mx = t[0];
for (std::size_t i = 1; i < t.size(); ++i)
{ if (t[i] < mn) mn = t[i]; if (t[i] > mx) mx = t[i]; }
std::cout << "min=" << mn << " max=" << mx << '\n';
}
Зверніть увагу: логіка та сама, «рецепт» той самий, змінилася лише межа циклу. Це хороший знак. Отже, ви не привʼязані до конкретної реалізації масиву, а мислите категоріями даних і проходу по них.
Приклад 5: пошук у std::array з маркером «не знайдено»
#include <array>
#include <cstddef>
#include <iostream>
int main() {
std::array<int, 7> t{};
for (std::size_t i = 0; i < t.size(); ++i) std::cin >> t[i];
int target = 0;
std::cin >> target;
std::size_t pos = t.size();
for (std::size_t i = 0; i < t.size(); ++i)
if (t[i] == target)
{ pos = i; break; }
std::cout << (pos == t.size() ? "не знайдено\n" : "знайдено\n"); // знайдено / не знайдено
}
Тут маркером стає t.size(). Це та сама ідея, що й pos = N, просто в такому вигляді вона краще «пояснює сама себе».
6. Практичний сценарій: «температури за тиждень»
Щоб усе це не залишилося набором окремих трюків, корисно уявити, що ми пишемо один міні‑застосунок, який виконує кілька операцій поспіль. У реальному житті так і буває: дані рідко обробляють однією командою, частіше це цілий ланцюжок кроків.
Ми вже майже зібрали таку програму: вводимо 7 температур, знаходимо min/max, шукаємо конкретну температуру, розвертаємо масив і друкуємо результат. Так, це звучить як «міні‑Excel у спрощеному вигляді», але саме такі вправи закладають базову моторику роботи з масивами.
Ось компактна версія, де все зібрано в одному main (і для поточного етапу це важливо: жодних користувацьких функцій).
#include <array>
#include <cstddef>
#include <iostream>
int main() {
std::array<int, 7> t{};
for (std::size_t i = 0; i < t.size(); ++i)
std::cin >> t[i];
int mn = t[0], mx = t[0];
for (std::size_t i = 1; i < t.size(); ++i)
{ if (t[i] < mn) mn = t[i]; if (t[i] > mx) mx = t[i]; }
int target = 0; std::cin >> target;
std::size_t pos = t.size();
for (std::size_t i = 0; i < t.size(); ++i)
if (t[i] == target) { pos = i; break; }
for (std::size_t i = 0; i < t.size() / 2; ++i)
{ int tmp = t[i]; t[i] = t[t.size() - 1 - i]; t[t.size() - 1 - i] = tmp; }
std::cout << "min=" << mn << " max=" << mx << '\n';
std::cout << (pos == t.size() ? "не знайдено\n" : "знайдено\n");
}
Так, тут код записано щільно. Але зверніть увагу на структуру: три проходи (min/max, пошук, розворот), і кожен із них — той самий знайомий цикл за індексами. У цьому й полягає мета лекції: щоб ви бачили в коді не хаос, а повторювані шаблони.
7. Типові помилки під час min/max, пошуку та розвороту
Помилка №1: починати min/max з «0» або «дуже великого числа».
Початківець часто пише int mn = 0; і потім дивується, чому мінімум масиву {-5, -2} раптом дорівнює 0. Проблема в тому, що стартове значення ви взяли не з масиву, а довільно. Надійніше починати з a[0], а цикл — з i = 1: тоді відповідь завжди ґрунтується на реальних даних.
Помилка №2: цикл для min/max починається з i = 0, але mn теж дорівнює a[0], і через це легко заплутатися.
Формально це не псує результат: порівняти a[0] з mn, де mn == a[0], не страшно. Але для початківця це додає зайве мисленнєве навантаження: «а навіщо ми порівнюємо елемент сам із собою?». Краще дотримуватися чистого шаблону mn = a[0]; for (i = 1; ...).
Помилка №3: у пошуку немає зрозумілого «не знайдено».
Якщо ви шукаєте позицію і заздалегідь не домовилися, що робити, коли збігу немає, зʼявляються дивні обхідні рішення: повертають 0, повертають -1 у size_t (а це перетворюється на величезне число) або друкують сміття. Найбезпечніший прийом на цьому рівні — pos = N (або pos = a.size()), бо це значення не може бути коректним індексом.
Помилка №4: загублений break або, навпаки, зайвий break.
Іноді ви хочете знайти перше входження, але забуваєте break — і отримуєте останнє. Іноді навпаки: хочете знайти останнє або порахувати всі збіги, але ставите break і зупиняєтеся після першого. Це не «помилка компіляції», а помилка в постановці задачі. Перед тим як писати цикл, варто чесно вирішити: «мені потрібен перший, останній чи всі збіги?».
Помилка №5: неправильна формула індекса під час розвороту (N - i замість N - 1 - i).
Це класика: ви памʼятаєте, що треба звернутися «в кінець масиву», але забуваєте, що індексація починається з нуля. У результаті ви звертаєтеся до a[N], а це вже вихід за межі масиву. Рятує проста мантра: «останній індекс — N - 1».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ