You are on page 1of 65

Tri

Tri par insertion


Dans le cas où les données n’ont qu’un ordre
pertinent, et si l’ajout de nouvelles données
est rare, il est plus judicieux de mettre
directement la nouvelle donnée à sa place
plutôt que de la mettre à la fin puis de tout
retrier. Ceci est d’autant plus valable lorsque
les données sont implantés sous la forme
d’une liste chaînée.
Faire ce tri en exercice.
Tri par sélection
• Principe : on recherche le minimum dans le
tableau, et on vient l’échanger avec le premier
élément du tableau. On recherche par la suite
le suivant, que l’on échange avec le
deuxième…
void tri_par_selection (int *tableau, int taille)
{ int min, ind_min, i, j, temp;
for (i=0; i<taille-1; i++)
{ min=tableau[i]; ind_min= i;
for (j=i+1; j<taille; j++)
if (tableau[j]<min)
{ min = tableau[j]; ind_min= j; }
if (i!=ind_min)
{temp=tableau[ind_min];
tableau[ind_min]=tableau[i];
tableau[i]=temp;
}
}
}
Tri à bulle
Principe de la première version :
Le principe consiste à parcourir le tableau à
partir du début jusqu’à trouver 2 éléments qui
ne sont pas dans l’ordre désiré. On les inverse
alors et on recommence. Le processus se
termine lorsqu’on a parcouru tout le tableau
sans avoir à effectuer d’échange.
void tri_bulle1( int *tableau, int taille)
{ int indice=0; int temp;
while (indice < taille-1)
if ( tableau[indice+1]< tableau[indice])
{ temp= tableau[indice]
tableau[indice]= tableau[indice+1];
tableau[indice+1]=temp;
indice=0;
}
else indice++;
}
Tri à bulles suite
• Principe de la seconde version :
On parcourt tout le tableau jusqu’au dernier
rang en inversant tous les couples d’éléments
consécutifs non ordonnés. Ceci a pour effet de
mettre à la fin du tableau le plus grand
élément et de pré-ordonner le reste. Puis on
recommence en s’arrêtant un rang avant. Le
processus continue jusqu’à ce que le tableau
soit entièrement trié.
void tri_bulle2 ( int *tableau, int taille)
{int max, i, temp; int trie=0;
for (max=taille; max>1 && !trie; max--)
{trie=1;
for (i=0; i<max-1; i++)
if (tableau[i+1] < tableau [i])
{temp= tableau[i]; tableau[i]=tableau[i+1];
tableau[i+1]=temp; trie=0;
}
}
}
Tri Rapide / Quick Sort
• Principe : on choisit un élément témoin au
hasard dans le tableau. On sépare le tableau
initial en 2 parties : une qui contiendra tous
les éléments inférieurs strictement au témoin,
l’autre tous ceux qui sont supérieurs ou égaux
à l’élément témoin. On recommence sur
chacun des 2 sous-tableaux obtenus. On
s’arrête quand un sous-tableau examiné ne
contient que zéro ou un élément.
void tri_rapide (int *tableau, int taille, int k)
{int temoin, ind_inf, ind_sup, temp;
if (taille > 1) { ind_inf=0; ind_sup=taille-1;
temoin=tableau[k];
while (ind_inf <= ind_sup)
{if (tableau[ind_inf]<temoin)
ind_inf++;
else {temp=tableau[ind_inf];
tableau[ind_inf]=tableau[ind_sup];
tableau[ind_sup]=temp;
ind_sup--;
}
}
if ((ind_inf !=0)&& (ind_sup!=taille-1))
{
tri_rapide(tableau,ind_inf,0);
tri_rapide(&(tableau[ind_inf]), taille- ind_inf,0);
}
else tri_rapide(tableau, taille, k+1);
}
}
/*l’appel initial se fait avec tri_rapide(tableau,taille,0); */
Tri Fusion
void tri_fusion( int *t, int n)
{
if (n>0)
tri_fusion_bis(t,0,n-1);
}
void tri_fusion_bis(int *t, int deb, int fin)
{ if (deb!=fin)
{ int milieu=(fin+deb)/2;
tri_fusion_bis(t,deb,milieu);
tri_fusion_bis(t,milieu+1,fin);
fusion(t,deb,milieu,fin);
}
}
void fusion(int *t, int deb1, int fin1, int fin2)
{ int *table1; int deb2=fin1+1; int compt1=deb1;
int compt2=deb2; int i;
table1=(int*)malloc((fin1-deb1+1)*sizeof(int));
for (i=deb1;i<=fin1;i++)
table1[i-deb1]=t[i];
for(i=deb1;i<=fin2;i++)
{ if (compt1==deb2)
break;
else if (compt2==(fin2+1))
{t[i]=table1[compt1-deb1];compt1++;}
else if (table1[compt1-deb1]<t[compt2])
{t[i]=table1[compt1-deb1];compt1++;}
else {t[i]=t[compt2];compt2++;}
}
free(table1);
}
Complexité
on divise une table de longueur n en deux tables
de longueur n/2, on trie, récursivement, ces
deux tables, puis on fusionne les tables triées.
Comme la fusion de deux tables ordonnées de
longueur n/2 se fait en O(n),
l'équation de complexité est
T(n)= 2T(n/2) + O(n)
dont la solution est en O(n log n).
Complexité des algorithmes
• Définition 1: la complexité temporelle d’un
algorithme est le temps mis par ce dernier
pour transformer les données du problème
considéré en un ensemble de résultats.

• Définition 2: la complexité spatiale d’un


algorithme est l’espace utilisé par ce
dernier pour transformer les données du
problème considéré en un ensemble de
résultats.
• Meilleur cas: notée par tmin(n) représentant la complexité de
l’algorithme dans le meilleur des cas en fonction du paramètre
n (ici le nombre d’éléments dans le tableau).

• Pire cas: notée par tmax(n) représentant la complexité de


l’algorithme dans le cas le plus défavorable en fonction du
paramètre n (ici le nombre d’éléments dans le tableau).

• Cas Moyen: notée par tmoy(n) représentant la complexité de


l’algorithme dans le cas moyen en fonction du paramètre n (ici
le nombre d’éléments dans le tableau). C’est-à-dire la
moyenne de toutes les complexités, t(i), pouvant apparaître
pour tout ensemble de données de taille n (t(i) représente
donc la complexité de l’algorithme dans le cas où C se trouve
en position i du tableau). Dans le cas où l’on connaît la
probabilité Pi de réalisation de la valeur t(i), alors par
définition, on a:
t moy ( n )  p1t (1)  ...  p n t ( n )
Notation grand-O
Définition: Soit T(n) une fonction non négative.
T(n) est en O(f(n)) s’il existe deux
La notation grand-O indique une borne constante
positives c etsur
supérieure n0 telles
le tempsque:d’exécution.
T(n) <= cf(n) pour tout n >= n0.
Exemple: Si T(n) = 3n2 +2
Utilité: Le temps d’exécution
alors est2).borné
T(n) Î O(n
Signification: Pour toutes les grandes entrées (i.e., n >=
On ndésire
0), on le
est plus
assuré de
queprécision possible:
l’algorithme ne prend pas plus
Biendeque
cf(n)T(n)
étapes.
= 3n2 +2 Î O(n3),
 Borne
on préfèresupérieure.
O(n2).
Grand-Omega
Définition: Soit T(N), une fonction non négative.
On a T(n) = W (g(n)) s’il existe deux
constantes positives c et n0 telles que T(n) >=
cg(n) pour n > n0.

Signification: Pour de grandes entrées,


l’exécution de l’algorithme nécessite au
moins cg(n) étapes.

 Borne inférieure.
La notation Theta
Lorsque le grand-O et le grand-omega d’une
fonction coïncident, on utilise alors la
notation grand-theta.

Définition: Le temps d’exécution d’un


algorithme est dans Q(h(n)) s’il est à la
fois en O(h(n)) et W(h(n)).
Règles de simplification 1

Si
f(n) = O(g(n))
et
g(n) = O(h(n)),

alors
f(n) = O(h(n)).
Règles de simplification 2
Si
f(n) = O(kg(n))
où k > 0 est une constante,

alors
f(n) = O(g(n)).
Règles de simplification 3
Si
f1(n) = O(g1(n))
et
f2(n) = O(g2(n)),

alors
(f1 + f2)(n) = O(max(g1(n), g2(n)))
Règles de simplification 4
Si
f1(n) = O(g1(n))
et
f2(n) = O(g2(n))

alors
f1(n)f2(n) = O(g1(n) g2(n))
Quelques règles pour calculer la
complexité d’un algorithme
• Règle 1: la complexité d’un ensemble
d’instructions est la somme des complexités
de chacune d’elles.
• Règle 2: Les opérations élémentaires telle que
l’affectation, test, accès à un tableau,
opérations logiques et arithmétiques, lecture
ou écriture d’une variable simple ... etc, sont
en O(1) (ou en Q(1))
• Règle 3: Instruction if: maximum entre le
then et le else

switch: maximum parmi les différents cas


Règle 4: Instructions de répétition

1. la complexité de la boucle for est calculée par la


complexité du corps de cette boucle multipliée
par le nombre de fois qu’elle est répétée.

2. En règle générale, pour déterminer la complexité


d’une boucle while, il faudra avant tout
déterminer le nombre de fois que cette boucle est
répétée, ensuite le multiplier par la complexité du
corps de cette boucle.
Règle 5: Procédure et fonction: leur complexité est
déterminée par celui de leur corps. L’appel à une
fonction est supposé prendre un temps constant en
O(1) (ou en Q(1))

Notons qu’on fait la distinction entre les fonctions


récursive et celles qui ne le sont pas:

• Dans le cas de la récursivité, le temps de calcul est


exprimé comme une relation de récurrence.
Exemples
Exemple 1: a = b;
Temps constant: Q(1).

Exemple 2:
somme = 0;
for (i=1; i<=n; i++)
somme += n;
Temps: Q(n)
Exemple 3:
somme = 0;
for (j=1; j<=n; j++)
for (i=1; i<=n; i++)
somme++;
for (k=0; k<n; k++)
A[k] = k;

Temps: Q(1) + Q(n2) + Q(n) = Q(n2)


Exemple 4:
somme = 0;
for (i=1; i<=n; i++)
for (j=1; j<=i; j++)
somme++;

Temps: Q(1) + O(n2) = O(n2)

On peut montrer : Q(n2)


Efficacité des algorithmes
• Définition: Un algorithme est dit efficace si sa
complexité (temporelle) asymptotique est dans
O(P(n)) où P(n) est un polynôme et n la taille des
données du problème considéré.
• Définition: On dit qu’un algorithme A est meilleur
qu’un algorithme B si et seulement si:

t A (n)  O(t B (n)); et


tB (n)  O(t A (n))

Où t A (n) et t B (n) sont les complexités des


algorithmes A et B, respectivement.
PGCD de deux nombres
int PGCD(int A, int B){
int reste;
reste = A % B;
while (reste !== 0)
{
A = B;
B = reste;
reste = A % B;
}
return(B);

} /* fin de la fonction */
Analyse PGCD suite
Avant de procéder, nous avons besoin du résultat suivant:

Proposition : Si reste = n % m alors reste < n/2


Preuve: Par définition, nous avons:
reste = n –q.m; q >=1
reste <= n –m (1)
On sait aussi que reste < m -1 (2)
En additionnant (1) avec (2), on obtient:
2 reste < n – 1
donc: reste < n / 2 CQFD
PGCD Suite
Durant les itérations de la boucle while, l’algorithme
génère, à travers la variable reste, la suite de
nombre de nombre {r0, r1, r2, r3 , ... },
représentant les valeurs que prennent les variable
n et m, où
r 0  n ; r1  m ;
r j  1  r j  1 mod rj; j  2
De la proposition précédente, on peut déduire
rj 1  rj 1 / 2
Par induction sur j, on obtient l’une des deux
relations suivantes, selon la parité de l’indice j
PGCD suite
rj < r0 / 2j/2 si j est pair
rj < r0 / 2(j-1)/2 si j est impair

Dans les deux cas, la relation suivantes est vérifiée:

rj < max(n,m) / 2j/2


Dès que rj < 1, la boucle while se termine, c’est-à-dire
dès que:
2j/2 = max(n,m)
Par conséquent, le nombre de fois que la boucle
while est répétée est égal à

2log(max(n,m)+1) = O(log max(n,m)).

Comme le corps de cette boucle est en O(1), alors la


complexité de tout l’algorithme est aussi en

O(log max(n,m))
PGCD de deux nombres
int PGCD(int A, int B){
int reste;
reste = A % B;
while (reste !== 0)
{
A = B;
B = reste;
reste = A % B;
}
return(B);

} /* fin de la fonction */
Analyse: Encore une fois, le gros problème consiste
à déterminer le nombre de fois que la boucle
while est répétée. Il est clair que dans ce cas, il y a
lieu normalement de distinguer les trois
complexités. En ce qui nous concerne, nous allons
nous limiter à celle du pire cas. Pour ce qui est de
celle du meilleur cas, elle est facile à déterminer;
mais, en revanche, celle du cas moyen, elle est
plus compliquée et nécessite beaucoup d’outils
mathématique qui sont en dehors de ce cours.
Pour ce faire, procédons comme suit pour la
complexité dans le pire cas:
Analyse PGCD suite
Avant de procéder, nous avons besoin du résultat suivant:

Proposition : Si reste = n % m alors reste <= n/2


Preuve: Par définition, nous avons:
reste = n –q.m; q >=1
reste <= n –m (1)
On sait aussi que reste <= m -1 (2)
En additionnant (1) avec (2), on obtient:
2 reste < n – 1
donc: reste <= n / 2 CQFD
PGCD Suite
Durant les itérations de la boucle while, l’algorithme
génère, à travers la variable reste, la suite de nombre
de nombre {r0, r1, r2, r3 , ... }, représentant les
valeurs que prennent les variable n et m, où

r 0  précédente,
De la proposition n ; r1  m ;on peut déduire
r j  1  r j  1 mod rj; j  2

Par induction sur j, on obtient l’une des deux relations


1  rj 1 / 2
suivantes, selonrjla parité de l’indice j
PGCD suite
rj <= r0 / 2j/2 si j est pair
rj <= r1 / 2(j-1)/2 si j est impair

Dans les deux cas, la relation suivante est vérifiée:

rj <= max(n,m) / 2j/2


Dès que rj < 1, la boucle while se termine, c’est-à-dire
dès que:
2j/2 = max(n,m)
Par conséquent, le nombre de fois que la boucle
while est répétée est égal à

2log(max(n,m)) = O(log max(n,m)).

Comme le corps de cette boucle est en O(1), alors la


complexité de tout l’algorithme est aussi en

O(log max(n,m))
Tours de hanoi
void hanoi(int n, int i, int j, int k){
/*Affiche les messages pour déplacer n disques
de la tige i vers la tige k en utilisant la tige j */
if (n > 0)
{
hanoi(n-1, i, k, j)
printf (‘Déplacer %d vers %d’, i,k);
hanoi(n-1, j, i, k)
}
} /* fin de la fonction */
Analyse de Hanoi
Pour déterminer la complexité de cette fonction,
nous allons déterminer combien de fois elle fait
appel à elle-même. Une fois ce nombre connu, il
est alors facile de déterminer sa complexité. En
effet, dans le corps de cette fonction, il a y a:
• Un test
• Deux appels à elle même
• Deux soustractions
• Une opération de sortie
En tout, pour chaque exécution de cette fonction, il
y a 6 opérations élémentaires qui sont
exécutées.
Hanoi suite
Soit t(n) la complexité de la fonction hanoi(n,i,j,k). Il
n’est pas difficile de voir, quelque que soit les trois
derniers paramètres, t(n-1) va représenter la
complexité de hanoi(n-1, -,-,-).
Par ailleurs, la relation entre t(n) et t(n-1) est comme
suit:
t(n) = t(n-1)+ t(n-1) + 6, si n > 0
t(0) = 1 (un seul test)
Autrement écrit, nous avons:
t(n) = 2 t(n-1) + 6, si n > 0
t(0) = 1 (un seul test)
Hanoi suite
Pour résoudre cette équation (de récurrence), on
procède comme suit:

t(n) = 2 t(n-1) + 6
2 t(n-1) = 4 t(n-2) + 2.6
4t(n-2) = 8 t(n-3) + 4.6
...................................
2(n-1) t(1) = 2n t(0) + 2(n-1) .6
En additionnant membre à membre, on obtient:

t(n) = 2n t(0) +6(1+2+4+ ... + 2(n-1) )


= 2n + 6. (2n - 1)
= O(2n).
Paradigme : Diviser pour régner
(Divide and Conquer)
• Principe
Nombres d’algorithmes ont une structure récursive
: pour résoudre un problème donné, ils s’appellent
eux-mêmes récursivement une ou plusieurs fois sur
des problèmes très similaires, mais de tailles
moindres, résolvent les sous problèmes de manière
récursive puis combinent les résultats pour trouver
une solution au problème initial.
Le paradigme « diviser pour régner » donne lieu à
trois étapes à chaque niveau de récursivité :

– Diviser : le problème en un certain nombre de


sous-problèmes ;
– Régner : sur les sous-problèmes en les
résolvant récursivement ou, si la taille d’un
sous-problème est assez réduite, le résoudre
directement ;
– Combiner : les solutions des sous-problèmes
en une solution complète du problème initial.
Vérifier qu’une liste de taille n est triée

P(n)


P(n/m) P(n/m) P(n/m)

Vérifier que la liste L est triée revient à :


•Vérifier que chaque liste Lk est triée (k=1..m)
•Vérifier que le dernier élement de Lk est inférieur au
premier élément de Lk+1 (k=1..m-1)
Complexité

• t(n) complexité de résolution de P(n)


• t(n)= m*t(n/m) + m-1
– m-1 pour les m-1 tests des bornes
– Le coût de la résolution est la somme des coût des
résolutions des problèmes élémentaires plus la
somme des coût des combinaisons
Evaluation des complexités et exemples

I- Récursivité de type :
t(n)=a* t(n/b) + f(n) (a étant le nombre
d’appel avec une taille n/b et f(n) étant le coût
des combinaisons)
t(n)=a*t(n/b)+f(n)
a1*t(n/b)=(a*t(n/b2)+f(n/b))*a1
a2*t(n/b2)=(a*t(n/b3)+f(n/b2))*a2
a3*t(n/b3)=(a*t(n/b4)+f(n/b3))*a3
……………

aq-1*t(n/bq-1)=(a*t(n/bq)+f(n/bq-1))*aq-1
aq*t(n/bq)=t(r)*aq
avec n=r*bq+1
q 1

t(n)= a *t(r) + 
q a i
f ( n / b i
)
i 0
Si f(n)= Q (nα) avec α ≥0
alors

si a<bα alors t(n)= Q (nα)

si a=bα alors t(n)= Q (nαlog n)

si a>bα alors t(n) = Q ( nlog)b a


Exemple : recherche dichotomique

a=1 b=2 et α=0


t(n)= t(n/2) + Q (1)

 t(n)= Q (log n)
Tri fusion

a=2 b=2 et α=1


t(n)=2t(n/2) + Q (n)
 t(n)= Q (n log n)
Vérification de tri pour un algorithme
récursif
t(n)= m*t(n/m) + Q (m-1)
a=m b=m et α=0

Cas 2 : a>bα
 t(n)=
 (n logm m
)   (n)
• Multiplication naïve de matrices
– Nous nous intéressons ici à la multiplication de
matrices carrés de taille n
MULTIPLIER-MATRICES(A, B)
Soit n la taille des matrices carrés A et B
Soit C une matrice carré de taille n
Pour i  1 à n faire
Pour j  1 à n faire
cij  0
Pour k  1 à n faire
cij  cij +aik * bkj
renvoyer C
– Cet algorithme effectue Q(n3) multiplications
et autant d’additions.
Nous supposons que n est une puissance exacte de 2.
Décomposons les matrices A, B et C en sous-matrices
de taille n/2 *n/2. L’équation C = A *B peut alors se
réecrire :

.
nous obtenons :

Chacune de ces quatre opérations correspond à


deux multiplications de matrices carrés de taille
n/2 et une addition de telles matrices. À partir de
ces équations on peut aisément dériver un
algorithme « diviser pour régner » dont la
complexité est donnée par la récurrence
l’addition des matrices carrés de taille n/2 étant en Q(n2).
• x1 = (b − d)(f + h) x5 = a(g − h)
• x2 = (a + d)(e + h) x6 = d(f − e)
• x3 = (a − c)(e + g) x7 = (c + d)e
• x4 = (a + b)h

x1 + x2 − x4 + x6 x4 + x5
x6 + x7 x2 − x3 + x5 − x7

T(n) = 7T(n/2) + O(n2)


a= 7 b=2 et α =2

t(n)=Q ( nlog2 7 )

You might also like