C++

Готові структури даних C++: ітератори та алгоритми STL

Ітератори як узагальнення покажчиків, категорії ітераторів, алгоритми STL (sort, find, transform, accumulate), лямбди як предикати, ідіома Erase-Remove, функціональний стиль програмування

Готові структури даних C++: ітератори та алгоритми STL

🎯 Мета лекції

  • Опанувати ітератори як універсальний механізм обходу контейнерів, що узагальнює покажчики.
  • Розібрати п'ять категорій ітераторів: Input, Output, Forward, Bidirectional, Random Access — їхні можливості та обмеження.
  • Застосувати алгоритми STL (std::sort, std::find, std::transform, std::accumulate) для обробки контейнерів без написання циклів.
  • Використати лямбда-функції як предикати для фільтрації, сортування та трансформації даних.
  • Опанувати ідіому Erase-Remove для ефективного видалення елементів із контейнерів.
  • Перейти від імперативного до функціонального стилю програмування у C++.

🔑 Ключові терміни

  • Iterator (Ітератор): об'єкт, що узагальнює покажчик, дозволяючи обходити елементи контейнера без знання його внутрішньої структури.
  • Iterator Category (Категорія ітератора): класифікація ітераторів за підтримуваними операціями (інкремент, декремент, арифметика, довільний доступ).
  • Algorithm (Алгоритм): шаблонна функція, що працює з діапазонами елементів через ітератори, незалежно від типу контейнера.
  • Predicate (Предикат): функція або функціональний об'єкт, що повертає bool та використовується для фільтрації/пошуку елементів.
  • Erase-Remove Idiom (Ідіома Erase-Remove): патерн ефективного видалення елементів із контейнера через std::remove + erase.

Hook: один алгоритм для всіх контейнерів — сила абстракції

Уявіть, що вам потрібно відсортувати елементи у трьох різних контейнерах:

std::vector<int> vec = {5, 2, 8, 1, 9};
std::deque<int> deq = {5, 2, 8, 1, 9};
std::list<int> lst = {5, 2, 8, 1, 9};

Без STL ви б писали три окремі реалізації:

  • Для vector — швидке сортування (QuickSort) з довільним доступом.
  • Для deque — модифікований QuickSort для блокової структури.
  • Для list — сортування злиттям (MergeSort), оскільки немає довільного доступу.

Це сотні рядків коду, тестування, відлагодження...

З STL усе зводиться до одного рядка для кожного контейнера:

std::sort(vec.begin(), vec.end());    // O(n log n)
std::sort(deq.begin(), deq.end());    // O(n log n)
lst.sort();                           // O(n log n), власний метод list

Чому це працює? Завдяки ітераторам — універсальному інтерфейсу, що дозволяє алгоритмам працювати з будь-яким контейнером, не знаючи його внутрішньої будови.

Філософія STL: Розділення даних (контейнери), доступу до даних (ітератори) та обробки даних (алгоритми). Це дозволяє комбінувати будь-який контейнер з будь-яким алгоритмом, якщо їхні вимоги збігаються.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

title Три стовпи STL

package "Контейнери\n(дані)" as containers #DBEAFE {
    rectangle "vector\ndeque\nlist\nset\nmap" as cont #DCFCE7
}

package "Ітератори\n(доступ)" as iterators #FEF3C7 {
    rectangle "begin()\nend()\noperator++\noperator*" as iter #FDE68A
}

package "Алгоритми\n(обробка)" as algorithms #E0E7FF {
    rectangle "sort()\nfind()\ntransform()\naccumulate()" as algo #C7D2FE
}

containers -right-> iterators : надають
iterators -right-> algorithms : використовують

note bottom of containers #DBEAFE
  Контейнери зберігають дані
  та надають ітератори для
  доступу до них
end note

note bottom of iterators #FEF3C7
  Ітератори узагальнюють покажчики,
  дозволяючи алгоритмам не знати
  про внутрішню структуру контейнера
end note

note bottom of algorithms #E0E7FF
  Алгоритми працюють з діапазонами
  [begin, end) через ітератори,
  не залежачи від контейнера
end note

@enduml

У цій лекції ми розглянемо другий (ітератори) та третій (алгоритми) стовпи STL, завершуючи повний огляд бібліотеки шаблонів C++.


Ітератори: узагальнення покажчиків

Концепція ітератора

Ітератор — це об'єкт, що поводиться як покажчик, але може працювати з будь-яким контейнером (не лише з масивами). Він надає уніфікований спосіб обходу елементів без знання деталей реалізації контейнера.

Базові операції ітератора:

ОпераціяОписАналог для покажчика
*itРозіменування (доступ до елемента)*ptr
++itПерехід до наступного елемента++ptr
it1 == it2Порівняння ітераторівptr1 == ptr2
it1 != it2Перевірка нерівностіptr1 != ptr2

