Come contare nel caso peggiore in tempo lineare?


8

Questa domanda e questa domanda mi hanno fatto pensare un po '. Per ordinare un array di lunghezza con elementi univoci in , dobbiamo essere in grado di memorizzare i conteggi dei valori nell'array. Ci sono alcuni suggerimenti, ma sto cercando un modo per farlo nel peggiore dei casi tempo lineare. Più specificamente:nkO(n+klogk)

Dato un elenco di elementi con elementi distinti, un elenco di tuple di tutti gli elementi unici tale che è il conteggio dell'elemento in .AnkU={(Xio,cio)}KXioUNcioXioUN

Ecco alcune idee (fallite) che ho avuto e sono state suggerite:

  1. Albero di ricerca binario bilanciato : con questo ci vorrà per inserirlo nell'albero e aumentare i valori. Dopo gli inserti potremmo fare un attraversamento di alberi in . Pertanto, il tempo totale esce su che è troppo lento.O(logK)O(K)O(nlogK)
  2. Hash Map - Con questo possiamo ottenere inserti previsti e quindi tempo previsto . Tuttavia, questo non è ancora il caso peggiore di .O(1) O(n) O(n)
  3. Spazio vuoto Mapping - Trova il minimo e il massimo elemento in . Alloca (ma non inizializza) memoria sufficiente per coprire questo intervallo. Usa questa memoria fondamentalmente come una mappa di hash e includi un hash casuale in modo che non proviamo ad accedere alla memoria danneggiata. Questa strategia presenta problemi. (1) È probabilistico con probabilità molto molto molto basse di fallimento, ma non è ancora garantito. L'uso della memoria in questo modo ci limita a vincoli in virgola mobile o interi.UN
  4. Associative Array - Ci sono molti altri array associativi che possono essere utilizzati, simili a mappe e BST hash, ma io non sto trovando alcun che corrispondono a questi vincoli.

Forse c'è qualche metodo ovvio che mi manca, ma penso anche che potrebbe non essere possibile. Quali sono i tuoi pensieri?


3
Non può essere fatto nel modello di confronto poiché il problema della distinzione degli elementi ha un limite inferiore della complessità dell'albero delle decisioni . Ω(nlogn)
John L.

@ Apass.Jack, oh giusto, è corretto. Una banale riduzione che non ho considerato. Se lo scrivi come risposta rapida, accetterò.
Ryan,

Perché HashMap non è garantito ammortizzato O (n) ?
javadba,

1
@javadba Ad esempio, supponiamo che tutti gli elementi siano sottoposti a hash sullo stesso valore.
John L.

Ah ok, quindi se si tratta di un hashing imperfetto.
javadba,

Risposte:


6

Questa è una bella domanda

Nel modello di confronto o, cosa più generale, nel modello algebrico dell'albero delle decisioni, il problema della distinzione degli elementi ha un limite inferiore di Θ(nlogn)complessità temporale nel peggiore dei casi, come detto in questo articolo di Wikipedia . Quindi non esiste un algoritmo per contare elementi distinti in tempo lineare nel peggiore dei casi, anche senza contare le doppiezze.

Tuttavia, non è chiaro se possa essere fatto in un altro modello computazionale. Sembra improbabile in qualsiasi modello computazionale deterministico ragionevole.


È davvero un'istanza del problema di distinzione tra elementi? La sola generazione delle tuple non richiede il controllo della distinzione. Non in disaccordo, solo curioso.
Mascoj,

2
Quello che sto dicendo è che se riesci a produrre quella tupla di elementi distinti, allora puoi anche risolvere il problema della distinzione tra elementi controllando se la dimensione della tupla è n.
John L.,

Ottima scelta. Grazie
mascoj

1

Esistono algoritmi randomizzati il ​​cui tempo di esecuzione previsto è O(n); o dove la probabilità che il tempo di esecuzione impieghi più tempo dicn è esponenzialmente piccolo in c.

In particolare, scegli casualmente una funzione hash 2-universale, quindi usala per eseguire l'hashing di tutti gli elementi dell'array. Ciò consente di raggiungere i tempi di funzionamento indicati, se si sceglie in modo appropriato la lunghezza dell'output dell'hash 2-universale.

Come altro esempio, puoi creare un algoritmo randomizzato il cui tempo di esecuzione peggiore è O(n) (funziona sempre in tempo lineare, non importa quale) e ha una probabilità di errore al massimo 1/2100. (Come? Eseguire l'algoritmo sopra e terminarlo se dura più a lungo dicn passaggi per alcuni scelti in modo appropriato c.) In pratica, è abbastanza buono, poiché la probabilità che il tuo computer generi una risposta sbagliata a causa di un raggio cosmico è già molto più alta di 1/2100.


1

Il tuo approccio 3 può essere reso sicuro usando una soluzione per esercitare 2.12 di Aho, Hopcroft e Ullman (1974) The Design and Analysis of Computer Algorithms come descritto, ad esempio, in Utilizzo di memoria non inizializzata per divertimento e profitto .

Fondamentalmente, oltre alla tua matrice di N elementi con i conteggi hai due matrici di N elementi e un conteggio ausiliario per creare un insieme sparso che indica quali dei conteggi sono validi.

Nello pseudocodice di tipo C:

uint* a = malloc(n);
uint* b = malloc(n);
uint* c = malloc(n);
uint len = 0;

get_count(uint x) {
    uint idx = a[x];
    return idx >= 0 && idx < len && b[idx] == x ? c[idx] : 0;
}

increment_count(uint x) {
    uint idx = a[x];
    if (idx < 0 || idx >= len || b[idx] != x) {
        idx = len;
        len++;
        a[x] = idx;
        b[idx] = x;
        c[idx] = 0;
    }
    c[idx]++;
}

L'implementazione pratica del set sparse è discussa in questa risposta StackOverflow .


PS cpuò essere indicizzato su xo idx, ma ho usato idxper una migliore localizzazione della cache.
Peter Taylor,

Mi piace la risposta, ma mi sono confuso su ciò che lo rende sicuro. Mentre, del tutto improbabile, non potresti accedere a una cella di memoria, che per miracolo ha una voce "valida" in essa anche se non è mai stata messa lì. Se sei stato sfortunato con malloc?
Ryan,

1
Questa soluzione funziona solo se si dispone di una memoria abbastanza grande: se tutti gli elementi dell'array si trovano nell'intervallo 1 ..u, quindi è necessario almeno una memoria di dimensioni u. In pratica questo è molto limitante. In pratica, il modo in cui creiamo un ampio spazio di indirizzi virtuali è l'utilizzo di tabelle di pagine, che sono una struttura di dati basata su alberi; l'hardware segue invisibilmente le tabelle delle pagine per noi. Di conseguenza, mentre pensiamo all'accesso alla memoria come a prendereO(1)tempo, se si lavora in un ampio spazio di indirizzi di memoria, ogni accesso alla memoria richiede effettivamente tempo logaritmico (per attraversare la struttura ad albero della tabella delle pagine).
DW

@ryan, vedi research.swtch.com/sparse per ciò che lo rende sicuro. È sicuramente un trucco molto intelligente.
DW

@DW, 3u+1, ma se uè molto grande, quindi puoi farlo su più livelli, usando una matrice di {a,b,c,len}strutture canziché una matrice di conteggi. Ad esempio, se si utilizza radix 512 in modo che ciascuno degli array si adatti a una pagina (con puntatori a 8 byte), è possibile passare au=5123=134217728 usando al massimo (3×512+1)(1+2K) memoria dove Kè il numero di elementi distinti visti.
Peter Taylor,
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.