Finden Sie den laufenden Median aus einem Strom von Ganzzahlen


223

Mögliches Duplikat:
Rollender Median-Algorithmus in C.

Vorausgesetzt, Ganzzahlen werden aus einem Datenstrom gelesen. Finden Sie den Median der bisher gelesenen Elemente auf effiziente Weise.

Lösung Ich habe gelesen: Wir können einen maximalen Heap auf der linken Seite verwenden, um Elemente darzustellen, die kleiner als der effektive Median sind, und einen minimalen Heap auf der rechten Seite, um Elemente darzustellen, die größer als der effektive Median sind.

Nach der Verarbeitung eines eingehenden Elements unterscheidet sich die Anzahl der Elemente in Heaps höchstens um 1 Element. Wenn beide Heaps die gleiche Anzahl von Elementen enthalten, finden wir den Durchschnitt der Stammdaten des Heaps als effektiven Median. Wenn die Heaps nicht ausgeglichen sind, wählen wir den effektiven Median aus der Wurzel des Heaps aus, der mehr Elemente enthält.

Aber wie würden wir einen maximalen und einen minimalen Haufen konstruieren, dh wie würden wir hier den effektiven Median kennen? Ich denke, wir würden 1 Element in max-heap und dann das nächste 1 Element in min-heap einfügen und so weiter für alle Elemente. Korrigieren Sie mich, wenn ich hier falsch liege.


10
Cleverer Algorithmus mit Haufen. Aus dem Titel konnte ich mir nicht sofort eine Lösung vorstellen.
Mooing Duck

1
Die Lösung von vizier sieht für mich gut aus, außer dass ich davon ausgegangen bin (obwohl Sie nicht angegeben haben), dass dieser Stream beliebig lang sein könnte, sodass Sie nicht alles im Speicher behalten können. Ist das der Fall?
Running Wild

2
@RunningWild Für beliebig lange Streams können Sie den Median der letzten N Elemente ermitteln, indem Sie Fibonacci-Heaps verwenden (so dass Sie log (N) -Löschungen erhalten) und Zeiger auf eingefügte Elemente in der Reihenfolge (z. B. in einer Deque) speichern und dann die ältesten entfernen Element bei jedem Schritt, sobald die Haufen voll sind (möglicherweise auch Verschieben von Dingen von einem Haufen zum anderen). Sie könnten etwas besser als N werden, wenn Sie die Anzahl der wiederholten Elemente speichern (wenn es viele Wiederholungen gibt), aber im Allgemeinen denke ich, dass Sie Verteilungsannahmen treffen müssen, wenn Sie den Median des gesamten Streams wollen.
Dougal

2
Sie können mit beiden leeren Haufen beginnen. Das erste int geht auf einen Haufen; Der zweite Punkt geht entweder in den anderen oder Sie verschieben das erste Element auf den anderen Heap und fügen es dann ein. Dies verallgemeinert sich auf "nicht zulassen, dass ein Heap größer als der andere +1 wird" und es ist kein spezielles Gehäuse erforderlich (der "Stammwert" eines leeren Heaps kann als 0 definiert werden)
Jon Watte

Ich habe diese Frage nur in einem MSFT-Interview bekommen. Vielen Dank für die Veröffentlichung
R Claven

Antworten:


383

Es gibt verschiedene Lösungen, um aus gestreamten Daten einen laufenden Median zu ermitteln. Ich werde am Ende der Antwort kurz darauf eingehen.

Die Frage bezieht sich auf die Details einer bestimmten Lösung (Max-Heap / Min-Heap-Lösung) und wie die Heap-basierte Lösung funktioniert, wird nachfolgend erläutert:

Fügen Sie für die ersten beiden Elemente links ein kleineres zum maxHeap und rechts ein größeres zum minHeap hinzu. Verarbeiten Sie dann die Stream-Daten nacheinander.

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

Dann können Sie zu jedem Zeitpunkt den Median wie folgt berechnen:

   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

