web-dev-qa-db-fra.com

Pourquoi le tri par insertion Θ (n ^ 2) dans le cas moyen?

tri par insertion a un temps d'exécution qui est Ω (n) (lorsque l'entrée est triée) et O (n2) (lorsque l'entrée est triée en sens inverse). En moyenne, il s'exécute en Θ (n2) temps.

Pourquoi est-ce? Pourquoi le cas moyen n'est-il pas plus proche de O (n log n), par exemple?

25
templatetypedef

Pour répondre à cette question, déterminons d'abord comment évaluer le temps d'exécution du tri par insertion. Si nous pouvons trouver une expression mathématique de Nice pour le runtime, nous pouvons alors manipuler cette expression pour déterminer le runtime moyen.

L'observation clé que nous devons avoir est que le temps d'exécution du tri par insertion est étroitement lié au nombre de inversions dans le tableau d'entrée. Une inversion dans un tableau est une paire d'éléments A [i] et A [j] qui sont dans le mauvais ordre relatif - c'est-à-dire, i <j, mais A [j] <A [i]. Par exemple, dans ce tableau:

0 1 3 2 4 5

Il y a une inversion: les 3 et 2 doivent être commutés. Dans ce tableau:

4 1 0 3 2

Il y a 6 inversions:

  • 4 et 1
  • 4 et 0
  • 4 et 3
  • 4 et 2
  • 1 et 0
  • 3 et 2

Une propriété importante des inversions est qu'un tableau trié n'a pas d'inversions, car chaque élément doit être plus petit que tout ce qui vient après et plus grand que tout ce qui vient avant.

La raison pour laquelle cela est significatif est qu'il existe un lien direct entre la quantité de travail effectuée dans le tri par insertion et le nombre d'inversions dans le tableau d'origine. Pour voir cela, passons en revue un pseudocode rapide pour le tri par insertion:

  • Pour i = 2 .. n: (en supposant une indexation 1)
    • Réglez j = i - 1.
    • Alors que A [j]> A [j + 1]:
      • Échangez A [j] et A [j + 1].
      • Réglez j = j - 1.

Normalement, lors de la détermination de la quantité totale de travail effectuée par une fonction comme celle-ci, nous pourrions déterminer la quantité maximale de travail effectuée par la boucle interne, puis la multiplier par le nombre d'itérations de la boucle externe. Cela donnera une limite supérieure, mais pas nécessairement une limite stricte. Une meilleure façon de rendre compte du travail total accompli consiste à reconnaître qu'il existe deux sources différentes de travail:

  • La boucle extérieure, qui compte 2, 3, ..., n et
  • La boucle intérieure, qui effectue des swaps.

Cette boucle externe fonctionne toujours Θ (n). La boucle interne, cependant, effectue une quantité de travail proportionnelle au nombre total de swaps effectués sur l'ensemble de l'exécution de l'algorithme. Pour voir le travail que fera cette boucle, nous devrons déterminer le nombre total de swaps effectués sur toutes les itérations de l'algorithme.

C'est là qu'interviennent les inversions. Notez que lorsque le tri par insertion s'exécute, il échange toujours les éléments adjacents dans le tableau, et il n'échange les deux éléments que s'ils forment une inversion. Alors, qu'arrive-t-il au nombre total d'inversions dans le tableau après avoir effectué un échange? Eh bien, graphiquement, nous avons ceci:

 [---- X ----] A[j] A[j+1] [---- Y ----]

Ici, X est la partie du tableau venant avant la paire échangée et Y est la partie du tableau venant après la paire échangée.

Supposons que nous échangeons A [j] et A [j + 1]. Qu'arrive-t-il au nombre d'inversions? Eh bien, considérons une inversion arbitraire entre deux éléments. Il y a 6 possibilités:

  • Les deux éléments sont en X, ou les deux éléments sont en Y, ou un élément est en X et un élément est en Y. Ensuite, l'inversion est toujours là, puisque nous n'avons déplacé aucun de ces éléments.
  • Un élément est en X ou Y et l'autre est soit A [j] ou A [j + 1]. Ensuite, l'inversion est toujours là, car les ordres relatifs des éléments n'ont pas changé, même si leurs positions absolues peuvent avoir changé.
  • Un élément est A [j] et l'autre A [j + 1]. Ensuite, l'inversion est supprimée après l'échange.

Cela signifie qu'après avoir effectué un swap, nous diminuons le nombre d'inversions d'exactement un, car seule l'inversion de la paire adjacente a disparu. Ceci est extrêmement important pour la raison suivante: si nous commençons avec des inversions I, chaque échange diminuera le nombre d'exactement un. Une fois qu'il n'y a plus d'inversions, plus aucun échange n'est effectué. Par conséquent, le nombre de swaps est égal au nombre d'inversions !

Compte tenu de cela, nous pouvons exprimer avec précision le temps d'exécution du tri par insertion comme Θ (n + I), où I est le nombre d'inversions du tableau d'origine. Cela correspond à nos limites d'exécution d'origine - dans un tableau trié, il y a 0 inversions, et le temps d'exécution est Θ (n + 0) = Θ (n), et dans un tableau trié inversement, il y a n (n - 1)/2 inversions, et le temps d'exécution est Θ (n + n(n-1)/2) = Θ (n2). Nifty!

Nous avons donc maintenant une façon super précise d'analyser le temps d'exécution du tri par insertion pour un tableau particulier. Voyons comment analyser sa durée d'exécution moyenne. Pour ce faire, nous devrons faire une hypothèse sur la distribution des entrées. Comme le tri par insertion est un algorithme de tri basé sur la comparaison, les valeurs réelles du tableau d'entrée n'ont pas vraiment d'importance; seul leur ordre relatif compte réellement. Dans ce qui suit, je vais supposer que tous les éléments du tableau sont distincts, mais si ce n'est pas le cas, l'analyse ne change pas beaucoup. Je montrerai où les choses se déroulent hors script lorsque nous y arriverons.

Pour résoudre ce problème, nous allons introduire un tas de variables indicatrices de la forme Xij, où Xij est une variable aléatoire qui vaut 1 si A [i] et A [j] forment une inversion et 0 sinon. Il y aura n (n - 1)/2 de ces variables, une pour chaque paire distincte d'éléments. Notez que ces variables représentent chaque inversion possible dans le tableau.

Compte tenu de ces X, nous pouvons définir une nouvelle variable aléatoire I qui est égale au nombre total d'inversions dans le tableau. Cela sera donné par la somme des X:

I = Σ Xij

Nous nous intéressons à E [I], le nombre attendu d'inversions dans le tableau. En utilisant la linéarité de l'attente, c'est

E [I] = E [Σ Xij] = Σ E [Xij]

Alors maintenant, si nous pouvons obtenir la valeur de E [Xij], nous pouvons déterminer le nombre prévu d'inversions et, par conséquent, le temps d'exécution attendu!

Heureusement, puisque tous les Xijsont des variables indicatrices binaires, nous avons cela

EXij] = Pr [Xij = 1] = Pr [A [i] et A [j] sont une inversion]

