JavaRush /Курси /C++ SELF /Типові операції над масивами

Типові операції над масивами

C++ SELF
Рівень 11 , Лекція 5
Відкрита

1. Вступ

Коли ви лише починаєте програмувати, масив здається просто коробкою з числами: поклали — дістали. Але на практиці він дуже швидко перетворюється на «дані», з якими постійно треба виконувати одні й ті самі дії: знайти мінімум, знайти максимум, перевірити, чи є потрібне значення, змінити порядок елементів. Саме такі повторювані прийоми — майже як кулінарні рецепти — і називають алгоритмічними шаблонами.

Головна ідея проста: масив фіксованого розміру найчастіше обробляють одним лінійним проходом за індексами 0..N-1. Під час цього проходу ви або оновлюєте поточну відповідь, як у min/max, або шукаєте збіг, як під час пошуку, або міняєте елементи місцями, як під час розвороту.

Закріпімо це у вигляді невеликої таблиці — не для іспиту, а щоб вам було легше зорієнтуватися:

Операція Що робимо в циклі Скільки проходів потрібно Ідея результату
min/max
порівнюємо поточний елемент із найкращим на цей момент 1 «найкраще значення»
лінійний пошук перевіряємо a[i] == target 1 «позиція» або «не знайдено»
розворот міняємо місцями пари елементів
~N/2
масив змінено «на місці»

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».

1
Опитування
Масиви та std::array, рівень 11, лекція 5
Недоступний
Масиви та std::array
Масиви та std::array
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