Quali strutture di dati è possibile utilizzare in modo da poter ottenere la rimozione e la sostituzione O (1)? O come puoi evitare situazioni quando hai bisogno di dette strutture?
ST
monade di Haskell lo fa in modo eccellente.
Quali strutture di dati è possibile utilizzare in modo da poter ottenere la rimozione e la sostituzione O (1)? O come puoi evitare situazioni quando hai bisogno di dette strutture?
ST
monade di Haskell lo fa in modo eccellente.
Risposte:
Esiste una vasta gamma di strutture dati che sfruttano la pigrizia e altri trucchi per ottenere un tempo costante ammortizzato o persino (per alcuni casi limitati, come le code ) aggiornamenti di tempo costante per molti tipi di problemi. La tesi di dottorato di Chris Okasaki "Strutture di dati puramente funzionali" e il libro con lo stesso nome sono un ottimo esempio (forse il primo importante), ma da allora il campo è avanzato . Queste strutture di dati sono in genere non solo puramente funzionali nell'interfaccia, ma possono anche essere implementate in Haskell puro e linguaggi simili e sono completamente persistenti.
Anche senza nessuno di questi strumenti avanzati, i semplici alberi di ricerca binaria bilanciata forniscono aggiornamenti del tempo logaritmico, quindi la memoria mutabile può essere simulata nel peggiore dei casi con un rallentamento logaritmico.
Esistono altre opzioni, che possono essere considerate imbrogli, ma sono molto efficaci per quanto riguarda lo sforzo di implementazione e le prestazioni del mondo reale. Ad esempio, i tipi lineari o i tipi di unicità consentono l'aggiornamento sul posto come strategia di implementazione per un linguaggio concettualmente puro, impedendo al programma di mantenere il valore precedente (la memoria che sarebbe mutata). Questo è meno generale delle strutture di dati persistenti: ad esempio, non è possibile creare facilmente un registro di annullamento archiviando tutte le versioni precedenti dello stato. È ancora uno strumento potente, sebbene AFAIK non sia ancora disponibile nelle principali lingue funzionali.
Un'altra opzione per introdurre in sicurezza uno stato mutabile in un ambiente funzionale è la ST
monade di Haskell. Può essere implementato senza mutazione e bloccando le unsafe*
funzioni, si comporta come se fosse solo un elaborato wrapper per il passaggio implicito di una struttura di dati persistente (cfr State
.). Ma a causa di un tipo di inganno del sistema che impone l'ordine di valutazione e previene la fuga, può essere tranquillamente implementato con una mutazione sul posto, con tutti i vantaggi in termini di prestazioni.
Una struttura mutevole economica è lo stack di argomenti.
Dai un'occhiata al tipico calcolo fattoriale in stile SICP:
(defn fac (n accum)
(if (= n 1)
accum
(fac (- n 1) (* accum n)))
(defn factorial (n) (fac n 1))
Come puoi vedere, il secondo argomento fac
è usato come accumulatore mutabile per contenere il prodotto in rapida evoluzione n * (n-1) * (n-2) * ...
. Tuttavia, non è in vista alcuna variabile mutabile e non è possibile modificare inavvertitamente l'accumulatore, ad esempio da un altro thread.
Questo è, ovviamente, un esempio limitato.
Puoi ottenere liste immutabili collegate con una sostituzione economica del nodo head (e per estensione qualsiasi parte che inizia dalla testa): fai semplicemente in modo che la nuova testa punti allo stesso nodo successivo della vecchia testa. Funziona bene con molti algoritmi di elaborazione di elenchi ( fold
basati su qualsiasi cosa ).
È possibile ottenere prestazioni piuttosto buone da array associativi basati ad esempio su HAMT . Logicamente ricevi un nuovo array associativo con alcune coppie chiave-valore modificate. L'implementazione può condividere la maggior parte dei dati comuni tra gli oggetti vecchi e quelli appena creati. Questo non è O (1) però; di solito ottieni qualcosa di logaritmico, almeno nel caso peggiore. Gli alberi immutabili, d'altra parte, di solito non subiscono alcuna penalità di prestazione rispetto agli alberi mutabili. Naturalmente, ciò richiede un certo sovraccarico di memoria, di solito tutt'altro che proibitivo.
Un altro approccio si basa sull'idea che se un albero cade in una foresta e nessuno lo sente, non deve produrre suono. Cioè, se puoi dimostrare che un po 'di stato mutato non lascia mai un ambito locale, puoi mutare i dati al suo interno in modo sicuro.
Clojure ha transitori che sono mutevoli "ombre" di strutture di dati immutabili che non perdono al di fuori dell'ambito locale. Clean usa Uniques per ottenere qualcosa di simile (se ricordo bene). Rust aiuta a fare cose simili con puntatori unici controllati staticamente.
ref
e limitarlo in un certo ambito. Vedi IORef
o STRef
. E poi naturalmente ci sono TVar
s e MVar
s che sono simili, ma con la semantica concomitanti sane (STM per TVar
s e mutex based per MVar
s)
Quello che stai chiedendo è un po 'troppo ampio. O (1) rimozione e sostituzione da quale posizione? Il capo di una sequenza? La coda? Una posizione arbitraria? La struttura dei dati da utilizzare dipende da questi dettagli. Detto questo, 2-3 Finger Trees sembrano una delle strutture di dati persistenti più versatili disponibili:
Presentiamo alberi a 2-3 dita, una rappresentazione funzionale di sequenze persistenti che supportano l'accesso alle estremità in tempo costante ammortizzato, e concatenazione e divisione nel tempo logaritmica nella dimensione del pezzo più piccolo.
(...)
Inoltre, definendo l'operazione di divisione in una forma generale, otteniamo una struttura di dati per scopi generali che può fungere da sequenza, coda di priorità, albero di ricerca, coda di ricerca prioritaria e altro.
Le strutture di dati generalmente persistenti hanno prestazioni logaritmiche quando si modificano posizioni arbitrarie. Questo può o non può essere un problema, poiché la costante in un algoritmo O (1) può essere elevata e il rallentamento logaritmico potrebbe essere "assorbito" in un algoritmo globale più lento.
Ancora più importante, le strutture di dati persistenti rendono più facile il ragionamento sul programma e dovrebbe sempre essere la modalità operativa predefinita. Dovresti preferire strutture di dati persistenti quando possibile e utilizzare una struttura di dati mutabili solo dopo aver profilato e determinato che la struttura di dati persistenti è un collo di bottiglia delle prestazioni. Nient'altro è l'ottimizzazione prematura.