Perché std :: list :: reverse ha complessità O (n)?


192

Perché la funzione inversa per la std::listclasse nella libreria standard C ++ ha un runtime lineare? Penso che per elenchi doppiamente collegati la funzione inversa avrebbe dovuto essere O (1).

L'inversione di un elenco doppiamente collegato dovrebbe comportare solo il cambio dei puntatori di testa e coda.


18
Non capisco perché le persone stiano votando a fondo questa domanda. È una domanda perfettamente ragionevole da porre. L'inversione di un elenco doppiamente collegato dovrebbe richiedere O (1) tempo.
Curioso

46
sfortunatamente, alcune persone confondono i concetti di "la domanda è buona" con "la domanda ha una buona idea". Adoro domande come questa, in cui fondamentalmente "la mia comprensione sembra diversa da una pratica comunemente accettata, ti preghiamo di aiutare a risolvere questo conflitto", perché espandere il modo in cui pensi ti aiuta a risolvere molti più problemi lungo la strada! Sembrerebbe che altri adottino l'approccio di "questo è uno spreco di elaborazione nel 99,9999% dei casi, non ci pensare nemmeno". Se è una consolazione, sono stato sottoposto a downgrade per molto, molto meno!
corsiKa

4
Sì, questa domanda ha ottenuto una quantità eccessiva di voti negativi per la sua qualità. Probabilmente è lo stesso di chi ha votato per la risposta di Blindy. In tutta onestà, "invertire un elenco doppiamente collegato dovrebbe comportare solo il cambio dei puntatori di testa e coda" non è generalmente vero per l'impl standard di elenchi collegati che tutti imparano nelle scuole superiori o per molte implementazioni che le persone usano. Molte volte nella reazione istintiva delle persone SO alla domanda o alla risposta spinge la decisione di voto positivo / negativo. Se tu fossi stato più chiaro in quella frase o l'avessi omessa, penso che avresti ottenuto meno voti negativi.
Chris Beck,

3
O lasciatemi mettere l'onere della prova con voi, @Curious: ho preparato un'implementazione dell'elenco doppiamente collegata qui: ideone.com/c1HebO . Puoi indicare come ti aspetti che la Reversefunzione venga implementata in O (1)?
CompuChip,

2
@CompuChip: in realtà, a seconda dell'implementazione, potrebbe non esserlo. Non hai bisogno di un valore booleano aggiuntivo per sapere quale puntatore utilizzare: basta usare quello che non punta indietro a te ... che potrebbe essere automatico con un elenco collegato XOR tra l'altro. Quindi sì, dipende da come viene implementata la lista e l'istruzione OP può essere chiarita.
Matthieu M.,

Risposte:


187

Ipoteticamente, reverseavrebbe potuto essere O (1) . Lì (di nuovo ipoteticamente) avrebbe potuto esserci un membro dell'elenco booleano che indica se la direzione dell'elenco collegato è attualmente la stessa o opposta a quella originale in cui è stato creato l'elenco.

Sfortunatamente, ciò ridurrebbe le prestazioni di praticamente qualsiasi altra operazione (anche se senza cambiare il runtime asintotico). In ogni operazione, un booleano dovrebbe essere consultato per valutare se seguire un puntatore "successivo" o "precedente" di un collegamento.

Dato che questo era presumibilmente considerato un'operazione relativamente poco frequente, lo standard (che non impone implementazioni, solo complessità), specificava che la complessità poteva essere lineare. Ciò consente ai puntatori "successivi" di indicare sempre la stessa direzione in modo inequivocabile, accelerando le operazioni del caso comune.


29
@MooseBoys: non sono d'accordo con la tua analogia. La differenza è che, nel caso di una lista, l'attuazione potrebbe fornire reversecon O(1)la complessità senza influenzare il big-o di qualsiasi altra operazione , utilizzando questo booleano bandiera trucco. Ma, in pratica, un ramo aggiuntivo in ogni operazione è costoso, anche se tecnicamente è O (1). Al contrario, non è possibile creare una struttura di elenco in cui sortè O (1) e tutte le altre operazioni hanno lo stesso costo. Il punto della domanda è che, apparentemente, puoi ottenere il O(1)contrario gratuitamente se ti preoccupi solo della grande O, quindi perché non l'hanno fatto.
Chris Beck,

