Перейти к содержимому

Include algorithm c что это

  • автор:

Алгоритмы стандартной библиотеки C++

Стандартная библиотека содержит большое количество алгоритмов для работы с контейнерами стандартной библиотеки. Доступные инструменты покрывают значительную часть встречающихся алгоритмических задач. Использование стандартных алгоритмов вместо их самостоятельной реализации является хорошим стилем программирования по следующим причинам:

  • Экономия времени. Мы не тратим время на реализацию и отладку алгоритма.
  • Гарантия отсутствия ошибок в логике работы алгоритма. Алгоритмы стандартной библиотеки протестированы многими программистами.
  • Лаконичность и выразительность кода. Вместо некоторого количества строчек, которые выполняют неочевидные манипуляции, мы видим название хорошо документированного алгоритма.

Мы рассмотрим лишь некоторые из доступных алгоритмов. Полный список можно найти в документации. Мы рекомендуем всегда проверять наличие стандартного решения при встрече с алгоритмической задачей.

iota, for_each и transform

Большое количество циклов for в коде, который выполняет манипуляции со структурами данных, обычно говорит о недостаточном использовании стандартных алгоритмов. Так, если необходимо применить некоторую функцию ко всем элементам контейнера, то можно рассмотреть использование алгоритма for_each .

Решим следующую задачу: вывести в стандартный поток квадраты натуральных чисел от 1 до 100. Следующий код показывает, что эта задача может быть решена в четырех строчках кода и без явного использования циклов:

#include #include #include // iota #include // for_each using namespace std; int main()  vectorint> v(100); iota(v.begin(), v.end(), 1); // v = [1, 2, 3, . 100] for_each(v.begin(), v.end(), [](int& a)a = a*a;>); // v = [1, 4, 9, . 10000] for_each(v.begin(), v.end(), [](int a)cout  <a  <' ';>); return 0; > 

Мы воспользовались алгоритмом iota из библиотеки , чтобы проинициализировать массив набором последовательных целых чисел. Затем мы два раза использовали алгоритм for_each : для вычисления квадратов и для вывода значений в стандартный поток.

Третьим аргументом алгоритм for_each принимает функцию одного аргумента. Тип аргумента должен соответствовать типу элементов контейнера. Вместо обычной функции бывает удобно передать лямбда-выражение, что мы и сделали оба раза в этом примере. Лямбда-выражение позволяет определить функцию в месте ее использования. Квадратные скобки [] указывают на начало лямбда-выражения; в круглых скобках указываются аргументы выражения; в фигурных скобках содержится тело лямбда-выражения.

Модифицируем немного нашу задачу. Предположим, что мы не хотим изменять исходный вектор, а значения квадратов хотим сохранить в другом векторе. Алгоритм transform позволяет выполнить такое преобразование:

#include #include #include // iota #include // for_each, transform using namespace std; int main()  vectorint> source(100); iota(source.begin(), source.end(), 1); // v = [1, 2, 3, . 100] vectorint> target(source.size()); transform(source.begin(), source.end(), target.begin(), [](int& a)return a*a;>); // v = [1, 4, 9, . 10000] for_each(target.begin(), target.end(), [](int a)cout  <a  <' ';>); return 0; > 

Третьим аргументом алгоритм transform принимает итератор на место целевого контейнера, с которого нужно начать заполнять значения. Обратите внимание, что мы заранее инициализировали вектор target нужной длины.

all_of, any_of, none_of

Довольно часто возникает задача проверки какого-либо условия для всех объектов контейнера. Здесь на помощь приходят алгоритмы all_of , any_of и none_of с очевидным поведением, которые принимают диапазон значений и унарный предикат — функцию одного аргумента, которая возвращает true или false . Так, например, можно проверить содержит ли множество хотя бы один отрицательный элемент:

setdouble> s1.1, -0.9, 2.4, 10.1, 3.1415>; bool neg_in_set = any_of(s.begin(), s.end(), [](double x)return x  0;>); // true 

Вторая строчка этого примера не поменяется, если вместо контейнера set будет использован другой контейнер, например, list , vector , array или unordered_set .

count, count_if, find, find_if

Алгоритм count позволяет посчитать количество элементов в контейнере, равных заданному. Модификация этого алгоритма count_if подсчитывает количество элементов, удовлетворяющих определенному условию. Рассмотрим следующий пример: мы имеем дело с историей авторизации пользователей на сайте, которая хранится в виде вектора строк. Каждая строка — это логин пользователя. Подсчитаем сколько раз авторизовывался пользователь с логином david:

vectorstring> history = /*. */>; size_t david_count = count(history.begin(), history.end(), "david"); 

Если нам захочется удалить запись для логина david, мы можем это сделать с помощью алгоритма find и метода vector::erase :

if (auto item = find(history.begin(), history.end(), "david"); item != history.end())  history.erase(item); > 

Алгоритм find возвращает итератор на найденный элемент. Версия алгоритма find_if позволяет найти первый элемент, удовлетворяющий некоторому условию.

Как и другие алгоритмы, find и count могут работать с контейнерами разных типов. Они проходят переданный диапазон значений последовательно, начиная с первого элемента. Использование такого подхода для контейнеров set и map — плохая идея, ведь они созданы для того чтобы выполнять поиск объектов быстрее. Это общее правило: если контейнер имеет метод, аналогичный общему алгоритму, то следуем использовать метод контейнера. В большинстве случаев это даст выигрыш в производительности.

sort, stable_sort, nth_element

Алгоритмы сортировки — это важный и интересный раздел теории алгоритмов. Работать с отсортированными элементами во многих ситуациях удобнее, в частности, сложность поиска элементов становится логарифмической вместо линейной. Стандартная библиотека C++ предлагает алгоритмы sort и stable_sort , которые выполняют сортировку за время, пропорциональное N log(N), где N — количество элементов массива. Стабильная сортировка stable_sort при этом гарантирует, что равные объекты не меняют своего относительного положения в контейнере.

