Il tempo costante e il tempo costante ammortizzato sono effettivamente considerati equivalenti?


16

Devo scrivere una RandomQueue che consenta di aggiungere e rimuovere casualmente in Constant Time (O (1)).

Il mio primo pensiero è stato di supportarlo con una sorta di array (ho scelto un ArrayList), poiché gli array hanno accesso costante tramite un indice.

Osservando la documentazione, però, mi sono reso conto che le aggiunte di ArrayLists sono considerate Tempo costante ammortizzato, poiché un'aggiunta può richiedere una riallocazione dell'array sottostante, che è O (n).

Il tempo costante ammortizzato e il tempo costante sono effettivamente uguali o devo esaminare una struttura che non richiede una riallocazione completa su ogni aggiunta?

Lo sto chiedendo perché a parte le strutture basate su array (che per quanto ne so avranno sempre aggiunte Amortized Constant Time), non riesco a pensare a nulla che soddisfi i requisiti:

  • Qualsiasi albero basato avrà al massimo l'accesso O (log n)
  • Un elenco collegato potrebbe potenzialmente avere aggiunte O (1) (se viene mantenuto un riferimento alla coda), ma una rimozione casuale dovrebbe essere nella migliore delle ipotesi O (n).

Ecco la domanda completa; nel caso in cui ho guardato alcuni dettagli importanti:

Progettare e implementare un RandomQueue. Questa è un'implementazione dell'interfaccia Queue in cui l'operazione remove () rimuove un elemento che viene scelto in modo uniforme a caso tra tutti gli elementi attualmente nella coda. (Pensa a RandomQueue come a un sacchetto in cui possiamo aggiungere elementi o raggiungere e rimuovere ciecamente alcuni elementi casuali.) Le operazioni add (x) e remove () in RandomQueue dovrebbero essere eseguite in tempo costante per operazione.


L'assegnazione specifica come vengono eseguite le rimozioni casuali? Ti viene dato un indice da rimuovere o un riferimento a un elemento coda?

Non fornisce alcun dettaglio. I requisiti sono solo una struttura che implementa l'interfaccia della coda e ha aggiunte e rimozioni di O (1).
Carcigenicato,

A parte questo, un array ridimensionabile con O (n) in crescita non ha necessariamente l'aggiunta di O (1): questo dipende da come coltiviamo l'array. Crescere di una quantità costante a è ancora O (n) per l'aggiunta (abbiamo una 1/apossibilità per un'operazione O (n)), ma crescere di un fattore costante a > 1è O (1) ammortizzato per l'aggiunta: abbiamo una (1/a)^npossibilità di una O (n) operazione, ma quella probabilità si avvicina a zero per grandi n.
amon,

Le liste di array usano quest'ultimo correttamente?
Carcigenicato,

1
L'autore della domanda (io) stava pensando alla soluzione del tempo costante ammortizzato. Lo chiarirò nella prossima edizione. (Anche se il tempo costante nel caso peggiore può essere raggiunto qui usando la tecnica del deprezzamento .)
Pat Morin,

Risposte:


10

Il tempo costante ammortizzato può quasi sempre essere considerato equivalente al tempo costante e, senza conoscere le specifiche dell'applicazione e il tipo di utilizzo che si prevede di fare in questa coda, la maggior parte delle probabilità è che tu sia coperto.

Un elenco di array ha il concetto di capacità , che è sostanzialmente uguale alla più grande dimensione / lunghezza / conteggio degli articoli che è mai stato richiesto finora. Quindi, ciò che accadrà è che all'inizio l'array list continuerà a riallocare se stesso per aumentare la sua capacità mentre continui ad aggiungere elementi ad esso, ma a un certo punto il numero medio di elementi aggiunti per unità di tempo corrisponderà inevitabilmente al numero medio di articoli rimosso per unità di tempo (altrimenti si esaurirebbe comunque la memoria), a quel punto l'array smetterà di riallocare se stesso e tutti gli accodamenti verranno soddisfatti a tempo costante di O (1).

Tuttavia, tieni presente che per impostazione predefinita, la rimozione casuale da un elenco di array non è O (1), è O (N), poiché gli elenchi di array spostano tutti gli elementi dopo l'elemento rimosso di una posizione verso il basso per sostituire il rimosso articolo. Per ottenere O (1) dovrai sostituire il comportamento predefinito per sostituire l'elemento rimosso con una copia dell'ultimo elemento dell'elenco di array, quindi rimuovere l'ultimo elemento, in modo che nessun elemento venga spostato. Ma poi, se lo fai, non hai più esattamente una coda.


1
Accidenti, buon punto sui traslochi; Non l'ho considerato. E dal momento che stiamo rimuovendo in modo casuale elementi, ciò non significa tecnicamente che non sia più una coda in quel senso?
Carcigenicato,

Sì, significa che non lo stai davvero trattando come una coda. Ma non so come stai pianificando di trovare gli elementi da rimuovere. Se il tuo meccanismo per trovarli si aspetta che siano presenti nella coda nell'ordine in cui sono stati aggiunti, allora sei sfortunato. Se non ti importa se l'ordine degli articoli viene confuso, allora stai bene.
Mike Nakis,

2
L'aspettativa è che io RandomQueueimplementi l' Queueinterfaccia e che il removemetodo fornito rimuova casualmente invece di far scoppiare la testa, quindi non ci dovrebbe essere alcun modo di fare affidamento su un ordine specifico. Penso quindi, data la sua natura casuale, che l'utente non dovrebbe aspettarsi che mantenga un ordine specifico. Ho citato l'incarico nella mia domanda per chiarimenti. Grazie.
Carcigenicato,

