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?
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:
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:
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:
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:
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!
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.
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.