Як реалізувати класичні алгоритми сортування в сучасних C ++?


331

std::sortАлгоритм (і його кузени std::partial_sortі std::nth_element) зі стандартної бібліотеки C ++ в більшості реалізацій складний і гібридна об'єднання більш елементарних алгоритмів сортування , таких як вибір сортування, вставки сортування, швидке сортування, сортування злиттям, або купи сортування.

Тут і на сестринських сайтах є багато питань, таких як https://codereview.stackexchange.com/, пов'язані з помилками, складністю та іншими аспектами реалізації цих класичних алгоритмів сортування. Більшість запропонованих реалізацій складаються з необроблених циклів, використання маніпуляцій з індексами та конкретних типів і, як правило, нетривіально для аналізу з точки зору правильності та ефективності.

Питання : як можна реалізувати вищезазначені класичні алгоритми сортування, використовуючи сучасні C ++?

  • немає необроблених циклів , але поєднуючи алгоритмічні будівельні блоки Стандартної бібліотеки з<algorithm>
  • інтерфейс ітератора та використання шаблонів замість маніпулювання індексами та конкретних типів
  • Стиль C ++ 14 , включаючи повну Стандартну бібліотеку, а також синтаксичні редуктори шуму, такі як autoпсевдоніми шаблонів, прозорі компаратори та поліморфні лямбда.

Примітки :

  • для подальших посилань на реалізацію алгоритмів сортування див. Wikipedia , Rosetta Code або http://www.sorting-algorithms.com/
  • згідно з умовами Шена Батька (слайд 39), необроблений цикл є for-пук довшим, ніж композиція двох функцій з оператором. Так f(g(x));чи f(x); g(x);або f(x) + g(x);не сире петлі, і ні один не є петлі в selection_sortі insertion_sortнижче.
  • Я слідую термінології Скотта Майєрса, щоб позначити поточний C ++ 1y вже як C ++ 14, і позначити C ++ 98 і C ++ 03 як C ++ 98, тому не пишіть мене з цього приводу.
  • Як запропоновано в коментарях @Mehrdad, я надаю чотири реалізації як Живий Приклад наприкінці відповіді: C ++ 14, C ++ 11, C ++ 98 та Boost і C ++ 98.
  • Сама відповідь представлена ​​лише в межах C ++ 14. Там, де це доречно, я позначаю синтаксичні та бібліотечні відмінності, де різні версії мови відрізняються.

8
Було б чудово додати тег C ++ Faq до питання, хоча це зажадає втратити хоча б одну з інших. Я б запропонував видалити версії (оскільки це загальне питання C ++, з реалізаціями, доступними в більшості версій з деякою адаптацією).
Матьє М.

@TemplateRex Ну, технічно, якщо це не поширені запитання, то це питання занадто широке (здогадуюсь - я не спростовував). Btw. хороша робота, багато корисної інформації, дякую :)
BartoszKP

Відповіді:


388

Алгоритмічні будівельні блоки

Почнемо зі складання алгоритмічних будівельних блоків зі стандартної бібліотеки:

#include <algorithm>    // min_element, iter_swap, 
                        // upper_bound, rotate, 
                        // partition, 
                        // inplace_merge,
                        // make_heap, sort_heap, push_heap, pop_heap,
                        // is_heap, is_sorted
#include <cassert>      // assert 
#include <functional>   // less
#include <iterator>     // distance, begin, end, next
  • інструменти ітератора, такі як не члени std::begin()/ std::end()а також з std::next(), доступні лише для C ++ 11 і вище. Для C ++ 98 потрібно написати їх сам. Є замінники Boost.Range в boost::begin()/ boost::end()і від Boost.Utility в boost::next().
  • std::is_sortedалгоритм доступний тільки для C ++ 11 і за її межами. Для C ++ 98 це може бути реалізовано в термінах std::adjacent_findі вручну написаним об'єктом функції. Boost.Algorithm також надає boost::algorithm::is_sortedяк заміну.
  • std::is_heapалгоритм доступний тільки для C ++ 11 і за її межами.

Синтаксичні смаколики

