• Aucun résultat trouvé

TP : Autour du tri rapide en C

N/A
N/A
Protected

Academic year: 2022

Partager "TP : Autour du tri rapide en C"

Copied!
11
0
0

Texte intégral

(1)

TP : Autour du tri rapide en C

Objectifs et conventions

On reprend l’algorithme du tri rapide vu en cours mais on l’adapte aux tableaux en C. On s’impose de travailler sans récursion eten place, c’est à dire sans utiliser de tableau auxiliaire. Cela nécessite d’utiliser deux indices entre lesquels il faut placer le pivot.

En application, on présente le « Quickselect » de Hoare qui permet de rechercher avec une bonne efficacité lek-ième plus petit élément d’un tableau sans passer par un tri préalable.

On se limite aux tableaux d’entiers à trier par ordre croissant.

Comme il est d’usage dans les sujet d’informatique, un même objetxest représenté avec deux polices de caractères : la notation droitexfait référence à un morceau de code dans lequel figurex tandis que la notation en italiquexdésignexdans une preuve mathématique.

Pour désigner le sous-tableau d’un tableau t entre les positions get d, nous écrivonst[g:d]

(out[g:d]dans une preuve). L’élémentidu tableau est désigné partidans une preuve et part[i]

dans un extrait de code.

1 Partitionnement

Le principe de lapartitionest de trouver dans un tableau labonne placed’un élément particulier appelépivot. Par « bonne place », on entend la position définitive du pivot dans la version triée du tableau.

La fonction int partition(int * t, int g, int d) prend en paramètres un tableau d’en- tierstet deux indicesg, ddu tableau avecg≤d. Le pivot est au départ en positiong(on améliorera ce point par la suite) et on cherche à le placer entre les indicesg et dpour le mettre à sa position définitive (la place qu’il occuperait si tous le tableau avait été trié). Cette dernière est la valeur retournée par la fonction.

Le pivot est l’élément tg. Le tableau est partitionné durant une simple boucle entreg+ 1etd.

Si l’indiceidésigne la position courante explorée dans l’intervalleJg+ 1, dK, on gère un indicej qui vérifie l’invariant suivant :

• Tous les éléments dont les positions sont dansJg, j−1Ksont plus petits que le pivot.

• Tous les éléments dont positions sont dansJj, iKsont strictement plus grands que le pivot.

La quantitéj−g mesure le nombre d’éléments plus petits que le pivot rencontrés jusqu’ici.

Au cours d’une itération, si l’élément courant est plus grand que le pivot on fait un echange entre la position courante et la positionj

Le pivot reste durant toute la boucle en position g. À la fin de la boucle, on échange l’élément positiongavec celui en positionj−1.

(2)

Par convention, pour la preuve, on suppose quei=g avant la boucle eti=daprès la boucle1. Q1 Écrire la fonction partitionen utilisant les indicesi, j décrits plus hauts et leurs spécifi-

cations.

Solution. Voici

1 int p a r t i t i o n (int t , int g , int d ) {

2 a s s e r t ( g<=d );

3 i f ( g==d )

4 return g;

5 int buf; // pour l e s é changes

6 int j = g+1; int p = t [ g ]; // l e p i v o t

7 for (int i = g+1; i<=d; i++)

8 i f ( t [ i ]<=p ) {

9 buf = t [ j ]; t [ j ] = t [ i ];

10 t [ i ] = buf;

11 j++;// i l y a un é l ément s u p l é mentaire p l u s p e t i t que l e p i v o t

12 }

13 buf = t [ j1];// ultime é change

14 t [ j1]=t [ g ]; t [ g]= buf;

15 return j1;

16 }

17

Q2 On note (dans les preuves de correction suivantes)xice qu’est devenu la variablexà la fin du tour de bouclei(les tours de boucles sont numérotés de gà d).

1. Montrer que l’invariant est vérifié avant l’entrée dans la boucle.

Solution. Avant l’entrée dans la boucle, on peut considérer que i =g. Alors t[g : i] = {tg}={p}. t[g:j−1] =t[g:g] ={p}. Tous les éléments det[g:j−1]sont plus petits que le pivot. Tous les éléments de t[j :i] =t[g+ 1 :g] =∅ sont strictement plus grands que le pivot. OK

