1/28 Qu’est-ce qu’un programme qui calcule bien ? Un problème Énoncé Soit un ensemble de pots de poids inconnus et une balance. Comment retourner les k pots les plus lourds grâce à la balance ? 2/28 Spécification de ce problème Précondition Entrée : Soient un tableau d’entiers t et un entier k. Précondition : k est plus petit que la longueur de t. Postcondition Sortie : un tableau d’entiers t 0 . Postcondition : t 0 contient les k éléments de t les plus grands. 3/28 Spécification Précondition Une précondition est une propriété attendue sur les entrées du programme. Si la précondition n’est pas vérifiée alors le comportement du programme n’est pas spécifié. Postcondition Une postcondition est une propriété promise sur les sorties du programme. Si la postcondition n’est pas vérifiée alors cela signifie que le programme contient une erreur. 4/28 Détecter une erreur 5/28 Comment détecter si un programme contient une erreur ? Erreurs de programmation Correction d’un programme Un programme est correct si pour toute entrée vérifiant sa précondition alors il produit une sortie vérifiant sa postcondition. Preuve de correction et d’incorrection I On peut prouver qu’un programme est incorrect en trouvant une entrée qui vérifie la précondition mais pour laquelle le programme produit une sortie qui ne vérifie pas la postcondition ou bien ne produit pas de sortie du tout. I On peut prouver qu’un programme est correct en faisant une démonstration mathématique. Valeur des tests Un test n’est donc pas une preuve de la correction d’un programme. 6/28 Exemple de preuve de programme (hors programme) 7/28 /* Precondition : le tableau a est non vide. */ /* Postcondition : la fonction termine et renvoie un entier m tel que : */ /* Pour tout i dans les bornes de a, a[i] >= m et il existe j, a[j] = m. */ public static int min (int[] a) { int m = a[0] ; for (int i = 1 ; i < getArrayLength (a) ; i++) { if (a[i] < m) { m = a[i] ; } } return m ; } 1. Pourquoi le programme termine-t-il ? 2. Pourquoi l’entier m vérifie-t-il la postcondition ? Terminaison du programme Arguments pour la terminaison I Une boucle “for” (de la forme « i allant de 1 à N ») termine toujours. I Une séquence d’instructions qui terminent se termine aussi. I On suppose que l’appel à getArrayLength termine. I Le programme termine donc. et en remplaçant la boucle “for” par la boucle “while” suivante ? int m = a[0] ; int i = 1 ; while (i < getArrayLength (a)) { if (a[i] < m) { m = a[i] ; } i++ ; } 8/28 La terminaison d’une boucle “while” 9/28 int m = a[0] ; int i = 1 ; while (i < getArrayLength (a)) { if (a[i] < m) { m = a[i] ; } i++ ; } La quantité “getArrayLength (a) - i” décroît strictement à chaque appel. Elle finit donc par atteindre 0. Or, quand elle est nulle, la condition est fausse et donc la boucle s’arrête. L’existence de cette quantité permet de prouver que la boucle termine. Une telle quantité est appelée un variant de la boucle. Prouver la terminaison grâce à des variants de boucle Variant de boucle Un variant d’une boucle est un entier naturel qui décroit strictement à chaque itération de la boucle. 10/28 Correction du programme 11/28 int m = a[0] ; int i = 1 ; while (i < getArrayLength (a)) { if (a[i] < m) { m = a[i] ; } i++ ; } /* Pour tout k entre 0 et getArrayLength (a) - 1, m <= a[k] */ À la fin de la boucle, la postcondition doit être vraie. Correction du programme 12/28 int m = a[0] ; int i = 1 ; while (i < getArrayLength (a)) { if (a[i] < m) { m = a[i] ; } i++ ; /* ? */ } /* Pour tout k entre 0 et getArrayLength (a) - 1, m <= a[k] */ Quelle propriété doit-on avoir à la fin de la dernière itération ? Correction du programme 13/28 int m = a[0] ; int i = 1 ; while (i < getArrayLength (a)) { if (a[i] < m) { m = a[i] ; } i++ ; /* Pour tout k entre 0 et i - 1, m <= a[k] */ } Il suffit de montrer que la propriété est vraie pour toutes les itérations ! Correction du programme 14/28 int m = a[0] ; int i = 1 ; /* Pour tout k entre 0 et i - 1, m <= a[k] */ while (i < getArrayLength (a)) { if (a[i] < m) { m = a[i] ; }; i++ ; } La propriété est vraie la première fois que l’on rentre dans la boucle. Correction du programme 15/28 int m = a[0] ; int i = 1 ; while (i < getArrayLength (a)) { /* Pour tout k entre 0 et i - 1, m <= a[k] */ if (a[i] < m) { /* Pour tout k entre 0 et i - 1, m <= a[k] et a[i] < m */ m = a[i] ; /* Pour tout k entre 0 et i, m = a[i] <= a[k] */ }; /* Pour tout k entre 0 et i, m <= a[k] */ i++ ; /* Pour tout k entre 0 et i - 1, m <= a[k] */ } En supposant qu’au début de chaque itération, la propriété est vraie, on peut montrer qu’elle est vraie à la fin de la boucle. Correction du programme 16/28 int m = a[0] ; int i = 1 ; while (i < getArrayLength (a)) { /* Pour tout k entre 0 et i - 1, m <= a[k] */ if (a[i] < m) { /* Pour tout k entre 0 et i - 1, m <= a[k] et a[i] < m */ m = a[i] ; /* Pour tout k entre 0 et i, m = a[i] <= a[k] */ }; /* Pour tout k entre 0 et i, m <= a[k] */ i++ ; /* Pour tout k entre 0 et i - 1, m <= a[k] */ } Par récurrence sur le nombre d’itérations de la boucle, on en conclut que la propriété est vraie pour toute itération ! Exemple : Le problème de l’urne 17/28 On place des boules noires et blanches dans une urne. On applique ensuite le processus suivant tant que possible : on tire deux boules au hasard et si elles sont de la même couleur alors on les jette toutes les deux et on rajoute une boule noire dans l’urne. Sinon, si elles sont de couleurs différentes, on remet la blanche dans l’urne et on jette la noire. 1. Est-ce que ce processus termine ? 2. À la fin du processus, combien reste-t-il de boules dans l’urne ? 3. À la fin du processus, de quelle(s) couleur(s) sont les boules dans l’urne ? 18/28 Comment savoir si un programme est meilleur qu’un autre ? Coût d’un programme Définition On peut comparer deux programmes en comparant le temps qu’ils mettent à résoudre le même problème. On pourrait utiliser l’unité de temps de la seconde et chronométrant les programmes mais on obtiendrait alors une comparaison peu reproductible. Une technique plus intéressante consiste à compter le nombre de fois qu’une opération élémentaire est utilisée par les deux programmes. Le coût d’un programme est alors la fonction C(I) qui associe le nombre d’opérations élémentaires nécessaires au traitement d’une donnée d’entrée I. Exemple Dans le cas du problème de la recherche des k poids les plus importants parmi n poids, nous pouvons compter le nombre de comparaisons effectuées par chaque programme. Dans cet exemple, la fonction de coût est donc une fonction de k et de n. 19/28 Algorithme Algorithme Un algorithme est une méthode de calcul visant à la résolution d’un problème. On peut décrire un algorithme dans un pseudo-langage de programmation. Cela permet d’ignorer certains détails d’implémentation pour simplifier la preuve de la terminaison, de la correction ou de l’étude de la fonction de coût de l’algorithme. 20/28 Exemple d’algorithme en pseudo-code 21/28 Entrée : t un tableau d’entiers Pour i allant de 1 à k : Pour j allant de 1 à n − i + 1 : Échanger les cases i et j de t si t[i] ≥ t[j] Complexité algorithmique Classes de comparaison des algorithmes Les performances de deux algorithmes peuvent différer à une constante près (ce qui n’est pas grand chose), ou de façon plus significative. On peut utiliser les notations de Landau pour classer les fonctions de coût en faisant abstraction des constantes. Exemples 22/28 I Le coût de la recherche du minimum dans un tableau est Θ(n). I Le coût de kbestBubble est k · n − I Le coût de kbestBubble est en Θ(n) pour k fixé. I Le coût de kbestTournament est n − 1 + k · log2 (n). I Le coût de l’initialisation de kbestTournament est en Θ(n) pour k fixé. I Le coût du calcul de chaque élément de kbest est en Θ(log(n)) pour k fixé. k·(k+1) . 2 Un nouveau problème : le bon parenthésage 23/28 { ... ( ... [ ] ... ) ... } Soit un programme source formé par une séquence de lettres a0 . . . aN . Comment déterminer si les accolades ouvrantes, les parenthèses ouvrantes et les crochets ouvrants sont correctement fermés ? Une pile Opérations On souhaite qu’une variable se comporte comme une pile, c’est-à-dire : 1. Qu’à sa création, la pile soit vide. 2. Que l’on puisse pousser un élément au sommet de la pile. 3. Que l’on puisse retirer le dernier élément poussé au sommet de la pile. 4. Que l’on puisse tester si une pile est vide. stack = createEmptyStack () ; push (stack, 42) ; push (stack, 21) ; b1 = isEmpty (stack) ; // Ici, b1 vaut “false” x = pop (stack) ; // Ici, x vaut 21. y = pop (stack) ; // Ici, y vaut 42. b2 = isEmpty (stack) ; // Ici, b2 vaut “true” 24/28 Retour sur le bon parenthésage 25/28 Comment vérifier le bon parenthésage à l’aide d’une pile ? Organiser un tableau sous la forme d’une pile 26/28 Comment utiliser un tableau pour y représenter une pile ? Une structure de données Structure de données On appelle structure de données une discipline d’organisation des données en mémoire qui permet de réaliser des opérations efficacement à partir de ces données ou sur ces données. La discipline d’organisation de la mémoire est souvent représentée par une propriété qui est toujours vraie sur les données entre deux opérations successives. Cette propriété s’appelle un invariant. Exemple Dans la pile représentée par un tableau, l’invariant de la pile est le fait que la première case du tableau représente le sommet de la pile et que les cases suivantes représentent les éléments de la pile lue du fond de la pile vers son sommet. 27/28 Chercher dans un dictionnaire 28/28 Qu’est-ce qu’une structure de donnée de dictionnaire et comment représenter un dictionnaire en mémoire ?