Una struttura dati efficiente che supporta Inserisci, Elimina e MostFrequent


14

Supponiamo di avere un set e che ciascun membro di sia una coppia di dati e chiavi. Vogliamo una struttura di dati che supporti le seguenti operazioni:DD

  • Inserisci in ,(d,k)D
  • Elimina membro , (non è necessario cercare per trovare , ad es. indica un membro in ),eeeD
  • MostFrequent, che restituisce un membro tale che sia una delle chiavi più frequenti in (nota che la chiave più frequente non deve essere unica).eDe.keyD

Quale sarebbe un'attuazione efficiente di questa struttura di dati?

La mia soluzione è un heap per i tasti e le loro frequenze con la priorità delle frequenze più una tabella hash in cui la funzione hash mappa i membri con lo stesso tasto sullo stesso slot nella tabella hash (con puntatori da una parte all'altra).

Questo può dare per le prime due operazioni e per la terza (peggiore tempo di esecuzione).Θ(lgn)Θ(1)

Mi chiedo se esiste una soluzione più efficiente? (o una soluzione più semplice con la stessa efficienza?)


Se lo desideri, puoi utilizzare un semplice albero di ricerca binario bilanciato anziché una tabella hash.
Joe,

La tabella hash utilizza molto spazio non essenziale, proporrei la coda di priorità. Ti darebbe la stessa complessità temporale all'inserimento e all'eliminazione, ma la complessità della memoria sarebbe migliore.
Bartosz Przybylski il

@Joe, l'uso di un BST al posto di una tabella hash renderebbe meno efficienti le operazioni MostFrequent, ma potrebbe essere un ragionevole compromesso per la memoria.
Kaveh,

2
Se si utilizzano solo confronti, almeno uno di Insert / MostFrequent deve essere ammortizzato , a causa dei limiti inferiori per il problema di distinzione tra elementi. Ω(logn)
Aryabhata,

1
Ci sono anche alcune strutture interessanti nel modello di streaming. springerlink.com/content/t17nhd9hwwry909p
Joe

Risposte:


7

Nel modello di calcolo basato sul confronto, è possibile implementare la coda di priorità utilizzando un heap di Fibonacci anziché un heap ordinario. Questo ti darà i seguenti limiti: tempo ammortizzato per inserimento e O ( log n ) tempo ammortizzato per operazioni di cancellazione.O(1)O(logn)

Se si discosta dal modello basato sul confronto e si adotta il modello RAM in cui le chiavi sono considerate stringhe binarie, ognuna contenuta in una o più parole macchina, è possibile implementare la coda di priorità in . In effetti, è possibile ottenere entrambe le operazioni di inserimento e cancellazione O ( o(logn)eO(1)tempo per l'operazione findMin. Thorup lo ha dimostratoO(loglogn)O(1)

Se possiamo ordinare chiavi nel tempo S ( n ) per chiave, allora possiamo implementare una coda di priorità che supporta find-min in tempo costante e aggiorna (inserisci ed elimina) in tempo S ( n ) .nS(n)S(n)

Vedi M. Thorup. Equivalenza tra le code prioritarie e l'ordinamento, 2002. in Proc. FOCS 2002

Dal momento che possiamo ordinare in tempo previsto e spazio lineare, come mostrato daO(nloglogn)

Y. Han e M. Thorup. Ordinamento intero in tempo previsto e spazio lineare. in Proc. FOCS 2002O(nloglogn)

il limite è dimostrato.


1

Puoi fare tutto ciò nel tempo ammortizzato previsto per . Il trucco essenziale è che non abbiamo bisogno della piena potenza di una coda prioritaria, poiché la frequenza della chiave cambia solo di 1 durante ogni inserimento o cancellazione.O(1)

La mia soluzione di seguito è davvero solo la tua soluzione con una coda di priorità "inefficiente" che sembra funzionare bene in questo caso: una coda di massima priorità implementata come elenchi doppiamente collegati di secchi di chiavi ha O (1) insertMin, deleteMax, removeFromBucket e increaseKey.


Mantenere un elenco doppiamente collegato di bucket, in cui ogni bucket ha un set di chiavi hash non vuoto (che chiamerò una coorte) e un numero intero positivo (che chiamerò ValCount). In un bucket b, ogni chiave k nella coorte di b ha lo stesso numero di valori univoci associati ad essa nell'insieme che si sta mantenendo. Ad esempio, se il tuo set ha le coppie (a, mela), (a, avocado), (b, banana), (c, cetriolo), (d, frutto del drago) dove le singole lettere sono le chiavi e i frutti sono i valori, quindi avresti due bucket: un bucket avrebbe un ValCount di 2 e una coorte composta da una sola chiave: a. L'altro bucket avrebbe un ValCount pari a 1 e una coorte composta dalle tre chiavi b, c e d.

L'elenco doppiamente collegato di Bucket deve essere ordinato da ValCount. Sarà importante che possiamo trovare la testa e la coda della lista in tempo e che possiamo unire un nuovo Secchio in O ( 1 ) tempo se conosciamo i suoi vicini. Inimmaginabilmente, chiamerò la lista di Buckets la BucketList.O(1)O(1)

Oltre alla BucketList, avremo bisogno di una SetMap, che è una mappa di hash che associa le chiavi a ValueBuckets. Un ValueBucket è una coppia costituita da ValueSet (un set di valori hash non vuoto) e un puntatore non null a un bucket. ValueSet associato a una chiave k contiene tutti i valori univoci associati a k. Il puntatore Bucket associato a ValueSet ha una coorte uguale alla dimensione di ValueSet. Il bucket associato a una chiave k in SetMap è anche associato alla chiave k in BucketList.

In C ++:

struct Bucket {
    unsigned ValCount;
    unordered_set<Key> Cohort;
    Bucket * heavier;
    Bucket * lighter;
};
Bucket * BucketListHead;
Bucket * BucketListTail;

struct ValueBucket {
  unordered_set<Value> ValueSet;
  Bucket * bucket;
};
unordered_map<Key, ValueBucket> SetMap;

Per trovare una coppia chiave-valore di frequenza massima, dobbiamo solo guardare in testa alla BucketList, trovare una chiave nella Coorte, cercare quella chiave in SetMap e trovare un valore nel ValueSet del suo ValueBucket. (Uff!)

Inserire ed eliminare le coppie chiave-valore è più complicato.

Per inserire o eliminare una coppia chiave-valore, dobbiamo prima inserirla o eliminarla in SetMap Questo cambierà la dimensione del ValueSet, quindi dobbiamo modificare la Bucket associata alla chiave. Gli unici bucket che dovremo esaminare per apportare questo cambiamento saranno i vicini immediati del bucket in cui si trovava la chiave. Ci sono diversi casi qui, e probabilmente non valgono la pena spiegarli completamente, anche se sarei felice per capire se hai ancora problemi.


Grazie. In realtà abbiamo avuto un'idea simile per una soluzione, ma era un problema. Devo controllare qual è stato il problema in quanto non lo ricordo in questo momento. Lo controllerò più attentamente la prossima settimana per vedere se evita il problema della nostra soluzione.
Kaveh,

Ecco un altro modo di pensare alla mia soluzione: è davvero solo la tua soluzione con una coda di priorità "inefficiente" che funziona bene in questo caso. Una coda con priorità massima implementata come elenchi doppiamente collegati di bucket di chiavi ha O (1) insertMin, deleteMax, removeFromBucket e aumentaKey.
jbapple

Il modo più efficiente (in termini del caso peggiore) di mantenere la mappatura dalle chiavi per ValueBuckets è probabilmente un albero di ricerca.
Raffaello

Raphael - Non sono sicuro di cosa stai arrivando. Stai dicendo che le tabelle di hash sono costose in pratica, o che hanno cattive prestazioni nel peggiore dei casi, o qualche terza cosa?
jbapple,

-3

Peggior complessità

O(1)

O(1)

O(1)

O(loglogn)

O(n)

Prova

[0,N) O(log log min{n,N})

Che è stabilito con una combinazione di:

τ(n,N) n[0,N) τ(n,N)τ(N,N)τ

e:

n[0,N)O(1+log log nlog log q)q

Riferimento

Thorup, Mikkel. "Code di priorità di numeri interi con chiave decrescente in tempo costante e problema dei percorsi più brevi a fonte singola". In Atti del trentacinquesimo simposio annuale ACM sulla teoria dell'informatica, 149-158. STOC '03. New York, New York, Stati Uniti: ACM, 2003.


Si noti che in tutte le code prioritarie è banale passare a una struttura che supporta "get-min" e "extract-min" su una struttura che supporta "get-max" e "extract-max".
AL

Ping ...: @Kaveh
AT
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.