Приклад: обхід вектора через ітератор

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers = {10, 20, 30, 40, 50};
    
    // Явний ітератор
    std::vector<int>::iterator it = numbers.begin();
    
    while (it != numbers.end())
    {
        std::cout << *it << ' ';  // Розіменування
        ++it;                      // Інкремент
    }
    std::cout << '\n';
    
    return 0;
}
Виконання програми
10 20 30 40 50

Range-based for loop як синтаксичний цукор:

for (int x : numbers)  // Компілятор розгортає у цикл з ітераторами
{
    std::cout << x << ' ';
}

Еквівалентний код (що генерує компілятор):

{
    auto&& __range = numbers;
    auto __begin = __range.begin();
    auto __end = __range.end();
    
    for (; __begin != __end; ++__begin)
    {
        int x = *__begin;
        std::cout << x << ' ';
    }
}

Основні методи контейнерів для роботи з ітераторами

Усі контейнери STL надають наступні методи:

МетодОписПовертає
begin()Ітератор на перший елементiterator
end()Ітератор на позицію після останнього елементаiterator
cbegin()Константний ітератор на перший елементconst_iterator
cend()Константний ітератор на позицію після останньогоconst_iterator
rbegin()Зворотний ітератор на останній елементreverse_iterator
rend()Зворотний ітератор на позицію перед першимreverse_iterator
end() повертає ітератор на позицію після останнього елемента (past-the-end iterator). Це спрощує написання циклів: while (it != end()) автоматично зупиняється після останнього елемента. Розіменовувати end() заборонено — це невизначена поведінка.

Зворотний обхід контейнера:

std::vector<int> numbers = {10, 20, 30, 40, 50};

// Обхід у зворотному порядку
for (auto it = numbers.rbegin(); it != numbers.rend(); ++it)
{
    std::cout << *it << ' ';  // Виведе: 50 40 30 20 10
}

Константні ітератори: захист від модифікації

const_iterator — це ітератор, що дозволяє читати елементи, але забороняє їх змінювати.

std::vector<int> numbers = {10, 20, 30};

// Константний ітератор
std::vector<int>::const_iterator it = numbers.cbegin();

std::cout << *it << '\n';  // ✅ Читання дозволене: 10

// *it = 100;  // ❌ ПОМИЛКА компіляції: присвоєння через const_iterator

Коли використовувати const_iterator:

  • У функціях, що не змінюють контейнер (read-only операції).
  • Для запобігання випадкової модифікації даних.
  • У константних методах класів (void print() const).
Якщо вам не потрібно змінювати елементи, використовуйте cbegin() та cend() замість begin() та end(). Це запобігає випадковим модифікаціям та робить код більш виразним.

П'ять категорій ітераторів

Ієрархія категорій

Ітератори STL поділяються на п'ять категорій за рівнем функціональності. Кожна наступна категорія успадковує можливості попередньої та додає нові.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

title Ієрархія категорій ітераторів

rectangle "Input Iterator\n(читання, ++)" as input #FEE2E2
rectangle "Output Iterator\n(запис, ++)" as output #FEE2E2

rectangle "Forward Iterator\n(читання/запис, ++, багаторазовий прохід)" as forward #FEF3C7

rectangle "Bidirectional Iterator\n(++ та --)" as bidir #DBEAFE

rectangle "Random Access Iterator\n(довільний доступ, арифметика)" as random #DCFCE7

input -down-> forward : успадковує
output -down-> forward : успадковує
forward -down-> bidir : успадковує
bidir -down-> random : успадковує

note right of input #FEE2E2
  Мінімальний ітератор:
  тільки читання,
  лише один прохід
end note

note right of random #DCFCE7
  Найпотужніший:
  працює як покажчик,
  підтримує it + 5, it - 3
end note

@enduml

Таблиця категорій та їхніх можливостей

КатегоріяОпераціїПриклад контейнераВикористання
Input Iterator*it (read), ++it, ==, !=std::istream_iteratorЧитання з потоку вводу
Output Iterator*it (write), ++itstd::ostream_iteratorЗапис у потік виводу
Forward IteratorInput + багаторазовий прохідstd::forward_listОднозв'язаний список
Bidirectional IteratorForward + --itstd::list, std::set, std::mapДвозв'язаний список, дерева
Random Access IteratorBidirectional + it + n, it - n, it[n], <, >std::vector, std::deque, std::arrayМасиви, динамічні масиви

Ключовий принцип: Алгоритм, що вимагає Random Access Iterator (наприклад, std::sort), не працюватиме з Forward Iterator (std::forward_list). Але алгоритм, що вимагає лише Forward Iterator (std::find), працюватиме з будь-яким контейнером.

Random Access Iterator: найпотужніша категорія

Random Access Iterator підтримує всі операції покажчиків, включаючи арифметику:

std::vector<int> numbers = {10, 20, 30, 40, 50};

auto it = numbers.begin();

// Арифметика ітераторів
it = it + 2;         // Переміщення на 2 позиції вперед → вказує на 30
std::cout << *it << '\n';  // 30