Рассмотрим простой пример сортировки:

vectorstring> v "David", "Ivan", "Adam", "Dmitry">; sort(v.begin(), v.end()); // ["Adam", "David", "Dmitry", "Ivan"] 
bool string_cmp(const string& lhs, const string& rhs)  return lhs.size() > rhs.size(); > vectorstring> v "David", "Ivan", "Adam", "Dmitry">; stable_sort(v.begin(), v.end(), string_cmp); // ["Dmitry", "David", "Ivan", "Adam"] 

Мы использовали стабильную версию сортировки. В этом случае Ivan гарантировано окажется левее Adam в отсортированном векторе.

Оказывается, что задача поиска n-го элемента (как если бы элементы стояли по порядку по какому-либо признаку) может быть решена быстрее, чем сортировка всего массива — за линейное время. Стандартная библиотека предлагает алгоритм nth_element для решения этой задачи.

lower_bound, upper_bound, binary_search

Коль скоро мы научились получать отсортированные массивы, рассмотрим алгоритмы для поиска элементов в них. Алгоритмы lower_bound и upper_bound позволяют найти в отсортированном массиве первый элемент не меньше данного и первый элемент больше данного, соответственно. Эти алгоритмы возвращают итератор, соответствующий найденному элементу.

Алгоритм binary_search проверяет, есть ли в отсортированном массиве данный элемент и возвращает true или false в зависимости от результата поиска.

Все три алгоритма выполняются за логарифмическое время.

Резюме

В этом материале мы рассмотрели примеры использования нескольких основных алгоритмов стандартной библиотеки C++. Обсудили, что применение стандартных алгоритмов является хорошим стилем программирования, позволяет писать код быстрее, и делает его более легким для прочтения.

Полезные алгоритмы стандартной библиотеки не ограничиваются рассмотренными выше. Мы рекомендуем посмотреть на полный список доступных алгоритмов, среди которых можно обратить внимание на алгоритмы copy , remove , generate и partition , которые вполне могут пригодиться.

Конечно, мы не ожидаем, что после прочтения этого материала вы сразу начнете свободно применять разнообразные алгоритмы. Только с практикой использование алгоритмов становится естественным и полезным инструментом разработки.

Документация

  • https://en.cppreference.com/w/cpp/algorithm
  • http://www.cplusplus.com/reference/algorithm/
  • https://en.cppreference.com/w/cpp/language/lambda

Алгоритмы диапазонов C++20 — 11 модифицирующих операций

В предыдущей статье серии «Диапазоны» я рассмотрел основы и некоторые немодифицирующие операции. Сегодня пришло время таких алгоритмов, как transform , copy , generate , shuffle и многих других… даже rotate . Подробности — к старту курса по разработке на С++.

Прежде чем мы начнём

Ключевые наблюдения об алгоритмах std::ranges :

  • Алгоритмы диапазонов определяются в заголовке , а инфраструктура диапазонов и их основные типы — в заголовке .
  • Обычно есть как минимум две перегрузки для алгоритмов диапазона: с парой итераторов и перегрузка с одним аргументом диапазона.
  • Версия, которая возвращает поддиапазон или итератор и принимает диапазон, возвращает заимствованный диапазон или заимствованный итератор. Это помогает обнаруживать итераторы для временных диапазонов.
  • Версии диапазона имеют проекции, что даёт больше гибкости; например, можно выполнить сортировку по некоторым выбранным элементам или перед сравнением выполнить дополнительные преобразования.
  • В версии с диапазонами нет опции параллельного выполнения (нельзя передать политику выполнения std::execution ).
  • Алгоритмы диапазонов, как и стандартные алгоритмы C++20, также являются constexpr .
  • Начиная с версии C++20, больше нет алгоритмов числовых диапазонов, соответствующих заголовку .

Примеры ниже показывают стандартный алгоритм и альтернативную версию с диапазонами. Они иллюстрируют некоторые основные концепции. Я стремился не использовать расширенную композицию диапазонов или представления. Мы рассмотрим их в порядке, указанном в cppreference/algorithms.

В этой части рассмотрим некоторые алгоритмы, позволяющие изменять последовательность, такие как копирование, удаление, преобразование или создание элементов.

  • Первая статья о «7 немодифицирующих операциях»

copy_if

У основного алгоритма copy много вариантов: copy_if , copy_n и даже copy_backward .

В простой форме copy_if определяется так:

// skipping all concept/templates declaration constexpr copy_if_result, O> copy_if( R&& r, O result, Pred pred, Proj proj = <> );

Посмотрим на пример:

#include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; >; std::ostream& operator <<(std::ostream& os, const Product& p) < os << p.name_ << ", " << p.value_; return os; >int main() < const std::vectorprods < < "box", 10.0 >, , , < "car", 1000.0 >, , >; // standard version: std::copy_if(begin(prods), end(prods), std::ostream_iterator(std::cout, "; "), [](const Product& p)< return !p.name_.starts_with("none"); >); std::cout << '\n'; // ranges version: std::ranges::copy_if(prods, std::ostream_iterator(std::cout, "; "), [](const Product& p)< return !p.name_.starts_with("none"); >); >

В примере я копирую элементы из вектора в выходной поток. Кроме того, на этапе фильтрации я выбираю только товары не «none». Поскольку в поток копируются целые элементы, мне пришлось реализовать operator< для класса Product .

Благодаря проекциям я также мог написать такой вариант кода:

std::ranges::copy_if(prods, std::ostream_iterator(std::cout, "; "), [](const std::string& name)< return !name.starts_with("none"); >, &Product::name_);