C ++ 14 забезпечує прозорі компаратори форми, std::less<>які діють поліморфно на їх аргументи. Це дозволяє уникнути надання типу ітератора. Це можна використовувати в поєднанні з аргументами шаблону функцій за замовчуванням C ++ 11 для створення єдиного перевантаження для сортування алгоритмів, які приймаються <за порівняння, та тих, які мають визначений користувачем об'єкт функції порівняння.

template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

У C ++ 11 можна визначити псевдонім шаблону для багаторазового використання для виведення типу значення ітератора, який додає незначне захаращення підписів алгоритмів сортування:

template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;

template<class It, class Compare = std::less<value_type_t<It>>>
void xxx_sort(It first, It last, Compare cmp = Compare{});

У C ++ 98 потрібно написати два перевантаження та використати багатослівний typename xxx<yyy>::typeсинтаксис

template<class It, class Compare>
void xxx_sort(It first, It last, Compare cmp); // general implementation

template<class It>
void xxx_sort(It first, It last)
{
    xxx_sort(first, last, std::less<typename std::iterator_traits<It>::value_type>());
}
  • Ще одна синтаксична принадність полягає в тому, що C ++ 14 полегшує перенесення визначених користувачем компараторів через поліморфні лямбдаautoпараметрами, що виводяться як аргументи шаблону функції).
  • C ++ 11 має лише мономорфні лямбда, які вимагають використання вищезгаданого псевдоніма шаблону value_type_t.
  • У C ++ 98 потрібно або написати окремий об'єкт функції або вдатися до синтаксису багатослівного std::bind1st/ std::bind2nd/ std::not1типу.
  • Boost.Bind покращує це за допомогою синтаксису boost::bindта _1/ _2placeholder.
  • C ++ 11 і за її межами також std::find_if_not, в той час як C ++ 98 Потреби std::find_ifз А std::not1навколо об'єкта функції.

C ++ Стиль

Загально прийнятного стилю C ++ 14 поки що немає. На краще або на гірше, я уважно стежу за проектом «Ефективний сучасний C ++» Скотта Майєрса та оновленим GotW Herb Sutter . Я використовую такі стильові рекомендації:

Вибір сортування

Сортування вибору жодним чином не адаптується до даних, тому час його виконання завжди єO(N²). Однак сортування селекції має властивість мінімізувати кількість свопів . У додатках, де вартість заміни товарів висока, сортування вибору може бути найкращим алгоритмом вибору.

Щоб реалізувати його за допомогою Стандартної бібліотеки, кілька разів використовуйте std::min_elementдля пошуку мінімального елемента, що залишився, і iter_swapпоміняйте його на місце:

template<class FwdIt, class Compare = std::less<>>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const selection = std::min_element(it, last, cmp);
        std::iter_swap(selection, it); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Зауважимо, що selection_sortвже оброблений діапазон [first, it)відсортований як його інваріантний цикл. Мінімальні вимоги є ітераторами переадресації , порівняно з std::sortітераторами випадкового доступу.

Деталі пропущені :

  • Сортування вибору може бути оптимізовано за допомогою раннього тестування if (std::distance(first, last) <= 1) return;(або для ітераторів вперед / двонаправлення:) if (first == last || std::next(first) == last) return;.
  • для двонаправлених ітераторів вищевказаний тест може поєднуватися з циклом протягом інтервалу [first, std::prev(last)), оскільки останній елемент гарантовано є мінімальним залишковим елементом і не потребує заміни.

Сортування вставки

Хоча це один із елементарних алгоритмів сортування з O(N²)найгіршим часом, сортування вставки - це алгоритм вибору або тоді, коли дані майже відсортовані (оскільки вони адаптивні ), або коли розмір проблеми невеликий (тому що він має низькі накладні витрати). З цих причин і через те, що він також стабільний , сортування вставки часто використовується як рекурсивний базовий випадок (коли розмір проблеми невеликий) для більш високих накладних алгоритмів сортування розділення та підкорення, таких як сортування об'єднань або швидке сортування.

Для реалізації insertion_sortз Стандартною бібліотекою кілька разів використовуйте std::upper_boundдля пошуку місця, куди потрібно перейти поточний елемент, і використовуйте std::rotateдля переміщення решти елементів вгору у діапазоні введення:

