Algorithme non trivial pour calculer la médiane d'une fenêtre coulissante

25

J'ai besoin de calculer la médiane en cours d'exécution:

  • Entrée: , , vecteur .k ( x 1 , x 2 , , x n )nk(x1,x2,,xn)

  • Sortie: vecteur , où est la médiane de .y i ( x i , x i + 1 , , x i + k - 1 )(y1,y2,,ynk+1)yi(xi,xi+1,,xi+k1)

(Pas de tricherie avec des approximations; je voudrais avoir des solutions exactes. Les éléments xi sont de grands entiers.)

Il existe un algorithme trivial qui maintient un arbre de recherche de taille k ; la durée totale d'exécution est O(nlogk) . (Ici, un "arbre de recherche" fait référence à une structure de données efficace qui prend en charge les insertions, les suppressions et les requêtes médianes en temps logarithmique.)

Cependant, cela me semble un peu stupide. Nous apprendrons efficacement toutes les statistiques de commande dans toutes les fenêtres de taille k , pas seulement les médianes. De plus, cela n'est pas trop attrayant dans la pratique, surtout si k est grand (les grands arbres de recherche ont tendance à être lents, la surcharge de consommation de mémoire n'est pas anodine, l'efficacité du cache est souvent médiocre, etc.).

Pouvons-nous faire quelque chose de nettement mieux?

Y a-t-il des limites inférieures (par exemple, l'algorithme trivial est-il asymptotiquement optimal pour le modèle de comparaison)?


Edit: David Eppstein a donné une belle borne inférieure pour le modèle de comparaison! Je me demande s'il est néanmoins possible de faire quelque chose d'un peu plus intelligent que l'algorithme trivial?

Par exemple, pourrions-nous faire quelque chose dans ce sens: diviser le vecteur d'entrée en parties de taille k ; trier chaque partie (en gardant une trace des positions d'origine de chaque élément); puis utiliser le vecteur trié par morceaux pour trouver efficacement les médianes en cours d'exécution sans structures de données auxiliaires? Bien sûr, ce serait toujours O(nlogk) , mais dans la pratique, le tri des tableaux a tendance à être beaucoup plus rapide que la maintenance des arbres de recherche.


Edit 2: Saeed voulait voir quelques raisons pour lesquelles je pense que le tri est plus rapide que les opérations d'arbre de recherche. Voici des repères très rapides, pour k=107 , n=108 :

  • ≈ 8s: tri de n/k vecteurs avec k éléments chacun
  • ≈ 10s: trier un vecteur avec élémentsn
  • ≈ Années 80: insertions et suppressions dans une table de hachage de taillenk
  • ≈ 390s: insertions et suppressions dans un arbre de recherche équilibré de taillenk

La table de hachage est là juste pour comparaison; il n'est d'aucune utilité directe dans cette application.

En résumé, nous avons presque un facteur 50 de différence dans les performances du tri par rapport aux opérations d'arborescence de recherche équilibrée. Et les choses empirent si nous augmentons .k