Этот код немного длиннее, но теперь предикат принимает string , а не весь объект Product .

fill

#include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; Product& operator=(int i) < name_ += std::to_string(i); return *this; >>; std::ostream& operator <<(std::ostream& os, const Product& p) < os << p.name_ << ", " << p.value_; return os; >int main() < std::vectorprods>; // standard version: std::fill(begin(prods), end(prods), 4); std::ranges::copy(prods, std::ostream_iterator(std::cout, "; ")); std::cout << '\n'; // ranges version: std::ranges::fill(prods, 2); std::ranges::copy(prods, std::ostream_iterator(std::cout, "; ")); >

Алгоритм fill проходит по диапазону, а затем выполняет присвоение с переданным вами value . Тип value может отличаться от типа элементов в контейнере.

while (first != last) *first++ = value;

В примере я использовал класс с пользовательским оператором преобразования, им можно воспользоваться для изменения члена данных name_ на основе интегрального входного значения.

generate

В то время как fill() использует одно и то же значение для присвоения всем элементам, generate() использует функциональный объект для генерации значения. В этом примере можно смоделировать генерацию iota :

#include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; Product& operator=(int i) < name_ += std::to_string(i); return *this; >>; std::ostream& operator <<(std::ostream& os, const Product& p) < os << p.name_ << ", " << p.value_; return os; >int main() < std::vectorprods>; // standard version: std::generate(begin(prods), end(prods), [v = 0]() mutable < return v++; >); std::ranges::copy(prods, std::ostream_iterator(std::cout, "; ")); std::cout ); std::ranges::copy(prods, std::ostream_iterator(std::cout, "; ")); >
Box 0, 1; Box 1, 1; Box 2, 1; Box 3, 1; Box 4, 1; Box 5, 1; Box 6, 1; Box 01, 1; Box 12, 1; Box 23, 1; Box 34, 1; Box 45, 1; Box 56, 1; Box 67, 1;

transform

transform() — надёжный алгоритм, имеющий множество вариаций.

В простой форме он выглядит так:

transform( R&& r, O result, F op, Proj proj = <> );

Он принимает диапазон r , а затем использует op для преобразования элементов из этого диапазона и вывода в result , который является итератором.

Вот простой пример:

#include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; >; int main() < std::vectorprods>; // standard version: std::transform(begin(prods), end(prods), begin(prods), [v = 0](const Product &p) mutable < return Product < p.name_ + std::to_string(v++), 1.0>; >); for (auto &p : prods) std::cout ; >, &Product::name_); for (auto &p : prods) std::cout
Box 0, Box 1, Box 2, Box 3, Box 4, Box 5, Box 6, Box 00, Box 11, Box 22, Box 33, Box 44, Box 55, Box 66,

Выше преобразуется тот же контейнер, но к каждому имени добавляются числа, сгенерированные с помощью функции.

Также есть версия, которая берет два диапазона и объединяет их с помощью бинарной операции:

transform( R1&& r1, R2&& r2, O result, F binary_op, Proj1 proj1 = <>, Proj2 proj2 = <> );

Этой версией можно воспользоваться для «объединения» двух контейнеров и получения одного значения:

std::vector prods>; std::vector numbers; std::ranges::transform(prods, numbers, begin(prods), [](const Product& p, int v) < return Product < p.name_ + std::to_string(v), 1.0>; >); for (auto &p : prods) std::cout 

remove

В C++20 есть способ удаления и стирания элементов из различных контейнеров эффективнее. Смотрите std::erase_if , набор перегруженных функций для согласованного удаления контейнера. Подробности — в моей статье: 20 небольших, но удобных функций C++20 — согласованное удаление контейнеров.

Для полноты картины сравним все три версии:

#include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; >; int main() < const std::vectorprods < < "box", 10.0 >, , , , < "car", 1000.0 >, , >; auto printCont = [](const std::vector& cont) < for (auto &p : cont) std::cout ; std::cout ; // standard version: auto tempProds = prods; tempProds.erase(std::remove_if(tempProds.begin(), tempProds.end(), checkNoPrefix), tempProds.end()); printCont(tempProds); // ranges version: tempProds = prods; tempProds.erase(std::ranges::remove_if(tempProds, checkNoPrefix).begin(), tempProds.end()); printCont(tempProds); // C++20 version: tempProds = prods; std::erase_if(tempProds, checkNoPrefix); printCont(tempProds); >

Версия ranges сокращает этот вызов:

tempProds.erase(std::remove_if(tempProds.begin(), tempProds.end(), checkNoPrefix), tempProds.end());
tempProds.erase(std::ranges::remove_if(tempProds, checkNoPrefix).begin(), tempProds.end());

Но, на мой взгляд, это выглядит не намного лучше. ranges::remove_if возвращает поддиапазон, поэтому всё равно нужно прописать его begin() и, возможно, end() .

Гораздо проще написать так:

std::erase_if(tempProds, checkNoPrefix);

replace

Как заменить элементы внутри контейнера:

#include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; friend bool operator==(const Product& a, const Product& b) < return a.name_ == b.name_ && abs(a.value_ - b.value_) < 0.0001; >>; std::ostream& operator <<(std::ostream& os, const Product& p) < os << p.name_ << ", " << p.value_; return os; >int main() < std::vectorprods < < "box", 10.0 >, , , < "car", 1000.0 >, , , , < "invalid", -10.0 >>; std::ostream_iterator out_iter(std::cout, "; "); // standard version: std::cout << "before: \n"; std::copy(begin(prods), end(prods), out_iter); std::replace(begin(prods), end(prods), Product, Product); std::cout << "\nafter: \n"; std::copy(begin(prods), end(prods), out_iter); std::cout << '\n'; // ranges version: std::cout << "before: \n"; std::ranges::copy(prods, out_iter); std::ranges::replace(prods, "invalid", Product, &Product::name_); std::cout
before: box, 10; tv, 100; rocket, 10000; car, 1000; toy, 40; none, 0; invalid, 0; invalid, -10; after: box, 10; tv, 100; rocket, 10000; car, 1000; toy, 40; default, 10; invalid, 0; invalid, -10; before: box, 10; tv, 100; rocket, 10000; car, 1000; toy, 40; default, 10; invalid, 0; invalid, -10; after: box, 10; tv, 100; rocket, 10000; car, 1000; toy, 40; default, 10; default, 10; default, 10;