Jetzt werde ich über das Problem im Allgemeinen sprechen, wie zu Beginn der Antwort versprochen. Das Ermitteln des laufenden Medians aus einem Datenstrom ist ein schwieriges Problem, und das effiziente Finden einer genauen Lösung mit Speicherbeschränkungen ist im allgemeinen Fall wahrscheinlich unmöglich. Wenn die Daten jedoch einige Eigenschaften aufweisen, die wir nutzen können, können wir effiziente, spezialisierte Lösungen entwickeln. Wenn wir beispielsweise wissen, dass die Daten ein integraler Typ sind, können wir die Zählsortierung verwenden, die Ihnen einen konstanten Speicherkonstanten-Zeitalgorithmus geben kann. Die Heap-basierte Lösung ist eine allgemeinere Lösung, da sie auch für andere Datentypen (Doppel) verwendet werden kann. Und schließlich, wenn der genaue Median nicht erforderlich ist und eine Annäherung ausreicht, können Sie einfach versuchen, eine Wahrscheinlichkeitsdichtefunktion für die Daten zu schätzen und den Median damit zu schätzen.


6
Diese Haufen wachsen ungebunden (dh ein Fenster mit 100 Elementen, das über 10 Millionen Elemente gleitet, würde erfordern, dass alle 10 Millionen Elemente im Speicher gespeichert werden). Weiter unten finden Sie eine weitere Antwort mit indizierbaren Skiplisten, bei der nur die zuletzt gesehenen 100 Elemente gespeichert werden müssen.
Raymond Hettinger

1
Sie können eine Lösung mit begrenztem Speicher auch mithilfe von Heaps verwenden, wie in einem der Kommentare zur Frage selbst erläutert.
Hakan Serce

1
Eine Implementierung der Heap-basierten Lösung finden Sie in c hier.
AShelly

1
Wow, das hat mir nicht nur geholfen, dieses spezielle Problem zu lösen, sondern mir auch geholfen, Haufen zu lernen. Hier ist meine grundlegende Implementierung in Python: github.com/PythonAlgo/DataStruct
swati saoji

2
@HakanSerce Kannst du bitte erklären, warum wir das getan haben, was wir getan haben? Ich meine, ich kann das sehen, aber ich kann es nicht intuitiv verstehen.
Shiva

51

Wenn Sie nicht alle Elemente gleichzeitig speichern können, wird dieses Problem erheblich schwieriger. Bei der Heap-Lösung müssen Sie alle Elemente gleichzeitig im Speicher halten. Dies ist in den meisten realen Anwendungen dieses Problems nicht möglich.

Verfolgen Sie stattdessen beim Anzeigen von Zahlen, wie oft Sie jede Ganzzahl sehen. Angenommen, 4-Byte-Ganzzahlen, das sind 2 ^ 32 Buckets oder höchstens 2 ^ 33 Ganzzahlen (Schlüssel und Anzahl für jedes int), was 2 ^ 35 Bytes oder 32 GB entspricht. Es wird wahrscheinlich viel weniger sein, da Sie den Schlüssel nicht speichern oder für die Einträge zählen müssen, die 0 sind (dh wie ein Standarddikt in Python). Das Einfügen jeder neuen Ganzzahl dauert konstant lange.

Um den Median zu finden, verwenden Sie zu jedem Zeitpunkt einfach die Anzahl, um zu bestimmen, welche Ganzzahl das mittlere Element ist. Dies dauert eine konstante Zeit (wenn auch eine große Konstante, aber dennoch konstant).


3
Wenn fast alle Zahlen einmal gesehen werden, benötigt eine spärliche Liste noch mehr Speicher. Und es ist ziemlich wahrscheinlich, dass wenn Sie so viele Zahlen haben, diese nicht in die Zahl passen, dass die meisten Zahlen einmal erscheinen. Ungeachtet dessen ist dies eine clevere Lösung für eine massive Anzahl von Zahlen.
Mooing Duck

1
Für eine spärliche Liste stimme ich zu, dass dies in Bezug auf das Gedächtnis schlechter ist. Wenn die ganzen Zahlen zufällig verteilt sind, werden Sie Duplikate viel früher erhalten, als es die Intuition impliziert. Siehe mathworld.wolfram.com/BirthdayProblem.html . Ich bin mir also ziemlich sicher, dass dies wirksam wird, sobald Sie auch nur ein paar GB Daten haben.
Andrew C

4
@ AndrewC können Sie bitte erklären, wie es konstant Zeit braucht, um den Median zu finden. Wenn ich n verschiedene Arten von ganzen Zahlen gesehen habe, kann das letzte Element im schlimmsten Fall der Median sein. Dies führt dazu, dass der Median die O (n) -Aktivität findet.
Shshnk

@shshnk Ist n nicht die Gesamtzahl der Elemente, die in diesem Fall >>> 2 ^ 35 ist?
VishAmdi

