std::partial_sort(3)

๐Ÿ“ฆ version 2024.07.05, in nixpkgs unstable, opensuse-leap 16.0

๐Ÿ’– NAME ๐Ÿ’–

std::partial_sort โ€” ok so it's literally just std::partial_sort, that's the whole name bestie ๐ŸŽ€ (yes this page is from the C++ Standard Libary manual, page std::partial_sort(3), and we're about to make it make sense ๐Ÿ’…)


๐ŸŒธ Synopsis ๐ŸŒธ

First things first: it's Defined in header <algorithm> ๐Ÿ“Œ

There are four overloads, and they are all serving โญ

template< class RandomIt >
void partial_sort( RandomIt first, RandomIt middle, RandomIt last );

โœจ overload (1), and it's constexpr since C++20 ๐Ÿซถ

template< class ExecutionPolicy, class RandomIt >
void partial_sort( ExecutionPolicy&& policy,
                   RandomIt first, RandomIt middle, RandomIt last );

โœจ overload (2), since C++17 ๐Ÿ’ซ

template< class RandomIt, class Compare >
void partial_sort( RandomIt first, RandomIt middle, RandomIt last,
                   Compare comp );

โœจ overload (3), and it's constexpr since C++20 ๐Ÿซถ

template< class ExecutionPolicy, class RandomIt, class Compare >
void partial_sort( ExecutionPolicy&& policy,
                   RandomIt first, RandomIt middle, RandomIt last,
                   Compare comp );

โœจ overload (4), since C++17 ๐Ÿ’ซ

๐Ÿฆ‹ What it actually does ๐Ÿฆ‹

So it rearranges elements such that the range [first, middle) contains the sorted middle - first smallest elements in the range [first, last). That's the whole vibe, no cap ๐Ÿ”ฅ

The order of equal elements is not guaranteed to be preserved ๐Ÿ™ˆ and the order of the remaining elements in the range [middle, last) is unspecified โ€” so don't come for those, they're doing their own thing ๐Ÿ’€

Now the detail drop, per overload ๐Ÿ‘‘

  • (1) โ€” elements are sorted with respect to operator< (until C++20), and with respect to std::less{} (since C++20) ๐Ÿงš
  • (3) โ€” elements are sorted with respect to comp ๐ŸŽฏ
  • (2),(4) โ€” same as (1),(3), but executed according to policy. These overloads participate in overload resolution only if std::is_execution_policy_v<std::decay_t<ExecutionPolicy>> is true (until C++20), or std::is_execution_policy_v<std::remove_cvref_t<ExecutionPolicy>> is true (since C++20) ๐Ÿ”
โš ๏ธ If any of the following conditions is satisfied, the behavior is undefined โ€” like, actual undefined behaviour, not the cute slang kind ๐Ÿ˜ญ - [first, middle) or [middle, last) is not a valid range. - The type of *first is not Swappable (until C++11). - RandomIt is not ValueSwappable. - The type of *first is not MoveConstructible (since C++11). - The type of *first is not MoveAssignable.

๐ŸŒˆ Parameters ๐ŸŒˆ

  • first, last โ€” random access iterators defining the range ๐Ÿ—บ๏ธ
  • middle โ€” random access iterator defining the one-past-the-end iterator of the range to be sorted ๐ŸŽฏ
  • policy โ€” the execution policy to use. See execution policy for details. ๐Ÿ›ธ
  • comp โ€” comparison function object (i.e. an object that satisfies the requirements of Compare) which returns true if the first argument is less than (i.e. is ordered before) the second. ๐Ÿง 

The signature of the comparison function should be equivalent to the following:

bool cmp(const Type1& a, const Type2& b);

And the fine print on comp, because it matters fr fr ๐Ÿ’… โ€” while the signature does not need to have const&, the function must not modify the objects passed to it and must be able to accept all values of type (possibly const) Type1 and Type2 regardless of value category (thus, Type1& is not allowed, nor is Type1 unless for Type1 a move is equivalent to a copy (since C++11)). The types Type1 and Type2 must be such that an object of type RandomIt can be dereferenced and then implicitly converted to both of them. ๐Ÿ“Ž


๐Ÿ’œ Type requirements ๐Ÿ’œ

  • RandomIt must meet the requirements of LegacyRandomAccessIterator ๐Ÿƒโ€โ™€๏ธ
  • Compare must meet the requirements of Compare โš–๏ธ

๐Ÿ”ฅ Complexity ๐Ÿ”ฅ

Given M as middle - first, and N as last - first ๐Ÿงฎ

  • (1),(2) โ€” approximately Nยทlog(M) comparisons using operator< (until C++20), std::less{} (since C++20) โฑ๏ธ
  • (3),(4) โ€” approximately Nยทlog(M) applications of the comparator comp โฑ๏ธ

(and yes the original writes it as Mยทlog(M)... sorry, Nยทlog(M), with the cute little math script โ€” same numbers, promise ๐Ÿซถ)


๐Ÿ’€ Exceptions ๐Ÿ’€

The overloads with a template parameter named ExecutionPolicy report errors as follows ๐Ÿ‘‡

  • If execution of a function invoked as part of the algorithm throws an exception and ExecutionPolicy is one of the standard policies, std::terminate is called. For any other ExecutionPolicy, the behavior is implementation-defined ๐Ÿ˜ต
  • If the algorithm fails to allocate memory, std::bad_alloc is thrown ๐ŸŽˆ

๐Ÿงš Possible implementation ๐Ÿงš

See also the implementations in libstdc++ and libc++ ๐Ÿ‘€

partial_sort (1):

template<typename RandomIt>
constexpr //< since C++20
void partial_sort(RandomIt first, RandomIt middle, RandomIt last)
{
    typedef typename std::iterator_traits<RandomIt>::value_type VT;
    std::partial_sort(first, middle, last, std::less<VT>());
}

partial_sort (3):

namespace impl
{
    template<typename RandomIt, typename Compare>
    constexpr //< since C++20
    void sift_down(RandomIt first, RandomIt last, const Compare& comp)
    {
        // sift down element at โ€œfirstโ€
        const auto length = static_cast<std::size_t>(last - first);
        std::size_t current = 0;
        std::size_t next = 2;
        while (next < length)
        {
            if (comp(*(first + next), *(first + (next - 1))))
                --next;
            if (!comp(*(first + current), *(first + next)))
                return;
            std::iter_swap(first + current, first + next);
            current = next;
            next = 2 * current + 2;
        }
        --next;
        if (next < length && comp(*(first + current), *(first + next)))
            std::iter_swap(first + current, first + next);
    }

    template<typename RandomIt, typename Compare>
    constexpr //< since C++20
    void heap_select(RandomIt first, RandomIt middle, RandomIt last, const Compare& comp)
    {
        std::make_heap(first, middle, comp);
        for (auto i = middle; i != last; ++i)
        {
            if (comp(*i, *first))
            {
                std::iter_swap(first, i);
                sift_down(first, middle, comp);
            }
        }
    }
} // namespace impl

template<typename RandomIt, typename Compare>
constexpr //< since C++20
void partial_sort(RandomIt first, RandomIt middle, RandomIt last, Compare comp)
{
    impl::heap_select(first, middle, last, comp);
    std::sort_heap(first, middle, comp);
}

๐ŸŒŸ Notes ๐ŸŒŸ

โš™๏ธ Algorithm

The algorithm used is typically heap select to select the smallest elements, and heap sort to sort the selected elements in the heap in ascending order ๐Ÿ“ˆ

To select elements, a heap is used (see heap). For example, for operator< as comparison function, max-heap is used to select middle - first smallest elements ๐Ÿ”๏ธ

Heap sort is used after selection to sort [first, middle) selected elements (see std::sort_heap) ๐Ÿงบ

๐ŸŽ€ Intended use

std::partial_sort algorithms are intended to be used for small constant numbers of [first, middle) selected elements ๐Ÿ‘‘ (like, small. small small. don't be out here doing the most ๐Ÿ’…)


๐Ÿ“ Example ๐Ÿ“

Run this code, bestie ๐Ÿƒโ€โ™€๏ธ

#include <algorithm>
#include <array>
#include <functional>
#include <iostream>

void print(const auto& s, int middle)
{
    for (int a : s)
        std::cout << a << ' ';
    std::cout << '\n';
    if (middle > 0)
    {
        while (middle-- > 0)
            std::cout << "--";
        std::cout << '^';
    }
    else if (middle < 0)
    {
        for (auto i = s.size() + middle; --i; std::cout << "  ")
        {}

        for (std::cout << '^'; middle++ < 0; std::cout << "--")
        {}
    }
    std::cout << '\n';
};

int main()
{
    std::array<int, 10> s{5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
    print(s, 0);
    std::partial_sort(s.begin(), s.begin() + 3, s.end());
    print(s, 3);
    std::partial_sort(s.rbegin(), s.rbegin() + 4, s.rend());
    print(s, -4);
    std::partial_sort(s.rbegin(), s.rbegin() + 5, s.rend(), std::greater{});
    print(s, -5);
}

๐ŸŒธ Possible output:

5 7 4 2 8 6 1 9 0 3

0 1 2 7 8 6 5 9 4 3
------^
4 5 6 7 8 9 3 2 1 0
          ^--------
4 3 2 1 0 5 6 7 8 9
        ^----------

๐Ÿฆ‹ Defect reports ๐Ÿฆ‹

The following behavior-changing defect reports were applied retroactively to previously published C++ standards ๐Ÿ“œ

  • P0896R4 โ€” applied to C++98. Behavior as published: [first, middle) and [middle, last) were not required to be valid ranges. Correct behavior: the behavior is undefined if any of them is invalid ๐Ÿšจ

๐Ÿ‘‘ See also ๐Ÿ‘‘

  • nth_element โ€” partially sorts the given range making sure that it is partitioned by the given element (function template) ๐Ÿงฉ
  • partial_sort_copy โ€” copies and partially sorts a range of elements (function template) ๐Ÿ“‹
  • stable_sort โ€” sorts a range of elements while preserving order between equal elements (function template) ๐Ÿซง
  • sort โ€” sorts a range into ascending order (function template) ๐Ÿ”ผ
  • ranges::partial_sort (C++20) โ€” sorts the first N elements of a range (niebloid) ๐ŸŒˆ

aaaaand that's the whole page, you ate it, I'm obsessed ๐Ÿซถ๐Ÿ’– this little guide came straight from http://cppreference.com, dated 2024.06.10 โ€” go forth and partially sort, queen ๐Ÿ‘‘โœจ

๐Ÿ“ฆ where this page comes from