Интересно, что в стандартной версии значение сравнивается с объектами в контейнере:

for (; first != last; ++first) < if (*first == old_value) < *first = new_value; >>

Вот почему мне пришлось определить оператор сравнения == (или "космический корабль" для большей гибкости).

Сравнение немного отличается, поэтому в версии с диапазонами можно использовать проекцию:

for (; first != last; ++first) < if (old_value == std::invoke(proj, *first)) < *first = new_value; >>

В этом примере необходимости в операторе == нет, ведь строки можно сравнивать напрямую. Это даёт больше гибкости, поскольку так можно найти больше недопустимых значений (значение value_ теперь не проверяется, чтобы найти и исправить — 0.0 или -10.0 ).

reverse

Попробуем вариант с reverse-копией с выводом в поток:

#include #include #include #include int main() < const std::vector numbers < "one", "two", "three", "four", "five", "six" >; auto outStream = std::ostream_iterator(std::cout, "; "); // standard version: std::copy(begin(numbers), end(numbers), outStream); std::cout 
one; two; three; four; five; six; six; five; four; three; two; one; Ranges one; two; three; four; five; six; six; five; four; three; two; one;

Как видите, работать с этой версией очень просто.

rotate

На этот раз давайте поработаем со словами и попробуем развернуть их:

#include #include #include #include #include int main() < std::vectorwords < "hello", "in", "the", "wonderful", "world", "of", "c++", "programming", >; std::ostream_iterator out(std::cout, " "); // standard version: std::ranges::copy(words, out); std::cout >

Пример начинается с предложения и поворачивает его так, что слово "the" становится первым словом. Позже в версии с диапазонами попытаемся найти первое слово исходного предложения, а затем снова сдвинуть его, чтобы добраться до начала.

hello in the wonderful world of c++ programming in the wonderful world of c++ programming hello "hello" is now at pos 7 hello in the wonderful world of c++ programming "in" is now at pos 1

shuffle

Напомню, что std::random_shuffle объявлен устаревшим и удалён в C++17. Начиная с C++11, лучше всего использовать std::shuffle или std::ranges::shuffle , которые принимают в качестве параметра объект случайного генератора, а не полагаются на rand() .

Давайте посмотрим на простой пример:

#include #include #include #include #include int main() < std::vectorwords < "box", "tv", "car", "bricks", "game", "ball" >; std::mt19937 rng()>; auto print = [](std::string_view str, const auto& cont) < std::cout ; print("before", words); // the standard version: std::shuffle(begin(words), end(words), rng); print("after ", words); // the ranges version: // the standard version: std::ranges::shuffle(words, rng); print("after ", words); >

sample

std::sample — относительно новый алгоритм, доступный с C++17. Он позволяет с равномерной вероятностью случайно выбрать n элементов из последовательности. Это не аналог constexpr . Давайте посмотрим на пример:

#include #include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; >; int main() < const std::vectorprods < < "box", 10.0 >, , , < "car", 1000.0 >, , , < "book", 45.0>, , >; std::mt19937 rng()>; const size_t firstRoundCount = 4; const size_t secondRoundCount = 2; // the standard version: std::vector selected; std::sample(begin(prods), end(prods), std::back_inserter(selected), firstRoundCount, rng); std::cout << firstRoundCount << " selected products: \n"; for (const auto &elem : selected) std::cout << elem.name_ << '\n'; // the ranges version: std::vectoronlyTwo; std::ranges::sample(selected, std::back_inserter(onlyTwo), secondRoundCount, rng); std::cout

unique

Алгоритм unique() позволяет очистить последовательную группу эквивалентных элементов. Например, из вы можете удалить все дубликаты и получить . Обратите внимание, что не все 5 были удалены, а только те, что были в той же «группе».

Давайте посмотрим на следующий пример, где я удаляю дубликаты слов:

#include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; >; int main() < std::vectorprods < < "box", 20.0>, , , < "box", 10.0 >, , , < "car", 1000.0 >, , , , >; auto print = [](std::string_view str, const std::vector& cont) < std::cout ; print("before: ", prods); auto ret = std::ranges::unique(prods, <>, &Product::name_); prods.erase(ret.begin(), ret.end()); print("after unique: ", prods); std::ranges::sort(prods, <>, &Product::name_); print("after sort: ", prods); ret = std::ranges::unique(prods, <>, &Product::name_); prods.erase(ret.begin(), ret.end()); print("another unique:", prods); >
before: : box, box, toy, box, tv, tv, car, box, toy, cake, after unique: : box, toy, box, tv, car, box, toy, cake, after sort: : box, box, box, cake, car, toy, toy, tv, another unique:: box, cake, car, toy, tv,

Как видите, этот пример не охватывает стандартную версию, а фокусируется только на ranges::unique .

После первого запуска unique() вектор prods модифицируется таким образом, что удаляемые элементы передаются в конец контейнера. Более того, они имеют неопределённое значение. Вот почему я использовал erase , чтобы удалить эти элементы из контейнера. Объект ret содержит поддиапазон, указывающий на первый «удалённый» элемент и конец входного диапазона.

