Disons que je dois récupérer la médiane d'une séquence de 1000000 valeurs numériques aléatoires.
Si vous utilisez quelque chose maisstd::list
, Je n'ai aucun moyen (intégré) de trier la séquence pour le calcul médian.
Si vous utilisez std::list
, Je ne peux pas accéder aléatoirement aux valeurs pour récupérer le milieu (médiane) de la séquence triée.
Est-il préférable de mettre en œuvre le tri moi-même et d'aller avec par exemple std::vector
, ou est-il préférable d'utiliser std::list
et utilise std::list::iterator
pour for-loop-walk jusqu'à la valeur médiane? Ce dernier semble moins lourd, mais se sent aussi plus laid.
Ou existe-t-il des alternatives plus nombreuses et meilleures pour moi?
Tout conteneur à accès aléatoire (comme std::vector
) peut être trié avec la norme std::sort
algorithme, disponible dans le <algorithm>
entête.
Pour trouver la médiane, il serait plus rapide d'utiliser std::nth_element
; cela fait assez de tri pour mettre un élément choisi dans la bonne position, mais ne trie pas complètement le conteneur. Vous pouvez donc trouver la médiane comme ceci:
int median(vector<int> &v)
{
size_t n = v.size() / 2;
nth_element(v.begin(), v.begin()+n, v.end());
return v[n];
}
La médiane est plus complexe que la réponse de Mike Seymour. La médiane diffère selon qu'il y a un nombre pair ou impair d'articles dans l'échantillon. S'il y a un nombre pair d'éléments, la médiane est la moyenne des deux éléments du milieu. Cela signifie que la médiane d'une liste d'entiers peut être une fraction. Enfin, la médiane d'une liste vide n'est pas définie. Voici le code qui passe mes cas de test de base:
///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;
}
}
Voici une version plus complète de la réponse de Mike Seymour:
// 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]);
}
}
Il gère les entrées de longueur paire ou impaire.
Cet algorithme gère efficacement les entrées paires et impaires en utilisant l'algorithme STL nth_element (amorti O(N)) et l'algorithme max_element (O (n)). Notez que nth_element a un autre effet secondaire garanti , à savoir que tous les éléments avant n
sont tous garantis inférieurs à v[n]
, mais pas nécessairement trié.
//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;
}
Vous pouvez trier un std::vector
en utilisant la fonction de bibliothèque std::sort
.
std::vector<int> vec;
// ... fill vector with stuff
std::sort(vec.begin(), vec.end());
rassemblant toutes les idées de ce fil, j'ai fini par avoir cette routine. il fonctionne avec n'importe quel conteneur stl ou n'importe quelle classe fournissant des itérateurs d'entrée et gère les conteneurs de tailles impaires et paires. Il fait également son travail sur une copie du conteneur, pour ne pas modifier le contenu d'origine.
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;
}
}
Il existe un algorithme de sélection à temps linéaire . Le code ci-dessous ne fonctionne que lorsque le conteneur a un itérateur à accès aléatoire, mais il peut être modifié pour fonctionner sans - vous devrez juste être un peu plus prudent pour éviter les raccourcis comme end - begin
et 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;
}
Voici une réponse qui prend en compte la suggestion de @MatthieuM. c'est-à-dire que ne modifie pas le vecteur d'entrée . Il utilise un seul tri partiel (sur un vecteur d'indices) pour les deux plages de cardinalité paire et impaire, tandis que les plages vides sont gérées avec des exceptions levées par la méthode at
d'un vecteur:
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)];
}