2. On suppose qu’on a la preuve que la position définitive du pivot après le tri du tableau est entre g et d (ce qui signifie qu’on sait que les éléments avant la position g dans la version triée du tableau sont plus petits que le pivot et ceux à droite de la positiond, plus grands).

Montrer que si notre invariant sur j, i est vérifié à la fin du dernier passage, alors la fonction est correcte :

• La valeur retournée est bien la place définitive du pivot (la place qu’il occuperait si tous le tableau avait été trié).

1. En fait, il faudrait exprimer le second point de l'invariant non avecimais avec un troisième indicekindiquant la dernière position parcourue. Avant la bouclek=g, pendant la bouclek=i, après la bouclek=d. J'ai pensé que 3 indices compliqueraient la preuve sans apporter grand chose, d'où la convention suri.

(3)

• Le pivot est à sa place définitive.

Solution. De la position g à j−1, il n’y a (par invariant) que des éléments plus petits que le pivot.

Enj et jusqu’àd, tous les éléments sont plus grands que le pivot (par invariant).

Donc, quand on échange l’élément en positionj−1avec celui en positiong, on se trouve dans la situation où :

• t[g:j−2](éventuellement vide) ne contient que des éléments plus petits que le pivot.

• le pivot est en positionj−1

• t[j : d] (éventuellement vide) ne contient que des éléments plus grands strictement que le pivot.

L’hypothèse admise est que les éléments à gauche degsont, après le tri, plus petits que le pivot, et, à droite ded, plus grands.

Comme on a réussi à placer le pivot correctement entreg et d, il est correctement placé dans le tableau tout entier.

3. On suppose qu’à la fin du touri, on aji > i. Qu’est ce que cela signifie ?

Solution. Cela veut dire queja été incrémenté à chaque tour de boucle et donc que tous les éléments rencontrés jusqu’alors sont plus petits que le pivot.

On a donc queJji, iK=∅. Et les éléments plus grands strictements que le pivot dans la zoneJg, iKconstituent l’ensemble vide.

4. Montrer que si l’invariant est vérifié à la fin du touri < d−1, alors il est réalisé à l’étape i+ 1.

Solution. Supposons l’invariant vérifié à la fin de l’étapeiet supposons qu’il y a un tour i+ 1. A la fin de l’étape i+ 1, soit il y a eu un échange et j a été incrémenté donc ji+1=ji+ 1, soitji=ji+1.

(4)

Si un échange a eu lieu : on a mis en positionji+1(c.a.dji+1) un élément plus petit que le pivot. Degàji−1, les élements sont plus petits que le pivot (par HR) et donc deg àji+1−1aussi : OK.

Un élément qui occupe une position dansJji+ 1, iK, est strictement plus grand que le pivot (par HR, puisque Jji+ 1, iK⊂Jji, iK). Donc aux positions dans Jji+1, iKon ne trouve que des éléments plus grands que le pivot.

Regardons ce qu’il se passe en positioni+ 1: du fait de l’échange, on y a mistji.

• Si ji ≤i, alors tji > p (par invariant). Donc la zone (non vide) Jji+1, i+ 1K ne contient que des éléments plus grands que le pivot.

• Si ji > i, alors ji+1 > i+ 1, et donc la zone (vide) Jji+1, i+ 1K ne contient que des éléments strictement plus grand que le pivot2.

S’il n’y a pas eu d’échange : alorsji=ji+1et le tableauti+1(tà l’étapei+ 1) est le même que à l’étapei. Donc à gauche deji+1 il n’y a que des éléments plus petits que le pivot par HR. De ji ài les éléments sont strictement plus grands que le pivot par HR, donc aussi de ji à i+ 1(l’absence d’échange indique que l’élément en position i+ 1est plus grand strictement que le pivot).

L’invariant est vérifié.