После первой «итерации» все еще есть некоторые повторяющиеся элементы, но они не принадлежат одной и той же «группе». Чтобы исправить это, можно отсортировать элементы (я использую проекцию, чтобы смотреть только на элемент данных name_ ). После сортировки элементов можно подчистить остальные дубликаты. Конечно, сортировку можно сделать перед всей очисткой.

Заключение

Как видите, с помощью версий с диапазонами можно упростить код и передать всю последовательность, весь контейнер. Во многих случаях это делает код куда более читаемым.

Научим вас аккуратно работать с данными, чтобы вы прокачали карьеру и стали востребованным IT-специалистом.

  • Профессия Fullstack-разработчик на Python (16 месяцев)
  • Профессия Data Scientist (24 месяца)

Краткий каталог курсов

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-разработчик
  • Профессия Веб-разработчик

Мобильная разработка

  • Профессия iOS-разработчик
  • Профессия Android-разработчик

Java и C#

  • Профессия Java-разработчик
  • Профессия QA-инженер на JAVA
  • Профессия C#-разработчик
  • Профессия Разработчик игр на Unity

От основ — в глубину

  • Курс «Алгоритмы и структуры данных»
  • Профессия C++-разработчик
  • Профессия «Белый хакер»

А также

Include algorithm c что это

Определяет функции шаблона контейнера стандартной библиотеки C++, которые выполняют алгоритмы.

Синтаксис

(see links below for specific algorithm syntax) 

Библиотека также использует инструкцию #include .

Замечания

Алгоритмы стандартной библиотеки C++ могут работать с различными структурами данных. Структуры данных, с которыми они могут работать, включают не только классы контейнеров стандартной библиотеки C++, такие как vector и , но и list пользовательские структуры данных и массивы элементов, если они удовлетворяют требованиям определенного алгоритма. Алгоритмы стандартной библиотеки C++ достигают такого уровня универсальности путем получения доступа к элементам контейнера и их просмотра опосредованным образом через итераторы.

Алгоритмы стандартной библиотеки C++ обрабатывают диапазоны итератора, которые обычно определяются их начальными или конечными позициями. Диапазоны, упомянутые ниже, должны быть допустимыми в том смысле, что все итераторы в диапазонах должны быть разыменовываемыми и в пределах последовательностей каждого диапазона, последняя позиция должна быть достигнута с первого, добавив итератор.

Алгоритмы стандартной библиотеки C++ могут работать с различными типами объектов контейнеров одновременно. Два суффикса использовались для передачи информации о назначении алгоритмов:

  • Суффикс _if указывает, что алгоритм используется с объектами-функциями, которые работают над значениями элементов, а не на самих элементах. Например, find_if алгоритм ищет элементы, значения которых удовлетворяют критерию, заданному объектом-функцией, а find алгоритм выполняет поиск определенного значения.
  • Суффикс _copy указывает, что алгоритм обычно изменяет скопированные значения, а не копирует измененные значения. Другими словами, они не изменяют элементы исходного диапазона, но помещают результаты в выходной диапазон или итератор. Например, reverse алгоритм изменяет порядок элементов в диапазоне, а reverse_copy алгоритм копирует обратный результат в диапазон назначения.

Алгоритмы стандартной библиотеки C++ часто классифицируются в группы, чтобы указать их назначение или требования. К ним относятся изменения алгоритмов, которые изменяют значение элементов по сравнению с не изменяющимися алгоритмами, которые не изменяются. Изменяющие алгоритмы меняют порядок элементов, но не значения элементов. Удаляющие алгоритмы могут исключать элементы из диапазона или копии диапазона. Алгоритмы сортировки переупорядочения элементов в диапазоне различными способами и отсортированные алгоритмы диапазона действуют только для диапазонов, элементы которых были отсортированы определенным образом.

Алгоритмы