@shshnk Sie haben Recht, dass die Anzahl der verschiedenen Ganzzahlen, die Sie gesehen haben, immer noch linear ist, wie VishAmdi sagte. Ich gehe davon aus, dass n die Anzahl der Zahlen ist, die Sie gesehen haben, was sehr viel ist größer als 2 ^ 33. Wenn Sie nicht so viele Zahlen sehen, ist die Maxheap-Lösung definitiv besser.
Andrew C

49

Wenn die Varianz der Eingabe statistisch verteilt ist (z. B. normal, logarithmisch normal usw.), ist die Reservoirabtastung eine vernünftige Methode zur Schätzung von Perzentilen / Medianwerten aus einem beliebig langen Strom von Zahlen.

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;
      }
  }
}

"Reservoir" ist dann eine laufende, einheitliche (faire) Stichprobe aller Eingaben - unabhängig von der Größe. Das Finden des Medians (oder eines Perzentils) ist dann eine einfache Angelegenheit, das Reservoir zu sortieren und den interessanten Punkt abzufragen.

Da das Reservoir eine feste Größe hat, kann die Sortierung als effektiv O (1) betrachtet werden - und diese Methode wird sowohl mit konstanter Zeit als auch mit konstantem Speicherverbrauch ausgeführt.


Warum brauchen Sie aus Neugier Varianz?
LazyCat

Der Stream gibt möglicherweise weniger als SIZE-Elemente zurück und lässt das Reservoir halb leer. Dies sollte bei der Berechnung des Medians berücksichtigt werden.
Alex

Gibt es eine Möglichkeit, dies zu beschleunigen, indem die Differenz anstelle des Medians berechnet wird? Reichen die entfernte und hinzugefügte Stichprobe und der vorherige Median dafür aus?
inf3rno

30

Der effizienteste Weg, ein Perzentil eines Stroms zu berechnen, den ich gefunden habe, ist der P²-Algorithmus: Raj Jain, Imrich Chlamtac: Der P²-Algorithmus zur dynamischen Berechnung von Quantilen und Histogrammen ohne Speichern von Beobachtungen. Kommun. ACM 28 (10): 1076 & ndash; 1085 (1985)

Der Algorithmus ist einfach zu implementieren und funktioniert sehr gut. Es ist jedoch eine Schätzung, denken Sie also daran. Aus der Zusammenfassung:

Für die dynamische Berechnung des Medians und anderer Quantile wird ein heuristischer Algorithmus vorgeschlagen. Die Schätzungen werden dynamisch erstellt, wenn die Beobachtungen generiert werden. Die Beobachtungen werden nicht gespeichert; Daher hat der Algorithmus unabhängig von der Anzahl der Beobachtungen einen sehr kleinen und festen Speicherbedarf. Dies macht es ideal für die Implementierung in einen Quantil-Chip, der in industriellen Steuerungen und Rekordern verwendet werden kann. Der Algorithmus wird weiter auf das Zeichnen von Histogrammen erweitert. Die Genauigkeit des Algorithmus wird analysiert.


2
Count-Min Sketch ist insofern besser als P ^ 2, als es auch eine Fehlerbindung ergibt, während letztere dies nicht tut.
SinoTrinity

1
Berücksichtigen Sie auch die "platzsparende Online-Berechnung von Quantilzusammenfassungen" von Greenwald und Khanna, die ebenfalls Fehlergrenzen angibt und einen guten Speicherbedarf aufweist.
Paul Chernoch

1
Einen probabilistischen Ansatz finden Sie in diesem Blog-Beitrag: research.neustar.biz/2013/09/16/…. Das Dokument, auf das verwiesen wird, finden Sie hier: arxiv.org/pdf/1407.1121v1.pdf Dies wird als "Frugal" bezeichnet Streaming "
Paul Chernoch

27

Wenn wir den Median der n zuletzt gesehenen Elemente ermitteln möchten , hat dieses Problem eine genaue Lösung, bei der nur die n zuletzt gesehenen Elemente gespeichert werden müssen. Es ist schnell und skaliert gut.

Eine indizierbare Skiplist unterstützt das Einfügen, Entfernen und indizierte Suchen beliebiger Elemente durch O (ln n) unter Beibehaltung der sortierten Reihenfolge. In Verbindung mit einer FIFO-Warteschlange , die den n-ten ältesten Eintrag verfolgt, ist die Lösung einfach:

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]

Hier finden Sie Links zum vollständigen Arbeitscode (eine leicht verständliche Klassenversion und eine optimierte Generatorversion mit dem indizierbaren Skiplist-Code):


