web-dev-qa-db-fra.com

Comment nth_element est-il implémenté?

Il y a beaucoup de revendications sur StackOverflow et ailleurs qui nth_element est O (n) et qu'il est généralement implémenté avec Introselect: http://en.cppreference.com/w/ cpp/algorithme/nth_element

Je veux savoir comment cela peut être réalisé. J'ai regardé l'explication de Wikipedia sur Introselect et cela m'a laissé plus confus. Comment un algorithme peut-il basculer entre QSort et Median-of-Medians?

J'ai trouvé le document Introsort ici: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.14.5196&rep=rep1&type=pdf Mais cela dit:

Dans cet article, nous nous concentrons sur le problème de tri et revenons brièvement sur le problème de sélection dans une section ultérieure.

J'ai essayé de lire la STL elle-même pour comprendre comment nth_element est implémenté, mais qui devient poilu très rapidement.

Quelqu'un pourrait-il me montrer un pseudo-code pour la mise en œuvre d'Introselect? Ou encore mieux, du vrai code C++ autre que la STL bien sûr :)

22
Jonathan Mee

Vous avez posé deux questions, la titulaire

Comment nth_element est-il implémenté?

Ce à quoi vous avez déjà répondu:

Il y a beaucoup de revendications sur StackOverflow et ailleurs que nth_element est O(n) et qu'il est généralement implémenté avec Introselect.

Ce que je peux également confirmer en regardant mon implémentation stdlib. (Plus d'informations à ce sujet plus tard.)

Et celui où vous ne comprenez pas la réponse:

Comment un algorithme peut-il basculer entre QSort et Median-of-Medians?

Jetons un coup d'œil au pseudo-code que j'ai extrait de mon stdlib:

nth_element(first, nth, last)
{ 
  if (first == last || nth == last)
    return;

  introselect(first, nth, last, log2(last - first) * 2);
}

introselect(first, nth, last, depth_limit)
{
  while (last - first > 3)
  {
      if (depth_limit == 0)
      {
          // [NOTE by editor] This should be median-of-medians instead.
          // [NOTE by editor] See Azmisov's comment below
          heap_select(first, nth + 1, last);
          // Place the nth largest element in its final position.
          iter_swap(first, nth);
          return;
      }
      --depth_limit;
      cut = unguarded_partition_pivot(first, last);
      if (cut <= nth)
        first = cut;
      else
        last = cut;
  }
  insertion_sort(first, last);
}

Sans entrer dans les détails sur les fonctions référencées heap_select Et unguarded_partition_pivot Nous pouvons clairement voir que nth_element Donne introsélectionnez 2 * log2(size) étapes de subdivision (deux fois plus que nécessaire) par quickselect dans le meilleur des cas) jusqu'à ce que heap_select entre en jeu et résout définitivement le problème.

15
Nobody

Avertissement: je ne sais pas comment std::nth_element est implémenté dans n'importe quelle bibliothèque standard.

Si vous savez comment fonctionne Quicksort, vous pouvez facilement le modifier pour faire ce qui est nécessaire pour cet algorithme. L'idée de base de Quicksort est qu'à chaque étape, vous partitionnez le tableau en deux parties de sorte que tous les éléments inférieurs au pivot se trouvent dans le sous-tableau de gauche et que tous les éléments égaux ou supérieurs au pivot se trouvent dans le sous-tableau de droite . (Une modification de Quicksort connue sous le nom de Quicksort ternaire crée un troisième sous-tableau avec tous les éléments égaux au pivot. Ensuite, le sous-tableau de droite ne contient que des entrées strictement supérieures au pivot.) Quicksort procède ensuite en triant récursivement les sous-marins gauche et droit -les tableaux.

Si vous souhaitez uniquement déplacer l'élément n - e en place, au lieu de revenir dans les sous-tableaux both, vous pouvez dire à chaque étape si vous devrez descendre dans le sous-tableau gauche ou droit. (Vous le savez parce que le n - e élément d'un tableau trié a un index n donc il s'agit de comparer les indices.) Donc - à moins que votre Quicksort ne subisse le pire des cas dégénérescence - vous divisez par deux la taille du tableau restant à chaque étape. (Vous ne regardez plus jamais l'autre sous-tableau.) Par conséquent, en moyenne, vous avez affaire à des tableaux de longueurs suivantes à chaque étape:

  1. Θ ( N )
  2. Θ ( N /2)
  3. Θ ( N /4)

Chaque étape est linéaire dans la longueur du tableau dont elle traite. (Vous bouclez dessus une fois et décidez dans quel sous-tableau chaque élément doit aller en fonction de la façon dont il se compare au pivot.)

Vous pouvez voir qu'après les étapes Θ (log ( N )), nous finirons par atteindre un tableau singleton et nous aurons terminé. Si vous résumez N (1 + 1/2 + 1/4 +…), vous obtiendrez 2 N . Ou, dans le cas moyen, puisque nous ne pouvons pas espérer que le pivot sera toujours exactement la médiane, quelque chose de l'ordre de Θ ( N ).

12
5gon12eder

Le code de la STL (version 3.3, je pense) est le suivant:

template <class _RandomAccessIter, class _Tp>
void __nth_element(_RandomAccessIter __first, _RandomAccessIter __nth,
                   _RandomAccessIter __last, _Tp*) {
  while (__last - __first > 3) {
    _RandomAccessIter __cut =
      __unguarded_partition(__first, __last,
                            _Tp(__median(*__first,
                                         *(__first + (__last - __first)/2),
                                         *(__last - 1))));
    if (__cut <= __nth)
      __first = __cut;
    else 
      __last = __cut;
  }
  __insertion_sort(__first, __last);
}

Simplifions un peu cela:

template <class Iter, class T>
void nth_element(Iter first, Iter nth, Iter last) {
  while (last - first > 3) {
    Iter cut =
      unguarded_partition(first, last,
                          T(median(*first,
                                   *(first + (last - first)/2),
                                   *(last - 1))));
    if (cut <= nth)
      first = cut;
    else 
      last = cut;
  }
  insertion_sort(first, last);
}

Ce que j'ai fait ici a été de supprimer les doubles soulignements et les trucs _Uppercase, ce qui est uniquement pour protéger le code des choses que l'utilisateur pourrait légalement définir comme macros. J'ai également supprimé le dernier paramètre, qui est uniquement censé aider à la déduction du type de modèle, et renommé le type d'itérateur par souci de concision.

Comme vous devriez le voir maintenant, il partitionne la plage à plusieurs reprises jusqu'à ce qu'il reste moins de quatre éléments dans la plage restante, qui est ensuite simplement triée.

Maintenant, pourquoi est-ce O (n)? Premièrement, le tri final de jusqu'à trois éléments est O (1), en raison du maximum de trois éléments. Maintenant, ce qui reste, c'est le partitionnement répété. Le partitionnement en soi est O (n). Ici cependant, chaque étape divise par deux le nombre d'éléments à toucher à l'étape suivante, vous avez donc O(n) + O(n/2) + O(n/4) + O(n/8) qui est inférieur à O(2n) si vous le résumez. Puisque O(2n) = O (n), vous avez en moyenne une complexité linéaire.

8
Ulrich Eckhardt