Каков правильный подход при использовании контейнера STL для вычисления медианы?

Скажем, мне нужно получить медиану из последовательности 1000000 случайных числовых значений.

Если я использую что-нибудь кроме std::list, у меня нет (встроенного) способа сортировки последовательности для вычисления медианы.

При использовании std::list я не могу произвольно обращаться к значениям для получения середины (медианы) отсортированной последовательности.

Лучше ли реализовать сортировку самостоятельно и пойти, например, std::vector, или лучше использовать std::list и использовать std::list::iterator для перехода к среднему значению? Последнее кажется менее громоздким, но и более уродливым.

Или для меня есть другие и лучшие альтернативы?


person sharkin    schedule 12.11.2009    source источник


Ответы (10)


Любой контейнер с произвольным доступом (например, std::vector) можно отсортировать с помощью стандартного алгоритма std::sort, доступного в заголовке <algorithm>.

Для нахождения медианы было бы быстрее использовать std::nth_element; этого достаточно для сортировки одного выбранного элемента в правильное положение, но не для полной сортировки контейнера. Итак, вы можете найти такую ​​медиану:

int median(vector<int> &v)
{
    size_t n = v.size() / 2;
    nth_element(v.begin(), v.begin()+n, v.end());
    return v[n];
}
person Mike Seymour    schedule 12.11.2009
comment
Хм. Я не понимал, что nth_element существует, я, видимо, повторно реализовал это в своем ответе ... - person ephemient; 12.11.2009
comment
Следует отметить, что nth_element изменяет вектор непредсказуемым образом! При необходимости вы можете отсортировать вектор индексов. - person Matthieu M.; 12.11.2009
comment
Если количество элементов четное, медиана - это среднее значение средних двух. - person sje397; 02.07.2010
comment
@ sje397 true, этот алгоритм неверен в половине случаев, а именно, когда вектор содержит четное количество элементов. Является ли вызов функции nth_element 2 раза (для 2 средних элементов) более затратным, чем однократный вызов sort? Спасибо. - person Agostino; 21.01.2015
comment
Сортировка только половины вектора с помощью partial_sort () также выполняет свою работу и позволяет получить правильную медиану для векторов одинакового размера. Кто-нибудь проверял, насколько дорого стоит nth_element при двойном вызове? - person Fabian; 10.07.2018
comment
@Fabian partial_sort по-прежнему O (N * log (N)), а nth_element - O (N) (или O (2N), если выполняется дважды, что по-прежнему линейно), поэтому я ожидал бы, что nth_element будет быстрее при увеличении N, но я не сделали никакого анализа, чтобы подтвердить это. - person ClydeTheGhost; 05.12.2018
comment
@ sje397 согласно этому определению mathworld.wolfram.com/StatisticalMedian.html медианы верно, что этот текущий ответ неверен, когда размер вектора четный ... но просто для удовольствия посмотрим, что написано в числовых рецептах: Когда N четно, статистические книги определяют медиану как среднее арифметическое элементов k = N / 2 и k = N / 2 + 1 (то есть N / 2 снизу и N / 2 сверху). Если вы принимаете такую ​​педантичность, вы должны выполнить два отдельных выбора, чтобы найти эти элементы. Для N ›100 мы обычно определяем k = N / 2 как средний элемент, будь прокляты педанты. - person Alessandro Jacopson; 05.08.2020

Медиана сложнее, чем ответ Майка Сеймура. Медиана различается в зависимости от того, есть ли в выборке четное или нечетное количество элементов. Если количество заданий четное, медиана - это среднее значение двух средних элементов. Это означает, что медиана списка целых чисел может быть дробной. Наконец, медиана пустого списка не определена. Вот код, который проходит мои основные тестовые случаи:

///Represents the exception for taking the median of an empty list
class median_of_empty_list_exception:public std::exception{
  virtual const char* what() const throw() {
    return "Attempt to take the median of an empty list of numbers.  "
      "The median of an empty list is undefined.";
  }
};