Имя Описание
adjacent_find Поиск двух соседних элементов, которые либо равны, либо удовлетворяют указанному условию.
all_of Возвращает значение true , если условие выполняется каждым элементом заданного диапазона.
any_of Возвращает значение true , если условие выполняется хотя бы один раз в указанном диапазоне элементов.
binary_search Проверяет, есть ли в отсортированном диапазоне элемент, равный указанному значению или эквивалентный ему в смысле, заданном двоичным предикатом.
clamp
copy Присваивает значения элементов из исходного диапазона диапазону назначения, выполняя итерации в исходной последовательности элементов и присваивая им новые позиции в прямом направлении.
copy_backward Присваивает значения элементов из исходного диапазона диапазону назначения, выполняя итерации в исходной последовательности элементов и присваивая им новые позиции в обратном направлении.
copy_if Копирует все элементы в заданном диапазоне, возвращающие true для заданного условия
copy_n Копирует указанное количество элементов.
count Возвращает количество элементов в диапазоне, значения которых соответствуют заданному значению.
count_if Возвращает количество элементов в диапазоне, значения которых соответствуют заданному условию.
equal Сравнивает два диапазона поэлементно либо на признак равенства или равноценности в смысле, заданном бинарным предикатом.
equal_range Находит пару позиций в упорядоченном диапазоне; первая из них меньше или равна позиции указанного элемента, а вторая — больше позиции элемента, где суть равноценности или порядка, используемая, чтобы установить позиции в последовательности, может быть задана бинарным предикатом.
fill Присваивает одно и то же новое значение каждому элементу в заданном диапазоне.
fill_n Присваивает новое значение указанному количеству элементов в диапазоне, начиная с определенного элемента.
find Находит позицию первого вхождения элемента с заданным значением в диапазон.
find_end Ищет в диапазоне последнюю подпоследовательность, совпадающую с заданной последовательностью, или эквивалентной согласно условию, заданному двоичным предикатом.
find_first_of Выполняет поиск первого вхождения любого из нескольких значений в заданный диапазон или первого вхождения любого из нескольких элементов, равноценных в смысле, заданном бинарным предикатом, в указанный набор элементов.
find_if Находит позицию первого вхождения элемента, удовлетворяющего определенному условию, в диапазон.
find_if_not Возвращает первый элемент в указанном диапазоне, который не удовлетворяет условию.
for_each Применяет заданный объект функции к каждому элементу в прямом порядке в пределах диапазона и возвращает объект функции.
for_each_n
generate Присваивает значения, создаваемые объектом функции, каждому элементу в диапазоне.
generate_n Присваивает значения, создаваемые объектом функции, указанному количеству элементов в диапазоне и возвращается на позицию, следующую за последним присвоенным значением.
includes Проверяет, содержит ли один отсортированный диапазон все элементы, содержащиеся во втором отсортированном диапазоне, где порядок сортировки или критерий эквивалентности элементов можно задать бинарным предикатом.
inplace_merge Объединяет элементы из двух последовательных упорядоченных диапазонов в один упорядоченный диапазон, где критерий порядка сортировки может быть указан бинарным предикатом.
is_heap Возвращает значение true , если элементы в указанном диапазоне образуют кучу.
is_heap_until Возвращает значение true , если указанный диапазон образует кучу до последнего элемента.
is_partitioned Возвращает значение true , если все элементы в заданном диапазоне, возвращающие true для какого-либо условия, расположены перед всеми элементами, возвращающими false .
is_permutation Определяет, образуют ли элементы в заданном диапазоне допустимую перестановку.
is_sorted Возвращает значение true , если элементы в указанном диапазоне расположены в порядке сортировки.
is_sorted_until Возвращает значение true , если элементы в указанном диапазоне расположены в порядке сортировки.
iter_swap Меняет местами два значения, указанные парой определенных итераторов.
lexicographical_compare Сравнивает две последовательности поэлементно для определения того, какой элемент из двух меньше.
lower_bound Находит позицию первого элемента в упорядоченном диапазоне, значение которого больше или равно указанному значению, где критерий упорядочивания может быть задан бинарным предикатом.
make_heap Преобразует элементы из указанного диапазона в кучу, в которой первый элемент является наибольшим и для которой критерий сортировки может быть определен бинарным предикатом.
max Сравнивает два объекта и возвращает больший из них, где критерий упорядочивания может быть указан бинарным предикатом.
max_element Находит первое вхождение наибольшего элемента в указанном диапазоне, где критерий упорядочивания может быть указан бинарным предикатом.
merge Объединяет все элементы из двух исходных упорядоченных диапазонов в один упорядоченный диапазон назначения, где критерий порядка сортировки может быть указан бинарным предикатом.
min Сравнивает два объекта и возвращает меньший из них, где критерий упорядочивания может быть указан бинарным предикатом.
min_element Находит первое вхождение наименьшего элемента в указанном диапазоне, где критерий упорядочивания может быть указан бинарным предикатом.
minmax Сравнивает два входных параметра и возвращает их в виде пары, в порядке от меньшего к большему.
minmax_element Выполняет работу, которую делают min_element и max_element , в одном вызове.
mismatch Сравнивает два диапазона поэлементно либо на равенство или равноценность в смысле, заданном бинарным предикатом, и находит первую позицию, где наблюдается разница.
move Перемещает элементы, связанные с заданным диапазоном.
move_backward Перемещает элементы одного итератора в другой. Перемещение начинается с последнего элементом в указанном диапазоне и завершается первым элементом в этом диапазоне.
next_permutation Изменяет порядок элементов в диапазоне, чтобы исходный порядок был заменен перестановкой "лексикографически следующий больший", если такая существует, где смысл термина "следующий" может быть задан бинарным предикатом.
none_of Возвращает значение true , если условие не выполняется ни одним элементом заданного диапазона.
nth_element Разделяет диапазон элементов, правильно находя n-й элемент последовательности в диапазоне, чтобы все элементы перед ним были меньше или равны ему, а все элементы в последовательности после него больше либо равны ему.
partial_sort Упорядочивает указанное число меньших элементов в диапазоне в не нисходящий порядок или согласно критерию упорядочивания, заданному бинарным предикатом.
partial_sort_copy Копирует элементы из исходного диапазона в диапазон назначения, где исходные элементы упорядочены по критерию "меньше либо равно" или согласно другому заданному бинарному предикату.
partition Разделяет элементы диапазона на два непересекающихся множества, при этом элементы, удовлетворяющие унарному предикату, расположены перед теми, которые ему не удовлетворяют.
partition_copy Копирует элементы, возвращающие true для какого-либо условия, в одно место назначения, а возвращающие false — в другое. Эти элементы должны поступать из указанного диапазона.
partition_point Возвращает первый элемент в заданном диапазоне, который не удовлетворяет условию. Элементы сортируются таким образом, чтобы те, которые удовлетворяют условию, приходят до тех, которые не имеют.
pop_heap Удаляет наибольший элемент из начала кучи до позиции, следующей за последней, в диапазоне, а затем формирует новую кучу из оставшихся элементов.
prev_permutation Изменяет порядок элементов в диапазоне, чтобы исходный порядок был заменен перестановкой "лексикографически следующий больший", если такая существует, где смысл термина "следующий" может быть задан бинарным предикатом.
push_heap Добавляет элемент, находящийся в конце диапазона, в существующую кучу, состоящую из предыдущих элементов диапазона.
random_shuffle Выполняет переупорядочивание последовательности N элементов в диапазоне в одном из N! возможных порядков, выбранном случайным образом.
remove Удаляет указанное значение из заданного диапазона без нарушения порядка остальных элементов и возвращает конец нового диапазона после удаления указанного значения.
remove_copy Копирует элементы из исходного диапазона в диапазон назначения, за исключением того, что элементы указанного значения не копируются, не нарушая порядок оставшихся элементов и возвращая конец нового диапазона назначения.
remove_copy_if Копирует элементы из исходного диапазона в диапазон назначения, за исключением того, что выполнение предиката не копируется, не нарушая порядок оставшихся элементов и возвращая конец нового диапазона назначения.
remove_if Удаляет элементы, соответствующие предикату, из заданного диапазона без нарушения порядка остальных элементов и возвращает конец нового диапазона после удаления указанного значения.
replace Проверяет каждый элемент в диапазоне и заменяет его, если он соответствует заданному значению.
replace_copy Проверяет каждый элемент в исходном диапазоне и заменяет его, если он соответствует заданному значению, одновременно копируя результат в новый диапазон назначения.
replace_copy_if Проверяет каждый элемент в исходном диапазоне и заменяет его, если он соответствует заданному предикату, одновременно копируя результат в новый диапазон назначения.
replace_if Проверяет каждый элемент в диапазоне и заменяет его, если он соответствует заданному предикату.
reverse Изменяет порядок элементов в диапазоне на обратный.
reverse_copy Изменяет порядок элементов в исходном диапазоне на обратный, одновременно копируя их в диапазон назначения
rotate Меняет местами элементы в двух соседних диапазонах.
rotate_copy Меняет местами элементы в двух соседних диапазонах в пределах исходного диапазона и копирует результат в диапазон назначения.
sample
search Выполняет поиск первого вхождения последовательности в целевой диапазон, элементы которого равны указанным в заданной последовательности элементов или элементы которого равноценны в смысле, заданным бинарным предикатом, элементам в заданной последовательности.
search_n Выполняет поиск первой подпоследовательности в диапазоне заданного числа элементов, имеющих определенное значение или связанных с этим значением отношением, указанным бинарным предикатом.
set_difference Объединяет все элементы, принадлежащие одному отсортированному исходному диапазону, но не второму отсортированному исходному диапазону, в один отсортированный диапазон назначения, где критерий упорядочивания может быть указан бинарным предикатом.
set_intersection Объединяет все элементы, входящие в оба исходных упорядоченных диапазона, в один упорядоченный диапазон назначения, где критерий порядка сортировки может быть указан бинарным предикатом.
set_symmetric_difference Объединяет все элементы, входящие в один, но не в оба исходных упорядоченных диапазона, в один упорядоченный диапазон назначения, где критерий порядка сортировки может быть указан бинарным предикатом.
set_union Объединяет все элементы, входящие в хотя бы один из двух исходных упорядоченных диапазонов, в один упорядоченный диапазон назначения, где критерий порядка сортировки может быть указан бинарным предикатом.
sort Упорядочивает элементы в указанном диапазоне в не нисходящем порядке или согласно критерию упорядочивания, заданному бинарным предикатом.
shuffle Перемешивает (изменяет порядок) элементы в указанном диапазоне, используя генератор случайных чисел.
sort_heap Преобразует кучу в упорядоченный диапазон.
stable_partition Разделяет элементы диапазона на два непересекающихся множества, при этом элементы, удовлетворяющие унарному предикату, расположены перед теми, которые ему не удовлетворяют, с сохранением относительного порядка равноценных элементов.
stable_sort Упорядочивает элементы в указанном диапазоне в не нисходящем порядке или согласно критерию упорядочивания, заданному бинарным предикатом, и сохраняет относительный порядок равноценных элементов.
swap Меняет местами значения элементов между двумя типами объектов, присваивая содержимое первого объекта второму объекту, а содержимое второго — первому.
swap_ranges Меняет местами элементы одного диапазона с элементами другого диапазона такого же размера.
transform Применяет заданный объект функции к каждому элементу в исходном диапазоне или к паре элементов из двух исходных диапазонов и копирует возвращаемые значения объекта функции в диапазон назначения.
unique Удаляет повторяющиеся элементы, которые находятся рядом друг с другом в указанном диапазоне.
unique_copy Копирует элементы из исходного диапазона в диапазон назначения, за исключением повторяющихся элементов, которые находятся рядом друг с другом.
upper_bound Находит позицию первого элемента в упорядоченном диапазоне, который имеет значение больше указанного значения, где критерий упорядочивания может быть задан бинарным предикатом.