Quelle est donc la probabilité, étant donné un tableau d'entrée aléatoire sans doublons, que A [i] et A [j] soient une inversion? Eh bien, la moitié du temps, A [i] sera inférieure à A [j], et l'autre moitié du temps A [i] sera supérieure à A [j]. (Si les doublons sont autorisés, il existe un terme supplémentaire sournois pour gérer les doublons, mais nous l'ignorerons pour l'instant). Par conséquent, la probabilité qu'il y ait une inversion entre A [i] et A [j] est 1/2. Par conséquent:

E [I] = ΣE [Xij] = Σ (1/2)

Puisqu'il y a n (n - 1)/2 termes dans la somme, cela revient à

E [I] = n (n - 1)/4 = Θ (n2)

Et donc, dans l'attente, il y aura Θ (n2) inversions, donc sur l'attente, le temps d'exécution sera Θ (n2 + n) = Θ (n2) . Cela explique pourquoi le comportement de cas moyen du tri par insertion est Θ (n2).

J'espère que cela t'aides!

37
templatetypedef

Pour le plaisir, j'ai écrit un programme qui parcourait toutes les combinaisons de données pour un vecteur de taille n comparaisons de comptage et a constaté que le meilleur cas est n-1 (tous triés) et le pire est (n * (n-1))/2.

Quelques résultats pour différents n:

  n min     ave     max ave/(min+max) ave/max

  2   1     1         1        0.5000
  3   2     2.667     3        0.5334
  4   3     4.917     6        0.5463
  5   4     7.717    10        0.5512
  6   5    11.050    15        0.5525
  7   6    14.907    21        0.5521
  8   7    19.282    28        0.5509
  9   8    24.171    36        0.5493
 10   9    29.571    45        0.5476
 11  10    35.480    55        0.5458
 12  11    41.897    66        0.5441

Il semble que la valeur moyenne suit de min plus près que de max.

EDIT: quelques valeurs supplémentaires

 13  12    48.820    78        0.5424        
 14  13    56.248    91        0.5408

EDIT: valeur pour 15

 15  14    64.182   105        0.5393

EDIT: valeurs supérieures sélectionnées

 16  15    72.619   120        -       0.6052
 32  31   275.942   496        -       0.5563
 64  63  1034.772  1953        -       0.5294
128 127  4186.567  8128        -       0.5151
256 255 16569.876 32640        -       0.5077

J'ai récemment écrit un programme pour calculer le nombre moyen de comparaisons pour le tri par insertion pour des valeurs plus élevées de n. J'en ai tiré la conclusion que lorsque n approche de l'infini, le cas moyen se rapproche du pire des cas divisé par deux.

2
Olof Forshell

La plupart des algorithmes ont un cas moyen identique au pire des cas. Pour comprendre pourquoi, appelons O le pire des cas et Ω le meilleur des cas. Vraisemblablement, O> = Ω lorsque n va à l'infini. Pour la plupart des distributions, le cas moyen va être proche de la moyenne du meilleur et du pire des cas, c'est-à-dire (O + Ω)/2 = O/2 + Ω/2. Puisque nous ne nous soucions pas des coefficients, et O> = Ω, c'est la même chose que O.

De toute évidence, il s'agit d'une simplification excessive. Il existe des distributions de temps d'exécution qui sont asymétriques de telle sorte que l'hypothèse selon laquelle le cas moyen est la moyenne du pire et du meilleur cas n'est pas valide *. Mais cela devrait vous donner une intuition décente quant à la raison de cela.

* Comme mentionné par templatetypedef dans les commentaires, certains exemples sont quicksort/quickselect, recherche BST (sauf si vous équilibrez l'arborescence), recherche de table de hachage et méthode simplex.

0
Aaron Dufour