Nichttrivialer Algorithmus zur Berechnung eines Gleitfenstermedians

25

Ich muss den laufenden Median berechnen:

  • Eingabe: n , k , Vektor (x1,x2,,xn) .

  • Ausgabe: Vektor (y1,y2,,ynk+1) , wobei yi der Median von (xi,xi+1,,xi+k1) .

(Kein Schummeln mit Näherungen; ich hätte gerne genaue Lösungen. Elemente sind große ganze Zahlen.)xi

Es gibt einen einfachen Algorithmus, der einen Suchbaum der Größe . Die Gesamtlaufzeit beträgt O ( n log k ) . (Hier bezieht sich ein "Suchbaum" auf eine effiziente Datenstruktur, die Einfügungen, Löschungen und mittlere Abfragen in logarithmischer Zeit unterstützt.)kO(nlogk)

Das kommt mir allerdings etwas blöd vor. Wir werden effektiv alle Ordnungsstatistiken in allen Fenstern der Größe lernen , nicht nur die Mediane. Darüber hinaus ist dies in der Praxis nicht allzu attraktiv, insbesondere wenn k groß ist (große Suchbäume sind in der Regel langsam, der Overhead beim Speicherverbrauch ist nicht trivial, die Cache-Effizienz ist oft schlecht usw.).kk

Können wir etwas wesentlich besser machen?

Gibt es Untergrenzen (z. B. ist der Trivialalgorithmus für das Vergleichsmodell asymptotisch optimal)?


Edit: David Eppstein gab eine schöne Untergrenze für das Vergleichsmodell! Ich frage mich, ob es trotzdem möglich ist, etwas Klügeres als den trivialen Algorithmus zu tun.

Könnten wir zum Beispiel etwas in diese Richtung tun: Teilen Sie den Eingabevektor in Teile der Größe ; sortiere jeden Teil (verfolge die ursprünglichen Positionen jedes Elements); und dann den stückweise sortierten Vektor verwenden, um die laufenden Mediane ohne zusätzliche Datenstrukturen effizient zu finden? Natürlich wäre dies immer noch O ( n log k ) , aber in der Praxis ist das Sortieren von Arrays in der Regel viel schneller als das Verwalten von Suchbäumen.kO(nlogk)


Bearbeiten 2: Saeed wollte einige Gründe sehen, warum das Sortieren meiner Meinung nach schneller ist als Suchbaumoperationen. Hier sind sehr schnelle Benchmarks für , n = 10 8 :k=107n=108

  • ≈ 8s: Sortieren von Vektoren mit jeweils k Elementenn/kk
  • ≈ 10s: Sortierung eines Vektors mit Elementenn
  • ≈ 80s: Einfügungen und Löschungen in einer Hash-Tabelle der Größe knk
  • ≈ 390s: Einfügungen und Löschungen in einem ausgeglichenen Suchbaum der Größe knk

Die Hash-Tabelle dient nur zum Vergleich. Es ist in dieser Anwendung nicht direkt von Nutzen.

Zusammenfassend lässt sich sagen, dass sich die Sortierleistung im Vergleich zu ausgeglichenen Suchbaumoperationen um fast den Faktor 50 unterscheidet. Und es wird noch schlimmer, wenn wir erhöhen .k

(Technische Details: Daten = zufällige 32-Bit-Ganzzahlen. Computer = ein typischer moderner Laptop. Der Testcode wurde in C ++ unter Verwendung der Standard-Bibliotheksroutinen (std :: sort) und Datenstrukturen (std :: multiset, std :: unsorted_multiset). Ich habe zwei verschiedene C ++ - Compiler (GCC und Clang) und zwei verschiedene Implementierungen der Standardbibliothek (libstdc ++ und libc ++) verwendet. Traditionell wurde std :: multiset als hochoptimierter Rot-Schwarz-Baum implementiert.)