On veut maintenant implanter le tri rapide. Au lieu de s’appuyer sur une récursion comme en cours, on choisit la version impérative. Celle-ci est appeléeLampSort et utilise une pile d’intervalles. Les intervalles sont représentés ici par la structure :

1 typedef struct {

2 int g; // borne gauche

3 int d; // borne d r o i t e

4 } i n t e r v a l l e; // modé l i s e l ' i n t e r v a l l e d ' e n t i e r s [ g , d ]

5

La structure de pile d’intervalles est décrite dans le fichierstack.cdont le fichier d’en-tête eststack.h. Tous deux sont disponibles dans l’archivestack.zip.

Voici les principales primitives :

1 typedef struct _m{

2 i n t e r v a l l e v a l;

3 struct _m prev;

4 } m a illo n;// m ai l l o n de p i l e

2. Ben oui, tout élément de l'ensemble vide vérie n'importe quoi !

(5)

5

6 typedef struct {

7 ma illo n top;

8 } s t a c k;// poign é e de p i l e : p o i n t e sur l ' é l ément l e + r é cent

9

10 extern void a f f i c h e ( s t a c k pg );// a f f i c h a g e de p i l e ( pour l e s t e s t s )

11

12 extern bool estVide ( s t a c k p );// i n d i q u e s i l a s t a c k e s t vide

13

14 extern s t a c k i n i t ( ); // cr é e une s t a c k vide ( r e n v o i e un p o i n t e u r sur s t a c k )

15

16 extern void a f f i c h e ( s t a c k p );// a f f i c h e l e contenu d ' une s t a c k

17

18 extern void push ( s t a c k p , i n t e r v a l l e v ); // a j o u t e un nouvel é l ément

19

20 extern i n t e r v a l l e pop ( s t a c k p );// dé p i l e r

21

On crée une pile vide dans laquelle on met l’intervalle de toutes les positions possibles.

A chaque tour, on dépile un intervalle :

• S’il est vide ou si c’est un singleton, on le retire simplement de la pile.

• Sinon, on le partitionne et on empile les deux sous-intervalles définis par cette partition.

L’algorithme s’arrête bien sûr quand la pile est vide.

Q3 1. Écrire la procédure lampsort(int * p, int n) qui prend en paramètres un tableau d’entiers et sa longueurnet trie le tableau selon le LampSort.

2. Un tri est ditstable si l’ordre des éléments de même valeur n’est pas modifié par le tri.

Le LampSort est-il stable ?

Solution. Q3 1. Il suffit de suivre l’algorithme

1 void lampsort (int t , int n ) {

2 s t a c k s = i n i t ( );// p i l e vide des i n t e r v a l l e s

3 int g =0, d=n1;

4 i n t e r v a l l e i t v = {g , d};

5 push ( s ,& i t v );

6 while (!estVide ( s ) ) {

7 i t v = pop ( s );

8 i f ( i t v . d i t v . g > 1) {

9 int p = p a r t i t i o n ( t , i t v . g , i t v . d );

10 i n t e r v a l l e i t v 1 = { i t v . g , p1}, i t v 2 = {p+1,d};

11 push ( s ,& i t v 1 ); push ( s ,& i t v 2 );

12 }

13 }

14 f r e e ( s );

15 }

(6)

16

2. Le Lampsort n’est pas stable carpartitionn’est pas stable. Supposons quet= [3,3].

Alorspartition t 0 1commence parj←1 puis on entre dans la boucle.

Lorsquei= 1,t1≤t0 donc on échange t1 avect1(aucun effet). Alors j←2.

On sort de la boucle et on échanget0avect1(échange final) : le 3 de gauche devient celui de droite et réciproquement.

Ainsi, l’ordre des éléments de même valeur est modifié.

2 Un peu de hasard

Nous avons déjà étudié la complexité du tri rapide : dans le pire des cas, le tableau est trié et l’un des sous-tableaux issus du partitionnement est vide à chaque étape. Dans ce cas la complexité en nombre de comparaisons est enO(n2).

Pour éviter ce problème, à chaque appel de la fonctionpartition t g d, une position aléatoire kest tirée entregetd. Puis un échange s’effectue entre les casesgetkce qui a pour effet d’introduire du hasard dans le choix du pivot. Le reste de l’algorithme est identique à ce qui a déjà été vu.