it += 3;             // Переміщення ще на 3 позиції → вказує на 50 (останній)
std::cout << *it << '\n';  // 50

// Доступ за індексом
std::cout << it[-2] << '\n';  // Доступ на 2 позиції назад → 30

// Різниця ітераторів
auto distance = numbers.end() - numbers.begin();
std::cout << "Розмір: " << distance << '\n';  // 5

// Порівняння ітераторів
if (it > numbers.begin())
{
    std::cout << "it вказує на елемент після початку\n";
}
Виконання програми
30
50
30
Розмір: 5
it вказує на елемент після початку

Bidirectional Iterator: рух у обох напрямках

Bidirectional Iterator підтримує ++ (вперед) та -- (назад), але не підтримує арифметику (it + 5).

std::list<int> numbers = {10, 20, 30, 40, 50};

auto it = numbers.begin();

++it;  // Переміщення до 20
++it;  // Переміщення до 30

std::cout << *it << '\n';  // 30

--it;  // Переміщення назад до 20
std::cout << *it << '\n';  // 20

// it = it + 2;  // ❌ ПОМИЛКА: list не підтримує арифметику

Для переміщення на N позицій у list використовуйте std::advance:

auto it = numbers.begin();
std::advance(it, 3);  // Переміститься на 3 позиції (викличе ++it тричі)
std::cout << *it << '\n';  // 40
std::advance(it, n) — універсальна функція, що працює з будь-яким ітератором. Для Random Access Iterator вона виконує it += n (O(1)), для інших — викликає ++it n разів (O(n)).

Алгоритми STL: обробка діапазонів

Загальна структура алгоритмів

Алгоритм STL — це шаблонна функція, що приймає діапазон елементів через пару ітераторів [begin, end) та виконує над ними операцію (пошук, сортування, трансформація).

Загальна сигнатура:

template <typename Iterator>
ReturnType algorithm(Iterator begin, Iterator end);

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

Основні категорії алгоритмів

КатегоріяАлгоритмиПризначення
Немодифікуючіfind, count, equal, searchПошук, підрахунок, порівняння
Модифікуючіcopy, fill, replace, removeКопіювання, заповнення, заміна, видалення
Сортуванняsort, partial_sort, stable_sortВпорядкування елементів
Операції над множинамиmerge, set_union, set_intersectionОб'єднання, перетин
Числовіaccumulate, inner_product, partial_sumОбчислення сум, добутків

Усі алгоритми знаходяться у заголовку <algorithm> (крім числових — вони у <numeric>).


Немодифікуючі алгоритми: пошук та підрахунок

std::find — пошук елемента

Сигнатура:

template <typename Iterator, typename T>
Iterator find(Iterator begin, Iterator end, const T& value);

Повертає: Ітератор на перший знайдений елемент або end, якщо елемент не знайдено.

Приклад:

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> numbers = {10, 20, 30, 40, 50};
    
    auto it = std::find(numbers.begin(), numbers.end(), 30);
    
    if (it != numbers.end())
    {
        std::cout << "Знайдено: " << *it << '\n';
        std::cout << "Індекс: " << std::distance(numbers.begin(), it) << '\n';
    }
    else
    {
        std::cout << "Не знайдено\n";
    }
    
    return 0;
}
Виконання програми
Знайдено: 30
Індекс: 2

std::distance(begin, it) — функція, що обчислює кількість елементів між двома ітераторами. Для Random Access Iterator — O(1), для інших — O(n).

std::find_if — пошук за предикатом

Предикат (predicate) — це функція або функціональний об'єкт, що повертає bool.

#include <iostream>
#include <vector>
#include <algorithm>

// Предикат: чи число парне?
bool isEven(int n)
{
    return n % 2 == 0;
}

int main()
{
    std::vector<int> numbers = {11, 13, 20, 25, 30};
    
    // Знайти перше парне число
    auto it = std::find_if(numbers.begin(), numbers.end(), isEven);
    
    if (it != numbers.end())
    {
        std::cout << "Перше парне: " << *it << '\n';  // 20
    }
    
    return 0;
}

З лямбда-функцією (сучасний стиль):

auto it = std::find_if(numbers.begin(), numbers.end(), 
                       [](int n) { return n % 2 == 0; });
У сучасному C++ (C++11+) лямбда-функції є стандартним способом передачі предикатів у алгоритми. Вони компактніші та дозволяють захоплювати локальні змінні.

std::count та std::count_if — підрахунок елементів

std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};

// Підрахунок кількості елементів зі значенням 2
int count = std::count(numbers.begin(), numbers.end(), 2);
std::cout << "Кількість двійок: " << count << '\n';  // 3

// Підрахунок парних чисел
int evenCount = std::count_if(numbers.begin(), numbers.end(),
                               [](int n) { return n % 2 == 0; });
std::cout << "Парних чисел: " << evenCount << '\n';  // 4

(Кінець частини 1. Продовжую частину 2...)