(Détails techniques: Données = nombres entiers aléatoires de 32 bits. Ordinateur = un ordinateur portable moderne typique. Le code de test a été écrit en C ++, en utilisant les routines de bibliothèque standard (std :: sort) et les structures de données (std :: multiset, std :: unsorted_multiset). J'ai utilisé deux compilateurs C ++ différents (GCC et Clang), et deux implémentations différentes de la bibliothèque standard (libstdc ++ et libc ++). Traditionnellement, std :: multiset a été implémenté comme un arbre rouge-noir hautement optimisé.)

Jukka Suomela
la source
1
Je ne pense pas que vous pourrez améliorer . La raison en est que si vous regardez une fenêtre , vous ne pouvez jamais exclure aucun des nombres d'être des médianes de la fenêtre future. Cela signifie qu'à tout moment, vous devez conserver au moins entiers dans une structure de données, et il ne semble pas se mettre à jour en moins de temps de journalisation. nlogk x t + kxt,...,xt+k1kxt+k2,...,xt+k1k2
RB
Votre algorithme trivial me semble être pas , est-ce que j'ai mal compris quelque chose? Et je pense qu'à cause de cela, vous avez un problème avec le grand , sinon le facteur logarithmique n'est rien dans les applications pratiques, il n'y a pas non plus de grande constante cachée dans cet algorithme. O ( n log k ) kO((nk)klogk)O(nlogk)k
Saeed
@Saeed: Dans l'algorithme trivial, vous traitez les éléments un par un; à l'étape vous ajoutez à l'arbre de recherche et (si ) vous supprimez également de l'arbre de recherche. Il s'agit de étapes, chacune prenant du temps . x i i > k x i - k n O ( log k )ixii>kxiknO(logk)
Jukka Suomela
Vous voulez donc dire que vous avez un arbre de recherche équilibré et non un arbre de recherche occasionnel?
Saeed
1
@Saeed: Veuillez noter que dans mes repères, je n'ai même pas essayé de trouver des médianes. Je viens de faire insertions et suppressions dans un arbre de recherche de taille , et ces opérations sont garanties de prendre du temps . Il vous suffit d'accepter que les opérations d'arborescence de recherche sont très lentes en pratique, par rapport au tri. Vous le verrez facilement si vous essayez d'écrire un algorithme de tri qui fonctionne en ajoutant des éléments à une arborescence de recherche équilibrée - cela fonctionne certainement en temps , mais il sera ridiculement lent en pratique, et gaspillera également un beaucoup de mémoire. nnkO(bûchek)O(nbûchen)
Jukka Suomela

Réponses:

32

Voici une limite inférieure du tri. Étant donné un ensemble d'entrée de longueur à trier, créez une entrée pour votre problème médian en cours composé de copies d'un nombre inférieur au minimum de , puis lui-même, puis copies d'un nombre supérieur à le maximum de , et fixons . Les médianes de fonctionnement de cette entrée sont les mêmes que l'ordre de tri de .Snn1SSn1Sk=2n1S

Ainsi, dans un modèle de comparaison de calcul, un temps est requis. Peut-être que si vos entrées sont des nombres entiers et que vous utilisez des algorithmes de tri des nombres entiers, vous pouvez faire mieux.Ω(nlogn)

David Eppstein
la source
6
Cette réponse me fait vraiment me demander si l'inverse est également vrai: étant donné un algorithme de tri efficace, obtenons-nous un algorithme médian efficace? (Par exemple, un algorithme de tri d'entiers efficace implique-t-il un algorithme médian de fonctionnement efficace pour les nombres entiers? Ou un algorithme de tri efficace IO fournit-il un algorithme médian de fonctionnement efficace IO?)
Jukka Suomela
1
Encore une fois, merci beaucoup pour votre réponse, cela m'a vraiment mis sur la bonne voie et a donné l'inspiration pour l'algorithme de filtrage médian basé sur le tri! En fin de compte, j'ai pu trouver un article de 1991 qui présentait essentiellement le même argument que ce que vous donnez ici, et Pat Morin a donné un pointeur vers un autre article pertinent de 2005; voir refs. [6] et [9] ici .
Jukka Suomela
9

Edit: Cet algorithme est maintenant présenté ici: http://arxiv.org/abs/1406.1717


Oui, pour résoudre ce problème, il suffit d'effectuer les opérations suivantes:

  • Trier vecteurs, chacun avec k éléments.n/kk
  • Faites un post-traitement en temps linéaire.