On donne ici un petit tutoriel, très inspiré de Développez.com. N’y passons pas trop de temps : l’objectif est juste de modifier la fonctionpartition.

En C, les fonctions usuelles de production de nombres aléatoires s’appelent rand et srand, l’appel de la seconde devant précéder les appels de la première.

1 int rand (void);

2

Cette fonction retourne un nombre aléatoire à chaque appel compris entre 0 etRAND_MAX.

1 void srand (unsigned int seed );

2

Cette fonction initialise le générateur de nombres pseudoaléatoires avec unegrainedifférente (1 par défaut). Elle ne doit être appelée qu’une seule fois avant tout appel àrand.

Dans un fichierhasard.cécrivons :

1 // t i r é de Developpez . com

2#include <s t d l i b . h>

3#include <s t d i o . h>

4#include <time . h>

5

6 int my_rand (void);

7

8 int main (void)

9 {

10 int i;

(7)

11

12 for ( i = 0; i<10; i++)

13 {

14 p r i n t f ("%d\n", my_rand ( ) );

15 }

16 return (EXIT_SUCCESS);

17 }

18

19 int my_rand (void)

20 {

21 return ( rand ( ) );

22 }

Q4 Compilez puis exécutez plusieurs fois. Que constate-t-on ?

Solution. Après compilation et exécution on obtient l’affichage de 10 nombres qui semblent bien aléatoires. Le problème est que deux exécutions différentes produisent la même suite de 10 nombres !

Pour l’inconvénient soulevé à la question précédente, il faut appelersrandà chaque démarrage du programme avec une graine différente. Le plus simple est de prendre comme graine le nombres de secondes écoulées depuis le 1er janvier 1970 à 0h00 : il est donné par la fonction time qui est présente dans l’entêtetime.h.

Q5 Cette méthode n’est pourtant pas la panacée. Quel problème peut-on immédiatement iden- tifier ? Peut-on facilement le résoudre ?

Solution. Il faut que nos appels successifs ne soient pas faits dans la même seconde !

Il suffit d’attendre une seconde à chaque démarrage du programme. Deux lancements successifs seront toujours distants d’au moins une seconde !

1#include <u n i s t d . h> // c o n t i e n t s l e e p

2

3 int my_rand (void)

4 {

5 s l e e p ( 1 ); // a t t e n d r e une seconde

6 some code . . .

7 }

8

Nous introduisons dans la fonctionmy_rand()une variable entière statiquefirst initialisée à zéro et modifiée lors du premier appel.

(8)

Q6 Dans quel segment de la mémoire virtuelle du programme se trouve la variable localefirst? Solution. Dans le segment BSS.

Lors du premier appel, la fonction initialise la graine en passanttime(NULL) en paramètre de srand. On passe en paramètre de time le pointeur NULL car il n’est pas nécessaire de stocker le nombre de secondes effectivement produit.

Pour les appels suivants,firstest différent de zéro et donc la graine n’est pas modifiée.

1 int my_rand (void)

2 {

3 stati c int f i r s t = 0;

4 i f ( f i r s t == 0)

5 {

6 srand ( time (NULL) );

7 f i r s t = 1;

8 }

9 return ( rand ( ) );

10 }

Q7 Compilez et exécutez plusieurs fois.

Le problème de la question Q4 semble résolu.

Nous voulons maintenant générer un nombre aléatoire entre 0 et une borne supérieure N−1 qui est bien souvent inférieure àRAND_MAX.

Q8 Une façon simple est de prendre le reste de la division par N du nombre pseudoaléatoire produit parrand. Mais cela ne fonctionne pas bien siN ne divise pasRAND_MAX. En prenant N = 10 et 25 pour RAND_MAXétablir que certains nombres apparaissent plus souvent que d’autres et donc que la répartition des nombres produits n’est pas uniforme.

Solution. Si le nombre produit par rand est dans J0,9Kou dans J10,19K le reste calculé vit dans J0,9K. Mais si le nombre produit est dansJ20,25K, le reste calculé appartient àJ0,5K.