9
Se si utilizzava un elenco collegato XOR, l'inversione diventerebbe tempo costante. Un iteratore sarebbe più grande, e aumentarlo / decrementarlo sarebbe leggermente più costoso dal punto di vista computazionale. Ciò potrebbe essere sminuito dagli inevitabili accessi alla memoria per qualsiasi tipo di elenco collegato.
Deduplicatore,

3
@IlyaPopov: ogni nodo ha davvero bisogno di questo? L'utente non pone mai domande al nodo elenco stesso, ma solo al corpo dell'elenco principale. Quindi l'accesso al booleano è facile per qualsiasi metodo che l'utente chiama. È possibile stabilire la regola secondo cui gli iteratori vengono invalidati se l'elenco viene invertito e / o archiviare una copia del valore booleano con l'iteratore, ad esempio. Quindi penso che non influenzerebbe necessariamente la grande O, necessariamente. Devo ammettere che non ho analizzato le specifiche. :)
Chris Beck,

4
@Kevin: Hm, cosa? Non puoi xor due puntatori direttamente in ogni caso, devi prima convertirli in numeri interi (ovviamente di tipo std::uintptr_t. Quindi puoi xorarli.
Deduplicatore

3
@Kevin, potresti sicuramente creare un elenco di link XOR in C ++, è praticamente il linguaggio poster-child per questo tipo di cose. Nulla dice che devi usare std::uintptr_t, puoi eseguire il cast su un chararray e quindi XOR i componenti. Sarebbe più lento ma portatile al 100%. Probabilmente potresti farlo selezionare tra queste due implementazioni e utilizzare la seconda solo come fallback se uintptr_tmanca. Alcuni se è descritto in questa risposta: stackoverflow.com/questions/14243971/…
Chris Beck,

61

Si potrebbe essere O (1) se la lista potrebbe memorizzare un flag che permette di scambiare il significato del “ prev” e “ next” puntatori ogni nodo ha. Se invertire l'elenco sarebbe un'operazione frequente, tale aggiunta potrebbe in effetti essere utile e non conosco alcun motivo per cui implementarla sarebbe vietata dalla norma attuale. Tuttavia, avere una tale bandiera renderebbe la corsa ordinaria dell'elenco più costosa (se non altro per un fattore costante) perché invece di

current = current->next;

nella operator++lista dell'iteratore, otterresti

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

che non è qualcosa che potresti decidere di aggiungere facilmente. Dato che gli elenchi vengono solitamente attraversati molto più spesso di quanto non siano invertiti, sarebbe poco saggio che lo standard imponga questa tecnica. Pertanto, l'operazione inversa può avere una complessità lineare. Si noti, tuttavia, che tO (1) ⇒ tO ( n ), quindi, come menzionato in precedenza, l'implementazione della "ottimizzazione" tecnicamente sarebbe consentita.

Se provieni da uno sfondo Java o simile, potresti chiederti perché l'iteratore deve controllare la bandiera ogni volta. Non potremmo invece avere due tipi di iteratori distinti, entrambi derivati ​​da un tipo di base comune, e avere std::list::begine std::list::rbeginrestituire polimorficamente l'iteratore appropriato? Se possibile, ciò renderebbe il tutto ancora peggio perché far avanzare l'iteratore sarebbe una chiamata di funzione indiretta (difficile da incorporare) ora. In Java, si paga comunque questo prezzo regolarmente, ma ancora una volta, questo è uno dei motivi per cui molte persone cercano C ++ quando le prestazioni sono fondamentali.

Come sottolineato da Benjamin Lindley nei commenti, poiché reversenon è consentito invalidare gli iteratori, l'unico approccio consentito dallo standard sembra essere quello di memorizzare un puntatore all'elenco all'interno dell'iteratore che provoca un accesso alla memoria doppio indiretto.


7
@galinette: std::list::reversenon invalida gli iteratori.
Benjamin Lindley,

1
@galinette Mi dispiace, ho letto male il tuo commento precedente come "flag per iteratore " anziché "flag per nodo " mentre lo scrivevi. Ovviamente, una bandiera per nodo sarebbe controproducente come dovresti, di nuovo, fare un attraversamento lineare per capovolgerle tutte.
5gon12eder,

2
@ 5gon12eder: è possibile eliminare la ramificazione a costi molto persi: memorizzare i puntatori nexte previn un array e memorizzare la direzione come 0o 1. Per iterare in avanti, dovresti seguire pointers[direction]e iterare al contrario pointers[1-direction](o viceversa). Ciò aggiungerebbe ancora un po 'di sovraccarico, ma probabilmente meno di un ramo.
Jerry Coffin,

4
Probabilmente non è possibile memorizzare un puntatore all'elenco all'interno degli iteratori. swap()è specificato come tempo costante e non invalida alcun iteratore.
Tavian Barnes,

1
@TavianBarnes Dannazione! Bene, tripla indiretta quindi ... (Voglio dire, in realtà non tripla. Dovresti archiviare la bandiera in un oggetto allocato dinamicamente, ma il puntatore nell'iteratore può ovviamente puntare direttamente a quell'oggetto invece di indirizzare indirettamente sulla lista.)
5gon12eder,

37

Sicuramente poiché tutti i contenitori che supportano gli iteratori bidirezionali hanno il concetto di rbegin () e rend (), questa domanda è controversa?

È banale costruire un proxy che inverte gli iteratori e acceda al contenitore attraverso quello.

Questa non operazione è effettivamente O (1).

ad esempio:

#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;
}