Jukka Suomela
quelle
1
Ich glaube nicht , Sie in der Lage sein zu verbessern . Der Grund dafür ist, wenn Sie an einem Fenster schauen x t , . . . , x t + k - 1nlogkxt,...,xt+k1 , können Sie niemals eine der Zahlen als Median des zukünftigen Fensters. Dies bedeutet, dass Sie in jeder Zeit mindestens k halten müssenxt+k2,...,xt+k1 Ganzzahlen in einer Datenstruktur, und die Aktualisierung scheint nicht kürzer als die Protokollierungszeit zu sein. k2
RB
Ihr trivialer Algorithmus scheint für mich nicht O ( n log k ) , habe ich etwas falsch verstanden? Und ich denke deswegen hast du ein Problem mit big k , sonst ist der logarithmische Faktor in praktischen Anwendungen nichts, und es gibt keine große versteckte Konstante in diesem Algorithmus. O((nk)klogk)O(nlogk)k
Saeed
@ Saeed: Im einfachen Algorithmus verarbeiten Sie Elemente nacheinander. In Schritt fügen Sie x i zum Suchbaum hinzu und (wenn i > k ) entfernen Sie auch x i - k aus dem Suchbaum. Dies sind n Schritte, von denen jeder eine Zeit von 0 ( log k ) benötigt . ixii>kxiknO(logk)
Jukka Suomela
Sie meinen also, Sie haben einen ausgeglichenen Suchbaum, keinen zufälligen Suchbaum?
Saeed
1
@ Saeed: Bitte beachten Sie, dass ich in meinen Benchmarks nicht einmal versucht habe, Mediane zu finden. Ich habe gerade Einfügungen und n Löschungen in einem Suchbaum der Größe k vorgenommen , und diese Operationen werden garantiert O ( log k ) Zeit in Anspruch nehmen . Sie müssen nur akzeptieren, dass Suchbaumoperationen in der Praxis im Vergleich zur Sortierung sehr langsam sind. Sie werden dies leicht erkennen, wenn Sie versuchen, einen Sortieralgorithmus zu schreiben, der durch Hinzufügen von Elementen zu einem ausgeglichenen Suchbaum funktioniert - dies funktioniert sicherlich in OnnkO(logk) -Zeit, ist aber in der Praxis lächerlich langsam und verschwendet auch viel der Erinnerung. O(nlogn)
Jukka Suomela

Antworten:

32

Hier ist eine Untergrenze für das Sortieren. Erstellen Sie für einen zu sortierenden Eingabesatz der Länge n eine Eingabe für Ihr laufendes Medianproblem, die aus n - 1 Kopien einer Zahl kleiner als das Minimum von S , dann aus S selbst und dann aus n - 1 Kopien einer Zahl größer als besteht das Maximum von S und setze k = 2 n - 1 . Die laufenden Mediane dieser Eingabe stimmen mit der sortierten Reihenfolge von S überein .Snn1SSn1Sk=2n1S

Also in einem Vergleichsmodell der Berechnung, Zeit erforderlich. Wenn es sich bei Ihren Eingaben möglicherweise um Ganzzahlen handelt und Sie Ganzzahl-Sortieralgorithmen verwenden, können Sie dies besser tun.Ω(nlogn)

David Eppstein
quelle
6
Diese Antwort lässt mich wirklich fragen, ob auch die Umkehrung zutrifft: Erhalten wir bei einem effizienten Sortieralgorithmus einen effizient ausgeführten Median-Algorithmus? (Bedeutet ein effizienter Ganzzahl-Sortieralgorithmus beispielsweise einen effizient ausgeführten Median-Algorithmus für Ganzzahlen? Oder liefert ein IO-effizienter Sortieralgorithmus einen IO-effizient ausgeführten Median-Algorithmus?)
Jukka Suomela,
1
Nochmals vielen Dank für Ihre Antwort, es hat mich wirklich auf den richtigen Weg gebracht und mich zu dem sortierungsbasierten Medianfilter-Algorithmus inspiriert! Am Ende konnte ich einen Artikel aus dem Jahr 1991 finden, der im Grunde das gleiche Argument enthielt wie Sie, und Pat Morin gab einen Hinweis auf einen anderen relevanten Artikel aus dem Jahr 2005; siehe refs. [6] und [9] hier .
Jukka Suomela
9

Bearbeiten: Dieser Algorithmus wird jetzt hier vorgestellt: http://arxiv.org/abs/1406.1717


Ja, um dieses Problem zu lösen, ist es ausreichend, die folgenden Vorgänge auszuführen:

  • Sortieren Sie Vektoren mit jeweils k Elementen.n/kk
  • Führen Sie eine zeitlineare Nachbearbeitung durch.

