Algoritmo non banale per il calcolo di una mediana a finestra scorrevole


25

Devo calcolare la mediana corrente:

  • Input: n , , vettore .( x 1 , x 2 , , x n )k(x1,x2,,xn)

  • Output: vettore , dove è la mediana di .y i ( x i , x i + 1 , , x i + k - 1 )(y1,y2,,ynk+1)yi(xi,xi+1,,xi+k1)

(Nessun imbroglio con approssimazioni; vorrei avere soluzioni esatte. Gli elementi sono numeri interi grandi).xi

Esiste un banale algoritmo che mantiene un albero di ricerca di dimensioni ; il tempo di esecuzione totale è . (Qui un "albero di ricerca" si riferisce ad una struttura dati efficiente che supporta inserimenti, eliminazioni e query mediane nel tempo logaritmico.)O ( n log k )kO(nlogk)

Tuttavia, questo mi sembra un po 'stupido. Impareremo efficacemente tutte le statistiche degli ordini in tutte le finestre di dimensione , non solo le mediane. Inoltre, ciò non è troppo attraente in pratica, specialmente se è grande (i grandi alberi di ricerca tendono ad essere lenti, i costi di gestione della memoria non sono banali, l'efficienza della cache è spesso scarsa, ecc.).kkk

Possiamo fare qualcosa di sostanzialmente migliore?