En conclusion, les nombres produits ont plus de chance d’être dansJ0,5K!

La solution a ce problème est appelée « mise à l’échelle » et consiste à prendre pour valeur randomValueretournée l’expression suivante :

1 int randomValue = (int) ( rand ( ) / (double)RAND_MAX (N 1) );

Q9 Ecrire la fonction int choix(int n) qui prend en paramètre un nombren et renvoie un entier pseudoaléatoire pris entre 0 etn−1.

Q10 Ecrire la fonction int partition_rand(t,g,d) qui réalise un échange aléatoire entre l’élément en positionget une postion aléatoire prise entregetdavant de partitionner de la façon usuelle.

Solution. Voici

(9)

1 int choix (int n ) {

2 stati c int f i r s t = 0;

3

4 i f ( f i r s t == 0)

5 {

6 srand ( time (NULL) );

7 f i r s t = 1;

8 }

9 int randomValue = (int) ( rand ( ) / (double)RAND_MAX ( n 1) );

10 return randomValue;

11 12 }

13

14 int partition_rand (int t , int g , int d ) {

15 a s s e r t ( g<d );

16 // dé but é change i n i t i a l

17 int toswap = choix (dg+1);

18 int buf = t [ g ]; // pour c e t é change et l e s s u i v a n t s

19 t [ g ] = t [ toswap ];

20 t [ toswap ] = buf;

21 p r i n t f (" partition_rand : swap(%d,%d ) \n", g , toswap );

22 // f i n é change i n i t i a l

23 int j = g+1;

24 int p = t [ g ]; // l e p i v o t

25 for (int i = g+1; i<=d; i++)

26 i f ( t [ i ]<=p ) {// un é l ément à d r o i t e de j e s t <=p

27 buf = t [ j ];

28 t [ j ] = t [ i ];

29 t [ i ] = buf;

30 j++;// i l y a un é l ément s u p l é mentaire p l u s p e t i t que l e p i v o t

31 }

32 buf = t [ j1];// ultime é change

33 t [ j1]=t [ g ];

34 t [ g]= buf;

35 return j1;

36 }

37 38

3 QuickSelect

Nous nous intéressons maintenant à la recherche duk-ième plus petit élément dans un tableau de longueurn. Par exemple 3 est le quatrième plus petit élément de{10;2;8;0;1;3;0;18} (si on commence à compter les positions à partir de zéro). Une façon simple de résoudre ce problème est de trier la liste (enO(nlogn)) et de renvoyer l’élément d’indicekdu tableau trié.

Mais nous voulons éviter de trier le tableau : c’est ce que permet le QuickSelect de Hoare.

Comme pour le tri rapide, on partitionne encore le tableau à chaque étape. La position obtenue pour le pivot permet de choisir entre s’arrêter, poursuivre en explorant la partie à droite du pivot ou explorer la partie à sa gauche. La différence avec le tri rapide est qu’on explore un seul des sous-tableaux séparés par le pivot.

Q11 1. Écrire la fonctionint quickselect(int * t, int k, int n)qui cherche lek-ième plus petit élément du tableautde longueurn. On suppose quek < n.

(10)

Cette fonction s’appuie sur une fonction récursive qui cherche lek-ième plus petit élément entre deux indicesg < d.

2. Etablir la complexité au mieux au pire et en moyenne duquickselect pour le nombre de compraisons en fonction de la longueurndu tableau d’entrée.

Solution. — Complexité au mieux : lorsque le premier élément du tableau est directement celui qui doit se trouver en positionk.O(n)

— Complexité au pire : comme pour le tri rapide, lorsque la partition crée un tableau vide à chaque étape.

La complexité est alors de la formeTn=Tn−1+n−1(n−1pour le nombre de comparaisons dans la partition). On a déjà vu qu’alors la complexité est quadratique.

— Complexité en moyenne.

On noteunla complexité en moyenne de la recherche de l’élément de rangrpour un tableau de taillen. On va voir que cette complexité ne dépend pas der.

