web-dev-qa-db-fra.com

Est-ce que list :: size () est vraiment O (n)?

Récemment, j'ai remarqué que certaines personnes mentionnent que std::list::size() a une complexité linéaire.
Selon certainessources , cela dépend en fait de la mise en œuvre car la norme ne dit pas quelle doit être la complexité.
Le commentaire dans cette entrée de blog dit:

En fait, cela dépend de la STL que vous utilisez. Microsoft Visual Studio V6 implémente size () comme {return (_Size); } alors que gcc (au moins dans les versions 3.3.2 et 4.1.0) le fait comme {return std :: distance (begin (), end ()); } Le premier a une vitesse constante, le second a o(N) vitesse

  1. Donc, je suppose que pour la foule de VC++, la fonction size() a une complexité constante, Dinkumware n’ayant probablement pas changé ce fait depuis VC6. Est-ce que je suis juste là?
  2. À quoi ressemble-t-il actuellement dans gcc? Si c'est vraiment O (n), pourquoi les développeurs ont-ils choisi de le faire?
58
foraidt

Réponse pré-C++ 11

Vous avez raison de dire que la norme n'indique pas quelle doit être la complexité de list :: size (). Toutefois, il est recommandé qu'elle "ait une complexité constante" (remarque A du tableau 65).

Voici un article intéressant de Howard Hinnant qui explique pourquoi certaines personnes pensent que la liste: size () devrait présenter une complexité O(N) (essentiellement parce qu’elles croient que cette liste O(1): : size () rend list :: splice () avoir O(N) complexité) et pourquoi une O(1) list :: size () est une bonne idée (de l'avis de l'auteur) :