Модифікуючі алгоритми: трансформація та заміна

std::transform — застосування функції до кожного елемента

Сигнатура:

template <typename InputIt, typename OutputIt, typename UnaryOp>
OutputIt transform(InputIt begin, InputIt end, OutputIt dest, UnaryOp op);

Призначення: Застосовує функцію op до кожного елемента діапазону [begin, end) та записує результат у dest.

Приклад: подвоєння кожного елемента

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> doubled(numbers.size());  // Резервуємо місце для результату
    
    // Подвоїти кожен елемент
    std::transform(numbers.begin(), numbers.end(), 
                   doubled.begin(),
                   [](int n) { return n * 2; });
    
    std::cout << "Подвоєні: ";
    for (int x : doubled)
    {
        std::cout << x << ' ';  // 2 4 6 8 10
    }
    std::cout << '\n';
    
    return 0;
}

Модифікація на місці (in-place):

// Подвоїти елементи у тому ж векторі
std::transform(numbers.begin(), numbers.end(),
               numbers.begin(),  // Записуємо результат у той же контейнер
               [](int n) { return n * 2; });

Приклад: перетворення рядків у верхній регістр

std::vector<std::string> words = {"hello", "world", "cpp"};

std::transform(words.begin(), words.end(), words.begin(),
               [](std::string s) {
                   std::transform(s.begin(), s.end(), s.begin(), ::toupper);
                   return s;
               });

// Результат: {"HELLO", "WORLD", "CPP"}

std::for_each — застосування дії до кожного елемента

На відміну від transform, std::for_each не повертає новий діапазон — він просто виконує дію для кожного елемента.

std::vector<int> numbers = {1, 2, 3, 4, 5};

// Вивести кожен елемент
std::for_each(numbers.begin(), numbers.end(),
              [](int n) { std::cout << n << ' '; });

Коли використовувати for_each vs range-based for:

  • Range-based for — простіший синтаксис для типових операцій.
  • std::for_each — коли потрібно передати функцію як параметр або захопити контекст у лямбді.
У C++20 з'явились ranges (std::ranges::transform, std::ranges::for_each), які роблять код ще більш виразним, але у цій лекції ми зосереджуємось на класичних алгоритмах STL.

std::replace та std::replace_if — заміна елементів

std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};

// Замінити всі 2 на 99
std::replace(numbers.begin(), numbers.end(), 2, 99);
// Результат: {1, 99, 3, 99, 4, 99, 5}

// Замінити всі парні числа на 0
std::replace_if(numbers.begin(), numbers.end(),
                [](int n) { return n % 2 == 0; },
                0);
// Результат: {1, 0, 3, 0, 0, 0, 5}

Ідіома Erase-Remove: ефективне видалення елементів

Проблема: наївне видалення у циклі

Типова помилка початківців:

std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};

// ❌ НЕПРАВИЛЬНО: видалення під час ітерації
for (auto it = numbers.begin(); it != numbers.end(); ++it)
{
    if (*it == 2)
    {
        numbers.erase(it);  // ❌ Інвалідує ітератор!
    }
}

Проблеми:

  1. erase() інвалідує ітератор it → наступний ++it працює з недійсним ітератором.
  2. Кожен erase() зсуває всі наступні елементи → O(n²) для N видалень.

Правильний спосіб (C++11):

for (auto it = numbers.begin(); it != numbers.end(); )
{
    if (*it == 2)
    {
        it = numbers.erase(it);  // erase() повертає новий дійсний ітератор
    }
    else
    {
        ++it;
    }
}

Але це все одно O(n²) через зсув елементів при кожному erase().

Ідіома Erase-Remove: O(n) видалення

Ідіома Erase-Remove — це патерн двофазового видалення:

  1. Фаза 1: std::remove переміщує всі елементи, що не видаляються, на початок контейнера, залишаючи "сміття" в кінці. Складність: O(n).
  2. Фаза 2: erase() видаляє "сміття" одним викликом. Складність: O(1) (для видалення діапазону в кінці).

Схема роботи std::remove:

Початковий вектор:   [1, 2, 3, 2, 4, 2, 5]
                           ↓
Після std::remove(2): [1, 3, 4, 5, ?, ?, ?]  ← повертає ітератор на ?
                                    ↑
                                 newEnd

Код:

std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};

// Фаза 1: std::remove переміщує елементи
auto newEnd = std::remove(numbers.begin(), numbers.end(), 2);

// Фаза 2: erase видаляє "хвіст"
numbers.erase(newEnd, numbers.end());

// Результат: {1, 3, 4, 5}

Одним рядком:

numbers.erase(std::remove(numbers.begin(), numbers.end(), 2), 
              numbers.end());

З предикатом (std::remove_if):

// Видалити всі парні числа
numbers.erase(std::remove_if(numbers.begin(), numbers.end(),
                              [](int n) { return n % 2 == 0; }),
              numbers.end());
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

title Ідіома Erase-Remove: видалення всіх 2