///Return the median of a sequence of numbers defined by the random
///access iterators begin and end.  The sequence must not be empty
///(median is undefined for an empty set).
///
///The numbers must be convertible to double.
template<class RandAccessIter>
double median(RandAccessIter begin, RandAccessIter end) 
  throw(median_of_empty_list_exception){
  if(begin == end){ throw median_of_empty_list_exception(); }
  std::size_t size = end - begin;
  std::size_t middleIdx = size/2;
  RandAccessIter target = begin + middleIdx;
  std::nth_element(begin, target, end);

  if(size % 2 != 0){ //Odd number of elements
    return *target;
  }else{            //Even number of elements
    double a = *target;
    RandAccessIter targetNeighbor= target-1;
    std::nth_element(begin, targetNeighbor, end);
    return (a+*targetNeighbor)/2.0;
  }
}
person Eponymous    schedule 05.04.2010
comment
Я знаю, что это было давным-давно, но поскольку я только что нашел это в Google: std::nth_element фактически также гарантирует, что любые предыдущие элементы являются ‹= целью, а любые последующие элементы -› =. Таким образом, вы можете просто использовать targetNeighbor = std::min_element(begin, target) и пропустить частичную сортировку, что, вероятно, немного быстрее. (nth_element в среднем линейный, а min_element, очевидно, линейный.) И даже если вы предпочтете снова использовать nth_element, было бы эквивалентно и, вероятно, немного быстрее просто выполнить nth_element(begin, targetNeighbor, target). - person Danica; 09.02.2012
comment
@ Дугал, я так понимаю, вы имели в виду targetNeighbor = std::max_element(begin, target) в данном случае? - person izak; 09.05.2013
comment
@Dougal Я знаю, что этот комментарий был написан давно;), но я понятия не имею, как ваш подход должен работать, вы уверены, что это дает правильный результат? - person 463035818_is_not_a_number; 02.09.2016
comment
@ tobi303 Твоя вечность вдвое длиннее моей. :) И да, точно должно: дело в том, что после вызова std::nth_element последовательность похожа на [smaller_than_target, target, bigger_than_target]. Итак, вы знаете, что target-1-й элемент находится в первой половине массива, и вам нужно только найти максимум элементов перед target, чтобы получить медиану. - person Danica; 02.09.2016
comment
@ Дугал, теперь я понял. Спасибо - person 463035818_is_not_a_number; 02.09.2016
comment
Обратите внимание, что nth_element() не работает (по крайней мере, в g ++), когда target равно end(). Итак, у вас есть еще два особых случая: когда диапазон составляет всего 1 или 2 элемента. - person Alexis Wilke; 09.10.2018
comment
@AlexisWilke, почему это особые случаи? Если есть 1 элемент, то size=1, middleIdx=0, target=begin, поэтому nth_element не работает. Если есть 2 элемента, size=2, middleIdx=1, target=begin+1=end-1, то первый nth_element вызывается с (begin, end-1, end), а второй с (begin,begin,end). target равный `нигде не заканчивается. - person Ruslan; 16.05.2019

Этот алгоритм эффективно обрабатывает входные данные как четного, так и нечетного размера, используя алгоритм STL nth_element (амортизированный O (N)) и алгоритм max_element (O (n)). Обратите внимание, что nth_element имеет еще один гарантированный побочный эффект, а именно то, что все элементы до n гарантированно будут меньше v[n], но не обязательно отсортированы.

//post-condition: After returning, the elements in v may be reordered and the resulting order is implementation defined.
double median(vector<double> &v)
{
  if(v.empty()) {
    return 0.0;
  }
  auto n = v.size() / 2;
  nth_element(v.begin(), v.begin()+n, v.end());
  auto med = v[n];
  if(!(v.size() & 1)) { //If the set size is even
    auto max_it = max_element(v.begin(), v.begin()+n);
    med = (*max_it + med) / 2.0;
  }
  return med;    
}
person Matthew Fioravante    schedule 03.12.2015
comment
Мне нравится ваш ответ, но возвращение нуля при пустом векторе не подходит для моего приложения, где я бы предпочел исключение в случае пустого вектора. - person Alessandro Jacopson; 05.08.2020

Вот более полная версия ответа Майка Сеймура:

// Could use pass by copy to avoid changing vector
double median(std::vector<int> &v)
{
  size_t n = v.size() / 2;
  std::nth_element(v.begin(), v.begin()+n, v.end());
  int vn = v[n];
  if(v.size()%2 == 1)
  {
    return vn;
  }else
  {
    std::nth_element(v.begin(), v.begin()+n-1, v.end());
    return 0.5*(vn+v[n-1]);
  }
}

Он обрабатывает ввод нечетной или четной длины.

person Alec Jacobson    schedule 16.06.2014
comment
Для передачи по копии вы хотели удалить ссылку (&) на входе? - person chappjc; 17.06.2014
comment
Я просто имел в виду этот комментарий как примечание о том, что один может использовать передачу по копии, и в этом случае да, следует удалить &. - person Alec Jacobson; 17.06.2014
comment
В этой версии есть ошибка. Вам нужно извлечь v[n] перед повторным выполнением nth_element, потому что после второго раунда v[n] может содержать другое значение. - person Matthew Fioravante; 05.12.2014
comment
@MatthewFioravante, понятно. Согласно документам, я полагаю, что nth_element не обязательно должен быть стабильным. (соответственно отредактировал свой ответ). - person Alec Jacobson; 11.12.2014
comment
Вместо того, чтобы вызывать nth_element во второй раз, не было бы более эффективным просто выполнить итерацию от v[0] до v[n] и определить максимум в этой половине? - person bluenote10; 23.10.2016

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

template <typename T = double, typename C>
inline const T median(const C &the_container)
{
    std::vector<T> tmp_array(std::begin(the_container), 
                             std::end(the_container));
    size_t n = tmp_array.size() / 2;
    std::nth_element(tmp_array.begin(), tmp_array.begin() + n, tmp_array.end());

    if(tmp_array.size() % 2){ return tmp_array[n]; }
    else
    {
        // even sized vector -> average the two middle values
        auto max_it = std::max_element(tmp_array.begin(), tmp_array.begin() + n);
        return (*max_it + tmp_array[n]) / 2.0;
    }
}
person Croc Dialer    schedule 14.09.2016
comment
Как пишет Мэтью Фиораванте stackoverflow.com/questions/1719070/ упомянул, вам нужно извлечь v [n] перед повторным выполнением nth_element, потому что после второго раунда v [n ] может содержать другое значение. Итак, пусть med = tmp_array [n], тогда правильная строка возврата: return (* max_it + med) / 2.0; - person trig-ger; 19.01.2017
comment
@ trig-ger nth_element используется в этом решении только один раз. Это не проблема. - person denver; 31.01.2017
comment
static_assert(std::is_same_v<typename C::value_type, T>, "mismatched container and element types") может быть? - person einpoklum; 21.05.2021

Вы можете отсортировать std::vector с помощью библиотечной функции std::sort.

std::vector<int> vec;
// ... fill vector with stuff
std::sort(vec.begin(), vec.end());
person Charles Salvia    schedule 12.11.2009

Существует алгоритм выбора с линейным временем. Приведенный ниже код работает только тогда, когда в контейнере есть итератор с произвольным доступом, но его можно изменить для работы, вам просто нужно быть немного более осторожным, чтобы избежать ярлыков, таких как end - begin и iter + n.

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <vector>

template<class A, class C = std::less<typename A::value_type> >
class LinearTimeSelect {
public:
    LinearTimeSelect(const A &things) : things(things) {}
    typename A::value_type nth(int n) {
        return nth(n, things.begin(), things.end());
    }
private:
    static typename A::value_type nth(int n,
            typename A::iterator begin, typename A::iterator end) {
        int size = end - begin;
        if (size <= 5) {
            std::sort(begin, end, C());
            return begin[n];
        }
        typename A::iterator walk(begin), skip(begin);
#ifdef RANDOM // randomized algorithm, average linear-time
        typename A::value_type pivot = begin[std::rand() % size];
#else // guaranteed linear-time, but usually slower in practice
        while (end - skip >= 5) {
            std::sort(skip, skip + 5);
            std::iter_swap(walk++, skip + 2);
            skip += 5;
        }
        while (skip != end) std::iter_swap(walk++, skip++);
        typename A::value_type pivot = nth((walk - begin) / 2, begin, walk);
#endif
        for (walk = skip = begin, size = 0; skip != end; ++skip)
            if (C()(*skip, pivot)) std::iter_swap(walk++, skip), ++size;
        if (size <= n) return nth(n - size, walk, end);
        else return nth(n, begin, walk);
    }
    A things;
};

int main(int argc, char **argv) {
    std::vector<int> seq;
    {
        int i = 32;
        std::istringstream(argc > 1 ? argv[1] : "") >> i;
        while (i--) seq.push_back(i);
    }
    std::random_shuffle(seq.begin(), seq.end());
    std::cout << "unordered: ";
    for (std::vector<int>::iterator i = seq.begin(); i != seq.end(); ++i)
        std::cout << *i << " ";
    LinearTimeSelect<std::vector<int> > alg(seq);
    std::cout << std::endl << "linear-time medians: "
        << alg.nth((seq.size()-1) / 2) << ", " << alg.nth(seq.size() / 2);
    std::sort(seq.begin(), seq.end());
    std::cout << std::endl << "medians by sorting: "
        << seq[(seq.size()-1) / 2] << ", " << seq[seq.size() / 2] << std::endl;
    return 0;
}
person ephemient    schedule 12.11.2009

Вот ответ, который учитывает предложение @MatthieuM. т.е. не изменяет вектор ввода. Он использует одну частичную сортировку (по вектору индексов) для обоих диапазонов четной и нечетной мощности, в то время как пустые диапазоны обрабатываются с исключениями, создаваемыми векторным методом at:

double median(vector<int> const& v)
{
    bool isEven = !(v.size() % 2); 
    size_t n    = v.size() / 2;

    vector<size_t> vi(v.size()); 
    iota(vi.begin(), vi.end(), 0); 

    partial_sort(begin(vi), vi.begin() + n + 1, end(vi), 
        [&](size_t lhs, size_t rhs) { return v[lhs] < v[rhs]; }); 

    return isEven ? 0.5 * (v[vi.at(n-1)] + v[vi.at(n)]) : v[vi.at(n)];
}

Демо

person Lorah Attkins    schedule 27.02.2017

Armadillo имеет реализацию, которая выглядит так же, как в ответе https://stackoverflow.com/a/34077478 от https://stackoverflow.com/users/2608582/matthew-fioravante .com / users / 2608582 / matthew-fioravante.

Он использует один вызов nth_element и один вызов max_element, и он находится здесь: https://gitlab.com/conradsnicta/armadillo-code/-/blob/9.900.x/include/armadillo_bits/op_median_meat.hpp#L380

//! find the median value of a std::vector (contents is modified)
template<typename eT>
inline 
eT
op_median::direct_median(std::vector<eT>& X)
  {
  arma_extra_debug_sigprint();
  
  const uword n_elem = uword(X.size());
  const uword half   = n_elem/2;
  
  typename std::vector<eT>::iterator first    = X.begin();
  typename std::vector<eT>::iterator nth      = first + half;
  typename std::vector<eT>::iterator pastlast = X.end();
  
  std::nth_element(first, nth, pastlast);
  
  if((n_elem % 2) == 0)  // even number of elements
    {
    typename std::vector<eT>::iterator start   = X.begin();
    typename std::vector<eT>::iterator pastend = start + half;
    
    const eT val1 = (*nth);
    const eT val2 = (*(std::max_element(start, pastend)));
    
    return op_mean::robust_mean(val1, val2);
    }
  else  // odd number of elements
    {
    return (*nth);
    }
  }
person Alessandro Jacopson    schedule 05.08.2020

person    schedule
comment
Пожалуйста, попробуйте добавить пояснения при добавлении фрагмента кода - person Yunus Temurlenk; 07.07.2020