Эта статья — третья и последняя в мини-серии об алгоритмах диапазонов. Мы рассмотрим некоторые алгоритмы сортировки, поиска и другие, а также познакомимся с готовящимися крутыми улучшениями этих алгоритмов в версии 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_heap
ranges::is_heap_until
ranges::make_heap
ranges::push_heap
ranges::pop_heap
ranges::sort_heap
Перестановки:
ranges::is_permutation
ranges::next_permutation
ranges::prev_permutation
Алгоритмы неинициализированной памяти:
ranges::uninitialized_copy
ranges::uninitialized_copy_n
ranges::uninitialized_fill
ranges::uninitialized_fill_n
ranges::uninitialized_move
ranges::uninitialized_move_n
ranges::uninitialized_default_construct
ranges::uninitialized_default_construct_n
ranges::uninitialized_value_construct
ranges::uninitialized_value_construct_n
ranges::destroy
ranges::destroy_n
ranges::destroy_at
ranges::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::iota
ranges::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
От основ — в глубину
А также