NFP136 – VARI 2 Algorithmique et Structures de données Prolongement de la partie « Algorithmique et Programmation » du cours NFP 135 (VARI 1) 1 L'Informatique a de multiples facettes - C'est à la fois une science : fondements théoriques de la calculabilité et de l'algorithmique, principes de fonctionnement et arithmétique des ordinateurs, etc. - C'est également, par nature, un domaine de savoirs technologiques : mise en œuvre par la programmation (syntaxe et bonnes pratiques de l'utilisation des langages de programmation), et applications directes dans le monde réel (vidéo, son, interfaces hommes-machines, jeux vidéo, etc.). - Dans le prolongement de NFP 135, cette UE se focalise sur la présentation des structures de données « usuelles » et des algorithmes associés, mais également sur leur implémentation et la prise en main d'un langage de haut niveau, ainsi que sur des connaissances de base liées aux systèmes d'exploitation. 2 Objectifs de cette partie du cours : - Pouvoir manipuler efficacement des données plus nombreuses (et plus complexes). - Ce qui nécessite de les stocker dans des structures de données « intelligentes », de façon à ce que les opérations (algorithmes) avec lesquelles on les exploite soient le plus efficace possible. - Les informaticiens, au cours du temps, ont défini des Structures de Données usuelles (tableaux, listes, arbres, graphes, etc.) et des Algorithmes usuels (tri, manipulation de données structurées, etc.). - Avec un soucis d’abstraction et de modularité : on cherche à dissocier une structure de données de la façon dont elle est implémentée (notion de Type Abstrait). 3 Contenu : - Structures de données élémentaires (tableaux, tables de hachage, listes, piles, files) et opérations élémentaires associées (création, parcours, tri). - Mesure d’efficacité de ces algorithmes (et des autres) : notions de complexité. - Des structures plus complexes dédiées : graphes, arbres (binaire ou non), tas. - Modèles de calcul : langages formels et automates (graphes d'état). 4 Contenu : ⇒ Langage d’implémentation : JAVA Car : - Langage de haut niveau et portable, - Facilite l’abstraction et la programmation modulaire par la structuration en classes d'objets, - Dispose d’un mécanisme de « garbage collector » (ramasse-miettes) rendant inutile la désallocation « à la main » de l'espace mémoire inutilisé. 5 QUELQUES RAPPELS DE PROGRAMMATION (JAVA : Paramètres et Tableaux) 6 Variables et références • Un programme JAVA utilise des variables pour garder trace des opérations effectuées. • En fait, le déroulement d'un programme peut se voir comme une suite d'états de la mémoire (vision de la programmation impérative). • A chaque variable est associée une place mémoire, qui possède une adresse. • Une référence vers une variable est la valeur de l'adresse de son emplacement mémoire. 7 Variables, fonctions et paramètres • Lors d'un appel de fonction, les valeurs passées en paramètres sont recopiées dans des variables locales (paramètres formels) : public static void f (int i) {i=5;} (…) int a=2; f(a); (…) /* Ici, i et a ne référencent pas du tout la même zone mémoire : modifier la valeur de l'un n'a donc aucune influence sur celle de l'autre ! */ • Ce mécanisme de recopie est systématique : quoi qu'un paramètre représente, sa valeur est recopiée dans une variable locale. 8 Variables de type Objet • Ce qu'on appelle un Objet en Java est en fait une référence vers un Objet : String str=new String("oui");//str contient l'adresse d'un objet String • Que vaut alors str après l'appel suivant ? public static void f2(String s) {s=new String("non");} (…) f2(str); (…) 9 Paramètres objets • Que valent p.x et p.y à la fin ? public class Point {public int x=0, y=0;} (…) public static void changePoint(Point p, int x, int y) {p.x=x; p.y=y;} (…) (…) Point pt=new Point(); (…) (…) changePoint(pt, 1, 2); (…) 10 Lexicologie • Dans un langage de programmation, passer un paramètre par référence signifie fournir l'adresse de son emplacement mémoire (et non sa valeur – cf passage par valeur). • Un objet Java est toujours passé par référence – Ainsi, une variable String str contient l'adresse d'un objet String, – Mais cette adresse est recopiée localement lorsque str est en paramètre d'une fonction. 11 Les tableaux 12 Tableau • Un tableau est une structure de données qui réunit un nombre prédéfini de données d'un même type. • On peut le voir comme une suite de cases contiguës, chacune identifiée (indicée) par un entier. 13 Tableau • La taille (nombre de cases) du tableau doit être connue au moment de sa création et ne peut pas être modifiée. La mémoire nécessaire à toutes les cases est allouée une fois pour toutes. • On a un accès direct en consultation et en modification à chaque case du tableau (repérée par son indice). 14 Tableaux en Java (rappels) • Il existe un type tableau prédéfini en Java. • Contrairement aux types primitifs (boolean, int, float, etc.), une variable de type tableau : – Contient une référence vers un espace mémoire composé d'emplacements mémoire consécutifs. – Plus précisément, si on déclare int[] t, alors t contient l'adresse de la 1ère case du tableau. 15 Tableaux en Java (rappels), suite • Exemple : int[] T; /*déclare le tableau d’entiers T et l’initialise à null*/ int[] Tbis={1,2,3,4}; /*déclare le tableau Tbis et l’initialise à une référence vers un espace mémoire contenant les 4 entiers 1, 2, 3, 4*/ int|] Tter=new int[4]; /*déclare le tableau d'entiers Tter et l'initialise à une référence vers un espace mémoire contenant 4 entiers*/ • On peut faire toutes sortes d’opérations, dès lors que le tableau n’est pas vide (!=null) : – Tbis[0]=Tbis[0]+10; – for(int i=0; i<Tbis.length; i++) System.out.println(Tbis[i]); 16 Tableaux d'objets en Java • Attention : – L'instruction new permet d'allouer de la place pour un certain nombre de références vers des objets, mais... – De l'espace mémoire doit ensuite être alloué pour chaque objet référencé par le tableau ! • Par exemple : – String[] Tab=new String[3]; – if(Tab[0].equals("toto")) {return true;} – Ce bout de code produit une exception, car Tab[0] vaut null ! – Il manque : Tab[0]=new String(); 17 Tableaux et références • On déclare int[] tab=new int[6]; – Puis, on écrit : (…) public static void f3(int[] t) {for(int i=0;i<6;i++) t[i]=i+1;} (…) (…) f3(tab); (…) /*que vaut tab ?*/ – On écrit ensuite : (…) public static void f4(int[] t) {t=new int[1]; t[0]=9;} (…) (…) f4(tab); (…) /*que vaut tab ?*/ 18 COURS ALGORITHMES ET COMPLEXITÉ • • • • • PLAN Définition d'un algorithme Un exemple Présentation des algorithmes Évaluation d'un algorithme Complexité 19 DÉFINITION D'UN ALGORITHME • Procédure de calcul bien définie qui prend en entrée un ensemble de valeurs et produit en sortie un ensemble de valeurs. (Cormen, Leiserson, Rivert) • Ensemble d'opérations de calcul élémentaires, organisé selon des règles précises dans le but de résoudre un problème donné. Pour chaque donnée du problème, il retourne une réponse après un nombre fini d'opérations. (Beauquier, Berstel, Chrétienne) 20 • Phase 1 ENONCÉ DU PROBLÈME Spécification • Phase 2 ÉLABORATION DE L'ALGORITHME • Phase 3 VÉRIFICATION (PREUVE) • Phase 4 MESURE DE L'EFFICACITÉ Complexité • Phase 5 MISE EN OEUVRE Programmation 21 UN EXEMPLE Le tri par sélection ordinaire ÉNONCÉ DU PROBLÈME • Entrée: Un tableau d’entiers • Sortie: Le même tableau, trié par ordre croissant 22 L'ALGORITHME - Tri par sélection Principe : tant que il reste plus d'un élément non trié faire - chercher le plus petit parmi les non triés, - échanger le premier élément non trié avec le plus petit trouvé. fait 23 EXEMPLE (suite) 7 5 5 5 5 5 8 8 7 7 7 7 éléments triés éléments non triés 15 5 10 15 7 10 15 8 10 8 15 10 8 10 15 8 10 15 24 Tri par sélection – en détails tri-selection (tableau A de n entiers) début pour i = 0 à n-2 faire // les éléments de 0 à i-1 sont déjà triés // i : place où on doit mettre le suivant plus petit // recherche de la place p du plus petit des non triés p = i; pour j = i+1 à n-1 faire si A[j] < A[p] alors p = j; finsi fait échanger (A[i], A[p]); fait fin 25 Evaluation des algorithmes Complexité des algorithmes 26 ÉVALUATION D'UN ALGORITHME EFFICACITÉ • TEMPS D'EXÉCUTION • MÉMOIRE OCCUPÉE EXEMPLE DE PROBLEME A RESOUDRE n entier est-il premier ? n = 1148 ? non, divisible par 2 n = 1147 ? ??? (en fait, 1147=31*37) 27 EVALUER LE NOMBRE D'OPÉRATIONS NECESSAIRES POUR LA RESOLUTION DU PROBLEME EN FONCTION DE LA TAILLE DES DONNÉES ? – OPERATIONS « ELEMENTAIRES » : +, -, *, /, >, <, =, ET, OU – TAILLE DES DONNEES = ESPACE OCCUPE ==> PLUSIEURS « MESURES » POSSIBLES ! – – – MEILLEUR CAS (PEU PERTINENT), PIRE DES CAS, EN MOYENNE. 28 EXEMPLE • PARTITIONNEMENT DE N ENTIERS – Données : N entiers positifs – Donc taille des données = nombre d'entiers = N – Problème : existe-t-il un sous-ensemble de ces entiers dont la somme est exactement la moitié de la somme totale des N entiers ? > Par exemple : 64, 36, 7, 10, 2, 15, 23, 74, 49, 30 – Algorithme pour résoudre le problème ? • Enumérer les 2N sous-ensembles possibles, • Evaluer la somme des entiers dans chacun d'entre eux, et conserver celui (ou ceux) qui convien(nen)t, s'il(s) existe(nt). 29 Temps de calcul si 10-9 secondes par opération (1 GHz) N 10 Somme de N nombres 0,01 µs 0,02 µs 0,05 µs Enumération 1 µs de 2N objets 20 1 ms 50 100 200 0,1 µs 0,2 µs >10 jours >30 000 >3.1043 milliards années d'années 30 EXEMPLE (ajout, sous condition, d’éléments du tableau B à des éléments du tableau A ; on suppose que les 2 tableaux ont au moins m+n éléments) AjoutTab (….) début pour i = 0 à m-1 faire A[i] := B[i] ; pour j = i à n+i-1 m n fois si A[i] < B[j] 1 faire alors 1 A[i] := A[i]+B[j] ; 2 fois finsi ; fait; fait; fin 31 nombre d'opérations Pire des cas, test A[i]<B[j] toujours vrai m(1+ 3n)= 3nm+m opérations Meilleur cas, test A[i]<B[j] toujours faux m(1+ n)= nm+m opérations 32 EXEMPLE (suite) tri-selection début pour i = 0 à n-2 faire p = i; 1 pour j = i+1 à n-1 faire si A[j] < A[p] alors 1 p = j; 1 Pour chaque i, n-i-1 fois 1+2(n-i-1)+3 opérations finsi fait échanger(A[i], A[p]); fait fin 3 33 nombre d'opérations (pire des cas) n −2 n −1 i =0 i =1 4( n − 1) + 2 ∑ ( n − i − 1) = 4( n − 1) + 2 ∑ i = 4 (n-1)+ n(n-1) = n2 +3n - 4 Rappel : somme des n-1 premiers entiers = n(n-1)/2 34 Algorithme "efficace" = Algorithme polynomial EXEMPLE (suite) : n2 + 3n - 4 Problèmes "intraitables" → Théorie de la complexité EXEMPLE : partitionnement d'entiers 35 COMPLEXITE DES ALGORITHMES D(n) = {données de taille n} coûtA(d) = coût (= temps de calcul) de l'algorithme A sur la donnée d • « PIRE DES CAS » MaxA(n) = Maxd∈D(n) {coûtA(d)} - borne supérieure du coût - assez facile à calculer (en général) - « souvent » réaliste 36 • COMPLEXITE EN MOYENNE p(d) = probabilité de la donnée d MoyA (n) = ∑ p(d)*coût (d) A d∈Dn - Nécessite de connaître (ou de pouvoir estimer) la distribution des données - Souvent difficile à calculer - Intéressant si le comportement « usuel » de l'algorithme est éloigné du pire cas 37 EXEMPLE : produit de 2 matrices static float[][] produitMatrices (float[][] A,float[][] B){ //Données A[m,n] et B[n,p] ; Résultat C[m,p] float[][] C = new float[A.length][B[0].length]; for(int i=0;i< A.length;i++){ for(int j=0;j< B[0].length;j++){ float s=0; p n for(int k=0;k< B.length;k++) s=s+A[i][k]*B[k][j]; fois fois C[i][j]=s; } } return C; } pire cas = cas moyen=1+mp(3n+2) opérations 38 m fois NOTATIONS DE LANDAU f , g fonctions ℕ→ ℕ • Notation O f = O(g) lire : “ f est en O de g ” ⇔ Il existe n0 ∈ℕ et c constante t.q. f(n) ≤ c.g(n) pour tout n ≥ n0 (En pratique : f=O(g) il existe c’ t.q. f(n) ≤ c’.g(n)) 39 EXEMPLE Algorithme avec f(n) = 4n3+2n+1 opérations - f(n)=O(?) pour tout n ≥ 1: f(n) ≤ 7n3 n0=1 c=7 g(n)=n3 donc f(n) = O(n3) 40 Notation O → majorant du nombre d'opérations d'un algorithme cg(n ) f(n) n0 f = O(g(n)) 41 Complexité → • comportement des algorithmes quand la taille des données augmente • comparaison entre algorithmes Algorithme polynomial f(n) = a0np + a1np-1 +...+ ap-1n + ap f = O(np) EXEMPLE (fin) f(n) = n2 + 3n - 4 Le tri par sélection est un algorithme polynomial en O(n2) 42 “Bonne complexité” O(log n) ou O(n) ou O(n log n) n3 n2 nlogn n logn Allure de quelques courbes n = 106 log2n 20µs 1 µs par opération n nlog2n n2 n3 1s 20 s 12 j 32 Ka 43 De l’effet des progrès de la technologie sur la résolution des problèmes nombre d' opérations taille max des problèmes traitables en 1 heure avec un ordinateur : actuel 100 fois plus rapide 1000 fois plus rapide (le plus rapide) N N1 100 N1 1000 N1 n2 N2 (soit N22 opérations) N3 (soit N35 opérations) N4 (soit 2N4 opérations) 10 N2 31,6 N2 2,5 N3 3,98 N3 N4 + 6,64 N4 + 9,97 N5 + 4,19 N5 + 6,29 n5 2n 3n N5 (soit 3N5 opérations) Même si la puissance des ordinateurs augmente, certains “gros” problèmes ne pourront sans doute jamais être résolus 44 Une illustration de l’amélioration de complexité : recherche séquentielle vs recherche dichotomique d’un élément dans un tableau trié 45 Le problème Etant donnés : - Un tableau T de n entiers triés par ordre croissant - Un entier x Ecrire un algorithme qui teste si x appartient à T : recherche de x dans T 46 Recherche séquentielle Idée de l’algorithme • On parcourt le tableau T, et on s’arrête : – Soit parce qu’on trouve x, – Soit parce qu’on trouve un élément > x. Complexité au pire cas ? Au pire, on parcourt les n cases du tableau en faisant pour chaque case un nombre constant de comparaisons, donc complexité au pire cas en O(n) 47 Recherche séquentielle (en Java) static boolean rechercheLineaire (int x, int[] T) { int i; for (i=0 ; i < T.length ; i++) { if (T[i]>=x) { System.out.print("Nb d'iterations="+(i+1)); return(T[i]==x); } } System.out.println("Nombre d'iterations="+i); return(false); } 48 Recherche dichotomique Idée de l’algorithme : - Eviter la recherche séquentielle en utilisant la structure triée du tableau - Complexité au pire cas en O(log n) ==> VOIR TD ! 49 Récursivité • On dit qu'une fonction est récursive lorsqu'elle s'appelle elle-même (en général, avec un jeu de paramètres différent) • La récursivité est un concept puissant, qui permet de traduire naturellement (et parfois plus simplement que par une syntaxe itérative) les processus inductifs (ou récurrents). Ex. : public static int factorielle(int n) { if(n>1) return(n*factorielle(n-1)); else return 1; } 50 Récursivité (suite) • Faire des appels récursifs en cascade peut amener à en générer un très grand nombre – Ainsi, calculer le déterminant d'une matrice de cette façon-là génère un nombre exponentiel d'appels • La complexité des algorithmes récursifs peut être difficile à évaluer (compter le nombre d'appels générés pouvant être problématique) • A chaque appel, on recopie les paramètres dans des variables locales : un algorithme récursif consomme donc parfois beaucoup plus de mémoire que son équivalent itératif 51 REMARQUES FINALES • Comptage « grossier » des opérations (+, -, /, *, tests, etc.) ; faire attention aux boucles ! • Ce qui est à l'extérieur des boucles est souvent « négligeable ». • On écrit plutôt O(log(n)) que O(log2(n)). • Hiérarchie des complexités (pour n > 1) : log(n) < n < n log(n) < n2 < n3 << 2n 52