Алгоритмы диапазонов C++20 — сортировка, множества, обновления C++23 и прочее

Эта статья — третья и последняя в мини-серии об алгоритмах диапазонов. Мы рассмотрим некоторые алгоритмы сортировки, поиска и другие, а также познакомимся с готовящимися крутыми улучшениями этих алгоритмов в версии C++23. Поехали! Подробности — к старту курса по разработке на С++.

Прежде чем мы начнём

Ключевые наблюдения для алгоритмов std::ranges :

  • Алгоритмы диапазонов определяются в заголовке , а инфраструктура диапазонов и основные типы определяются в заголовке .
  • Существует как минимум две перегрузки алгоритмов диапазона: с парой итераторов и перегрузка с одним аргументом диапазона.
  • Версия, которая возвращает поддиапазон или итератор и принимает диапазон, возвращает заимствованный диапазон или заимствованный итератор. Это помогает обнаруживать итераторы для временных диапазонов.
  • Версии диапазона имеют проекции, что обеспечивает большую гибкость; например, вы можете выполнить сортировку по некоторым выбранным элементам или выполнить дополнительные преобразования перед сравнением.
  • В версии с диапазонами нет опции параллельного выполнения (вы не можете передать политику std::execution ).
  • Алгоритмы диапазонов, как и стандартные алгоритмы C++20, также являются constexpr .
  • Начиная с версии C++20, не существует алгоритмов числовых диапазонов, соответствующих заголовку .

