Trova la mediana corrente da un flusso di numeri interi


223

Possibile duplicato:
algoritmo rolling medio in C

Dato che gli interi vengono letti da un flusso di dati. Trova la mediana degli elementi letti finora in modo efficiente.

Soluzione che ho letto: possiamo usare un heap massimo sul lato sinistro per rappresentare elementi che sono inferiori alla mediana effettiva e un heap minimo sul lato destro per rappresentare elementi che sono maggiori della mediana effettiva.

Dopo aver elaborato un elemento in entrata, il numero di elementi negli heap differisce al massimo per 1 elemento. Quando entrambi gli heap contengono lo stesso numero di elementi, troviamo la media dei dati radice dell'heap come mediana efficace. Quando i cumuli non sono bilanciati, selezioniamo la mediana effettiva dalla radice dell'heap contenente più elementi.

Ma come costruiremmo un heap massimo e un heap minimo, ad esempio come potremmo conoscere la mediana effettiva qui? Penso che inseriremmo 1 elemento in max-heap e poi il successivo 1 elemento in min-heap, e così via per tutti gli elementi. Correggimi se sbaglio qui.


10
Algoritmo intelligente, usando heap. Dal titolo non sono riuscito a pensare immediatamente a una soluzione.
Mooing Duck,

1
La soluzione di Visir mi sembra buona, tranne per il fatto che stavo assumendo (anche se non hai dichiarato) che questo flusso potrebbe essere arbitrariamente lungo, quindi non puoi tenere tutto in memoria. È così?
Running Wild,

2
@RunningWild Per flussi arbitrariamente lunghi, è possibile ottenere la mediana degli ultimi N elementi utilizzando heap di Fibonacci (in modo da ottenere le cancellazioni del registro (N)) e archiviando i puntatori sugli elementi inseriti in ordine (ad esempio un deque), quindi rimuovendo il più vecchio elemento ad ogni passaggio una volta che gli heap sono pieni (forse anche spostando le cose da un mucchio all'altro). Potresti ottenere un po 'meglio di N memorizzando il numero di elementi ripetuti (se ci sono molte ripetizioni), ma in generale, penso che devi fare una sorta di ipotesi distributive se vuoi la mediana dell'intero flusso.
Dougal,

2
Puoi iniziare con entrambi i cumuli vuoti. Il primo int va in un heap; il secondo va nell'altro oppure si sposta il primo elemento nell'altro heap e si inserisce. Questo generalizza a "non permettere che un heap diventi più grande dell'altro +1" e non è necessario un involucro speciale (il "valore di radice" di un heap vuoto può essere definito come 0)
Jon Watte,

Ho appena ricevuto questa domanda durante un'intervista con MSFT. Grazie per la pubblicazione
R Claven,

Risposte:


383

Esistono diverse soluzioni per trovare la mediana in esecuzione dai dati trasmessi in streaming, ne parlerò brevemente alla fine della risposta.

La domanda riguarda i dettagli di una soluzione specifica (soluzione heap max / heap max) e come funziona la soluzione basata su heap è spiegata di seguito:

Per i primi due elementi aggiungi uno più piccolo a maxHeap a sinistra e uno più grande a minHeap a destra. Quindi elaborare i dati di flusso uno per uno,

Step 1: Add next item to one of the heaps

   if next item is smaller than maxHeap root add it to maxHeap,
   else add it to minHeap

Step 2: Balance the heaps (after this step heaps will be either balanced or
   one of them will contain 1 more item)

   if number of elements in one of the heaps is greater than the other by
   more than 1, remove the root element from the one containing more elements and
   add to the other one

Quindi in qualsiasi momento puoi calcolare la mediana in questo modo:

   If the heaps contain equal amount of elements;
     median = (root of maxHeap + root of minHeap)/2
   Else
     median = root of the heap with more elements

Ora parlerò del problema in generale, come promesso all'inizio della risposta. Trovare la mediana in esecuzione da un flusso di dati è un problema difficile e trovare una soluzione esatta con vincoli di memoria in modo efficiente è probabilmente impossibile per il caso generale. D'altra parte, se i dati hanno alcune caratteristiche che possiamo sfruttare, possiamo sviluppare soluzioni specializzate efficienti. Ad esempio, se sappiamo che i dati sono di tipo integrale, allora possiamo usare l' ordinamento di conteggio, che può fornire un algoritmo a tempo costante di memoria costante. La soluzione basata su heap è una soluzione più generale perché può essere utilizzata anche per altri tipi di dati (doppi). E infine, se non è richiesta la mediana esatta e un'approssimazione è sufficiente, puoi semplicemente provare a stimare una funzione di densità di probabilità per i dati e stimare la mediana usando quella.


6
Questi cumuli crescono senza limiti (vale a dire una finestra di 100 elementi che scorre su 10 milioni di elementi richiederebbe che i 10 milioni di elementi siano tutti archiviati in memoria). Vedi sotto per un'altra risposta usando gli skiplist indicizzabili che richiedono solo la memorizzazione degli ultimi 100 elementi visti più di recente.
Raymond Hettinger,

1
Puoi avere una soluzione di memoria limitata anche usando heap, come spiegato in uno dei commenti alla domanda stessa.
Hakan Serce,

1
Puoi trovare un'implementazione della soluzione basata su heap in c qui.
AShelly,

1
Wow, questo mi ha aiutato non solo a risolvere questo specifico problema, ma mi ha anche aiutato a imparare un mucchio di cose: ecco la mia implementazione di base in python: github.com/PythonAlgo/DataStruct
swati saoji

2
@HakanSerce Puoi per favore spiegare perché abbiamo fatto quello che abbiamo fatto? Voglio dire, posso vedere che funziona, ma non sono in grado di capirlo intuitivamente.
Shiva,

51

Se non riesci a conservare tutti gli elementi in memoria contemporaneamente, questo problema diventa molto più difficile. La soluzione heap richiede di conservare tutti gli elementi in memoria contemporaneamente. Questo non è possibile nella maggior parte delle applicazioni del mondo reale di questo problema.

Invece, mentre vedi i numeri, tieni traccia del conteggio del numero di volte che vedi ogni numero intero. Supponendo numeri interi a 4 byte, ovvero 2 ^ 32 bucket, o al massimo 2 ^ 33 numeri interi (chiave e conteggio per ogni int), ovvero 2 ^ 35 byte o 32 GB. Probabilmente sarà molto meno di questo perché non è necessario archiviare la chiave o contare per quelle voci che sono 0 (cioè come un defaultdict in Python). Questo richiede tempo costante per inserire ogni nuovo numero intero.

Quindi, in qualsiasi momento, per trovare la mediana, basta usare i conteggi per determinare quale numero intero è l'elemento centrale. Questo richiede tempo costante (anche se una grande costante, ma costante).


3
Se quasi tutti i numeri vengono visualizzati una volta, un elenco sparso occuperà ancora più memoria. E sembra piuttosto probabile che se hai così tanti numeri che non rientrano nel numero che la maggior parte dei numeri apparirà una volta. Nonostante ciò, questa è una soluzione intelligente per un numero enorme di numeri.
Mooing Duck,

1
Per un elenco sparso, sono d'accordo, questo è peggio in termini di memoria. Tuttavia, se gli interi sono distribuiti casualmente, inizierai a ottenere duplicati molto prima di quanto l'intuizione implichi. Vedi mathworld.wolfram.com/BirthdayProblem.html . Quindi sono abbastanza sicuro che questo diventerà effettivo non appena avrai anche pochi GB di dati.
Andrew C,

4
@AndrewC puoi spiegare come ci vorrà tempo costante per trovare la mediana. Se ho visto n diversi tipi di numeri interi, nel peggiore dei casi l'ultimo elemento potrebbe essere la mediana. Questo rende la ricerca mediana di attività O (n).
shshnk,

@shshnk Non è n il numero totale di elementi che è >>> 2 ^ 35 in questo caso?
VishAmdi

@shshnk Hai ragione sul fatto che è ancora lineare nel numero di diversi numeri interi che hai visto, come ha detto VishAmdi, il presupposto che sto facendo per questa soluzione è che n è il numero di numeri che hai visto, che è molto più grande di 2 ^ 33. Se non vedi così tanti numeri, la soluzione maxheap è decisamente migliore.
Andrew C

49

Se la varianza dell'ingresso è statisticamente distribuita (ad es. Normale, log-normale, ecc.), Il campionamento del serbatoio è un modo ragionevole di stimare percentili / mediane da un flusso di numeri arbitrariamente lungo.

int n = 0;  // Running count of elements observed so far  
#define SIZE 10000
int reservoir[SIZE];  

while(streamHasData())
{
  int x = readNumberFromStream();

  if (n < SIZE)
  {
       reservoir[n++] = x;
  }         
  else 
  {
      int p = random(++n); // Choose a random number 0 >= p < n
      if (p < SIZE)
      {
           reservoir[p] = x;
      }
  }
}

Il "serbatoio" è quindi un campione funzionante, uniforme (discreto) di tutti gli input, indipendentemente dalle dimensioni. Trovare la mediana (o qualsiasi altro percentile) è quindi una questione semplice di smistamento del serbatoio e polling del punto interessante.

Poiché il serbatoio ha dimensioni fisse, l'ordinamento può essere considerato efficacemente O (1) - e questo metodo funziona con tempo e consumo di memoria costanti.


per curiosità, perché hai bisogno di varianza?
LazyCat,

Il flusso potrebbe restituire meno degli elementi SIZE lasciando il serbatoio mezzo vuoto. Questo dovrebbe essere considerato quando si calcola la mediana.
Alex,

C'è un modo per renderlo più veloce calcolando la differenza anziché la mediana? Il campione rimosso e aggiunto e la mediana precedente sono sufficienti per questo?
inf3rno,

30

Il modo più efficiente per calcolare un percentile di un flusso che ho trovato è l'algoritmo P²: Raj Jain, Imrich Chlamtac: l'algoritmo P² per il calcolo dinamico di quantiiles e istogrammi senza memorizzazione delle osservazioni. Commun. ACM 28 (10): 1076-1085 (1985)

L'algoritmo è semplice da implementare e funziona estremamente bene. È una stima, tuttavia, tienilo a mente. Dall'abstract:

Viene proposto un algoritmo euristico per il calcolo dinamico della mediana e di altri quantili. Le stime sono prodotte in modo dinamico man mano che vengono generate le osservazioni. Le osservazioni non sono memorizzate; pertanto, l'algoritmo ha un requisito di archiviazione molto piccolo e fisso indipendentemente dal numero di osservazioni. Ciò lo rende ideale per l'implementazione in un chip quantile che può essere utilizzato in controller e registratori industriali. L'algoritmo è ulteriormente esteso alla rappresentazione dell'istogramma. L'accuratezza dell'algoritmo viene analizzata.


2
Count-Min Sketch è meglio di P ^ 2 in quanto fornisce anche errori associati mentre quest'ultimo non lo fa.
sinoTrinity,

1
Considera anche "Calcolo online efficiente in termini di spazio dei riepiloghi quantitativi" di Greenwald e Khanna, che fornisce anche limiti di errore e ha buoni requisiti di memoria.
Paul Chernoch,

1
Inoltre, per un approccio probabilistico, vedi questo post sul blog: research.neustar.biz/2013/09/16/… e il documento a cui fa riferimento è qui: arxiv.org/pdf/1407.1121v1.pdf Questo si chiama "Frugale Streaming "
Paul Chernoch,

27

Se vogliamo trovare la mediana degli n elementi visti più di recente, questo problema ha una soluzione esatta che necessita solo degli n elementi visti più di recente per essere tenuti in memoria. È veloce e si adatta bene.

Uno skiplist indicizzabile supporta l'inserimento, la rimozione e la ricerca indicizzata di elementi arbitrari O (ln n) mantenendo l'ordine ordinato. Se abbinata a una coda FIFO che tiene traccia dell'n-esima voce più antica, la soluzione è semplice:

class RunningMedian:
    'Fast running median with O(lg n) updates where n is the window size'

    def __init__(self, n, iterable):
        self.it = iter(iterable)
        self.queue = deque(islice(self.it, n))
        self.skiplist = IndexableSkiplist(n)
        for elem in self.queue:
            self.skiplist.insert(elem)

    def __iter__(self):
        queue = self.queue
        skiplist = self.skiplist
        midpoint = len(queue) // 2
        yield skiplist[midpoint]
        for newelem in self.it:
            oldelem = queue.popleft()
            skiplist.remove(oldelem)
            queue.append(newelem)
            skiplist.insert(newelem)
            yield skiplist[midpoint]

Ecco i collegamenti per completare il codice di lavoro (una versione di classe di facile comprensione e una versione del generatore ottimizzata con il codice skiplist indicizzabile in linea):


7
Se lo capisco correttamente, però, questo ti dà solo una mediana degli ultimi N elementi visti, non tutti gli elementi fino a quel punto. Questa sembra comunque una soluzione davvero semplice per quell'operazione.
Andrew C,

16
Destra. La risposta suona come se fosse possibile trovare la mediana di tutti gli elementi semplicemente mantenendo gli ultimi n elementi in memoria - questo è impossibile in generale. L'algoritmo trova solo la mediana degli ultimi n elementi.
Hans-Peter Störr,

8
Il termine "running median" viene in genere utilizzato per fare riferimento alla mediana di un sottoinsieme di dati. L'OP viene utilizzato un termine comune in modo non standard.
Rachel Hettinger,

18

Un modo intuitivo per pensarci è che se avessi un albero di ricerca binario completamente bilanciato, allora la radice sarebbe l'elemento mediano, poiché ci sarebbe lo stesso numero di elementi più piccoli e più grandi. Ora, se l'albero non è pieno, non sarà così, poiché mancheranno elementi dell'ultimo livello.

Quindi quello che possiamo fare è avere la mediana e due alberi binari bilanciati, uno per elementi inferiori alla mediana e uno per elementi maggiori della mediana. I due alberi devono essere mantenuti delle stesse dimensioni.

Quando otteniamo un nuovo numero intero dal flusso di dati, lo confrontiamo con la mediana. Se è maggiore della mediana, la aggiungiamo all'albero giusto. Se le due dimensioni dell'albero differiscono più di 1, rimuoviamo l'elemento minimo dell'albero destro, lo rendiamo la nuova mediana e posizioniamo la vecchia mediana nell'albero di sinistra. Allo stesso modo per i più piccoli.


Come intendi farlo? "rimuoviamo l'elemento minimo dell'albero giusto"
Hengameh,

2
Intendevo alberi di ricerca binari, quindi l'elemento min è completamente lasciato dalla radice.
Irene Papakonstantinou,

7

Efficiente è una parola che dipende dal contesto. La soluzione a questo problema dipende dalla quantità di query eseguite rispetto alla quantità di inserzioni. Supponiamo che tu stia inserendo N numeri e K volte verso la fine che eri interessato alla mediana. La complessità dell'algoritmo basato sull'heap sarebbe O (N log N + K).

Considera la seguente alternativa. Inserisci i numeri in un array e, per ogni query, esegui l'algoritmo di selezione lineare (usando il pivot quicksort, diciamo). Ora hai un algoritmo con tempo di esecuzione O (KN).

Ora se K è sufficientemente piccolo (query poco frequenti), quest'ultimo algoritmo è in realtà più efficiente e viceversa.


1
Nell'esempio di heap, la ricerca è tempo costante, quindi penso che dovrebbe essere O (N log N + K), ma il tuo punto è ancora valido.
Andrew C,

Sì, buon punto, lo modificherò. Hai ragione N log N è ancora il termine principale.
Peteris,

-2

Non puoi farlo con un solo mucchio? Aggiornamento: no. Vedi il commento

Invariante: dopo aver letto gli 2*ninput, l'heap min contiene il npiù grande di essi.

Loop: leggi 2 ingressi. Aggiungili entrambi all'heap e rimuovi il minimo dell'heap. Ciò ristabilisce l'invariante.

Quindi, quando gli 2ninput sono stati letti, il minimo dell'heap è l'ennesimo più grande. Dovrà esserci una piccola complicazione in più per calcolare la media dei due elementi attorno alla posizione mediana e gestire le query dopo un numero dispari di input.


1
Non funziona: puoi eliminare cose che in seguito si rivelano vicine alla cima. Ad esempio, prova l'algoritmo con i numeri da 1 a 100, ma in ordine inverso: 100, 99, ..., 1.
zellyn

Grazie Zellyn. Sciocco da parte mia convincermi che l'invariante fu ristabilito.
Darius Bacon,
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.