rectangle "Крок 1: Початковий вектор" as step1 #F1F5F9 {
    rectangle "1" as e1a #DCFCE7
    rectangle "2" as e2a #FEE2E2
    rectangle "3" as e3a #DCFCE7
    rectangle "2" as e4a #FEE2E2
    rectangle "4" as e5a #DCFCE7
    rectangle "2" as e6a #FEE2E2
    rectangle "5" as e7a #DCFCE7
}

rectangle "Крок 2: Після std::remove(2)" as step2 #F1F5F9 {
    rectangle "1" as e1b #DCFCE7
    rectangle "3" as e2b #DCFCE7
    rectangle "4" as e3b #DCFCE7
    rectangle "5" as e4b #DCFCE7
    rectangle "?" as e5b #E2E8F0
    rectangle "?" as e6b #E2E8F0
    rectangle "?" as e7b #E2E8F0
}

rectangle "Крок 3: Після erase(newEnd, end)" as step3 #F1F5F9 {
    rectangle "1" as e1c #DCFCE7
    rectangle "3" as e2c #DCFCE7
    rectangle "4" as e3c #DCFCE7
    rectangle "5" as e4c #DCFCE7
}

step1 -down-> step2 : std::remove
step2 -down-> step3 : erase

note right of step2 #FEF3C7
  std::remove повертає
  ітератор newEnd,
  що вказує на перший
  "сміттєвий" елемент
end note

note right of step3 #DCFCE7
  erase(newEnd, end)
  видаляє всі елементи
  після newEnd за O(1)
end note

@enduml
std::remove виконує один прохід по контейнеру, переміщуючи елементи, що не видаляються, на початок. Немає зсувів при кожному видаленні. Фінальний erase() видаляє діапазон за O(1), оскільки це кінець вектора.

Сортування: std::sort та його варіації

std::sort — швидке сортування

Сигнатура:

template <typename RandomIt>
void sort(RandomIt begin, RandomIt end);

Вимоги: Random Access Iterator (працює з vector, deque, array, але не з list).

Складність: O(n log n) у середньому (зазвичай реалізовано як IntroSort — гібрид QuickSort, HeapSort, InsertionSort).

Приклад:

std::vector<int> numbers = {5, 2, 8, 1, 9, 3};

std::sort(numbers.begin(), numbers.end());
// Результат: {1, 2, 3, 5, 8, 9}

Сортування за спаданням:

std::sort(numbers.begin(), numbers.end(), std::greater<int>());
// Результат: {9, 8, 5, 3, 2, 1}

Сортування з custom компаратором:

struct Person
{
    std::string name;
    int age;
};

std::vector<Person> people = {
    {"Олександр", 25},
    {"Марія", 30},
    {"Дмитро", 22}
};

// Сортувати за віком
std::sort(people.begin(), people.end(),
          [](const Person& a, const Person& b) {
              return a.age < b.age;
          });

// Результат: Дмитро(22), Олександр(25), Марія(30)

std::stable_sort — сортування зі збереженням порядку

Відмінність від std::sort: Якщо два елементи еквівалентні (компаратор повертає false для обох напрямків), stable_sort гарантує, що їхній відносний порядок збережеться.

Приклад:

struct Student
{
    std::string name;
    int grade;
};

std::vector<Student> students = {
    {"Анна", 85},
    {"Борис", 90},
    {"Віктор", 85},
    {"Галина", 90}
};

// Сортувати за оцінкою (stable — зберігає порядок для однакових оцінок)
std::stable_sort(students.begin(), students.end(),
                 [](const Student& a, const Student& b) {
                     return a.grade > b.grade;
                 });

// Результат: Борис(90), Галина(90), Анна(85), Віктор(85)
// Порядок всередині груп (90 та 85) збережено!

Коли використовувати stable_sort:

  • Коли потрібна гарантія стабільності (наприклад, сортування таблиці за кількома стовпцями).
  • Коли порядок еквівалентних елементів має значення для бізнес-логіки.

Ціна: stable_sort повільніший за sort (O(n log² n) у гіршому випадку) та потребує додаткової пам'яті.

std::partial_sort — часткове сортування

Призначення: Відсортувати лише топ-N елементів, залишаючи решту несортованими.

std::vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};

// Відсортувати лише перші 3 елементи
std::partial_sort(numbers.begin(), numbers.begin() + 3, numbers.end());

// Результат: {1, 2, 3, ...решта в довільному порядку}

Складність: O(n log k), де k — кількість елементів, що сортуються. Швидше за std::sort + обрізання, якщо k << n.

Коли використовувати: Знаходження топ-K елементів (альтернатива priority_queue для великих N).


Числові алгоритми: accumulate та inner_product

std::accumulate — обчислення суми (редукція)

Сигнатура:

template <typename InputIt, typename T>
T accumulate(InputIt begin, InputIt end, T init);

Призначення: Обчислює суму всіх елементів діапазону, починаючи з початкового значення init.

Приклад:

#include <iostream>
#include <vector>
#include <numeric>  // accumulate знаходиться тут!

int main()
{
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    int sum = std::accumulate(numbers.begin(), numbers.end(), 0);
    std::cout << "Сума: " << sum << '\n';  // 15
    
    // Добуток (через custom операцію)
    int product = std::accumulate(numbers.begin(), numbers.end(), 1,
                                   [](int acc, int x) { return acc * x; });
    std::cout << "Добуток: " << product << '\n';  // 120
    
    return 0;
}

Custom операція (функціональне програмування):

// Обчислити суму квадратів
int sumOfSquares = std::accumulate(numbers.begin(), numbers.end(), 0,
                                    [](int acc, int x) {
                                        return acc + x * x;
                                    });
// Результат: 1 + 4 + 9 + 16 + 25 = 55

Об'єднання рядків:

std::vector<std::string> words = {"Hello", " ", "World", "!"};

std::string sentence = std::accumulate(words.begin(), words.end(), std::string(""));
std::cout << sentence << '\n';  // "Hello World!"
Якщо ви обчислюєте середнє значення double, початкове значення має бути 0.0, а не 0:
std::vector<double> values = {1.5, 2.5, 3.5};

double sum = std::accumulate(values.begin(), values.end(), 0.0);  // ✅ Правильно
// double sum = std::accumulate(values.begin(), values.end(), 0);  // ❌ Поверне int!

double average = sum / values.size();

std::inner_product — скалярний добуток векторів

std::vector<int> a = {1, 2, 3};
std::vector<int> b = {4, 5, 6};

// Скалярний добуток: 1*4 + 2*5 + 3*6 = 32
int dotProduct = std::inner_product(a.begin(), a.end(), b.begin(), 0);
std::cout << "Скалярний добуток: " << dotProduct << '\n';  // 32

Лямбда-функції як предикати: сила функціонального програмування

Синтаксис лямбда-функцій (повторення)

[захоплення](параметри) -> тип_повернення { тіло }

Приклади:

// Без параметрів
auto greet = []() { std::cout << "Hello!\n"; };
greet();

// З параметрами
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 5) << '\n';  // 8

// Автоматичне виведення типу повернення
auto multiply = [](int a, int b) { return a * b; };  // Тип повернення: int

// Явне вказання типу повернення
auto divide = [](int a, int b) -> double { return static_cast<double>(a) / b; };

Захоплення змінних у лямбдах

Захоплення (capture) дозволяє лямбді використовувати змінні з зовнішньої області видимості.

СинтаксисОпис
[]Не захоплює нічого
[=]Захоплює всі змінні за значенням (копіювання)
[&]Захоплює всі змінні за посиланням
[x]Захоплює лише x за значенням
[&x]Захоплює лише x за посиланням
[x, &y]Захоплює x за значенням, y за посиланням

Приклад:

int threshold = 10;

std::vector<int> numbers = {5, 15, 8, 20, 3};

// Підрахувати кількість чисел більших за threshold
int count = std::count_if(numbers.begin(), numbers.end(),
                           [threshold](int n) { return n > threshold; });

std::cout << "Чисел > " << threshold << ": " << count << '\n';  // 2

Зміна змінної через посилання:

int sum = 0;

std::for_each(numbers.begin(), numbers.end(),
              [&sum](int n) { sum += n; });

std::cout << "Сума: " << sum << '\n';  // 51
  • [=] (за значенням): Для read-only доступу до зовнішніх змінних (безпечніше, немає ризику висячих посилань).
  • [&] (за посиланням): Коли потрібно модифікувати зовнішні змінні або уникнути копіювання великих об'єктів.

(Кінець частини 2. Продовжую частину 3 — остання...)

Практичне застосування: обробка тексту функціональним стилем

Задача: аналіз тексту

Умова: Дано текстовий файл. Необхідно:

  1. Розбити текст на слова.
  2. Видалити слова коротші за 3 символи.
  3. Перетворити всі слова у нижній регістр.
  4. Знайти 5 найчастіших слів.

Рішення функціональним стилем:

#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
#include <sstream>
#include <cctype>

std::vector<std::string> tokenize(const std::string& text)
{
    std::vector<std::string> words;
    std::istringstream stream(text);
    std::string word;
    
    while (stream >> word)
    {
        words.push_back(word);
    }
    
    return words;
}

std::string toLower(std::string s)
{
    std::transform(s.begin(), s.end(), s.begin(),
                   [](unsigned char c) { return std::tolower(c); });
    return s;
}

int main()
{
    std::string text = "Hello world hello programming world hello "
                      "C++ programming world hello C++ STL algorithms";
    
    // Крок 1: Розбити на слова
    std::vector<std::string> words = tokenize(text);
    
    // Крок 2: Видалити короткі слова (< 3 символи)
    words.erase(std::remove_if(words.begin(), words.end(),
                                [](const std::string& w) { return w.length() < 3; }),
                words.end());
    
    // Крок 3: Перетворити у нижній регістр
    std::transform(words.begin(), words.end(), words.begin(), toLower);
    
    // Крок 4: Підрахувати частоту
    std::unordered_map<std::string, int> freq;
    for (const auto& word : words)
    {
        ++freq[word];
    }
    
    // Крок 5: Знайти топ-5 найчастіших
    std::vector<std::pair<std::string, int>> sorted(freq.begin(), freq.end());
    
    std::partial_sort(sorted.begin(), sorted.begin() + 5, sorted.end(),
                      [](const auto& a, const auto& b) {
                          return a.second > b.second;
                      });
    
    std::cout << "Топ-5 найчастіших слів:\n";
    for (size_t i = 0; i < std::min(size_t(5), sorted.size()); ++i)
    {
        std::cout << "  " << sorted[i].first << ": " << sorted[i].second << '\n';
    }
    
    return 0;
}
Виконання програми
Топ-5 найчастіших слів:
hello: 4
world: 3
c++: 2
programming: 2
stl: 1

Ключові патерни:

  • Ідіома Erase-Remove для видалення коротких слів.
  • std::transform для перетворення у нижній регістр.
  • std::partial_sort для знаходження топ-K (ефективніше за повне сортування).
  • Лямбди для передачі предикатів та компараторів.

Зведена таблиця найпопулярніших алгоритмів STL

Немодифікуючі алгоритми

АлгоритмСкладністьПризначенняПриклад
std::findO(n)Знайти перший елемент зі значеннямfind(v.begin(), v.end(), 42)
std::find_ifO(n)Знайти перший елемент за предикатомfind_if(v.begin(), v.end(), isEven)
std::countO(n)Підрахувати кількість елементівcount(v.begin(), v.end(), 42)
std::count_ifO(n)Підрахувати за предикатомcount_if(v.begin(), v.end(), isEven)
std::all_ofO(n)Перевірити, чи всі елементи задовольняють предикатall_of(v.begin(), v.end(), isPositive)
std::any_ofO(n)Перевірити, чи хоча б один елемент задовольняє предикатany_of(v.begin(), v.end(), isNegative)
std::none_ofO(n)Перевірити, чи жоден елемент не задовольняє предикатnone_of(v.begin(), v.end(), isZero)

Модифікуючі алгоритми

АлгоритмСкладністьПризначенняПриклад
std::transformO(n)Застосувати функцію до кожного елементаtransform(v.begin(), v.end(), v.begin(), negate)
std::for_eachO(n)Виконати дію для кожного елементаfor_each(v.begin(), v.end(), print)
std::replaceO(n)Замінити всі входження значенняreplace(v.begin(), v.end(), 2, 99)
std::replace_ifO(n)Замінити елементи за предикатомreplace_if(v.begin(), v.end(), isEven, 0)
std::removeO(n)Перемістити елементи для видалення в кінецьremove(v.begin(), v.end(), 42)
std::remove_ifO(n)Перемістити елементи за предикатомremove_if(v.begin(), v.end(), isEven)
std::uniqueO(n)Видалити послідовні дублікатиunique(v.begin(), v.end())

Сортування та операції над відсортованими діапазонами

АлгоритмСкладністьПризначенняПриклад
std::sortO(n log n)Відсортувати діапазонsort(v.begin(), v.end())
std::stable_sortO(n log² n)Відсортувати зі збереженням порядкуstable_sort(v.begin(), v.end())
std::partial_sortO(n log k)Відсортувати лише перші k елементівpartial_sort(v.begin(), v.begin()+3, v.end())
std::binary_searchO(log n)Перевірити наявність елемента у відсортованому діапазоніbinary_search(v.begin(), v.end(), 42)
std::lower_boundO(log n)Знайти перший елемент ≥ значенняlower_bound(v.begin(), v.end(), 42)
std::upper_boundO(log n)Знайти перший елемент > значенняupper_bound(v.begin(), v.end(), 42)

Числові алгоритми (<numeric>)

АлгоритмСкладністьПризначенняПриклад
std::accumulateO(n)Обчислити суму (редукція)accumulate(v.begin(), v.end(), 0)
std::inner_productO(n)Скалярний добуток векторівinner_product(a.begin(), a.end(), b.begin(), 0)
std::partial_sumO(n)Обчислити часткові сумиpartial_sum(v.begin(), v.end(), out.begin())

Порівняння імперативного та функціонального стилів

Імперативний стиль (цикли)

// Задача: подвоїти всі парні числа та обчислити їхню суму

std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

int sum = 0;
for (size_t i = 0; i < numbers.size(); ++i)
{
    if (numbers[i] % 2 == 0)
    {
        numbers[i] *= 2;
        sum += numbers[i];
    }
}

std::cout << "Сума подвоєних парних: " << sum << '\n';

Характеристики:

  • ❌ Багато деталей реалізації (індекс i, умова циклу, інкремент).
  • ❌ Змішана логіка (подвоєння та підрахунок у одному циклі).
  • ❌ Важко паралелізувати.

