Notons
n la longueur de
valeurs et
L la liste initiale. Si
n≤1, la boucle externe ne fait aucun tour et la liste est déjà triée ; on suppose désormais
n≥2.
1) Déroulement sur
[4,1,3,2].
def tri_bulles_trace(valeurs):
"""Trie la liste en place et renvoie son contenu après chaque tour
de la boucle externe, ainsi que le nombre de comparaisons effectuées."""
n = len(valeurs)
etats = []
comparaisons = 0
for i in range(n - 1, 0, -1):
for j in range(i):
comparaisons = comparaisons + 1
if valeurs[j] > valeurs[j + 1]:
valeurs[j], valeurs[j + 1] = valeurs[j + 1], valeurs[j]
etats.append(list(valeurs))
return etats, comparaisons
print(tri_bulles_trace([4, 1, 3, 2]))
print(tri_bulles_trace([1, 2, 3, 4]))
([[1, 3, 2, 4], [1, 2, 3, 4], [1, 2, 3, 4]], 6)
([[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], 6)
Après le tour
i=3 :
[1,3,2,4] ; après le tour
i=2 :
[1,2,3,4] ; après le tour
i=1 :
[1,2,3,4]. Chaque tour externe fait remonter un plus grand élément de la partie encore désordonnée jusqu'à la position
i.
2) Boucle interne. Fixons
i∈{1,…,n−1} et notons
M le contenu de la tranche
valeurs[0:i+1] au début de ce tour externe. Pour
j∈{0,…,i}, notons
R(j) la propriété : avant le tour d'indice
j de la boucle interne, la tranche
valeurs[0:i+1] contient encore les éléments de
M, à l'ordre près, et
valeurs[j] est un plus grand élément de la tranche
valeurs[0:j+1].
Initialisation : pour
j=0, rien n'a été modifié, et la tranche
valeurs[0:1] se réduit à
valeurs[0], qui en est donc un plus grand élément. Donc
R(0) est vraie.
Conservation : soit
j∈{0,…,i−1} tel que
R(j) soit vraie. Notons
a et
b les valeurs de
valeurs[j] et
valeurs[j + 1] au début de ce tour. Le corps échange ou non deux cases voisines, donc il permute le contenu de
valeurs[0:i+1] : la première clause est conservée. Pour la seconde, deux cas.
Si
a>b, l'échange a lieu et la nouvelle valeur de
valeurs[j + 1] est
a. Or
a est un plus grand élément de
valeurs[0:j+1] par
R(j), et
a>b, donc
a est un plus grand élément de la tranche
valeurs[0:j+2], dont le contenu est celui de
valeurs[0:j+1] augmenté de
b.
Si
a≤b, aucun échange n'a lieu et
valeurs[j + 1] vaut
b. Tout élément de
valeurs[0:j+1] est inférieur ou égal à
a par
R(j), donc à
b ; ainsi
b est un plus grand élément de
valeurs[0:j+2].
Dans les deux cas
R(j+1) est vraie.
Sortie : la boucle interne s'arrête après le tour d'indice
i−1, donc
R(i) est vraie :
valeurs[i] est un plus grand élément de
valeurs[0:i+1], et cette tranche contient encore les éléments de
M.
3) Boucle externe. Pour
i∈{0,…,n−1}, notons
P(i) la conjonction des trois propriétés suivantes, considérées avant le tour d'indice
i :
(a)
valeurs est une permutation de
L ;
(b) la tranche
valeurs[i+1:n] est triée par ordre croissant ;
(c) tout élément de
valeurs[0:i+1] est inférieur ou égal à tout élément de
valeurs[i+1:n].
Initialisation : avant le tour d'indice
n−1, qui est le premier, rien n'a été modifié, donc (a) est vraie ; la tranche
valeurs[n:n] est vide, donc (b) et (c) portent sur des familles vides et sont vraies. Donc
P(n−1) est vraie.
Conservation : soit
i∈{1,…,n−1} tel que
P(i) soit vraie, et exécutons le tour d'indice
i. La boucle interne ne touche que les cases d'indices
0 à
i, et elle n'y fait que des échanges, donc (a) est conservée et le contenu de
valeurs[0:i+1] est conservé à l'ordre près ; la clause (c) au rang
i reste donc vraie après ce tour, et la tranche
valeurs[i+1:n] n'ayant pas bougé, (b) au rang
i reste vraie.
D'après l'étude de la boucle interne,
valeurs[i] est ensuite un plus grand élément de
valeurs[0:i+1]. Donc tout élément de
valeurs[0:i] est inférieur ou égal à
valeurs[i], et, par (c) au rang
i,
valeurs[i] est inférieur ou égal à tout élément de
valeurs[i+1:n]. Ces deux faits donnent (c) au rang
i−1. Ils donnent aussi (b) au rang
i−1 : la tranche
valeurs[i:n] est formée de
valeurs[i], suivi d'une tranche triée dont tous les éléments lui sont supérieurs ou égaux. Donc
P(i−1) est vraie.
Sortie : la boucle externe s'arrête après le tour d'indice
1, donc
P(0) est vraie. La tranche
valeurs[1:n] est triée, et
valeurs[0] est inférieur ou égal à tous ses éléments : la liste entière est triée. Elle est de plus une permutation de
L. La fonction termine, ses deux boucles étant des
for sur des
range, donc elle est totalement correcte relativement à sa spécification.
4) Nombre de comparaisons. Pour chaque
i de
n−1 à
1, la boucle interne effectue exactement
i comparaisons, une par tour. Le total vaut
∑i=1n−1i=2n(n−1).Ce nombre ne dépend pas du contenu de la liste : le tri à bulles, tel qu'il est écrit, coûte
Θ(n2) dans le meilleur comme dans le pire des cas. Le comptage exécuté ci-dessus le confirme : sur une liste déjà triée de quatre éléments, il effectue les mêmes
6 comparaisons que sur une liste désordonnée. Seul le nombre d'échanges varie, de
0 à
2n(n−1).
Le tri à bulles n'est donc pas adaptatif sous cette forme. Il le devient si l'on ajoute un drapeau qui note qu'aucun échange n'a eu lieu pendant un tour externe et interrompt alors la boucle : le coût tombe à
Θ(n) sur une liste déjà triée, et reste
Θ(n2) dans le pire des cas.
5) Stabilité. Le tri à bulles est stable, pour deux raisons qui comptent toutes les deux. D'abord il n'échange que des cases
voisines, donc deux éléments ne peuvent se croiser qu'au moment où ils sont comparés l'un à l'autre. Ensuite le test est une inégalité
stricte, donc deux éléments de même clé ne sont jamais échangés. Deux éléments de même clé conservent ainsi leur ordre de départ.
def tri_bulles_sur_cle(elements):
"""Trie en place la liste de couples elements, selon leur première
composante, par ordre croissant."""
n = len(elements)
for i in range(n - 1, 0, -1):
for j in range(i):
if elements[j][0] > elements[j + 1][0]:
elements[j], elements[j + 1] = elements[j + 1], elements[j]
couples = [(2, "a"), (1, "b"), (2, "c"), (1, "d")]
tri_bulles_sur_cle(couples)
print(couples)
[(1, 'b'), (1, 'd'), (2, 'a'), (2, 'c')]
Les deux clés égales à
1 sortent dans l'ordre
"b" puis
"d", et les deux clés égales à
2 dans l'ordre
"a" puis
"c" : l'ordre de départ est respecté. Un tri par sélection écrit sur la même clé, lui, échangerait deux cases éloignées : il laisserait la liste dans l'état
[(1, 'b'), (1, 'd'), (2, 'c'), (2, 'a')], où les deux clés égales à
2 ont été interverties.
À retenir : deux boucles imbriquées se prouvent avec deux invariants, et la stabilité d'un tri se lit sur deux détails du code, la portée des échanges et la rigueur du test.