Ниже вы можете найти примеры, демонстрирующие стандартный алгоритм и альтернативную версию с диапазонами. В них мы иллюстрируем некоторые основные концепции, стараясь не использовать расширенную композицию диапазонов или представления. Мы пойдём в порядке, указанном в cppreference/algorithms.

В этой части рассмотрим алгоритмы сортировки, разбиения на разделы, бинарный поиск и некоторые другие функции.

  • первая статья: «Алгоритмы диапазонов C++20 — 7 немодифицирующих операций»;
  • вторая статья: «Алгоритмы диапазонов C++20 — 11 модифицирующих операций»;

Разбиение на разделы и сортировка

sort и is_sorted

Алгоритм сортировки часто используют для объявления диапазонов. Если у вас есть контейнер, то благодаря диапазонам вы можете написать:

std::ranges::sort(myContainer);

Вот пример для лучшего понимания:

#include #include #include #include struct Product < std::string name; double value < 0.0 >; >; void print(std::string_view intro, const std::vector& container) < std::cout int main() < const std::vectorprods < < "box", 10.0 >, , , < "car", 1000.0 >, , , < "book", 45.0>, , >; print("input", prods); // the standard version: std::vector 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

Во многих реализациях используется интросортировка (см. Википедию). Это гибридное решение, обычно с быстрой сортировкой/сортировкой кучей, а затем сортировкой вставками для небольших (под)диапазонов.

Другие версии алгоритмов сортировки:

  • partial_sort — сортирует первые N элементов диапазона.
  • stable_sort — порядок эквивалентных элементов гарантированно сохраняется.

Как видите, в версии с диапазонами легко передать проекцию и отсортировать по заданной части элемента. В обычной версии нужно отдельное лямбда-выражение…

partition

Разбиение — неотъемлемая часть быстрой сортировки. Для данного предиката операция перемещает элементы, соответствующие предикату, в первую часть контейнера, а не соответствующие — во вторую. Иногда можно разделить контейнер, а не выполнять полную операцию сортировки:

#include #include #include #include void print(std::string_view intro, const std::vector& container) < std::cout 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 ); print("partition till 7", copy); std::cout
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:: ).

Операции бинарного поиска

Если контейнер уже отсортирован, можно выполнять операции логарифмического бинарного поиска.

binary_search

#include #include #include #include #include void print(std::string_view intro, const auto& container) < std::cout int main() < std::vectorvec(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 

Кроме того, можно использовать связанные алгоритмы:

  • std::ranges::lower_bound — cppreference.com возвращает итератор к первому элементу не меньше заданного значения;
  • std::ranges::upper_bound — cppreference.com — возвращает итератор к первому элементу больше заданного значения;

Операции с множествами

В библиотеке есть много функций, связанных с множествами, вот некоторые из них:

  • ranges::merge — объединяет два отсортированных диапазона
  • ranges::inplace_merge — объединяет два упорядоченных диапазона in-place
  • ranges::includes — возвращает true, если одна отсортированная последовательность является подпоследовательностью другой отсортированной последовательности
  • ranges::set_difference — вычисляет разницу между двумя множествами
  • ranges::set_intersection — вычисляет пересечение двух множеств
  • ranges::set_symmetric_difference — вычисляет симметричную разность двух множеств
  • ranges::set_union — вычисляет объединение двух множеств

Рассмотрим один пример с includes :

includes

Возвращает true , если отсортированный диапазон является подпоследовательностью другого отсортированного диапазона.

#include #include #include #include #include struct Product < std::string name; double value < 0.0 >; >; void print(std::string_view intro, const std::vector& container) < std::cout int main() < std::vectorprods < < "box", 10.0 >, , , < "car", 1000.0 >, , , < "book", 45.0>, , >; std::vector vecToCheck < , < "box", 10.0 >, >; std::ranges::sort(prods, <>, &Product::name); std::vector namesToCheck ; 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 , &Product::name); std::cout

Версия с диапазонами проще и предлагает способ проверки различных контейнеров. При подходе std:: итератор необходимо разыменовать, а затем неявно преобразовать в оба типа элементов входного контейнера.

Прочее

max_element

Поиск наибольшего элемента в контейнере (без сортировки):

#include #include #include #include #include struct Product < std::string name_; double value_ < 0.0 >; >; int main() < const std::vectorprods < < "box", 10.0 >, , , < "car", 1000.0 >, , , < "book", 45.0>, , >; // 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 value_ // 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 value_ >

equal

#include #include #include #include #include struct Product < std::string name; double value < 0.0 >; >; int main() < const std::vectorprods < < "box", 10.0 >, , , < "car", 1000.0 >, , , >; const std::vector moreProds < < "box", 11.0 >, , , < "car", 10.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 , &Product::name, &Product::name); std::cout

Ещё больше

Мой список алгоритмов не полон. Почти у всех стандартных алгоритмов есть альтернатива 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 у нас есть большинство соответствующих алгоритмов диапазонов из заголовка , но отсутствует заголовок .

Скоро в 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-специалистом.

  • Профессия Fullstack-разработчик на Python (16 месяцев)
  • Профессия Data Scientist (24 месяца)

Краткий каталог курсов

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-разработчик
  • Профессия Веб-разработчик

Мобильная разработка

  • Профессия iOS-разработчик
  • Профессия Android-разработчик

Java и C#

  • Профессия Java-разработчик
  • Профессия QA-инженер на JAVA
  • Профессия C#-разработчик
  • Профессия Разработчик игр на Unity

От основ — в глубину

  • Курс «Алгоритмы и структуры данных»
  • Профессия C++-разработчик
  • Профессия «Белый хакер»

А также

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *