Pourquoi la fonction inverse de la std::list
classe dans la bibliothèque standard C ++ a-t-elle une exécution linéaire? Je pense que pour les listes à double liaison, la fonction inverse aurait dû être O (1).
Inverser une liste à double lien devrait simplement impliquer de changer la tête et les pointeurs de queue.
c++
c++11
stl
linked-list
Curieuse
la source
la source
Reverse
fonction soit implémentée dans O (1)?Réponses:
Hypothétiquement,
reverse
aurait pu être O (1) . Il pourrait y avoir (encore une fois hypothétiquement) un membre de liste booléenne indiquant si la direction de la liste chaînée est actuellement la même ou opposée à celle d'origine où la liste a été créée.Malheureusement, cela réduirait les performances de pratiquement toute autre opération (mais sans changer le runtime asymptotique). Dans chaque opération, un booléen devrait être consulté pour déterminer s'il faut suivre un pointeur "suivant" ou "précédent" d'un lien.
Comme cela était vraisemblablement considéré comme une opération relativement peu fréquente, la norme (qui ne dicte pas les implémentations, seulement la complexité), spécifiait que la complexité pouvait être linéaire. Cela permet aux pointeurs "suivants" de toujours signifier la même direction sans ambiguïté, accélérant ainsi les opérations courantes.
la source
reverse
de laO(1)
complexité sans affecter le big-o de toute autre opération , en utilisant cette astuce booléenne. Mais, en pratique, une branche supplémentaire dans chaque opération est coûteuse, même si c'est techniquement O (1). En revanche, vous ne pouvez pas créer une structure de liste dans laquellesort
est O (1) et toutes les autres opérations ont le même coût. Le point de la question est que, apparemment, vous pouvez obtenir l'O(1)
inverse gratuitement si vous ne vous souciez que du grand O, alors pourquoi n'ont-ils pas fait cela.std::uintptr_t
. Ensuite, vous pouvez les xor.std::uintptr_t
, vous pouvez convertir en unchar
tableau, puis XOR les composants. Ce serait plus lent mais 100% portable. Vous pourriez probablement le faire choisir entre ces deux implémentations et n'utiliser la seconde comme solution de secours que si elleuintptr_t
est manquante. Certains s'il est décrit dans cette réponse: stackoverflow.com/questions/14243971/…Ce pourrait être O (1) si la liste stockait un drapeau qui permet d'échanger la signification des pointeurs «
prev
» et «next
» de chaque nœud. Si l'inversion de la liste était une opération fréquente, un tel ajout pourrait en fait être utile et je ne connais aucune raison pour laquelle sa mise en œuvre serait interdite par la norme actuelle. Cependant, avoir un tel drapeau rendrait la traversée ordinaire de la liste plus coûteuse (ne serait-ce que par un facteur constant) car au lieu dedans l'
operator++
itérateur de la liste, vous obtiendrezce qui n'est pas quelque chose que vous décidez d'ajouter facilement. Étant donné que les listes sont généralement parcourues beaucoup plus souvent qu'elles ne sont inversées, il serait très imprudent pour la norme d' imposer 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, implémenter techniquement votre «optimisation» serait autorisé.
Si vous venez d'un environnement Java ou similaire, vous vous demandez peut-être pourquoi l'itérateur doit vérifier l'indicateur à chaque fois. Ne pourrions-nous pas avoir à la place deux types d'itérateurs distincts, tous deux dérivés d'un type de base commun, et avoir
std::list::begin
etstd::list::rbegin
renvoyer de manière polymorphe l'itérateur approprié? Bien que possible, cela rendrait le tout encore pire car l'avancement de l'itérateur serait un appel de fonction indirect (difficile à intégrer) maintenant. En Java, vous payez ce prix régulièrement de toute façon, mais là encore, c'est l'une des raisons pour lesquelles de nombreuses personnes optent pour le C ++ lorsque les performances sont critiques.Comme le souligne Benjamin Lindley dans les commentaires, puisqu'il
reverse
n'est pas permis d'invalider les itérateurs, la seule approche permise par le standard semble être de stocker un pointeur vers la liste à l'intérieur de l'itérateur ce qui provoque un double accès mémoire indirect.la source
std::list::reverse
n'invalide pas les itérateurs.next
etprev
dans un tableau et stockez la direction sous forme de0
ou1
. Pour itérer vers l'avant, vous suivriezpointers[direction]
et itéreriez à l'enverspointers[1-direction]
(ou vice versa). Cela ajouterait encore un tout petit peu de frais généraux, mais probablement moins qu'une branche.swap()
est spécifié comme étant un temps constant et n'invalide aucun itérateur.Puisque tous les conteneurs qui prennent en charge les itérateurs bidirectionnels ont le concept de rbegin () et de 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 via cela.
Cette non-opération est bien O (1).
tel que:
production attendue:
Compte tenu de cela, il me semble que le comité des normes n'a pas pris le temps de mandater O (1) l'ordre inverse du conteneur parce que ce n'est pas nécessaire, et la bibliothèque standard est en grande partie construite sur le principe de ne rendre obligatoire que ce qui est strictement nécessaire tout en éviter la duplication.
Juste mon 2c.
la source
Parce qu'il doit traverser chaque nœud (
n
total) et mettre à jour leurs données (l'étape de mise à jour l'est en effetO(1)
). Cela rend l'opération entièreO(n*1) = O(n)
.la source
Il échange également le pointeur précédent et suivant pour chaque nœud. C'est pourquoi il faut Linear. Bien que cela puisse être fait dans O (1) si la fonction utilisant ce LL prend également des informations sur LL comme entrée, comme si elle accède normalement ou inversement.
la source
Seulement une explication d'algorithme. Imaginez que vous avez un tableau avec des éléments, alors vous devez l'inverser. L'idée de base est d'itérer sur chaque élément en changeant l'élément de la première position en dernière position, l'élément en seconde position en avant-dernière position, et ainsi de suite. Lorsque vous atteignez le milieu du tableau, vous aurez tous les éléments modifiés, donc en (n / 2) itérations, ce qui est considéré comme O (n).
la source
C'est O (n) simplement parce qu'il doit copier la liste dans l'ordre inverse. Chaque opération d'élément individuel est O (1) mais il y en a n dans la liste entière.
Bien sûr, il y a des opérations à temps constant impliquées dans la configuration de l'espace pour la nouvelle liste, et 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.
la source