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+klog⁡k)

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)}KXio∈UNcioXioUN

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(log⁡K)O(K)O(nlog⁡K)
  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 . Ω(nlog⁡n)
— 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 Θ(nlog⁡n)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.