uscita prevista:

mat
the
on
sat
cat
the

Alla luce di ciò, mi sembra che il comitato per le norme non abbia impiegato del tempo per imporre O (1) l'ordine inverso del contenitore perché non è necessario, e la biblioteca standard è in gran parte costruita sul principio di imporre solo ciò che è strettamente necessario mentre evitando la duplicazione.

Solo il mio 2c.


18

Perché deve attraversare ogni nodo ( ntotale) e aggiornare i loro dati (il passaggio di aggiornamento è effettivamente O(1)). Questo rende l'intera operazione O(n*1) = O(n).


27
Perché è necessario aggiornare anche i collegamenti tra ogni elemento. Estrarre un pezzo di carta e disegnarlo invece di effettuare il downgrade.
Blindy,

11
Perché chiedere se sei così sicuro allora? Stai sprecando il nostro tempo.
Blindy,

28
@Curious I nodi della lista doppiamente collegati hanno un senso dell'orientamento. Motivo da lì.
Scarpa

11
@Blindy Una buona risposta dovrebbe essere completa. "Prendi un pezzo di carta e disegnalo", quindi non dovrebbe essere un componente necessario per una buona risposta. Le risposte che non sono buone risposte sono soggette a voti negativi.
RM

7
@Shoe: Devono? Si prega di ricercare l'elenco collegato a XOR e simili.
Deduplicatore,

2

Scambia anche il puntatore precedente e successivo per ogni nodo. Ecco perché ci vuole Linear. Sebbene possa essere fatto in O (1) se la funzione che utilizza questo LL prende anche informazioni su LL come input, come se accedesse normalmente o al contrario.


1

Solo una spiegazione dell'algoritmo. Immagina di avere un array con elementi, quindi devi invertirlo. L'idea di base è quella di iterare su ciascun elemento cambiando l'elemento nella prima posizione nell'ultima posizione, l'elemento nella seconda posizione per penultima posizione e così via. Quando raggiungi il centro dell'array avrai tutti gli elementi cambiati, quindi in (n / 2) iterazioni, che è considerato O (n).


1

È O (n) semplicemente perché deve copiare l'elenco in ordine inverso. Ogni singola operazione di elemento è O (1) ma ce ne sono n nell'intero elenco.

Naturalmente ci sono alcune operazioni a tempo costante coinvolte nell'impostazione dello spazio per il nuovo elenco e nella successiva modifica dei puntatori, ecc. La notazione O non considera le singole costanti una volta incluso un fattore n del primo ordine.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.