web-dev-qa-db-fra.com

Pourquoi std :: list :: reverse a-t-il une complexité O(n))?

Pourquoi l'inverse fonctionne-t-il pour le std::list La classe dans la bibliothèque standard C++ a un runtime linéaire? Je pense que pour les listes à double liaison, la fonction inverse aurait dû être O (1).

Inverser une liste à double lien ne devrait impliquer que le changement de pointeur.

187
Curious

Hypothétiquement, reverse aurait pu être O (1). Là encore (encore hypothétiquement), il pourrait y avoir un membre de la liste booléenne indiquant si l'orientation de la liste liée est identique ou opposée à celle de la liste d'origine.

Malheureusement, cela réduirait les performances de toute autre opération (sans toutefois modifier le temps d'exécution asymptotique). Dans chaque opération, il faudrait consulter un booléen pour déterminer s'il convient de suivre un pointeur "suivant" ou "précédent" d'un lien.

Comme il s'agissait probablement d'une opération relativement peu fréquente, la norme (qui ne dicte pas les mises en œuvre, mais uniquement la complexité), spécifiait que la complexité pouvait être linéaire. Cela permet aux "prochains" pointeurs de toujours désigner la même direction sans ambiguïté, ce qui accélère les opérations courantes.

182
Ami Tavory

Il pourrait être O(1) si la liste stocke un drapeau qui permet d’échanger la signification des pointeurs "prev" et "next" de chaque nœud. Si inverser la liste était une opération fréquente, un tel ajout pourrait en fait être utile et je ne connais aucune raison pour laquelle son implémentation serait interdite par la norme actuelle. Cependant, avoir un tel drapeau rendrait plus onéreuse la traversée ordinaire de la liste (ne serait-ce que par un facteur constant) car au lieu de

current = current->next;

dans le operator++ de la liste itérateur, vous obtiendrez

if (reversed)
  current = current->prev;
else
  current = current->next;

ce que vous ne voudriez pas ajouter facilement. Etant donné que les listes sont généralement parcourues beaucoup plus souvent qu'elles ne sont inversées, il serait très peu judicieux que le standard ( prescrive cette technique. Par conséquent, l'opération inverse peut avoir une complexité linéaire. Notez cependant que t ∈ O(1) ⇒ t ∈ O(n) donc, comme mentionné précédemment, la mise en œuvre technique de votre "optimisation" serait autorisée.

Si vous venez d'un Java ou d'un arrière-plan similaire, vous pourriez vous demander pourquoi l'itérateur doit vérifier l'indicateur à chaque fois. Ne pourrions-nous pas plutôt avoir deux types d'itérateurs distincts, tous deux dérivés d'une base commune tapez et avez std::list::begin et std::list::rbegin retourne polymorphiquement l'itérateur approprié? Bien que possible, cela aggraverait encore la situation car faire avancer l'itérateur constituerait un appel de fonction indirect (difficile à intégrer). De toute façon, en Java, vous payez régulièrement ce prix, mais c’est là une des raisons pour lesquelles de nombreuses personnes optent pour le C++ lorsque les performances sont critiques.

Comme souligné par Benjamin Lindley dans les commentaires, puisque reverse ne permet pas d'invalider les itérateurs, la seule approche autorisée par la norme semble être de stocker un pointeur dans la liste à l'intérieur l'itérateur qui provoque un accès mémoire indirect double.

59
5gon12eder

Sûrement puisque tous les conteneurs qui supportent les itérateurs bidirectionnels ont le concept de rbegin () et rend (), cette question est sans objet.

Il est trivial de créer un proxy qui inverse les itérateurs et d'accéder au conteneur par ce biais.

Cette non-opération est bien O (1).

tel que:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

production attendue:

mat
the
on
sat
cat
the

Compte tenu de cela, il me semble que le comité de normalisation n'a pas pris de temps pour imposer l'ordre inverse O(1) du conteneur, car ce n'est pas nécessaire, et la bibliothèque standard repose en grande partie sur le principe de mandater uniquement est strictement nécessaire tout en évitant les doubles emplois.

Juste mon 2c.

37
Richard Hodges

Parce qu'il doit traverser chaque nœud (n total) et mettre à jour leurs données (l'étape de mise à jour est bien O(1)). Cela rend toute l'opération O(n*1) = O(n).

18
Blindy

Il échange également les pointeurs précédent et suivant pour chaque nœud. Thats pourquoi il faut linéaire. Bien que cela puisse être fait dans O(1) si la fonction utilisant cette LL prend également des informations sur LL en entrée, comme si elle accédait normalement ou inversement.

2
techcomp

Seulement une explication d'algorithme. Imaginez que vous ayez un tableau avec des éléments, puis vous devez l’inverser. L'idée de base est d'effectuer une itération sur chaque élément en modifiant l'élément de la première position à la dernière position, l'élément de la deuxième position à l'avant-dernière position, etc. Lorsque vous atteignez au milieu du tableau, tous les éléments sont modifiés, donc dans (n ​​/ 2) itérations, ce qui est considéré comme O (n).

1
danilobraga

C’est O(n) simplement parce qu’il faut copier la liste dans l’ordre inverse. Chaque opération d’élément individuel est O(1) mais il y a n eux dans la liste complète.

Bien sûr, il y a des opérations à temps constant impliquées dans la configuration de l'espace pour la nouvelle liste, puis dans le changement des pointeurs par la suite, etc. La notation O ne prend pas en compte les constantes individuelles une fois que vous avez inclus un facteur n de premier ordre.

1
SDsolar