En gros, l'idée est la suivante:

  • Considérons deux blocs d'entrée adjacents, et b , tous deux avec k éléments; laisser les éléments soient un 1 , un 2 , . . . , A k et b 1 , b 2 , . . . , b k dans l'ordre d'apparition dans le vecteur d'entrée x .abka1,a2,...,akb1,b2,...,bkx
  • Triez ces blocs et apprenez le rang de chaque élément dans le bloc.
  • Augmentez les vecteurs et b avec des pointeurs prédécesseurs / successeurs afin qu'en suivant les chaînes de pointeurs, nous puissions parcourir les éléments dans un ordre croissant. De cette façon, nous avons construit des listes doublement liées a aba et .b
  • Un par un, supprimer tous les éléments de la liste chaînée , dans l'ordre inverse d'apparition b k , b k - 1 , . . . , b 1 . Chaque fois que nous supprimons un élément, rappelez - vous quel était son successeur et prédécesseur au moment de la suppression .bbk,bk1,...,b1
  • Maintenant, maintenez les "pointeurs médians" et q qui pointent vers les listes a ' et b ' , respectivement. Initialisez p au milieu de a ' et initialisez q à la fin de la liste vide b ' .pqabpaqb
  • Pour chaque :i

    • Supprimez de la liste a (il s'agit de l' heure O ( 1 ) , supprimez simplement de la liste chaînée). Comparez a i avec l'élément pointé par p pour voir si nous avons supprimé avant ou après p .aiaO(1)aipp
    • Remettez à la liste b dans sa position d'origine (c'est O ( 1 ) fois, nous avons mémorisé le prédécesseur et successeur de b i ). Comparer b i avec l'élément pointé par qbibO(1)bibiq pour voir si nous avons ajouté l'élément avant ou après .q
    • Mettez à jour les pointeurs et q de sorte que la médiane de la liste jointe a b soit soit en p, soit en q . (C'est O ( 1 ) fois, suivez simplement les listes chaînées une ou deux étapes pour tout réparer. Nous garderons une trace du nombre d'éléments avant / après p et q dans chaque liste, et nous maintiendrons l'invariant que les deux p etpqabpqO(1)pqp pointer vers des éléments qui se rapprochent le plus possible de la médiane.)q

Les listes chaînées sont juste tableaux d'index k éléments, elles sont donc légères (sauf que la localité d'accès à la mémoire est médiocre).k


Voici un exemple d'implémentation et de benchmarks:

Voici un graphique des temps de fonctionnement (pour ):n2106

  • Bleu = tri + post-traitement, .O(nlogk)
  • Vert = maintenir deux tas, O(nlogk) , implémentation depuis https://github.com/craffel/median-filter
  • Rouge = maintenir deux arbres de recherche, .O(nlogk)
  • Noir = maintenir un vecteur trié, .O(nk)
  • Axe X = taille de la fenêtre ( ).k/2
  • Axe Y = durée de fonctionnement en secondes.
  • Données = entiers 32 bits et entiers aléatoires 64 bits, à partir de diverses distributions.

temps de course

Jukka Suomela
la source
3

Étant donné la limite de David, il est peu probable que vous puissiez faire mieux dans le pire des cas, mais il existe de meilleurs algorithmes sensibles à la sortie. Plus précisément, si dans le nombre de médianes dans le résultat, nous pouvons résoudre le problème dans le temps O ( n log m + m log n ) .mO(nlogm+mlogn)

Pour ce faire, remplacez l'arbre binaire équilibré par un arbre binaire équilibré composé uniquement des éléments qui étaient des médianes dans le passé, plus deux tas de Fibonacci entre chaque paire de médianes précédentes (une pour chaque direction), plus les nombres pour que nous puissions localiser le tas Fibonacci qui contient un élément particulier dans la commande. Ne vous embêtez jamais à supprimer des éléments. Lorsque nous insérons un nouvel élément, nous pouvons mettre à jour notre structure de données en temps . Si les nouveaux dénombrements indiquent que la médiane se trouve dans l'un des tas de Fibonacci, il faut un O supplémentaire ( log n ) pour extraire la nouvelle médiane. Ce O ( log n )O(logm)O(logn)O(logn) la charge n'a lieu qu'une fois par médiane.

O(nlogm+mlogk)

Geoffrey Irving
la source
Oups, cela ne fonctionne pas comme écrit, car si vous ne supprimez pas les éléments, les décomptes ne refléteront pas la nouvelle fenêtre. Je ne sais pas si cela peut être corrigé, mais je laisserai la réponse au cas où il y aurait un moyen.
Geoffrey Irving
O(nlogm)
note complémentaire: la question n'est pas claire, la structure des données sous-jacentes n'est pas définie, nous savons juste quelque chose de très vague. comment voulez-vous améliorer quelque chose dont vous ne savez pas ce que c'est? comment voulez-vous comparer votre approche?
Saeed
1
Je m'excuse pour le travail incomplet. J'ai posé la question concrète nécessaire pour corriger cette réponse ici: cstheory.stackexchange.com/questions/21778/… . Si vous pensez que c'est approprié, je peux supprimer cette réponse jusqu'à ce que la question secondaire soit résolue.
Geoffrey Irving