template<class FwdIt, class Compare = std::less<>>
void insertion_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last; ++it) {
        auto const insertion = std::upper_bound(first, it, *it, cmp);
        std::rotate(insertion, it, std::next(it)); 
        assert(std::is_sorted(first, std::next(it), cmp));
    }
}

Зауважимо, що insertion_sortвже оброблений діапазон [first, it)відсортований як його інваріантний цикл. Сортування вставки також працює з ітераторами вперед.

Деталі пропущені :

  • Сортування вставки можна оптимізувати за допомогою раннього тестування if (std::distance(first, last) <= 1) return;(або для ітераторів вперед / бік:) if (first == last || std::next(first) == last) return;та циклу через інтервал [std::next(first), last), оскільки перший елемент гарантовано є на місці і не потребує повороту.
  • для двонаправлених ітераторів дворядний пошук для пошуку точки вставки може бути замінений зворотним лінійним пошуком за допомогою std::find_if_notалгоритму Стандартної бібліотеки .

Чотири живі приклади ( C ++ 14 , C ++ 11 , C ++ 98 та Boost , C ++ 98 ) для фрагменту нижче:

using RevIt = std::reverse_iterator<BiDirIt>;
auto const insertion = std::find_if_not(RevIt(it), RevIt(first), 
    [=](auto const& elem){ return cmp(*it, elem); }
).base();
  • Для випадкових входів це дає O(N²)порівняння, але це покращує O(N)порівняння майже впорядкованих даних. Двійковий пошук завжди використовує O(N log N)порівняння.
  • Для невеликих вхідних діапазонів краща локальність пам'яті (кеш, попереднє завантаження) лінійного пошуку також може домінувати над бінарним пошуком (це, звичайно, слід перевірити).

Швидкий сорт

При ретельному впровадженні швидке сортування є надійним і O(N log N)очікує складності, але з O(N²)найгіршим складною можливістю, яке може бути спричинене суперечливо вибраними вхідними даними. Коли стабільний сорт не потрібен, швидкий сорт - це чудовий сорт загального призначення.

Навіть для найпростіших версій швидке сортування є дещо складнішим у здійсненні за допомогою Стандартної бібліотеки, ніж інші класичні алгоритми сортування. У нижченаведеному підході використовується декілька утиліт ітератора, щоб знайти середній елемент вхідного діапазону [first, last)як стрижневий, а потім використати два виклики для std::partition(тривалих O(N)) розділів діапазону вводу на сегменти елементів, менших за, рівних, і більше, ніж вибраний шарнір відповідно. Нарешті, два зовнішні сегменти з елементами, меншими та більшими за шарнір, рекурсивно сортуються:

template<class FwdIt, class Compare = std::less<>>
void quick_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;
    auto const pivot = *std::next(first, N / 2);
    auto const middle1 = std::partition(first, last, [=](auto const& elem){ 
        return cmp(elem, pivot); 
    });
    auto const middle2 = std::partition(middle1, last, [=](auto const& elem){ 
        return !cmp(pivot, elem);
    });
    quick_sort(first, middle1, cmp); // assert(std::is_sorted(first, middle1, cmp));
    quick_sort(middle2, last, cmp);  // assert(std::is_sorted(middle2, last, cmp));
}

Однак швидке сортування є досить складним, щоб отримати правильний та ефективний, оскільки кожен з перерахованих вище кроків повинен бути ретельно перевірений та оптимізований для коду рівня виробництва. Зокрема, для O(N log N)складності поворот повинен призвести до врівноваженого розподілу вхідних даних, що взагалі не може бути гарантовано для O(1)стрижня, але який можна гарантувати, якщо встановити стрижень як O(N)медіану вхідного діапазону.

