Эта статья — третья и последняя в мини-серии об алгоритмах диапазонов. Мы рассмотрим некоторые алгоритмы сортировки, поиска и другие, а также познакомимся с готовящимися крутыми улучшениями этих алгоритмов в версии C++23. Поехали! Подробности — к старту курса по разработке на С++.
Прежде чем мы начнём
Ключевые наблюдения для алгоритмов std::ranges:
- Алгоритмы диапазонов определяются в заголовке
<algorithm>, а инфраструктура диапазонов и основные типы определяются в заголовке<ranges>. - Существует как минимум две перегрузки алгоритмов диапазона: с парой итераторов и перегрузка с одним аргументом диапазона.
- Версия, которая возвращает поддиапазон или итератор и принимает диапазон, возвращает заимствованный диапазон или заимствованный итератор. Это помогает обнаруживать итераторы для временных диапазонов.
- Версии диапазона имеют проекции, что обеспечивает большую гибкость; например, вы можете выполнить сортировку по некоторым выбранным элементам или выполнить дополнительные преобразования перед сравнением.
- В версии с диапазонами нет опции параллельного выполнения (вы не можете передать политику
std::execution). - Алгоритмы диапазонов, как и стандартные алгоритмы C++20, также являются
constexpr. - Начиная с версии C++20, не существует алгоритмов числовых диапазонов, соответствующих заголовку
<numeric>.
Ниже вы можете найти примеры, демонстрирующие стандартный алгоритм и альтернативную версию с диапазонами. В них мы иллюстрируем некоторые основные концепции, стараясь не использовать расширенную композицию диапазонов или представления. Мы пойдём в порядке, указанном в cppreference/algorithms.
В этой части рассмотрим алгоритмы сортировки, разбиения на разделы, бинарный поиск и некоторые другие функции.
Это третья статья об алгоритмах диапазонов. Смотрите также:
- первая статья: «Алгоритмы диапазонов C++20 — 7 немодифицирующих операций»;
- вторая статья: «Алгоритмы диапазонов C++20 — 11 модифицирующих операций»;
Разбиение на разделы и сортировка
sort и is_sorted
Алгоритм сортировки часто используют для объявления диапазонов. Если у вас есть контейнер, то благодаря диапазонам вы можете написать:
std::ranges::sort(myContainer);
Вот пример для лучшего понимания:
#include <iostream> #include <algorithm> #include <ranges> #include <vector> struct Product { std::string name; double value { 0.0 }; }; void print(std::string_view intro, const std::vector<Product>& container) { std::cout << intro << '\n'; for (const auto &elem : container) std::cout << elem.name << ", " << elem.value << '\n'; } int main() { const std::vector<Product> prods { { "box", 10.0 }, {"tv", 100.0}, {"ball", 30.0}, { "car", 1000.0 }, {"toy", 40.0}, {"cake", 15.0}, { "book", 45.0}, {"pc game", 35.0}, {"wine", 25} }; print("input", prods); // the standard version: std::vector<Product> copy = prods; std::sort(begin(copy), end(copy), [](const Product& a, const Product& b) { return a.name < b.name; } ); print("after sorting by name", copy); // the ranges version: copy = prods; std::ranges::sort(copy, {}, &Product::name); print("after sorting by name", copy); std::ranges::sort(copy, {}, &Product::value); print("after sorting by value", copy); auto sorted = std::ranges::is_sorted(copy, {}, &Product::value); std::cout << "is sorted by value: " << sorted << '\n'; }
Запустить @Compiler Explorer
Во многих реализациях используется интросортировка (см. Википедию). Это гибридное решение, обычно с быстрой сортировкой/сортировкой кучей, а затем сортировкой вставками для небольших (под)диапазонов.
Другие версии алгоритмов сортировки:
partial_sort— сортирует первыеNэлементов диапазона.stable_sort— порядок эквивалентных элементов гарантированно сохраняется.
Как видите, в версии с диапазонами легко передать проекцию и отсортировать по заданной части элемента. В обычной версии нужно отдельное лямбда-выражение…
Подробнее: ranges::sort @Cppreference.
partition
Разбиение — неотъемлемая часть быстрой сортировки. Для данного предиката операция перемещает элементы, соответствующие предикату, в первую часть контейнера, а не соответствующие — во вторую. Иногда можно разделить контейнер, а не выполнять полную операцию сортировки:
#include <iostream> #include <algorithm> #include <ranges> #include <vector> void print(std::string_view intro, const std::vector<auto>& container) { std::cout << intro << '\n'; for (const auto &elem : container) std::cout << elem << ", "; std::cout << '\n'; } int main() { const std::vector vec { 11, 2, 3, 9, 5, 4, 3, 8, 4, 1, 11, 12, 10, 4}; print("input", vec); // the standard version: auto copy = vec; auto it = std::partition(begin(copy), end(copy), [](int a) { return a < 7; } ); print("partition till 7", copy); std::cout << "pivot at " << std::distance(begin(copy), it) << '\n'; // ranges version: copy = vec; auto sub = std::ranges::partition(copy, [](int a) { return a < 7; } ); print("partition till 7", copy); std::cout << "pivot at " << std::distance(begin(copy), sub.begin()) << '\n'; }
Запустить @Compiler Explorer
Вывод:
input 11, 2, 3, 9, 5, 4, 3, 8, 4, 1, 11, 12, 10, 4, partition till 7 4, 2, 3, 1, 5, 4, 3, 4, 8, 9, 11, 12, 10, 11, pivot at 8 partition till 7 4, 2, 3, 1, 5, 4, 3, 4, 8, 9, 11, 12, 10, 11, pivot at 8
Как видите, мы легко можем разделить контейнер на две группы: первая часть содержит элементы меньше 7, а вторая часть — элементы >= 7. Относительный порядок между элементами может быть изменен (для сохранения этого порядка нужно использовать stable_partition).
Интерфейс для partition относительно прост. Версия алгоритма дополнительно принимает проекцию, но в примере она не использовалась. Отличие состоит в том, что ranges::partition возвращает поддиапазон, а не итератор (как в версии std::).
Подробнее об алгоритмах: ranges::is_partitioned и ranges::partition @C++Reference.
Операции бинарного поиска
Если контейнер уже отсортирован, можно выполнять операции логарифмического бинарного поиска.
binary_search
#include <iostream> #include <algorithm> #include <ranges> #include <vector> #include <numeric> void print(std::string_view intro, const auto& container) { std::cout << intro << '\n'; for (const auto &elem : container) std::cout << elem << ", "; std::cout << '\n'; } int main() { std::vector<int> vec(100, 0); std::iota(begin(vec), end(vec), 0); print("first ten elements of input", vec | std::views::take(10)); // the standard version: auto copy = vec; auto found = std::binary_search(begin(copy), end(copy), 13); std::cout << "found 13: " << found << '\n'; // ranges version: copy = vec; found = std::ranges::binary_search(copy, 13); std::cout << "found 13: " << found << '\n'; }
Запустить @Compiler Explorer
Читать подробнее:ranges::binary_search @C++Reference.
Кроме того, можно использовать связанные алгоритмы:
- std::ranges::lower_bound — cppreference.com возвращает итератор к первому элементу не меньше заданного значения;
- std::ranges::upper_bound — cppreference.com — возвращает итератор к первому элементу больше заданного значения;
Операции с множествами
В библиотеке есть много функций, связанных с множествами, вот некоторые из них:
ranges::merge— объединяет два отсортированных диапазонаranges::inplace_merge— объединяет два упорядоченных диапазона in-placeranges::includes— возвращает true, если одна отсортированная последовательность является подпоследовательностью другой отсортированной последовательностиranges::set_difference— вычисляет разницу между двумя множествамиranges::set_intersection— вычисляет пересечение двух множествranges::set_symmetric_difference— вычисляет симметричную разность двух множествranges::set_union— вычисляет объединение двух множеств
Рассмотрим один пример с includes:
includes
Возвращает true, если отсортированный диапазон является подпоследовательностью другого отсортированного диапазона.
#include <iostream> #include <algorithm> #include <ranges> #include <vector> #include <string> struct Product { std::string name; double value { 0.0 }; }; void print(std::string_view intro, const std::vector<Product>& container) { std::cout << intro << '\n'; for (const auto &elem : container) std::cout << elem.name << ", " << elem.value << '\n'; } int main() { std::vector<Product> prods { { "box", 10.0 }, {"tv", 100.0}, {"ball", 30.0}, { "car", 1000.0 }, {"toy", 40.0}, {"cake", 15.0}, { "book", 45.0}, {"pc game", 35.0}, {"wine", 25} }; std::vector<Product> vecToCheck { {"ball", 30.0}, { "box", 10.0 }, {"wine", 25} }; std::ranges::sort(prods, {}, &Product::name); std::vector<std::string> namesToCheck {"ball", "box", "wine"}; print("input", prods); // the standard version: auto ret = std::includes(begin(prods), end(prods), begin(vecToCheck), end(vecToCheck), [](const Product& a, const Product& b) { return a.name < b.name; } ); std::cout << "contains the name set: " << ret << '\n'; // the ranges version: ret = std::ranges::includes(prods, namesToCheck, {}, &Product::name); std::cout << "contains the name set: " << ret << '\n'; }
Запустить @Compiler Explorer
Версия с диапазонами проще и предлагает способ проверки различных контейнеров. При подходе std:: итератор необходимо разыменовать, а затем неявно преобразовать в оба типа элементов входного контейнера.
Дополнительную информацию см.: std::includes @cppreference.com.
Прочее
max_element
Поиск наибольшего элемента в контейнере (без сортировки):
#include <iostream> #include <random> #include <iterator> #include <algorithm> #include <ranges> struct Product { std::string name_; double value_ { 0.0 }; }; int main() { const std::vector<Product> prods { { "box", 10.0 }, {"tv", 100.0}, {"ball", 30.0}, { "car", 1000.0 }, {"toy", 40.0}, {"cake", 15.0}, { "book", 45.0}, {"PC game", 35.0}, {"wine", 25} }; // the standard version: auto res = std::max_element(begin(prods), end(prods), [](const Product& a, const Product& b) { return a.value_ < b.value_; }); if (res != end(prods)) { const auto pos = std::distance(begin(prods), res); std::cout << "std::max_element at pos " << pos << ", val " << res->value_ << '\n'; } // the ranges version: auto it = std::ranges::max_element(prods, {}, &Product::value_); if (it != end(prods)) { const auto pos = std::distance(begin(prods), it); std::cout << "std::max_element at pos " << pos << ", val " << res->value_ << '\n'; } }
Запустить @Compiler Explorer
equal
#include <iostream> #include <random> #include <iterator> #include <algorithm> #include <ranges> struct Product { std::string name; double value { 0.0 }; }; int main() { const std::vector<Product> prods { { "box", 10.0 }, {"tv", 100.0}, {"ball", 30.0}, { "car", 1000.0 }, {"toy", 40.0}, {"cake", 15.0}, }; const std::vector<Product> moreProds { { "box", 11.0 }, {"tv", 120.0}, {"ball", 30.0}, { "car", 10.0 }, {"toy", 39.0}, {"cake", 15.0} }; // the standard version: auto res = std::equal(begin(prods), end(prods), begin(moreProds), end(moreProds), [](const Product& a, const Product& b) { return a.name == b.name; }); std::cout << "equal: " << res << '\n'; // the ranges version: res = std::ranges::equal(prods, moreProds, {}, &Product::name, &Product::name); std::cout << "equal: " << res << '\n'; }
Запустить @Compiler Explorer
Подробнее см.: ranges::equal @C++Reference.
Еще больше
Мой список алгоритмов не полон. Почти у всех стандартных алгоритмов есть альтернатива std::ranges::. Взгляните на следующие интересные, не упомянутые мной алгоритмы:
Операции с кучей:
ranges::is_heapranges::is_heap_untilranges::make_heapranges::push_heapranges::pop_heapranges::sort_heap
Перестановки:
ranges::is_permutationranges::next_permutationranges::prev_permutation
Алгоритмы неинициализированной памяти:
ranges::uninitialized_copyranges::uninitialized_copy_nranges::uninitialized_fillranges::uninitialized_fill_nranges::uninitialized_moveranges::uninitialized_move_nranges::uninitialized_default_constructranges::uninitialized_default_construct_nranges::uninitialized_value_constructranges::uninitialized_value_construct_nranges::destroyranges::destroy_nranges::destroy_atranges::construct_at
Numeric
С версии C++20 у нас есть большинство соответствующих алгоритмов диапазонов из заголовка <algorithm>, но отсутствует заголовок <numeric>.
Скоро в C++23
Спецификация C++23 почти закончена и находится в режиме feature-freeze. На момент написания статьи мне известны следующие алгоритмы, которые появятся в новой версии C++:
ranges::starts_withиranges::ends_with(по состоянию на июнь 2022 г. доступно в компиляторе MSVC)ranges::contains(P2302)ranges::shift_leftиranges::shift_right,ranges::iotaranges::foldв качестве альтернативыstd::accumulate
Заключение
Эта статья завершает наше путешествие по алгоритмам C++, доступным в стандартной библиотеке (кроме числовых). У большинства алгоритмов есть аналоги ranges::, а в C++23 появится ещё больше дополнений.
А мы научим вас аккуратно работать с данными, чтобы вы прокачали карьеру и стали востребованным IT-специалистом.
Data Science и Machine Learning
- Профессия Data Scientist
- Профессия Data Analyst
- Курс «Математика для Data Science»
- Курс «Математика и Machine Learning для Data Science»
- Курс по Data Engineering
- Курс «Machine Learning и Deep Learning»
- Курс по Machine Learning
Python, веб-разработка
- Профессия Fullstack-разработчик на Python
- Курс «Python для веб-разработки»
- Профессия Frontend-разработчик
- Профессия Веб-разработчик
Мобильная разработка
Java и C#
- Профессия Java-разработчик
- Профессия QA-инженер на JAVA
- Профессия C#-разработчик
- Профессия Разработчик игр на Unity
От основ — в глубину
А также
ссылка на оригинал статьи https://habr.com/ru/company/skillfactory/blog/707946/


Добавить комментарий