On suppose que tous les éléments de la liste sont distincts et que deux listes denéléments distincts ont la même probabilité d’apparaître.

Pour un tableau de taillen >1, une fois la partition effectuée enΘ(n), il faut ensuite effectuer une recherche dans un tableau de taillek pour k∈J1, n−1K. Si on suppose que toutes les tailles du sous-tableau où chercher sont équiproblables, on obtient la relation suivante :

un= 1 n

n−1

X

k=1

(uk+n−1) = n−1

n +1

n

n−1

X

k=1

uk.

On observe donc que

nun=n(n−1) +

n−1

X

k=1

uk.

Donc

(n−1)un−1= (n−1)(n−2) +

n−2

X

k=1

uk.

En soustrayant :

nun−(n−1)un−1= (n−1)(n−(n−2)) +un−1 D’où :

nun−nun−1= 2n−2 Il vient alors que

un−un−1= 2− 2 n En sommant cette série telescopique :

un− u0

|{z}

=0

= 2n− 2

n

X

k=1

1 n

| {z }

+∞2 lnn+2γ

= Θ(n).

(11)

Pour un tableau trié de longueur N, la médiane est l’élément situé au milieu du tableau si celui-ci existe :

• SiN est impair, la médiane est exactement l’élément milieu.

• SiN est pair et le tableau est non vide, le milieu du tableau n’est pas un indice car il n’est pas entier. L’usage est de retourner la moyenne des deux éléments dont les positions sont les plus proches du milieu.

Q12 Écrire la fonctiondouble mediane(int * t, int n)qui renvoie la médiane du tableau tde longueurnselon la définition ci-dessus.

Solution. Voici

1 int s e l e c t i o n (int t , int g , int d , int k ) {

2 int pp = p a r t i t i o n ( t , g , d );

3 i f ( k==pp )

4 return t [ pp ];

5 else i f ( k<pp ) { // l a k i ème p o s i t i o n e s t à gauche du p i v o t

6 return s e l e c t i o n ( t , g , pp1,k );

7 }

8 else{ // l a k i ème p o s i t i o n e s t à d r o i t e du p i v o t

9 return s e l e c t i o n ( t , pp+1,d , k );

10 }

11 }

12

13 int q u i c k s e l e c t (int t , int k , int n ) {

14 return s e l e c t i o n ( t , 0 , n1,k );

15 }

16 17

18 double mediane (int t , int n ) {

19 double v1 = (double) q u i c k s e l e c t ( t , n /2 , n );

20 i f ( n%2==1)

21 return v1;

22 double v2 = (double) q u i c k s e l e c t ( t , n/21,n );

23 return ( v1+v2 ) / 2 . 0;

24 25 }

26

Références

Documents relatifs

De nombreux chercheurs, enseignants-chercheurs, médecins et étudiants bénévoles viendront partager avec le public les avancées obtenues dans les laboratoires de recherche

Lorsqu’une base de données est très volumineuse et non ordonnée, trouver une information dans cette base est très long et très ingrat.. En effet la seule solution est

Vous trouverez dans le fichier lance_tests.py les commandes permettant de générer les douze combinaisons possibles (quatre méthodes pour générer un tableau, trois fonctions de

Or pour que l’élément de rang i et l’élément de rang j soient comparés il faut que le pivot ne soit jamais choisi entre l’élément de rang i et de rang j avant le choix de i ou

1) De la même façon que sur cette page : http://lwh.free.fr/pages/algo/tri/tri_selection.html lors du tri par sélection de tonneaux on compte le nombre de comparaisons et

Utiliser les fonctions tic() et toc() pour comparer le temps de tri d’un tableau contenant un grand nombre de valeurs aléatoires (10000 ?) comprises entre -100 et +100 pour les

Remarque sur la deuxi`eme partie : on peut aussi montrer la version affine de cette propri´et´e : il existe dans R n une famille infinie de vecteurs telle que d`es qu’on en prend n +

A partir de l’entier n = 2, à tour de rôle, Zig, qui joue le premier, puis Puce ajoutent au nombre précédemment affiché un diviseur de ce nombre qui lui est strictement