Grob gesagt lautet die Idee:

  • Man betrachte zwei benachbarte Eingangsblöcke und b , beide mit k Elementen; lassen Sie die Elemente sein , eine 1 , eine 2 , . . . , A k und b 1 , b 2 , . . . , b k in der Reihenfolge des Auftretens im Eingabevektor x .abka1,a2,...,akb1,b2,...,bkx
  • Sortieren Sie diese Blöcke und lernen Sie den Rang jedes Elements innerhalb des Blocks.
  • Erweitern Sie die Vektoren und b mit Vorgänger- / Nachfolgerzeigern, sodass wir die Elemente in aufsteigender Reihenfolge durchlaufen können, indem wir den Zeigerketten folgen. Auf diese Weise haben wir doppelt verknüpfte Listen a ' und b ' konstruiert .abab
  • Einer nach dem anderen, löschen Sie alle Elemente aus der verknüpften Liste , in der umgekehrten Reihenfolge des Erscheinens b k , b k - 1 , . . . , B 1 . Wenn wir ein Element löschen, merken Sie sich, was zum Zeitpunkt des Löschens sein Nachfolger und Vorgänger war .bbk,bk1,...,b1
  • Behalten Sie nun die "Medianzeiger" und q bei , die auf die Listen a ' und b ' zeigen . Initialisiere p bis zum Mittelpunkt von a ' und initialisiere q bis zum Ende der leeren Liste b ' .pqabpaqb
  • Für jedes :i

    • Löschen Sie aus der Liste a ' (dies ist 0 ( 1 ) Mal, löschen Sie es einfach aus der verknüpften Liste). Vergleichen Sie a i mit dem Element, auf das p zeigt, um festzustellen, ob wir vor oder nach p gelöscht haben .aiaO(1)aipp
    • Setzen Sie wieder in die Liste b ' an seiner ursprünglichen Position (dies ist O ( 1 ) Mal, wir haben den Vorgänger und Nachfolger von b i auswendig gelernt ). Vergleichen Sie b i mit dem Element, auf das q zeigt , um festzustellen , ob wir das Element vor oder nach q hinzugefügt haben .bibO(1)bibiqq
    • Update Zeiger und q , so dass der Medianwert der verbundenen Liste a 'B ' ist entweder auf p oder q . (Dies ist O ( 1 ) Mal. Folgen Sie einfach den verknüpften Listen in ein oder zwei Schritten, um alles zu reparieren. Wir werden verfolgen, wie viele Elemente vor / nach p und q in jeder Liste sind, und wir werden die Invariante beibehalten, dass beide p und q auf Elemente zeigen, die so nah wie möglich am Median liegen.)pqabpqO(1)pqpq

Die verknüpften Listen sind nur Element-Arrays von Indizes, daher sind sie leichtgewichtig (mit der Ausnahme, dass die Lokalität des Speicherzugriffs schlecht ist).k


Hier ist eine Beispielimplementierung und Benchmarks:

Hier ist eine Darstellung der Laufzeiten (für ):n2106

  • Blau = Sortieren + Nachbearbeitung, O(nlogk) .
  • Grün = Erhalte zwei Haufen, O(nlogk) , Implementierung von https://github.com/craffel/median-filter
  • Rot = zwei Suchbäume pflegen, O(nlogk) .
  • Schwarz = Beibehaltung eines sortierten Vektors, O(nk) .
  • X - Achse = Fenstergröße ( k/2 ).
  • Y-Achse = Laufzeit in Sekunden.
  • Daten = 32-Bit-Ganzzahlen und zufällige 64-Bit-Ganzzahlen aus verschiedenen Verteilungen.

Laufzeiten

Jukka Suomela
quelle
3

In Anbetracht von Davids Grenzen ist es unwahrscheinlich, dass Sie einen besseren Worst-Case-Fall erzielen können, aber es gibt bessere ausgangssensitive Algorithmen. Insbesondere wenn in der Anzahl der Mediane im Ergebnis ist, können wir das Problem in der Zeit O ( n log m + m log n ) lösen .mO(nlogm+mlogn)

O(logm)O(logn)O(logn) Die Aufladung erfolgt nur einmal pro Median.

O(nlogm+mlogk)

Geoffrey Irving
quelle
Hoppla, dies funktioniert nicht wie geschrieben, da die Anzahl der Elemente nicht das neue Fenster widerspiegelt, wenn Sie keine Elemente löschen. Ich bin nicht sicher, ob es behoben werden kann, aber ich werde die Antwort hinterlassen, falls es einen Weg gibt.
Geoffrey Irving
O(nLogm)
Randnotiz: Frage ist nicht klar, untergeordnete Datenstruktur ist nicht definiert, wir wissen nur etwas sehr vages. Wie wollen Sie etwas verbessern, von dem Sie nicht wissen, was es ist? Wie wollen Sie Ihren Ansatz vergleichen?
Saeed
1
Ich entschuldige mich für die unvollständige Arbeit. Ich habe hier die konkrete Frage gestellt, die zur Behebung dieser Antwort erforderlich ist: cstheory.stackexchange.com/questions/21778/… . Wenn Sie es für angebracht halten, kann ich diese Antwort entfernen, bis die zweite Frage geklärt ist.
Geoffrey Irving