Функціональний стиль (алгоритми STL)

std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Крок 1: Подвоїти парні числа
std::transform(numbers.begin(), numbers.end(), numbers.begin(),
               [](int n) { return (n % 2 == 0) ? n * 2 : n; });

// Крок 2: Обчислити суму парних (вже подвоєних)
int sum = std::accumulate(numbers.begin(), numbers.end(), 0,
                          [](int acc, int n) {
                              return (n % 2 == 0) ? acc + n : acc;
                          });

std::cout << "Сума подвоєних парних: " << sum << '\n';

Характеристики:

  • ✅ Декларативний стиль: що робимо, а не як.
  • ✅ Розділена логіка: трансформація та редукція — окремі кроки.
  • ✅ Легко читати та підтримувати.
  • ✅ Можливість паралелізації (C++17: std::execution::par).

Ще більш функціональний підхід (C++20 ranges):

// C++20 (поза рамками цієї лекції, але для ознайомлення)
auto evenDoubled = numbers 
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * 2; });

int sum = std::accumulate(evenDoubled.begin(), evenDoubled.end(), 0);

Резюме та практичні рекомендації

Ключові висновки

  1. Ітератори — це узагальнені покажчики, що дозволяють алгоритмам працювати з будь-яким контейнером без знання його внутрішньої структури.
  2. П'ять категорій ітераторів визначають можливості операцій: від мінімального Input Iterator до потужного Random Access Iterator.
  3. Алгоритми STL замінюють ручні цикли на виразні однорядкові виклики: std::sort, std::find, std::transform, std::accumulate.
  4. Лямбда-функції роблять код компактним та виразним, дозволяючи передавати предикати та операції безпосередньо у виклик алгоритму.
  5. Ідіома Erase-Remove — стандартний патерн для ефективного видалення елементів із контейнерів (O(n) замість O(n²)).
  6. Функціональний стиль кращий за імперативний для більшості задач: читабельніший, безпечніший, легший для паралелізації.

Правила використання алгоритмів STL

✅ Завжди використовуйте алгоритми STL

Замість циклу:

// ❌ Погано
for (size_t i = 0; i < v.size(); ++i) {
    if (v[i] == 42) return true;
}
return false;

Використайте алгоритм:

// ✅ Добре
return std::find(v.begin(), v.end(), 42) != v.end();

Переваги: Виразність, стандартизація, оптимізація компілятора.

🔍 Вибирайте правильний алгоритм

  • Пошук: std::find, std::find_if, std::binary_search (для відсортованих).
  • Сортування: std::sort (універсальний), std::partial_sort (топ-K), std::stable_sort (стабільний).
  • Трансформація: std::transform (нові дані), std::for_each (побічні ефекти).
  • Редукція: std::accumulate (сума, добуток, об'єднання).
  • Видалення: Ідіома Erase-Remove (remove + erase).

🎯 Використовуйте лямбди для предикатів

Замість окремих функцій:

bool isEven(int n) { return n % 2 == 0; }
std::count_if(v.begin(), v.end(), isEven);

Використайте inline лямбду:

std::count_if(v.begin(), v.end(), [](int n) { return n % 2 == 0; });

Переваги: Локальність коду, захоплення зовнішніх змінних, менше глобальних функцій.

⚠️ Пам'ятайте про категорії ітераторів

  • std::sort вимагає Random Access Iterator → працює з vector, deque, але не з list.
  • std::list::sort() — спеціалізований метод для list.
  • std::advance(it, n) — універсальна функція для переміщення ітератора (O(1) для Random Access, O(n) для інших).

Зв'язок із попередніми темами

Завершення огляду STL (статті 93–95):

  • Стаття 93: Послідовні контейнери (vector, deque, list, array) — зберігання даних.
  • Стаття 94: Асоціативні контейнери (map, set, unordered_*) та адаптери (stack, queue, priority_queue) — спеціалізовані структури.
  • Стаття 95 (ця лекція): Ітератори та алгоритми — універсальні інструменти обробки даних для всіх контейнерів.
Тепер ви володієте повним арсеналом STL! Ви можете обирати правильний контейнер, обходити його через ітератори та обробляти дані алгоритмами без написання циклів. Це сучасний C++ — виразний, безпечний, ефективний.

Практичні завдання


Наступний етап (статті 96–99): Move-семантика та розумні покажчики — останній рубіж сучасного C++. Ви навчитесь оптимізувати код через rvalue references, опануєте std::unique_ptr, std::shared_ptr, std::weak_ptr, та зрозумієте Правило п'яти vs Правило нуля.

Вітаємо! Ви завершили фундаментальний блок STL! Тепер ви можете писати виразний, безпечний та ефективний C++ код, використовуючи контейнери, ітератори та алгоритми професійного рівня. Залишилась остання частина курсу ООП — move-семантика — і ви станете експертом у сучасному C++!
Copyright © 2026