Какая практическая разница между std:: nth_element и std:: sort?

Я смотрел на алгоритм std:: nth_element, который, по-видимому:

Переупорядочивает элементы в диапазоне [первый, последний], таким образом, чтобы элемент на полученной n-й позиции является элементом, который будет в этом положении в отсортированной последовательности, причем ни один из элементов предшествующий ему, больше, и ни один из элементов, следующих за ним меньше. Ни предшествующие ему элементы, ни элементы после этого гарантируется, что они будут заказаны.

Однако с моим компилятором выполняется следующее:

    vector<int> myvector;
    srand(GetTickCount());

    // set some values:
    for ( int i = 0; i < 10; i++ )
        myvector.push_back(rand());

    // nth_element around the 4th element
    nth_element (myvector.begin(), myvector.begin()+4, myvector.end());

    // print results
    for (auto it=myvector.begin(); it!=myvector.end(); ++it)
        cout << " " << *it;

    cout << endl;

Всегда возвращает полностью отсортированный список целых чисел точно так же, как и std:: sort. Я что-то упускаю? Для чего этот алгоритм полезен?

РЕДАКТИРОВАТЬ: Ok, следующий пример с использованием гораздо большего набора показывает, что существует большая разница:

    vector<int> myvector;
    srand(GetTickCount());

    // set some values:
    for ( int i = 0; i < RAND_MAX; i++ )
        myvector.push_back(rand());

    // nth_element around the 4th element
    nth_element (myvector.begin(), myvector.begin()+rand(), myvector.end());

    vector<int> copy = myvector;
    std::sort(myvector.begin(), myvector.end());

    cout << (myvector == copy ? "true" : "false") << endl;

Ответы

Ответ 1

Это действительно справедливо для std::nth_element, чтобы отсортировать весь диапазон для выполнения документированной семантики - однако это приведет к сбою при выполнении требуемого сложность (линейная). Главное, что он может это сделать, но этого не нужно.

Это означает, что std::nth_element может выйти из строя раньше - как только он сможет определить, что будет n'th элементом вашего диапазона, он может остановиться. Например, для диапазона

[9,3,6,2,1,7,8,5,4,0]

попросив дать вам четвертый элемент, может дать что-то вроде

[2,0,1,3,8,5,6,9,7,4]

Список был частично отсортирован, достаточно хорош, чтобы можно было сказать, что четвертый элемент в порядке будет 3.

Следовательно, если вы хотите ответить, "какой номер является четвертым наименьшим" или "который является четырьмя наименьшими", тогда std::nth_element является вашим другом.

Если вы хотите получить четыре наименьших числа, чтобы вы могли использовать std::partial_sort.

Ответ 2

Реализация std:: nth_element выглядит следующим образом:

void _Nth_element(_RanIt _First, _RanIt _Nth, _RanIt _Last, _Pr _Pred)
{
    for (; _ISORT_MAX < _Last - _First; )
        {   // divide and conquer, ordering partition containing Nth
        pair<_RanIt, _RanIt> _Mid =
            _Unguarded_partition(_First, _Last, _Pred);

        if (_Mid.second <= _Nth)
            _First = _Mid.second;
        else if (_Mid.first <= _Nth)
            return; // Nth inside fat pivot, done
        else
            _Last = _Mid.first;
        }

    _Insertion_sort(_First, _Last, _Pred);  // sort any remainder
}

где ISORT_MAX определяется как 32.

Таким образом, если ваша последовательность является дробителем, чем 32 элемента, она просто выполняет InsertionSort. Поэтому ваша короткая последовательность полностью отсортирована.

Ответ 3

std::sort сортирует все элементы. std::nth_elenemt нет. Он просто помещает n-й элемент в n-ое положение, с меньшими или равными элементами на одной стороне и большими или равными элементами - с другой. Он используется, если вы хотите найти n-й элемент (очевидно) или хотите, чтобы n наименьших или самых больших элементов. Полная сортировка удовлетворяет этим требованиям.

Итак, почему бы просто не выполнить полный сортировку и получить n-й элемент? Поскольку std::nth_element имеет требование иметь сложность O (N), тогда как std::sort - O (Nlog (N)). std::sort не может удовлетворить требованиям сложности std::nth_element. Если вам не нужна полная сортировка диапазона, целесообразно использовать его.

Что касается вашего примера, когда я запускаю аналогичный код в GCC 4.7, я получаю ожидаемые результаты:

  for ( int i = 0; i < 10; i++ )
    myvector.push_back(rand()%32); // make the numbers small

  cout << myvector << "\n";
// nth_element around the 4th element
  nth_element (myvector.begin(), myvector.begin()+4, myvector.end());
  cout << myvector << "\n";
  std::sort(myvector.begin(), myvector.end());
  cout << myvector << "\n";

производит

{ 7, 6, 9, 19, 17, 31, 10, 12, 9, 13 }
{ 9, 6, 9, 7, 10, 12, 13, 31, 17, 19 }
{ 6, 7, 9, 9, 10, 12, 13, 17, 19, 31 }
               ^

где я использовал созданный на заказ ostream operator<<, чтобы распечатать результаты.