1) On énumère tous les couples d'indices.
def inversions_naif(valeurs):
"""Renvoie le nombre de couples (i, j) tels que i < j et valeurs[i] > valeurs[j]."""
total = 0
for i in range(len(valeurs)):
for j in range(i + 1, len(valeurs)):
if valeurs[i] > valeurs[j]:
total = total + 1
return total
Le nombre de couples testés est
(2n)=2n(n−1), et chaque test coûte un temps constant, donc le coût est en
Θ(n2), dans tous les cas.
Comme il y a
2n(n−1) couples
(i,j) avec
i<j, il y a au plus autant d'inversions. Ce maximum est atteint exactement pour une liste rangée par ordre strictement décroissant : tout couple
(i,j) avec
i<j y vérifie
valeurs[i] > valeurs[j].
2) On reprend la fusion de deux listes triées, en comptant au passage les couples qui se croisent.
def fusionner(gauche, droite):
"""Renvoie (fusionnee, croisements) : la liste triée des éléments de gauche et droite
réunis, et le nombre de couples (u, v) avec u dans gauche, v dans droite et u > v.
Précondition : gauche et droite sont triées par ordre croissant."""
fusionnee = []
i = 0
j = 0
croisements = 0
while i < len(gauche) and j < len(droite):
if gauche[i] <= droite[j]:
fusionnee.append(gauche[i])
i = i + 1
else:
fusionnee.append(droite[j])
j = j + 1
croisements = croisements + len(gauche) - i
return fusionnee + gauche[i:] + droite[j:], croisements
Justification du comptage. Plaçons-nous au moment où l'élément
droite[j], noté
v, est placé : c'est que
v est strictement inférieur à
gauche[i].
Les éléments de
gauche non encore placés sont ceux d'indices
i,i+1,…, au nombre de
len(gauche) - i. La liste
gauche étant triée, ils sont tous supérieurs ou égaux à
gauche[i], donc tous strictement supérieurs à
v : chacun forme avec
v un couple qui se croise.
Les éléments de
gauche déjà placés, eux, sont tous inférieurs ou égaux à
v. En effet, un tel élément
u a été placé à un moment où l'indice courant de
droite valait un certain
j′≤j, et il l'a été parce que
u≤ droite[j'] ; or
droite est triée, donc
droite[j'] ≤ droite[j] =v, d'où
u≤v. Ces éléments ne forment donc aucun couple avec
v.
Le compte ajouté au moment où
v est placé est donc exactement le nombre de couples qui se croisent et dont le second membre est
v.
Aucun couple n'est compté deux fois. Soit
(u,v) un couple avec
u dans
gauche,
v dans
droite et
u>v. Montrons que
v est placé avant
u : si
u l'était avant, il l'aurait été à un moment où l'indice courant de
droite désignait un élément
droite[j''] avec
u≤ droite[j''], et
v, encore présent, vérifierait
droite[j''] ≤v par croissance de
droite, d'où
u≤v, ce qui contredit
u>v. Donc
v est placé le premier, et à cet instant
u fait partie des
len(gauche) - i éléments comptés, une seule fois. Enfin, la boucle s'arrête dès qu'une liste est épuisée, et la concaténation finale ne compte rien : si
droite est épuisée la première, les éléments restants de
gauche ne forment plus aucun couple non compté ; si c'est
gauche, alors
i vaut
len(gauche) et tout compte ajouté serait nul.
3) Les inversions se répartissent en trois familles selon la position des deux indices.
def tri_et_inversions(valeurs):
"""Renvoie (triee, inversions) : la liste triée des éléments de valeurs et le nombre
de couples (i, j) tels que i < j et valeurs[i] > valeurs[j] ;
la liste valeurs n'est pas modifiée."""
if len(valeurs) <= 1:
return list(valeurs), 0
milieu = len(valeurs) // 2
gauche, inversions_gauche = tri_et_inversions(valeurs[:milieu])
droite, inversions_droite = tri_et_inversions(valeurs[milieu:])
fusionnee, croisements = fusionner(gauche, droite)
return fusionnee, inversions_gauche + inversions_droite + croisements
Terminaison. Le variant est la longueur de
valeurs : pour
n≥2, les deux appels portent sur des listes de longueurs
⌊n/2⌋ et
n−⌊n/2⌋, toutes deux comprises entre
1 et
n−1, donc strictement plus petites.
Correction, par récurrence forte sur
n. Pour
n≤1, la liste renvoyée est une copie triée et le nombre d'inversions est nul, ce qui est correct.
Soit
n≥2 et supposons le résultat acquis pour toute liste plus courte. Notons
G la portion des indices
0 à
⌊n/2⌋−1 et
D celle des indices suivants.
Une inversion
(i,j) vérifie l'un des trois cas : les deux indices sont dans
G, les deux sont dans
D, ou
i est dans
G et
j dans
D. Le cas où
i est dans
D et
j dans
G est impossible, car il imposerait
j<i.
Les inversions du premier cas sont exactement celles de la liste
valeurs[:milieu], et l'hypothèse de récurrence donne leur nombre,
inversions_gauche, ainsi que la liste triée
gauche de ses éléments ; de même pour le deuxième cas.
Les inversions du troisième cas sont les couples formés d'un élément de la portion gauche strictement supérieur à un élément de la portion droite : leur nombre ne dépend que des deux familles d'éléments, avec leurs multiplicités, et non de leur ordre. Comme
gauche et
droite contiennent exactement ces éléments et sont triées, le lemme de la question 2 donne ce nombre,
croisements.
Les trois familles étant disjointes et épuisant les inversions, leur somme est le nombre d'inversions de
valeurs, et
fusionnee est la liste triée de tous les éléments, ce qui achève l'hérédité.
4) Le coût vérifie
T(n)=2T(2n)+Θ(n) : deux appels sur des moitiés, plus le découpage et la fusion, qui parcourent chacun la liste entière.
D'après le formulaire des récurrences usuelles, ou d'après le cas
2 du théorème maître avec
a=b=2 et
c=log2(2)=1, on obtient
T(n)=Θ(nlogn), dans tous les cas, contre
Θ(n2) pour la méthode naïve. La mémoire supplémentaire est en
Θ(n) et la profondeur de récursion en
Θ(logn).
Vérification.
print(inversions_naif([5, 3, 8, 1, 9, 2, 7]))
print(tri_et_inversions([5, 3, 8, 1, 9, 2, 7]))
print(tri_et_inversions([]), tri_et_inversions([7]), tri_et_inversions([4, 3, 2, 1]))
10
([1, 2, 3, 5, 7, 8, 9], 10)
([], 0) ([7], 0) ([1, 2, 3, 4], 6)
Les deux méthodes s'accordent sur l'exemple, et la liste décroissante
[4, 3, 2, 1] atteint bien le maximum
24×3=6. Sur
300 listes tirées au hasard, de longueurs allant de
0 à
12 et à valeurs répétées, les deux méthodes donnent toujours le même nombre. Sur une liste décroissante de
20000 éléments, la méthode par fusion compte
199990000 inversions en
0,03 seconde ; sur une liste de
20000 éléments tirés au hasard, elle en compte
100050691 en
0,04 seconde, là où la méthode naïve demande
10 secondes pour le même résultat.
À retenir : on ne compte pas les inversions en triant puis en comparant, on les compte
pendant la fusion, parce que c'est le seul moment où l'on connaît, d'un coup, tous les éléments d'une moitié qui dépassent un élément de l'autre.