Tris optimaux - INFO-MPSI

Lyc´ee Thiers
mpsi 123
MÉTHODES DE TRI / PARTIE 2
On présente deux méthodes
de tri par comparaison dont la complexité en moyenne est quasi-linéaire,
c’est-à-dire en Θ n log (n) . Ces méthodes, traitées ici dans le cas des listes, sont le tri par fusion et le
tri par segmentation.
1. Tri par fusion
1.1. Principe. Si la liste est vide ou réduite à un élément, elle est déjà triée. Sinon, on la scinde en deux
parties de longueur moitié, que l’on trie récursivement. Pour finir, on fusionne les deux morceaux.
1.2. Implémentation 1. Elle suit de près le principe indiqué ci-dessus.
1.2.1. Division d’une liste en deux sous-listes. On peut penser à des cartes que l’on distribue à deux
joueurs, en donnant alternativement une carte à l’un, puis à l’autre ...
let rec divise = function
| [] → ([], [])
| [x] → ([x], [])
| x::x’::r → let (l,l’) = divise r in (x::l, x’::l’)
;;
divise : ’a list → ’a list * ’a list = <fun>
divise [1;2;3;4;5;6;7;8;9];;
- : int list * int list = [1; 3; 5; 7; 9], [2; 4; 6; 8]
1.2.2. Fusion de deux listes triées.
let
|
|
|
rec fusion = fun
l [] → l
[] l → l
(h::t) (h’::t’) → if h < h’ then h::(fusion t (h’::t’))
else h’::(fusion (h::t) t’)
;;
fusion : ’a list → ’a list → ’a list = <fun>
fusion [1;4;7;8] [0;2;3;5;6];;
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8]
1.2.3. Tri d’une liste.
let
|
|
|
rec tri_f = function
[] → []
[x] → [x]
l → let (l1,l2) = divise l in
fusion (tri_f l1) (tri_f l2)
;;
tri_f : ’a list → ’a list = <fun>
tri_f [3;1;4;1;5;9;2;6;5;3;5];;
- : int list = [1; 1; 2; 3; 3; 4; 5; 5; 5; 6; 9]
MÉTHODES DE TRI / PARTIE 2
2
1.2.4. Complexité de l’implémentation 1. La complexité de cette implémentation du tri par fusion est
quasi-linéaire :
(1) Tout d’abord, la fonction
divise
appliquée à une liste de longueur n renvoie des listes de
n
n
longueurs respectives
et
.
2
2
(2) Etant données deux listes triées ` et `0 , de longueurs respectives n et n0 , notons c (`, `0 ) le nombre
de comparaisons engendrées par l’appel fusion ` `0 . Alors, en raisonnant par récurrence sur
n + n0 , on vérifie (exercice !) que :
min n, n0 ≤ c (`, `0 ) ≤ n + n0 − 1
En outre, cet encadrement n’est pas améliorable ; en effet :
• si chaque terme de la plus courte liste précède chaque terme de l’autre, alors :
c (`, `0 ) = min n, n0
• si le dernier terme de ` est intercalé entre les deux derniers termes de `0 , alors :
c (`, `0 ) = n + n0 − 1
(3) Notons T (n) le nombre de comparaisons requises pour le tri d’une liste de longueur n. Cette
notation est abusive puisque T (n) ne dépend pas seulement de n mais plutôt de la liste
considérée. La phase de division ne comporte aucune comparaison, donc :
n
n
T (n) = T
+T
+ ϕn
2
2
où le terme ϕn désigne le nombre de comparaisons induites par la fusion des deux demi-listes
triées. D’après le point précédent :
n
n
n
n
n
= min
,
≤ ϕn ≤
+
−1=n−1
2
2
2
2
2
Ainsi ϕn = Θ (n) . Le théorème DPR montre alors que : T (n) = Θ (n ln (n)) .
demtri
Le tri par fusion est un tri optimal : on peut
l montrer
m (et nous l’admettrons) qu’un algorithme
l
par comparaison doit effectuer au moins log2 (n!) comparaisons ; or, lorsque n → +∞ : log2 (n!) =
Θ (n ln (n)) .
1.3. Implémentation 2. Il s’agit d’une version améliorée de l’implémentation précédente. Les concepteurs de caml light l’ont retenue pour la fonction sort (du module “sort”).
1.3.1. Fragmentation. On découpe la liste initiale en listes triées de longueur 2, plus éventuellement
une liste finale de longueur 1.
let rec frag = function
| [] → []
| [x] → [[x]]
| e1::e2::r → (if e1 < e2 then [e1;e2] else [e2;e1])::(frag r)
;;
frag : ’a list → ’a list list = <fun>
frag [3;1;4;1;5;9;2;6;5;3;5];;
- : int list list = [[1; 3]; [1; 4]; [5; 9]; [2; 6]; [3; 5]; [5]]
MÉTHODES DE TRI / PARTIE 2
3
h
i
1.3.2. Fusion par deux. Etant donnée une liste l0 , · · · , lp−1 de listes, on fusionne, au moyen de la
fonction décrite en 1.2.2, les listes l0 et l1 , les listes l2 et l3 , etc ...
Si pour tout i, la liste li est triée, alors les listes qui résultent de cette procédure le sont aussi.
let rec fusion_2 = function
| l1::l2::r → (fusion l1 l2) :: (fusion_2 r)
| x → x
;;
fusion_2 : ’a list list → ’a list list = <fun>
fusion_2 [[1;3];[0;2;2;4];[0;3;4;7];[1;2;8];[1;3]];;
- : int list list = [[0; 1; 2; 2; 3; 4]; [0; 1; 2; 3; 4; 7; 8]; [1; 3]]
1.3.3. Fusion complète. Il s’agit d’une application répétée de la fusion par deux.
let rec fusion_complete = function
| [] → []
| [l] → l
| llist → fusion_complete (fusion_2 llist)
;;
fusion_complete : ’a list list → ’a list = <fun>
1.3.4. Tri d’une liste. On combine les fonctions précédentes ...
let tri_caml l = fusion_complete (frag l);;
tri_caml : ’a list → ’a list = <fun>
L’exemple qui suit montre en détail les différentes phases du processus de tri :
liste initiale
fragmentation
fusion par 2
fusion par 2
fusion par 2
liste triée
:
→
→
→
→
:
[10; 7; 8; 3; 2; 0; 9; 11; 4; 3]
[[7; 10]; [3; 8]; [0; 2]; [9; 11]; [3; 4]]
[[3; 7; 8; 10]; [0; 2; 9; 11]; [3; 4]]
[[0; 2; 3; 7; 8; 9; 10; 11]; [3; 4]]
[[0; 2; 3; 3; 4; 7; 8; 9; 10; 11]]
[0; 2; 3; 3; 4; 7; 8; 9; 10; 11]
1.3.5. Complexité de l’implémentation 2. On peut montrer qu’elle est quasi-linéaire. Son intérêt, relativement à l’implémentation 1, provient du fait qu’on accède très vite à une phase où la liste initiale a
été découpée en listes triées de longueur 1 ou 2. Dans l’implémentation 1, cette phase n’est atteinte
qu’après une arborescence d’appels à la fonction divise. La recombinaison, c’est-à-dire la fusion
proprement dite, est ensuite identique dans les deux implémentations.
2. Tri par segmentation
Cet algorithme, aussi nommé “quicksort”, a été proposé par Hoare 1 vers 1960.
2.1. Principe. Etant donnée une liste h::t, on sépare la liste t en deux sous-listes g et d, telles que
g (resp. d) soit composée des éléments strictement inférieurs à h (resp. supérieurs ou égaux à h). On
trie récursivement g et d et l’on renvoie la liste g@(h::d).
1. Sir Charles Antony Richard HOARE (1934 - )
MÉTHODES DE TRI / PARTIE 2
4
2.2. Implémentation.
let rec segmente e = function
| [] → ([],[])
| h::t → let (g,d) = segmente e t in
if h < e then (h::g,d)
else (g,h::d)
;;
segmente : ’a → ’a list → ’a list * ’a list = <fun>
let rec tri_rapide = function
| [] → []
| h::t → let (g,d) = segmente h t in
(tri_rapide g) @ (h :: (tri_rapide d))
;;
tri_rapide : ’a list → ’a list = <fun>
2.3. Complexité en moyenne du tri rapide. Cette partie, un peu technique, n’est pas exigible ...
On retiendra surtout que le tri par segmentation est :
• semi-linéaire en moyenne,
• quadratique dans le pire cas (liste déjà triée).
Pour tout σ ∈ Sn , notons T (σ) le coût, en nombre de comparaisons, du tri par segmentation de la liste
[σ (1) ; · · · ; σ (n)] . En considérant les différentes permutations comme équiprobables, le coût moyen
du tri d’une liste de longueur n est :
1 X
T (σ)
Cn =
n!
σ∈Sn
Notons un (k) le coût moyen du tri d’une liste de longueur n, lorsque la tête de liste est l’entier k. En
regroupant dans la somme précédente les termes associés aux permutations σ telles que σ (1) = k, on
obtient :
n
1 X
Cn =
un (k)
n
k=1
Enfin, un (k) est le nombre (moyen) de comparaisons requises pour la segmentation ; soit n − 1 (il faut
comparer chaque terme de la queue de liste à l’élément de tête) augmenté des côuts (moyens) pour
chacun des deux appels récursifs :
un (k) = n − 1 + Ck−1 + Cn−k
(à condition de poser C0 = 0). Ainsi :
Cn = n − 1 +
n
n−1
1 X
2 X
(Ck−1 + Cn−k ) = n − 1 +
Ck
n
n
k=1
k=1
En multipliant par n :
n Cn = n (n − 1) + 2
n−1
X
Ck
(1)
k=1
et, en remplaçant n par n + 1 :
(n + 1) Cn+1 = (n + 1) n + 2
n
X
Ck
k=1
D’où, en retranchant (2) et (1) membre à membre :
(n + 1) Cn+1 − nCn = 2n + 2Cn
ou encore :
(n + 1) Cn+1 − (n + 2) Cn = 2n
(2)
MÉTHODES DE TRI / PARTIE 2
On divise ensuite par (n + 1) (n + 2) :
Cn+1
4
Cn
2n
2
=
−
=
−
n + 2 n + 1 (n + 1) (n + 2) n + 2 n + 1
puis on somme (pour n = 0 à N − 1) :
N−1
X 4
2
CN
=
−
N+1
n+2 n+1
n=0
En posant enfin HN =
N
X
1
; il vient :
n
n=1
CN
1
= 4 (HN+1 − 1) − 2HN = 2HN + 4
−1
N+1
N+1
Comme HN ∼ ln (N) lorsque N → +∞, on peut conclure :
CN ∼ 2N ln (N)
La complexité en moyenne du tri rapide est donc bien quasi-linéaire.
5