Деталі пропущені :

  • вищезазначена реалізація особливо вразлива для спеціальних входів, наприклад, вона має O(N^2)складність для вводу " органної труби " 1, 2, 3, ..., N/2, ... 3, 2, 1(тому що середина завжди більша, ніж усі інші елементи).
  • середній вибір з 3 стрижнів від випадково вибраних елементів із захисних діапазонів вхідних сигналів проти майже відсортованих входів, для яких складність інакше погіршиться доO(N^2).
  • 3-х напрямне розділення (розділення елементів менших, рівних та більших за зріз), як показано двома дзвінкамиstd::partition, не є найбільш ефективнимO(N)алгоритмом для досягнення цього результату.
  • для ітераторів довільного доступу гарантована O(N log N)складність може бути досягнута за допомогою медіанного вибору зсуву з std::nth_element(first, middle, last)подальшими рекурсивними викликами до quick_sort(first, middle, cmp)та quick_sort(middle, last, cmp).
  • ця гарантія, однак, коштує, оскільки постійний коефіцієнт O(N)складності std::nth_elementможе бути дорожчим, ніж O(1)складність опорної межі 3 з подальшим O(N)викликом std::partition(що є зручним для кешу єдиним переходом вперед дані).

Злиття сортування

Якщо використання O(N)додаткового простору не викликає занепокоєнь, то сортування злиття є відмінним вибором: це єдиний стабільний O(N log N) алгоритм сортування.

Це легко здійснити за допомогою стандартних алгоритмів: використовуйте кілька утиліт ітератора, щоб знайти середину вхідного діапазону [first, last)та комбінувати два рекурсивно відсортовані сегменти із std::inplace_merge:

template<class BiDirIt, class Compare = std::less<>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare{})
{
    auto const N = std::distance(first, last);
    if (N <= 1) return;                   
    auto const middle = std::next(first, N / 2);
    merge_sort(first, middle, cmp); // assert(std::is_sorted(first, middle, cmp));
    merge_sort(middle, last, cmp);  // assert(std::is_sorted(middle, last, cmp));
    std::inplace_merge(first, middle, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

Сортування сортування вимагає двонаправлених ітераторів, вузьким місцем яких є std::inplace_merge. Зауважте, що при сортуванні пов'язаних списків сортування злиття потребує лише O(log N)додаткового місця (для рекурсії). Останній алгоритм реалізований std::list<T>::sortу Стандартній бібліотеці.

Купи сорту

Сортування купи просто провести у виконанні, виконує наO(N log N)місці сортування, але не є стабільним.

Перший цикл, O(N)фаза "heapify", наводить масив у порядок купи. Другий цикл, O(N log Nфаза "сортування", багаторазово витягує максимум і відновлює порядок купи. Стандартна бібліотека робить це надзвичайно просто:

template<class RandomIt, class Compare = std::less<>>
void heap_sort(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    lib::make_heap(first, last, cmp); // assert(std::is_heap(first, last, cmp));
    lib::sort_heap(first, last, cmp); // assert(std::is_sorted(first, last, cmp));
}

У разі , якщо ви вважаєте це «обман» , щоб використовувати std::make_heapі std::sort_heap, ви можете піти на один рівень глибше і писати ці функції самостійно з точки зору std::push_heapі std::pop_heap, відповідно:

namespace lib {

// NOTE: is O(N log N), not O(N) as std::make_heap
template<class RandomIt, class Compare = std::less<>>
void make_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = first; it != last;) {
        std::push_heap(first, ++it, cmp); 
        assert(std::is_heap(first, it, cmp));           
    }
}

template<class RandomIt, class Compare = std::less<>>
void sort_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
    for (auto it = last; it != first;) {
        std::pop_heap(first, it--, cmp);
        assert(std::is_heap(first, it, cmp));           
    } 
}

}   // namespace lib

Стандартна бібліотека визначає як складність, так push_heapі pop_heapскладність O(log N). Однак зауважимо, що зовнішня петля за діапазоном [first, last)призводить до O(N log N)складності для make_heap, тоді як std::make_heapмає лише O(N)складність. Для загальної O(N log N)складності heap_sortце не має значення.

Деталі опущені : O(N)реалізаціяmake_heap

Тестування

Ось чотири живі приклади ( C ++ 14 , C ++ 11 , C ++ 98 та Boost , C ++ 98 ), які перевіряють усі п'ять алгоритмів на різноманітних входах (не маючи на увазі вичерпних чи суворих). Зауважте лише великі відмінності у LOC: C ++ 11 / C ++ 14 потребує приблизно 130 LOC, C ++ 98 та Boost 190 (+ 50%) та C ++ 98 понад 270 (+ 100%).


13
Хоча я не згоден з вашим використаннямauto (і багато людей не згодні зі мною), мені сподобалося добре бачити стандартні алгоритми бібліотеки. Я хотів побачити кілька прикладів подібного коду, побачивши розмову Шона Батька. Крім того, я не мав ідеї, що std::iter_swapіснує, хоча мені здається дивним, що воно є <algorithm>.
Джозеф Менсфілд

32
@sbabbi Вся стандартна бібліотека заснована на принципі, що ітератори копіювати дешево; він передає їх за значенням, наприклад. Якщо копіювання ітератора недешеве, то у вас всюди будуть виникати проблеми з продуктивністю.
Джеймс Канзе

2
Чудовий пост. Щодо частини обману [std ::] make_heap. Якщо std :: make_heap вважається обманом, то також std :: push_heap. Тобто обман = не реальна поведінка, визначена для структури купи. Я вважаю, що також повчальним є включення push_heap.
Капітан Жирафа

3
@gnzlbg Стверджує, що ви можете коментувати, звичайно. Ранній тест може бути відправлений тегами за категорією ітератора, з поточною версією для випадкового доступу та if (first == last || std::next(first) == last). Я можу це оновити пізніше. Реалізація матеріалів у розділах "Пропущені деталі" виходить за рамки питання, IMO, оскільки вони містять посилання на цілі запитання та відповіді. Реалізувати рутинні сортування в реальному слові важко!
TemplateRex

3
Чудовий пост. Хоча, ви обдурили свою квочку, використовуючи, nth_elementна мою думку. nth_elementвже половина квакіспорту (включаючи етап розділення та рекурсію на половину, що включає n-й елемент, який вас цікавить).
sellibitze

14

Ще один невеличкий і досить елегантний, який спочатку був знайдений у коді . Я думав, що варто поділитися.

Підрахунок сортування

Хоча вона досить спеціалізована, підрахунок сортування - це простий алгоритм сортування цілих чисел і часто може бути дуже швидким за умови, що значення цілих чисел для сортування не надто далеко одна від одної. Це, мабуть, ідеально, якщо коли-небудь потрібно сортувати колекцію з мільйона цілих чисел, наприклад, від 0 до 100.

Щоб реалізувати дуже простий підрахунок сортування, який працює як з підписаними, так і без підписаними цілими числами, потрібно знайти найменші та найбільші елементи колекції для сортування; їх різниця підкаже розмір масиву лічильників для виділення. Потім робиться другий прохід через колекцію, щоб підрахувати кількість виникнень кожного елемента. Нарешті, ми записуємо необхідну кількість кожного цілого числа до початкової колекції.

template<typename ForwardIterator>
void counting_sort(ForwardIterator first, ForwardIterator last)
{
    if (first == last || std::next(first) == last) return;

    auto minmax = std::minmax_element(first, last);  // avoid if possible.
    auto min = *minmax.first;
    auto max = *minmax.second;
    if (min == max) return;

    using difference_type = typename std::iterator_traits<ForwardIterator>::difference_type;
    std::vector<difference_type> counts(max - min + 1, 0);

    for (auto it = first ; it != last ; ++it) {
        ++counts[*it - min];
    }

    for (auto count: counts) {
        first = std::fill_n(first, count, min++);
    }
}

Хоча це корисно лише тоді, коли діапазон цілих чисел для сортування, як відомо, невеликий (як правило, не більший, ніж розмір колекції для сортування), але підрахунок сортування більш універсальний зробив би повільніше для кращих випадків. Якщо діапазон, як відомо, не малий, інший алгоритм, такий як радіаційний сорт , ska_sort або замість цього може бути використаний Spreadsort .

Деталі пропущені :

  • Ми могли пройти межі діапазону значень, прийнятих алгоритмом як параметрів, щоб повністю позбутися першого std::minmax_elementпроходу через колекцію. Це зробить алгоритм ще швидшим, коли корисно-малий межа діапазону буде відомий іншими способами. (Це не обов'язково бути точним; проходження константи від 0 до 100 все ще набагато краще, ніж зайвий пропуск понад мільйон елементів, щоб з'ясувати, що справжні межі - від 1 до 95. Навіть 0 до 1000 варто того; додаткові елементи записуються один раз з нулем і читаються один раз).

  • Вирощування countsна льоту - ще один спосіб уникнути окремого першого проходу. Подвоєння countsрозміру щоразу, коли воно має зростати, дає амортизований час O (1) на відсортований елемент (див. Аналіз вартості вставки хеш-таблиць для підтвердження того, що експоненціальна величина є ключовою). Вирощувати в кінці нове maxлегко, std::vector::resizeякщо додати нові нульові елементи. Зміна minна льоту та вставлення нових нульових елементів спереду можна зробити std::copy_backwardпісля вирощування вектора. Потім std::fillнульові нові елементи.

  • countsЦикл приріст являє собою гістограму. Якщо дані, швидше за все, повторюються, а кількість бункерів невелика, можна буде розгортати декілька масивів, щоб зменшити вузьке місце зберігання / повторного завантаження залежностей від даних щодо зберігання / перезавантаження у той самий контейнер. Це означає, що більше рахунків до нуля на початку та більше, щоб перетворити цикл на кінці, але це варто того, що це стосується більшості процесорів для нашого прикладу з мільйонів від 0 до 100 номерів, особливо якщо введення вже може бути (частково) відсортовано та мають довгі пробіги однакової кількості.

  • В алгоритмі вище ми використовуємо min == maxчек, щоб повернутися рано, коли кожен елемент має однакове значення (у цьому випадку колекція сортується). Насправді можна взагалі повністю перевірити, чи колекція вже сортована, знаходячи крайні значення колекції без додаткового витраченого часу (якщо перший пропуск все ще залишається у вузькій пам'яті з додатковою роботою оновлення min та max). Однак такого алгоритму не існує в стандартній бібліотеці, і написання було б більш втомливим, ніж написання решти підрахунку самого сортування. Це залишається як вправа для читача.

  • Оскільки алгоритм працює лише з цілими значеннями, статичні твердження можуть бути використані для запобігання користувачам помилок очевидних типів. У деяких контекстах std::enable_if_tможе бути кращим збій заміни на .

  • Незважаючи на те, що сучасний C ++ крутий, майбутні C ++ можуть бути ще крутішими: структуровані прив’язки та деякі частини діапазону TS зробили б алгоритм ще більш чистим.


@TemplateRex Якби він зміг взяти довільний об'єкт порівняння, він зробив би підрахунок сортування порівняльного сортування, а сортування порівнянь не може мати кращий гірший випадок, ніж O (n log n). Сортування підрахунку має найгірший випадок O (n + r), а це означає, що це не може бути порівнянням порівняння. Цілі числа можна порівняти, але це властивість не використовується для виконання сортування (воно використовується лише в тому, std::minmax_elementщо збирає лише інформацію). Використовується властивість полягає в тому, що цілі числа можуть використовуватися як індекси або зсуви, і що вони збільшуються при збереженні останньої властивості.
Морвен

Діапазони TS дійсно дуже приємні, наприклад, остаточний цикл може закінчитися, counts | ranges::view::filter([](auto c) { return c != 0; })тому вам не доведеться повторно перевіряти наявність ненульових підрахунків всередині fill_n.
TemplateRex

(Я знайшов помилку в і - можу чи я тримати їх сезам редагування щодо reggae_sort?)small ratherappart
глиняний глечик

@greybeard Ви можете робити все, що завгодно: p
Morwenn

Я підозрюю, що вирощування counts[]на льоту буде виграшним шляхом, minmax_elementніж проходження входу до гістограмування. Особливо для випадків використання, коли це ідеально, дуже великого введення з безліччю повторів у невеликому діапазоні, оскільки ви швидко зросте countsдо повного розміру, маючи кілька непередбачуваних гілок або подвоєння розмірів. (Звичайно, знання недостатньо обмеженого діапазону дозволить вам уникнути minmax_elementсканування та уникнути перевірки меж всередині циклу гістограми.)
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.