Je pense que les points principaux dans le papier sont:

  • il y a peu de situations où maintenir un compte interne pour que list::size() puisse être O(1) rend l'opération d'épissure devenir linéaire 
  • il y a probablement beaucoup d'autres situations dans lesquelles une personne peut ne pas être consciente des effets négatifs pouvant survenir du fait qu'elle appelle une O(N) size() (comme son exemple où list::size() est appelé alors qu'il est verrouillé).
  • qu'au lieu de permettre à size() d'être O (N), dans l'intérêt de la «moindre surprise», la norme devrait imposer à tout conteneur implémentant size() de le mettre en œuvre de manière O(1). Si un conteneur ne peut pas faire cela, il ne devrait pas implémenter size() du tout. Dans ce cas, l'utilisateur du conteneur sera averti que size() n'est pas disponible et, s'il souhaite ou doit toujours obtenir le nombre d'éléments du conteneur, il peut toujours utiliser container::distance( begin(), end()) pour obtenir cette valeur, mais il en sera parfaitement conscient. qu'il s'agit d'une opération O(N).

Je pense que j'ai tendance à être d'accord avec la plupart de ses raisonnements. Cependant, je n'aime pas son ajout proposé à la surcharge splice(). Devoir passer une n qui doit être égale à distance( first, last) pour obtenir un comportement correct semble être une recette pour un diagnostic difficile à identifier.

Je ne suis pas sûr de ce qui devrait ou devrait être fait pour aller de l'avant, car tout changement aurait un impact significatif sur le code existant. Mais dans l'état actuel des choses, je pense que le code existant est déjà affecté - le comportement peut être assez différent d'une implémentation à une autre pour une chose qui aurait dû être bien définie. Peut-être que le commentaire de onebyone à propos de la taille 'mise en cache' et marqué connue/inconnue pourrait bien fonctionner - vous obtenez un comportement amorti O(1) - la seule fois où vous obtenez un comportement O(N) est modifié par certaines opérations splice (). La bonne chose à ce sujet est que cela peut être fait par les implémenteurs aujourd'hui sans modification de la norme (à moins que quelque chose ne me manque). 

Autant que je sache, C++ 0x ne change rien dans ce domaine.

50
Michael Burr

En C++ 11, il est nécessaire que, pour tout conteneur standard, l'opération .size() soit complète dans une complexité "constante" (O (1)). (Tableau 96 - Exigences relatives aux conteneurs). Auparavant dans C++ 03 .size()devrait avoir une complexité constante, mais n'est pas obligatoire (voir Est-ce que std :: string size () est une opération O(1)? ) . 

Le changement de norme est introduit par n2923: Spécification de la complexité de size () (Révision 1)

Cependant, l'implémentation de .size() dans libstdc ++ utilise toujours un algorithme O(N) dans gcc jusqu'à la 4.8:

  /**  Returns the number of elements in the %list.  */
  size_type
  size() const _GLIBCXX_NOEXCEPT
  { return std::distance(begin(), end()); }

Voir aussi Pourquoi std :: list est-il plus gros sur c ++ 11? pour les détails, pourquoi est-il conservé de cette façon.

Update: std::list::size() est correctement O(1) lors de l'utilisation de gcc 5.0 en mode C++ 11 (ou supérieur) ).


À propos, la .size() dans libc ++ est correctement O (1):

_LIBCPP_INLINE_VISIBILITY
size_type size() const _NOEXCEPT     {return base::__sz();}

...

__compressed_pair<size_type, __node_allocator> __size_alloc_;

_LIBCPP_INLINE_VISIBILITY
const size_type& __sz() const _NOEXCEPT
    {return __size_alloc_.first();}
65
kennytm

J'ai déjà eu à regarder la liste :: taille de gcc 3.4 :: taille, je peux donc dire ceci:

  1. il utilise std :: distance (tête, queue)
  2. std :: distance a deux implémentations: pour les types qui satisfont RandomAccessIterator, il utilise "tail-head", et pour les types qui satisfont simplement InputIterator, il utilise un algorithme O(n) reposant sur "iterator ++", compter jusqu'à atteindre la queue donnée.
  3. std :: list ne spécifie pas RandomAccessIterator, la taille est donc O (n).

En ce qui concerne le "pourquoi", je peux seulement dire que std :: list est approprié pour les problèmes nécessitant un accès séquentiel. Stocker la taille en tant que variable de classe introduirait des frais généraux sur chaque insertion, suppression, etc., et ce gaspillage est un gros non-non selon l'intention du TSL. Si vous avez vraiment besoin d'une taille () constante, utilisez std :: deque.

14
introp

Personnellement, je ne vois pas le problème avec l'épissure étant O(N) comme la seule raison pour laquelle la taille est autorisée à être O (N). Vous ne payez pas pour ce que vous n'utilisez pas est une devise importante pour C++. Dans ce cas, le maintien de la taille de la liste nécessite une incrémentation/réduction supplémentaire à chaque insertion/effacement, que vous vérifiiez ou non la taille de la liste. C'est un petit surcoût fixe, mais il est toujours important de le prendre en compte.

Vérifier la taille d'une liste est rarement nécessaire. Itérer du début à la fin sans se soucier de la taille totale est infiniment plus commun.

11
Greg Rogers

Je voudrais aller à la source . La page STL de SGI indique qu'il est permis d'avoir une complexité linéaire. Je pense que la directive de conception qu'ils ont suivie était de permettre à la mise en œuvre de la liste d'être aussi générale que possible et donc de permettre plus de flexibilité dans l'utilisation des listes.

4
Yuval F

Ce rapport de bogue: [C++ 0x] std :: list :: taille complexité , capture avec des détails atroces le fait que la mise en oeuvre dans GCC 4.x est un temps linéaire et comment le passage à un temps constant pour C + +11 a été lent à venir (disponible dans la version 5.0) en raison de problèmes de compatibilité ABI.

La page de manuel de la série GCC 4.9 inclut toujours la clause de non-responsabilité suivante:

La prise en charge de C++ 11 est encore Expérimentale et pourrait changer de manière incompatible avec les versions futures.


Le même rapport de bogue est référencé ici: Est-ce que std :: list :: size doit avoir une complexité constante en C++ 11?

1
nobar

Si vous utilisez correctement les listes, vous ne remarquerez probablement aucune différence.

Les listes conviennent aux structures de données volumineuses que vous souhaitez réorganiser sans les copier, et aux données que vous souhaitez conserver des pointeurs valides après leur insertion.

Dans le premier cas, cela ne fait aucune différence, dans le second, je préférerais l'ancienne implémentation de size () plus petite.

Quoi qu'il en soit, std est plus une question de correction et de comportement standard et de "convivialité" que de vitesse brute.

0
Luke Givens