7
Wenn ich es jedoch richtig verstehe, erhalten Sie nur einen Median der letzten N gesehenen Elemente, nicht alle Elemente bis zu diesem Punkt. Dies scheint jedoch eine wirklich clevere Lösung für diese Operation zu sein.
Andrew C

16
Richtig. Die Antwort klingt so, als ob es möglich wäre, den Median aller Elemente zu finden, indem nur die letzten n Elemente im Speicher bleiben - das ist im Allgemeinen unmöglich. Der Algorithmus findet nur den Median der letzten n Elemente.
Hans-Peter Störr

8
Der Begriff "laufender Median" wird normalerweise verwendet, um sich auf den Median einer Teilmenge von Daten zu beziehen . Das OP wird in nicht standardmäßiger Weise als gebräuchlicher Begriff verwendet.
Rachel Hettinger

18

Eine intuitive Möglichkeit, darüber nachzudenken, besteht darin, dass bei einem vollständig ausgeglichenen binären Suchbaum die Wurzel das Medianelement ist, da es die gleiche Anzahl kleinerer und größerer Elemente gibt. Wenn der Baum nicht voll ist, ist dies nicht ganz der Fall, da in der letzten Ebene Elemente fehlen.

Wir können also stattdessen den Median und zwei ausgeglichene Binärbäume haben, einen für Elemente, die kleiner als der Median sind, und einen für Elemente, die größer als der Median sind. Die beiden Bäume müssen gleich groß sein.

Wenn wir eine neue Ganzzahl aus dem Datenstrom erhalten, vergleichen wir sie mit dem Median. Wenn es größer als der Median ist, fügen wir es dem rechten Baum hinzu. Wenn sich die beiden Baumgrößen um mehr als 1 unterscheiden, entfernen wir das min-Element des rechten Baums, machen es zum neuen Median und setzen den alten Median in den linken Baum. Ähnliches gilt für kleinere.


Wie wirst du das machen? "Wir entfernen das min-Element des rechten Baumes"
Hengameh

2
Ich meinte binäre Suchbäume, also ist das min-Element den ganzen Weg von der Wurzel entfernt.
Irene Papakonstantinou

7

Effizient ist ein Wort, das vom Kontext abhängt. Die Lösung für dieses Problem hängt von der Anzahl der ausgeführten Abfragen im Verhältnis zur Anzahl der Einfügungen ab. Angenommen, Sie fügen gegen Ende des Medians N-Zahlen und K-mal ein. Die Komplexität des Heap-basierten Algorithmus wäre O (N log N + K).

Betrachten Sie die folgende Alternative. Stellen Sie die Zahlen in ein Array und führen Sie für jede Abfrage den linearen Auswahlalgorithmus aus (z. B. mithilfe des QuickSort-Pivots). Jetzt haben Sie einen Algorithmus mit der Laufzeit O (KN).

Wenn nun K ausreichend klein ist (seltene Abfragen), ist der letztere Algorithmus tatsächlich effizienter und umgekehrt.


1
Im Heap-Beispiel ist die Suche eine konstante Zeit, daher denke ich, dass es O (N log N + K) sein sollte, aber Ihr Punkt gilt immer noch.
Andrew C

Ja, guter Punkt, wird dies herausarbeiten. Sie haben Recht N log N ist immer noch der führende Begriff.
Peteris

-2

Kannst du das nicht mit nur einem Haufen machen? Update: nein. Siehe den Kommentar.

Invariante: Nach dem Lesen von 2*nEingaben enthält der Min-Heap die ngrößte davon.

Schleife: 2 Eingänge lesen. Fügen Sie beide dem Heap hinzu und entfernen Sie die min. Dies stellt die Invariante wieder her.

Wenn also 2nEingaben gelesen wurden, ist die min des Heaps die n-te größte. Es muss eine zusätzliche Komplikation geben, um die beiden Elemente um die Medianposition zu mitteln und Abfragen nach einer ungeraden Anzahl von Eingaben zu bearbeiten.


1
Funktioniert nicht: Sie können Dinge fallen lassen, die sich später als ganz oben herausstellen. Versuchen Sie zum Beispiel Ihren Algorithmus mit den Zahlen 1 bis 100, aber in umgekehrter Reihenfolge: 100, 99, ..., 1.
Zellyn

Danke, Zellyn. Es war dumm von mir, mich davon zu überzeugen, dass die Invariante wiederhergestellt wurde.
Darius Bacon
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.