2
Sì, allora, sembrerà che tu vada bene se ti assicuri solo che la rimozione degli oggetti sia fatta come ho suggerito.
Mike Nakis,

Un'ultima cosa se non ti dispiace. Ci ho pensato di più, e non sembra che sia possibile avere sia aggiunte "vere" O (1) sia rimozione "vera" O (1); sarà un compromesso tra i 2. O hai una struttura allocata singolarmente (come una matrice) che fornisce la rimozione ma non additon, o una struttura allocata a pezzi come una lista collegata che fornisce aggiunte ma non rimozione. È vero? Grazie ancora.
Carcigenicato,

14

La domanda sembra richiedere specificamente un tempo costante e non un tempo costante ammortizzato . Quindi, rispetto alla domanda citata, no, non sono effettivamente gli stessi *. Sono comunque nelle applicazioni del mondo reale?

Il problema tipico con la costante ammortizzata è che occasionalmente devi pagare il debito accumulato. Quindi, mentre gli inserti sono generalmente costanti, a volte devi subire il sovraccarico di reinserire di nuovo tutto quando viene allocato un nuovo blocco.

Laddove la differenza tra tempo costante e tempo costante ammortizzato è rilevante per un'applicazione dipende se questa velocità molto lenta occasionale è accettabile. Per un numero molto grande di domini questo è generalmente ok. Soprattutto se il contenitore ha una dimensione massima effettiva (come cache, buffer di temperatura, contenitori di lavoro), è possibile pagare i costi in modo efficace solo una volta durante l'esecuzione.

In risposta ad applicazioni critiche questi tempi potrebbero essere inaccettabili. Se ti viene richiesto di soddisfare una garanzia di inversione di breve durata, non puoi fare affidamento su un algoritmo che a volte lo supererà. Ho già lavorato su progetti di questo tipo, ma sono estremamente rari.

Dipende anche da quanto è alto questo costo. I vettori tendono a funzionare bene poiché il loro costo di riallocazione è relativamente basso. Se vai alla mappa hash, tuttavia, la riallocazione può essere molto più elevata. Anche in questo caso, per la maggior parte delle applicazioni probabilmente va bene, soprattutto server di lunga durata con un limite superiore per gli elementi nel contenitore.

* Qui c'è un piccolo problema. Al fine di rendere qualsiasi contenitore per scopi generici un tempo costante per l'inserimento, una delle due cose deve contenere:

  • Il contenitore deve avere una dimensione massima fissa; o
  • puoi supporre che l'allocazione di memoria dei singoli elementi sia un tempo costante.

"server epatico" sembra una frase strana da usare qui. Intendi forse "live server"?
Pieter Geerkens,

6

Dipende se si sta ottimizzando la velocità effettiva o la latenza:

  • I sistemi sensibili alla latenza richiedono prestazioni costanti. Per tale scenario, dobbiamo enfatizzare il comportamento nel caso peggiore del sistema. Esempi sono i sistemi soft in tempo reale come i giochi che vogliono ottenere un framerate coerente o i server Web che devono inviare una risposta entro un certo lasso di tempo: sprecare cicli della CPU è meglio che arrivare in ritardo.
  • I sistemi ottimizzati per il throughput non si preoccupano di bancarelle occasionali, a condizione che la massima quantità di dati possa essere elaborata a lungo termine. Qui, siamo principalmente interessati alla performance ammortizzata. Questo è generalmente il caso dello scricchiolio dei numeri o di altri processi di elaborazione batch.

Si noti che un sistema può avere componenti diversi che devono essere classificati in modo diverso. Ad esempio, un moderno elaboratore di testi avrebbe un thread dell'interfaccia utente sensibile alla latenza, ma thread ottimizzati per il throughput per altre attività come il controllo ortografico o le esportazioni di PDF.

Inoltre, la complessità algoritmica spesso non importa quanto potremmo pensare: quando un problema è limitato a un certo numero, le caratteristiche prestazionali effettive e misurate sono più importanti del comportamento "per n molto grandi ".


Sfortunatamente, ho pochissimo sfondo. La domanda termina con: "Le operazioni add (x) e remove () in un RandomQueue dovrebbero essere eseguite a tempo costante per operazione".
Carcigenicato,

2
@Carcigenicato a meno che non si sappia per certo che il sistema è sensibile alla latenza, l'utilizzo della complessità ammortizzata per selezionare una struttura di dati dovrebbe essere assolutamente sufficiente.
amon,

Ho l'impressione che questo potrebbe essere un esercizio di programmazione o un test. E certamente non è facile. Assolutamente vero che importa molto raramente.
gnasher729,

1

Se ti viene richiesto un algoritmo "tempo costante ammortizzato", a volte il tuo algoritmo potrebbe impiegare molto tempo. Ad esempio, se si utilizza std :: vector in C ++, un vettore di questo tipo può aver allocato spazio per 10 oggetti e quando si alloca l'undicesimo oggetto, viene allocato lo spazio per 20 oggetti, vengono copiati 10 oggetti e l'undicesimo aggiunto, che richiede molto tempo. Ma se aggiungi un milione di oggetti, potresti avere 999.980 operazioni veloci e 20 operazioni lente, con un tempo medio veloce.

Se ti viene chiesto un algoritmo di "tempo costante", il tuo algoritmo deve essere sempre veloce, per ogni singola operazione. Ciò sarebbe importante per i sistemi in tempo reale in cui potresti aver bisogno di una garanzia che ogni singola operazione è sempre veloce. Il "tempo costante" spesso non è necessario, ma sicuramente non è lo stesso del "tempo costante ammortizzato".

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.