I(n)

publicité
Complexité algorithmique
• De quoi dépend le temps d'exécution d'un programme ? Surtout du nombre
d'instructions machine effectivement exécutées, lui-même déterminé par :
... les données à traiter, surtout le volume de ces données
... le code source, surtout les algorithmes utilisés
• Pour évaluer l'efficacité en temps de calcul d'un algorithme donné, on veut
estimer le nombre d'instructions exécutées en fonction du volume des données
• Exemple (à discuter) :
public class Pg1 {
public static void main(String[] args) {
printTable(7);
// 1x (?)
}
private static void printTable(int n) {
int i,j;
// 1x (?)
for(i=1; i<=n; i++) {
// 1x; 8x; 7x
for(j=1; j<=n; j++)
// 7x; 56x; 49x
System.out.print(" " + i*j); // 49x; 49x; 49x
System.out.println();
// 7x
}
}
// total = 284 instructions
}
// (plus le "détail" (!!) de print)
// 5n2 + 5n + 4
n2
® dépend de n2
-8-
Compter les instructions à exécuter
• On s'intéresse au temps de calcul. On suppose qu'il dépend surtout du
nombre d'instructions à exécuter
• Soit n défini comme la "taille du problème" (p. ex. la taille du tableau à trier)
• Pour simplifier la discussion, on supposera que les "instructions élémentaires" sont
toutes équivalentes en temps de calcul
• Comment calculer le nombre I(n) d'instructions du programme en fct de n
• Séquences d'instructions :
I(n) = Ia(n) + Ib(n) + Ic(n)
{
}
a;
b;
c;
• Boucles, où a est le nbre d'itérations (de passages effectués)
while(test) {
corps;
}
• Appels de méthodes
(a+1) *(Itest (n))
I(n) =
+
a
I(n) = 1 + Icorps
*(Icorps(n))
de la méthode(n)
meth();
-9-
Ordre de grandeur, notation O(...)
• Le nombre exact d'instructions est peu informatif
On cherche à caractériser la croissance de la fonction quand n varie
• Pour de grandes valeurs de n, une fonction linéaire finit toujours par être
inférieure à une fonction quadratique
• (5n2+n) et (4n2+3n)
(50n+8)
sont de la même famille (fonctions quadratiques)
est d'une "meilleure" famille (fonction linéaire)
• Ce qui compte, c'est donc la partie dominante ! On ne retient que le terme de
plus haut degré, et on ignore les coefficents constants
• Exemple : Nombre d'instruction
Partie dominante
I(n) = 5n2 + 3n + 127
I(n) est en O(n2)
• On dit que I(n) est du même ordre de grandeur que f(n) et on note
I(n) est en O(f(n)) si $ c, n0 tq " n ³ n0, I(n) £ cf(n)
"… finit par être majoré par un multiple de …"
• Exemples :
3n3 + 2n + 10
2n + 300n200
log10(n) + 212
0.0001n + log(n)
est
est
est
est
en
en
en
en
O(n3)
O(2n)
O(ln(n))
O(n)
• Sur un ordinateur plus puissant, un algorithme quadratique s'exécutera
peut-être 5 fois plus rapidement.... mais il restera quadratique !
- 10 -
• Evolution de quelques fonctions de complexité pour des tailles de problèmes...
petites
plus grandes
- 11 -
Estimation ou vérification empirique
• Soit un programme P qui est censé dépendre du nombre n de données
• On peut lancer P avec des données différentes
et mesurer les temps de calcul
(10, 50, 100, 1000),
(T10, T50, T100, T1000)
• Pour savoir si Tn est en O(f(n)), on teste si (Tn / f(n)) semble constant
• Exemple : Soit Tn = 3n2 +48n +131;
Pour m suffisamment grand, (Tm / m2) @ 3
Tn est donc en O(n2)
• Mesurer un intervalle de temps en Java :
// in class System : static long currentTimeMillis()
long ta, tb;
ta = System.currentTimeMillis();
doSomething();
tb = System.currentTimeMillis();
System.out.println("Elapsed time = " + (tb-ta));
• Granularité (ms, ns) : il s'en passe des choses, en une milliseconde....
• Quand un programme fait des entrées/sorties, l'hypothèse que toutes les
instructions se valent n'est vraiment pas vérifiée
• L'analyse de complexité peut porter sur n'importe quelle ressource (p. ex
RAM), pas seulement le temps de calcul CPU.
- 12 -
Exemple
• Ici, il semble que l'algorithme soit en O(n ln n)
• Attention à la fiabilité des mesures. Deux exécutions successives ne
présentent pas forcément le même temps de calcul. Pourquoi ?
- 13 -
"Au pire des cas", "Au meilleur des cas", "Dans le cas moyen"
• Il n'y a pas que des séquences, et des boucles avec un nombre précis de
passages ! Attention aux instructions conditionnelles...
• Suivant les données, on peut tomber dans différentes instructions...
n = myTab.length;
if (n % 4 != 0){
a = 0;
} else {
for(int i=0; i<n; i++) {
a = a + i;
}
}
• Ici, on fait un "long" traitement seulement si n est divisible par 4
• Complexité...
au pire des cas
au meilleur des cas
cas moyen
O(n)
O(1)
¼ O(n) + ¾ O(1) = O((3+n)/4) = O(n)
• C'est surtout le "pire des cas" qui nous intéresse (il donne une garantie)
• On parle de cas moyen si on fait une moyenne de tous les inputs possibles (ici
toutes les valeurs possibles de n).
• Définir et calculer la complexité théorique dans le cas moyen n'est pas
toujours facile. Parfois, c'est même impossible
- 14 -
Classement des complexités
• Le plus souvent, les complexités tombent dans une des catégories suivantes,
classées dans l'ordre :
- O(1)
- O(ln n)
- O(n)
constante
logarithmique
linéaire
-
O(n ln n)
O(n2)
O(n3)
O(2n)
quasi-linéaire
quadratique
cubique
exponentielle
• Quand tout est "séparable", les règles sont assez simples :
- séquence d'opérations
- boucles imbriquées
- conditions (if)
« additionner les complexités
« multiplier les complexités
« pire des cas (ignorer le cas favorable)
• Mais on ne peut pas toujours analyser les complexités "par morceaux"
int i, j, sum=0;
for(i=0; i<n; i++) {
for(j=0; j<i; j++)
sum++;
}
• Outils mathématiques utilisés :
progressions...
analyse combinatoire, statistiques...
void f(boolean[] t) {
int n=t.length;
for(int i=0; i<n; i++)
while(i<n && t[i])
i++;
}
1+2+3+...+n = n*(n+1)/2 est en O(n2)
- 15 -
Etude de cas 1 : Recherche de sous-séquence maximale
• Soit un tableau d'entiers (positifs et négatifs)
Problème : trouver la sous-séquence de somme maximale
(il y a une sous-séquence spéciale, de longueur nulle, dont la somme == 0)
• Exemple :
Résultat :
{-2, 11, -4, 13, -5, 2}
20
{1, -3, 4, -2, -1, 6}
7
Algorithme 1, en O(n3)
• Idée : Tester chaque suite qui commence à la position i et se termine en j
public static int maxSubSum1(int[] a){
int maxSum = 0;
for(int i=0; i<a.length; i++)
// variante algo no 2 :
for(int j=i; j<a.length; j++){
int thisSum = 0;
int thisSum = 0;
for(int j=i;j<a.length;j++){
for(int k=i; k<=j; k++)
thisSum += a[j];
thisSum += a[k];
if(thisSum > maxSum)
if(thisSum > maxSum)
maxSum = thisSum;
maxSum
= thisSum;
}
}
return maxSum;
}
Algorithme 2, en O(n2)
• Idée : Tester chaque suite qui commence à la position i
• Une addition pour passer de {¼, 5,-1,8,4,¼} à {¼, 5,-1,8,4,¼}
- 16 -
• Les 2 algorithmes précédents sont dits de force brute (brute-force) : ils testent
toutes les combinaisons possibles
Algorithme 3, en O(n)
• Idée : ne tester que des suites qui ne commencent pas par une suite de
somme négative. Exemple :
{¼, 5,-7,8,4,¼}
inutile de tester les suites plus grandes partant de 5 !
public static int maxSubSum3( int[] a ) {
int maxSum = 0;
int thisSum = 0;
for(int j = 0; j< a.length; j++ ) {
thisSum += a[j];
if( thisSum > maxSum )
maxSum = thisSum;
else if( thisSum < 0 ) {
thisSum = 0; // resets the start index !
}
}
return maxSum;
}
• Encore faut-il se convaincre que l'algorithme est toujours correct.
Penser aux cas dégénérés (tous nuls, tous négatifs, tableau de taille 1 ou 0...)
• Si cet algo vous semble évident, sachez qu'on l'a longtemps cherché...
... ou trouvez l'équivalent à 2 dimensions (on cherche toujours)
- 17 -
Etude de cas 2 : Recherche d'élément dans un tableau
• Problème : tester si un tableau contient un élément donné
Algorithme 1, pour un tableau quelconque (trié ou non), en O(n)
public static boolean isPresent(int[] a, int x) {
for(int i=0; i<a.length; i++)
if(a[i] == x)
return true;
return false;
}
• Cet algorithme est appelé la recherche séquentielle
Algorithme 2, pour un tableau trié, en O(ln n)
• Idée : Tester si l'élément recherché est
- celui du milieu
(on l'a trouvé)
- dans la moitié gauche
(on restreint l'intervalle possible)
- dans la moitié droite
(on restreint l'intervalle possible)
ou si l'intervalle possible est vide
(on ne l'a pas trouvé)
• Cet algorithme, très utilisé, est appelé
recherche binaire ou recherche dichotomique ("qui divise en deux")
- 18 -
• Codage en Java :
public static boolean binarySearch(int[] t, int x) {
int low = 0;
int high = t.length - 1;
int mid;
{1, 3, 5, 6, 7, 8, 9}
4?
while( low <= high ) {
{1, 3, 5, 6, 7, 8, 9}
4?
mid = (low + high) / 2;
if(t[mid] < x)
{1, 3, 5, 6, 7, 8, 9}
4?
low = mid + 1;
else if( t[mid] > x)
{1, 3, 5, 6, 7, 8, 9}
4?
high = mid - 1;
NON !
else
return true;
}
return false;
(trouvez le petit bug !... cf. Java in 2006)
}
• Cet algorithme en en O(ln n) : le temps de calcul est proportionnel au
logarithme de la taille du tableau
• La meilleure complexité serait O(1), c'est-à-dire que le temps de calcul est
constant, ne dépend pas du tout de la taille du tableau. Il n'y a pas
d'algorithme en O(1) pour ce problème
- 19 -
Complexité logarithmique
• Dans l'algorithme de recherche binaire, au départ, l'intervalle des cases à
chercher, c'est tout le tableau, soit n cases.
• Après une étape, cet intervalle ne contient plus que (n-1)/2 cases
• Après deux étapes, la taille de cet intervalle est de nouveau divisé par 2, etc
• A la fin, il ne reste plus qu'une case (ou même aucune)
• Combien d'étapes a-t-il fallu ?
• Le logarithme, c'est l'opération inverse de l'élévation à la puissance
2 * (2 * (2* (2 * (x)))) = 24 * x
• Et donc :
int x = n;
while (x > 1)
x = x / 2;
// la boucle sera exécutée log2(n) fois
• La fonction logarithme est monotone croissante... Mais à long terme, elle
grandit de moins en moins. En pratique, elle finit par être presque constante.
• Conséquence :
avec la recherche dichotomique, on peut traiter
pratiquement n'importe quelle taille de problème ! Mais pas avec la recherche
séquentielle
- 20 -
Complexité temporelle vs spatiale
• L'analyse de complexité peut porter sur n'importe quelle ressource, pas
seulement le temps de calcul CPU.
• Complexité temporelle = en temps de calcul CPU
• Complexité spatiale = en espace mémoire (on mesure le "pic" de consommation)
• Un algorithme peut demander O(n2) en CPU, et O(n) en mémoire. Exemple :
int i,j;
CharStack s = new CharStack();
for(i=0; i<n; i++) {
for(j=0; j<i; j++)
s.push( (char)i);
while(!s.isEmpty())
s.pop();
}
int[] t, u, v;
int i,j;
t=new int[n];
for (int i=0;i<n;i++) {
u=new int[n];
for(j=0; j<n; j++)
v=new int[100];
}
Remarque sur la complexité de push()
• Dans notre implémentation avec tableau, on double le tableau s'il est plein.
On peut donc affirmer que, de temps en temps, l'opération push() ne
demande pas un temps constant, mais linéaire
• On va ici ignorer ce cas, qui arrive "rarement"
• A voir en 2ème année (complexité amortie)...
- 21 -
Téléchargement