Esistono limiti inferiori (ad esempio, l'algoritmo banale è asintoticamente ottimale per il modello di confronto)?


Modifica: David Eppstein ha dato un bel limite inferiore per il modello di confronto! Mi chiedo se sia comunque possibile fare qualcosa di leggermente più intelligente dell'algoritmo banale?

Ad esempio, possiamo fare qualcosa in tal senso: dividere il vettore di input in parti di dimensione ; ordina ogni parte (tenendo traccia delle posizioni originali di ciascun elemento); e quindi utilizzare il vettore ordinato a tratti per trovare in modo efficiente le mediane in esecuzione senza alcuna struttura di dati ausiliaria? Ovviamente questo sarebbe ancora , ma in pratica gli array di ordinamento tendono ad essere molto più veloci rispetto al mantenimento degli alberi di ricerca.O ( n log k )kO(nlogk)


Modifica 2: Saeed voleva vedere alcuni motivi per cui penso che l'ordinamento sia più veloce delle operazioni dell'albero di ricerca. Ecco alcuni benchmark molto rapidi, per , : n = 10 8k=107n=108

  • ≈ 8s: ordinamento di vettori con elementi ciascunokn/kk
  • ≈ 10s: ordinamento di un vettore con elementin
  • ≈ 80: inserimenti ed eliminazioni in una tabella hash di dimensioniknk
  • ≈ 390s: inserimenti ed eliminazioni in un albero di ricerca bilanciato di dimensioniknk

La tabella hash è lì solo per il confronto; non ha alcuna utilità diretta in questa applicazione.

In sintesi, abbiamo quasi una differenza di fattore 50 nelle prestazioni dell'ordinamento rispetto alle operazioni dell'albero di ricerca bilanciata. E le cose peggiorano molto se aumentiamo .k

(Dettagli tecnici: Dati = numeri interi casuali a 32 bit. Computer = un tipico laptop moderno. Il codice di test è stato scritto in C ++, usando le routine di libreria standard (std :: sort) e le strutture di dati (std :: multiset, std :: unsorted_multiset). Ho usato due diversi compilatori C ++ (GCC e Clang) e due diverse implementazioni della libreria standard (libstdc ++ e libc ++). Tradizionalmente, std :: multiset è stato implementato come un albero rosso-nero altamente ottimizzato.)


1
Non credo che sarete in grado di migliorare la . La ragione è che, se si guarda a una finestra x t , . . . , x t + k - 1 , non puoi mai escludere nessuno dei numeri x t + knlogkxt,...,xt+k1dall'essere mediani della finestra futura. Ciò significa che in qualsiasi momento devi mantenere almenokxt+k2,...,xt+k1 numeri interi in una struttura di dati e non sembra aggiornarsi in meno del tempo di log. k2
RB

Il tuo banale algoritmo per me sembra essere non O ( n log k ) , ho capito male qualcosa? E penso per questo che tu abbia problemi con big k , altrimenti il ​​fattore logaritmico non è nulla nelle applicazioni pratiche, inoltre non c'è una grande costante nascosta in questo algoritmo. O((nk)klogk)O(nlogk)k
Saeed,

@Saeed: Nell'algoritmo banale, elabori gli elementi uno per uno; nel passaggio aggiungi x i all'albero di ricerca e (se i > k ) rimuovi anche x i - k dall'albero di ricerca. Si tratta di n passaggi, ognuno dei quali richiede tempo O ( log k ) . ixii>kxiknO(logk)
Jukka Suomela,

Quindi vuoi dire che hai un albero di ricerca bilanciato e non un albero di ricerca casuale?
Saeed,

1
@Saeed: tieni presente che nei miei benchmark non ho nemmeno provato a trovare le mediane. Ho appena fatto inserimenti e n eliminazioni in un albero di ricerca di dimensioni k e queste operazioni sono garantite per prendere O ( log k ) . Devi solo accettare che le operazioni dell'albero di ricerca sono molto lente nella pratica, rispetto all'ordinamento. Lo vedrai facilmente se provi a scrivere un algoritmo di ordinamento che funziona aggiungendo elementi a un albero di ricerca bilanciato - sicuramente funzionerà in tempo O ( n log n ) , ma sarà ridicolmente lento nella pratica e sprecherà anche molto di memoria. nnkO(logk)O(nlogn)
Jukka Suomela,

Risposte:


32

Ecco un limite inferiore dall'ordinamento. Dato un set di input di lunghezza n da ordinare, crea un input per il tuo problema mediano in esecuzione costituito da n - 1 copie di un numero inferiore al minimo di S , quindi S stesso, quindi n - 1 copie di un numero maggiore di il massimo di S e impostare k = 2 n - 1 . Le mediane esecuzione di questo ingresso sono le stesse della sequenza ordinata di S .Snn1SSn1Sk=2n1S

Quindi in un modello di confronto di calcolo, è richiesto il tempo . Forse se i tuoi input sono numeri interi e usi algoritmi di ordinamento dei numeri interi, puoi fare di meglio.Ω(nlogn)


6
Questa risposta mi fa davvero domandare se vale anche il contrario: dato un algoritmo di ordinamento efficiente, otteniamo un algoritmo mediano funzionante efficiente? (Ad esempio, su un algoritmo di ordinamento intero efficiente implica un algoritmo mediano funzionante efficiente per numeri interi? O un algoritmo di ordinamento efficiente IO fornisce un algoritmo mediano funzionante IO efficiente?)
Jukka Suomela

1
Ancora una volta, molte grazie per la tua risposta, mi ha davvero messo sulla buona strada e ha dato l'ispirazione per l'algoritmo di filtro mediano basato sull'ordinamento! Alla fine sono stato in grado di trovare un documento del 1991 che presentava sostanzialmente lo stesso argomento di quello che dai qui, e Pat Morin ha dato un puntatore a un altro documento pertinente del 2005; vedi rif. [6] e [9] qui .
Jukka Suomela,

9

Modifica: questo algoritmo è ora presentato qui: http://arxiv.org/abs/1406.1717


Sì, per risolvere questo problema è sufficiente eseguire le seguenti operazioni:

  • Ordina i vettori , ciascuno con k elementi.n/kk
  • Esegui post-elaborazione lineare.

Molto approssimativamente, l'idea è questa:

  • Considera due blocchi adiacenti di input, e b , entrambi con elementi k ; lasciare che gli elementi siano un 1 , un 2 , . . . , Un k e b 1 , b 2 , . . . , b k nell'ordine di apparizione nel vettore di input x .abka1,a2,...,akb1,b2,...,bkx
  • Ordina questi blocchi e impara il rango di ciascun elemento all'interno del blocco.
  • Aumentare i vettori e B con puntatori predecessore / successore affinché seguendo le catene puntatore poter attraversare gli elementi in ordine crescente. In questo modo abbiamo costruito elenchi doppiamente collegati a e b .abab
  • Uno per uno, eliminare tutti gli elementi dalla lista collegata , in ordine inverso di apparizione b k , b k - 1 , . . . , b 1 . Ogni volta che eliminiamo un elemento, ricorda qual era il suo successore e predecessore al momento della cancellazione .bbk,bk1,...,b1
  • Ora mantenere "puntatori mediana" e q quel punto alle liste di ' e b ' , rispettivamente. Inizializza p sul punto medio di a e inizializza q sulla coda dell'elenco vuoto b .pqabpaqb
  • Per ogni :i

    • Elimina dalla lista a (questa è O ( 1 ) volta, basta cancellare dalla lista collegata). Confronta una i con l'elemento puntato da p per vedere se abbiamo eliminato prima o dopo p .aiaO(1)aipp
    • Riporta alla lista b ' nella sua posizione originale (questa è O ( 1 ) volta, abbiamo memorizzato il predecessore e successore di b i ). Confronta b i con l'elemento puntato da q per vedere se abbiamo aggiunto l'elemento prima o dopo q .bibO(1)bibiqq
    • Aggiorna i puntatori e q in modo che la mediana dell'elenco unito a b sia a p che a q . (Questo è il tempo O ( 1 ) , basta seguire gli elenchi collegati uno o due passaggi per correggere tutto. Terremo traccia di quanti elementi sono presenti prima / dopo p e q in ciascun elenco e manterremo invariante che entrambi p e q indicano elementi il ​​più vicino possibile alla mediana.)pqabpqO(1)pqpq

Gli elenchi collegati sono solo matrici di indici -element, quindi sono leggere (tranne per il fatto che la località di accesso alla memoria è scarsa).k


Ecco un'implementazione di esempio e parametri di riferimento:

n2106

  • O(nlogk)
  • O(nlogk)
  • O(nlogk)
  • O(nk)
  • k/2
  • Asse Y = tempo di funzionamento in secondi.
  • Dati = numeri interi a 32 bit e numeri interi casuali a 64 bit, provenienti da varie distribuzioni.

tempi di esecuzione


3

mO(nlogm+mlogn)

O(logm)O(logn)O(logn) la carica si verifica una sola volta per mediana.

O(nlogm+mlogk)


Oops, questo non funziona come scritto, poiché se non elimini gli elementi i conteggi non rispecchieranno la nuova finestra. Non sono sicuro che possa essere risolto, ma lascerò la risposta nel caso ci fosse un modo.
Geoffrey Irving,

O(nlogm)

nota a margine: la domanda non è chiara, la struttura dei dati alla base non è definita, sappiamo solo qualcosa di molto vago. come vuoi migliorare qualcosa che non sai di cosa si tratta? come vuoi confrontare il tuo approccio?
Saeed,

1
Mi scuso per il lavoro incompleto. Ho posto la domanda concreta necessaria per risolvere questa risposta qui: cstheory.stackexchange.com/questions/21778/… . Se ritieni che sia appropriato, posso rimuovere questa risposta fino alla risoluzione della domanda secondaria.
Geoffrey Irving,
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.