Spécification 4 : arc courant Techniques de...

26
- 25 - Spécification 4 : pas d'élément courant, mais un arc courant public class List { List() boolean isEmpty(); int size(); } public class ListItr { ListItr(List l); void insertAfter (int e); int consultAfter(); void removeAfter (); void goToNext (); void goToPrev (); void goToFirst(); void goToLast (); boolean isFirst (); boolean isLast (); } • Avec bien sûr des PRE-conditions (p. ex. gotoNext() est interdit si isLast()) • Critiques : - élégant, mais pas très répandu - on pourrait ajouter void updateAfter(int e) - on peut tout implémenter en O(1) • Avoir plusieurs itérateurs sur une même liste, c'est très bien. Mais on interdit que l'un de ces itérateurs fasse une modification de la liste !! current List ListItr - 26 - Type abstrait liste - Exemple d'utilisation • Le problème Am-Stram-Gram (Josephus problem) • Soient n personnes alignées. A chaque étape, on élimine la k ième personne (passé le dernier, on continue à compter depuis le premier). • Question : Quelle sera la dernière personne qui restera ? • Pas de "formule" simple pour une telle fonction mathématique... • Algorithme : simulation du jeu • Pseudo-code : proc AmStram (données: int n, int k; résultat: int winner) { construire une liste, remplir avec les n entiers; se placer au début de la liste; tant qu'il reste plusieurs éléments dans la liste { faire k fois { avancer au suivant de la liste; si on est au dernier, alors se placer au premier; } supprimer l'élément courant (et passer au suivant); } retourner l'élément courant; } - 27 - Techniques de chaînage • Idée : définir un objet, dont l'un des attributs... désigne un autre objet similaire ! class ListNode { int elt; ListNode next; ListNode prev; } • Rappel : ListNode représente bien un type référence. Ici, on exploite pleinement cette idée de lien vers quelque chose ! ListNode first, a, b, c; a = new ListNode(); b = new ListNode(); c = new ListNode(); a.elt = 4; a.prev = null; a.next = b; b.elt = 2; b.prev = a; b.next = c; c.elt = 7; c.prev = b; c.next = null; first = a; a=null; b=null; c=null; prev elt next prev elt next first prev elt next ListNode(s) 4 2 7 - 28 - Type abstrait liste - Implémentation class ListNode { // classe non accessible par l'utilisateur int elt; // attributs accessibles par les ListNode next; // classes du même paquetage ListNode prev; // (surtout List) // -------------------- ListNode(int theElt, ListNode thePrev, ListNode theNext) { elt = theElt; next = theNext; prev = thePrev; } } public class List { ListNode first, last; int size ; // -------------------- public List() { first = null; last = null; size = 0; } public boolean isEmpty() { return size == 0;} public int size() { return size; } } prev elt next prev elt next first size last ListNode ListNode List ...

Transcript of Spécification 4 : arc courant Techniques de...

Page 1: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 25 -

Spécification 4 : pas d'élément courant, mais un arc courant

public class List { List() boolean isEmpty(); int size(); }

public class ListItr { ListItr(List l); void insertAfter (int e); int consultAfter(); void removeAfter (); void goToNext (); void goToPrev (); void goToFirst(); void goToLast (); boolean isFirst (); boolean isLast (); }

• Avec bien sûr des PRE-conditions (p. ex. gotoNext() est interdit si isLast()) • Critiques :

- élégant, mais pas très répandu - on pourrait ajouter void updateAfter(int e) - on peut tout implémenter en O(1)

• Avoir plusieurs itérateurs sur une même liste, c'est très bien.

Mais on interdit que l'un de ces itérateurs fasse une modification de la liste !!

current

List

ListItr

- 26 -

Type abstrait liste - Exemple d'utilisation • Le problème Am-Stram-Gram (Josephus problem) • Soient n personnes alignées. A chaque étape, on élimine la kième personne

(passé le dernier, on continue à compter depuis le premier). • Question : Quelle sera la dernière personne qui restera ? • Pas de "formule" simple pour une telle fonction mathématique... • Algorithme : simulation du jeu • Pseudo-code :

proc AmStram (données: int n, int k; résultat: int winner) { construire une liste, remplir avec les n entiers; se placer au début de la liste; tant qu'il reste plusieurs éléments dans la liste { faire k fois { avancer au suivant de la liste; si on est au dernier, alors se placer au premier; } supprimer l'élément courant (et passer au suivant); } retourner l'élément courant; }

- 27 -

Techniques de chaînage • Idée : définir un objet, dont l'un des attributs... désigne un autre objet similaire ! class ListNode { int elt; ListNode next; ListNode prev; }

• Rappel : ListNode représente bien un type référence. Ici, on exploite

pleinement cette idée de lien vers quelque chose ! ListNode first, a, b, c; a = new ListNode(); b = new ListNode(); c = new ListNode(); a.elt = 4; a.prev = null; a.next = b; b.elt = 2; b.prev = a; b.next = c; c.elt = 7; c.prev = b; c.next = null; first = a; a=null; b=null; c=null;

prev elt

next prev elt

next first

prev elt

next

ListNode(s)

4 2 7

- 28 -

Type abstrait liste - Implémentation class ListNode { // classe non accessible par l'utilisateur int elt; // attributs accessibles par les ListNode next; // classes du même paquetage ListNode prev; // (surtout List) // -------------------- ListNode(int theElt, ListNode thePrev, ListNode theNext) { elt = theElt; next = theNext; prev = thePrev; } } public class List { ListNode first, last; int size ; // -------------------- public List() { first = null; last = null; size = 0; } public boolean isEmpty() { return size == 0;} public int size() { return size; } }

prev elt

next prev elt

next

first size

last

ListNode ListNode

List ...

Page 2: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 29 -

Classe pour l'itérateur Algorithme pour insertAfter(int e) : 4 situations possibles 3 5 7 3 5 e 7

3 5 7 e 3 5 7

3 5 7 3 5 7 e

e

// aux = new node // succ.prev = aux // succ = aux // ...

// aux = new node // l.first = aux // l.last = aux // ...

List size

last first

ListItr l succ pred

ListNodes

elt next prev

elt next prev

elt next prev

elt next prev

- 30 -

Codage Java • Attention à mettre à jour correctement chaque donnée :

- chaînage entre noeuds (next, prev) - attributs de la liste (size, first, last) - attributs de l'itérateur (pred, succ)

• Attention aux cas spéciaux :

- ajouter à une liste vide - supprimer le dernier élément - supprimer l'unique élément - ...

• C'est très important de se représenter graphiquement la situation !

Il faut pouvoir se convaincre que c'est correct • Variante : utiliser un chaînage simple plutôt qu'un chaînage double :

- économie de place mémoire - mais parcours (efficace) possible dans un seul sens

- 31 -

Algorithmes de tri Définitions et "règles du jeu" • Tri interne : toutes les données sont en mémoire vive, dans un tableau • Tri externe : la majeure partie des données doit rester sur fichier (pas en RAM) • Eléments à trier : supportent une relation d'ordre (comparaison entre 2)

(d'autres algo supposent une représentation binaire des données) • Opérations de base : comparer 2 éléments

échanger les contenus de 2 cases • On dit qu'un algo de tri est stable s'il préserve l'ordre des éléments égaux :

{5, 2a, 2b} → {2b, 2a, 5} instable ! Algo 0 : force brute • générer toutes les permutations du tableau t • vérifier que t[i] <= t[i+1] et quitter si c'est le cas • complexité (pire qu') exponentielle ! Evidemment jamais utilisé

- 32 -

Algo 1 : tri par insertion • Idée de l'algo :

- l'intervalle [0..i-1] est déjà trié - insérer correctement l'élément t[i] dans cet intervalle (décalage à droite)

public static void insertionSort(int[] a){ int i, j, v; for (i=1; i<a.length; i++) { v = a[i]; // v is the element to insert j = i; while (j>0 && a[j-1] > v) { a[j] = a[j-1]; // move to the right j--; } a[j] = v; // insert the element } }

Algo 2 : tri par sélection • Idée de l'algo :

- l'intervalle [0..i-1] est déjà trié, et même à sa place définitive - trouver le minimum parmi le reste, et l'échanger avec l'élément t[i]

Page 3: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 33 -

Illustration du tri par insertion

A S O R T I N G E X A M P L E A S O R T I N G E X A M P L E A O S R T I N G E X A M P L E A O R S T I N G E X A M P L E A O R S T I N G E X A M P L E A I O R S T N G E X A M P L E A I N O R S T G E X A M P L E A G I N O R S T E X A M P L E A E G I N O R S T X A M P L E A E G I N O R S T X A M P L E A A E G I N O R S T X M P L E A A E G I M N O R S T X P L E A A E G I M N O P R S T X L E A A E G I L M N O P R S T X E A A E E G I L M N O P R S T X

- 34 -

Illustration du tri par sélection

- 35 -

Algo 3 : tri par bulles (Bubble sort) • Idée de l'algo :

- en 1 passage, échanger si nécessaire l'élément t[i] avec t[i+1] - recommencer tant qu'il peut encore y avoir un échange

• En fait, après un passage, le maximum est à sa place ! public static void bubbleSort(int[] a) { int i, j, t; for (i=a.length-1; i>=1; i--) for (j=1; j<=i; j++) if (a[j-1] > a[j]) { t = a[j-1]; a[j-1]=a[j]; a[j] = t; // swap j-1 and j } } } // Variante : return dès qu'il n'y a plus d'échanges

Complexité des 3 algorithmes simples classiques • les 3 algorithmes sont en O(n2) au pire des cas, et aussi dans le cas moyen • aucun des 3 ne nécessite de mémoire supplémentaire (O(1)) • cas moyen :

Nbre de comparaisons Nbre d'échanges Tri par sélection n2/2 n Tri par insertion n2/4 n2/8

Tri par bulles n2/2 n2/2 • Cas des tableaux déjà triés : tri par insertion en O(n)

- 36 -

Algo 4 : tri selon Shell (Shell sort) • Idée de l'algo :

- un tableau est K-Trié si t[i] <= t[i+k] pour tout i - rendre le tableau K-Trié, pour des valeurs (décroissantes) successives de

K, en terminant par K=1 9-4-5-3-6-2-7-1-8 3-1-2-7-4-5-9-6-8 2-1-3-5-4-6-8-7-9 1-2-3-4-5-6-7-8-9 • Peu importe les valeurs de K, pourvu qu'on finisse par K=1 k0,k1,k2,…,klast=1

• Quand K=1, c'est exactement le mécanisme du tri par insertion • On prend parfois une suite des K selon la formule suivante :

3-Trié

1-Trié 2-Trié

1, 4, 13, 40, 121, 364... jusqu'à n klast = 1 ki-1 = 1+3ki

Page 4: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 37 -

• Pseudo-code principal du tri Shell :

proc shellSort(données-résultat int[] a) { pour plusieurs valeurs décroissantes de k, jusqu'à k=1 rendre le tableau k-trié }

• L'idée de l'algorithme pour rendre le tableau k-trié est celle du tri par insertion :

- l'intervalle [0..i-1] est déjà k-trié - "k-insérer" correctement l'élément t[i] dans cet intervalle

pour chaque case i de k à n-1 k-insérer t[i] (cf. insertionSort())

• Analyse de performance pour ShellSort : très difficile !!!

- dépend de la progression des valeurs K - pas d'analyse théorique dans le cas général ! - on sait garantir une complexité O(n 1.5) (on pense que c'est même légèrement mieux...)

• Intuitivement, on gagne en faisant faire des "sauts" importants (pour les

éléments très mal placés) dans les premières étapes !

0

i déjà k-trié

- 38 -

Illustration du tri Shell

13-trié

4-trié

1-trié ...

- 39 -

Récursivité • Méthode récursive = qui contient un appel à elle-même !

int f() { return 2 * f();} // très mauvais exemple ! • Fondements théoriques : induction mathématique

Prouver qu'une proposition A est vraie pour tout i, c'est prouver que : - A est vraie pour i=0; - si A est vraie pour i, elle est aussi vraie pour i+1

Exemple 1 : Nombres triangulaires • Approche itérative : s(n) = 1 + 2 +... + n

public int s(int n) { int sum = 0; for (int i=1; i<=n; i++) sum += i; return sum; }

• Approche récursive : s(n) = n + s(n-1); s(1) = 1

public int s(int n) { if (n==1) return 1; return n + s(n-1); }

- 40 -

"[...] C'est l'histoire d'un petit garçon qui ne voulait pas dormir et dont la mère lui raconte l'histoire de la petite grenouille qui ne voulait pas dormir et dont la mère lui raconte l'histoire de l'ourson qui ne voulait pas dormir et dont la mère lui raconte l'histoire du bébé écureuil qui s'est endormi, et alors l'ourson s'endormit, et la petite grenouille s'endormit, et le petit garçon s'endormit."

Page 5: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 41 -

Exemple 2 : Palindromes • Un palindrome est un mot "qui peut se lire à l'envers" • Exemple : SUGUS, RADAR, ICI • Idée d'algorithme récursif :

- un mot de 0 lettre est un palindrome - un mot de 1 lettre est un palindrome - un mot de n lettres est un palindrome, s'il commence et se termine par la

même lettre, et que le mot restant (n-2 lettres) est aussi un palindrome Codage en Java :

public static boolean isPalindrom(String s) { if (s.length() < 2) return true; if (s.charAt(0) != s.charAt(s.length()-1)) return false; return (isPalindrom(s.substring(1, s.length()-1))); }

Remarque : tout programme récursif peut être réécrit sans récursivité, à l'aide

de boucles et (parfois) du type abstrait Pile. Exemple 3 : Calcul d'une puissance • Plusieurs relations récursives utiles : an = a * a(n-1) a2n = an * an

- 42 -

Exemple 4 : Réglette • Graduer une réglette de façon "binaire" • Idée d'algorithme récursif :

- graduer la portion a..b à 0 niveau : rien à faire - graduer la portion a..b à n niveau : tracer un trait de hauteur n en (a+b)/2,

et graduer à n-1 niveaux les deux moitiés public void drawLine(int x, int height); ... public void drawRuler(int left, int right, int level) { if (level < 1) return; int mid = (left + right) / 2; drawLine(mid, level); drawRuler(left, mid, level-1); drawRuler(mid, right, level-1); }

Les règles d'or de la récursivité 1. Définir au moins un cas de base 2. Toujours progresser vers un cas de base 3. Supposer que l'appel récursif fonctionne • La récursivité se passe généralement de variables globales....

a b

- 43 -

Exemple 5 : Tours de Hanoï • Soit 3 piliers, et n disques. Problème :

déplacer la pile du pilier 1 au pilier 3, mais : - utiliser les piliers 1, 2, ou 3 - déplacer un seul disque à la fois - ne jamais placer un disque au-dessus d'un disque plus petit

• Idée d'algorithme récursif : - déplacer une pile de 1 disque, c'est trivial - pour déplacer une pile de n disques du pilier 1 vers le pilier 3 : déplacer une pile de (n-1) disques du pilier 1 vers le pilier 2, déplacer le disque restant vers le pilier 3 déplacer une pile de (n-1) disques du pilier 2 vers le pilier 3

• Codage en Java (qui affiche les déplacements successifs) public void doTowers(int n, int from, int to, int aux) { if (n == 1) { System.out.println("FROM" + from + " TO " + to); return; } doTowers(n-1, from, aux, to); System.out.println("FROM" + from + " TO " + to); doTowers(n-1, aux, to, from); }

1 2 3

- 44 -

Récursivité avec ou sans accumulateur • On parle d'accumulateur lorsqu'on ajoute un paramètre supplémentaire à la

méthode récursive pour "accumuler" les résultats; quand on atteint le cas de base, l'accumulateur contient le résultat !

• Exemple : calcul d'une puissance xy. Version récursive normale static int power(int x, int y) { if (y==0) return 1; return x * power(x, y-1); }

• Exemple : calcul d'une puissance xy. Version récursive avec accumulateur

public static int power(int x, int y) { return power(x, y, 1); // méthode d'amorce } private static int power(int x, int y, int acc) { if (y==0) return acc; return power(x, y-1, x*acc); }

• Généralement, il est plus propre d'éviter la technique d'accumulateur

En tout cas, les méthodes ont une spécification moins claire : power(x,y) calcule xy power(x,y,a) calcule a(xy)

• La récursivité peut aussi être indirecte : f() appelle g() qui appelle f()

+* +*

+* +*

Page 6: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 45 -

Types abstraits ensemble et dictionnaire Ensemble • Modèle des données : uniquement la propriété d'appartenance à l'ensemble • Opérations essentielles :

- créer un ensemble vide - ajouter un élément (sans effet s'il y est déjà) - retirer un élément (sans effet s'il n'y est pas) - tester si l'ensemble est vide - tester l'appartenance d'un élément à l'ensemble - parcourir les éléments (p. ex. grâce à un itérateur)

• Opérations supplémentaires : union, intersection, différence, complément (?) • Type abstrait très important • La réalisation (efficace !) dépend des propriétés offertes par les éléments

- nombres entiers : ensemble Bitset - objets avec relation d'ordre : arbre de fouille - objets avec fonction de hachage : table de hachage - objets comme suite de symboles : arbre préfixe ("trie")

Multi-ensemble (multiset, bag) • On autorise plusieurs occurrences d'un élément

5 -3 6

1 7 F E

- 46 -

Une spécification possible du type ensemble :

public class SetOfShorts { public SetOfShorts (); public boolean isEmpty(); public int size (); public void add (short e); public void remove (short e); public boolean contains(short e); public void union (SetOfShorts s); public void intersection(SetOfShorts s); public SetOfShortsItr iterator() }

• Et une spécification possible d'un itérateur :

public interface SetOfShortsItr { public boolean hasMoreElements(); public short nextElement(); }

- 47 -

Une implémentation (recherche séquentielle) (inefficace !) • Idée : un tableau (non trié) d'éléments, un entier pour la taille • On parle de recherche séquentielle • Algo de recherche (test d'appartenance) :

- parcourir chaque case et comparer • Algo d'ajout :

- si l'élt est présent, rien à faire - incrémenter la taille - placer l'élément à la dernière case.

• Algo de retrait :

- si l'élt est absent, rien à faire - trouver la case de l'élt à retirer - écraser le contenu de cette case avec l'élt de la dernière case - décrémenter la taille

Type abstrait "Dictionnaire" (Table, Fonction, Map) • Map = ensemble de couples <key, value>, où key identifie le couple • Modèle des données : association de valeurs (=image) à des éléments (=clé) • La recherche d'une valeur connaissant la clé devrait être efficace • Type abstrait encore plus important ! (extrême : bases de données)

size

buffer

- 48 -

Une spécification possible d'un dictionnaire :

public class ShortToStringMap { public ShortToStringMap(); // put(k,i) : if k is absent, it is added, otherwise // it gets a new image value public void put (short key, String value); public String get (short key); public void remove(short key); public int size(); public boolean isEmpty(); public boolean containsKey(short key); public ShortToStringMapItr iterator() { }

• Et l'itérateur :

public interface ShortToStringMapItr { public boolean hasMoreKeys(); public short nextKey(); }

Une implémentation (inefficace) • Analogue à l'ensemble ci-dessus, mais avec 2 informations par cases : 1 tableau d'objets à 2 champs, ou bien 2 "tableaux parallèles"

key val

keys

values

Page 7: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 49 -

Algorithmes de tri récursifs Idée d'algorithme récursif QuickSort :

- trier un tableau vide, ou un tableau de 1 élément, c'est trivial; - pour trier un tableau non vide : choisir un élément pivot; partitionner les autres éléments en 2 tableaux P et G; trier P et trier G; concaténer le tableau P, l'élément pivot, et le tableau G

- 50 -

• Partitionner en fonction du pivot, c'est placer correctement un pivot, avec à sa gauche les éléments plus petits ou égaux, et à sa droite les autres.

• En fait, on peut travailler avec un unique tableau • Un algorithme possible pour partitionner en O(n) :

- échanger le pivot avec la dernière case - parcourir le tableau à partir des 2 bouts, et échanger les éléments mal

classés par rapport au pivot - échanger finalement le pivot // chooses a pivot, and partitions the subarray // returns the final position of the pivot static int partition(char[] t, int left, int right) { ... }

• Problème du choix du pivot :

- prendre le premier, le dernier, ou celui du milieu - au pire des cas : le minimum ou le maximum de l'intervalle - prendre le médian de tout l'intervalle - prendre le médian entre 3 éléments - tirer au sort la position

• Cet algorithme est très utilisé en pratique

- 51 -

Illustration de Partition (on prend le dernier élt comme pivot)

A S O R T I N G E X A M P L E A S A M P L E A A O E X S M P L E A A E R T I N G O X S M P L E A A E E T I N G O X S M P L R

Complexité • Au meilleur des cas, et en moyenne : O(n ln n) • Au pire des cas (peu probable) : O(n2)

n n/2 n/2

n/4 n/4 n/4 n/4 ...

n n-1 1

n-2 1 ...

- 52 -

Illustration de Quicksort (on prend ici le dernier élt comme pivot)

A A E E T I N G O X S M P L R A A E A A L I N G O P M R X T S L I G M O P N G I L I L N P O O P S T X T X A A E E G I L M N O P R S T X

Page 8: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 53 -

Codage en Java

static void quickSort(char[] t, int left, int right) { int p; if (left>=right) return; p = partition(t, left, right); quickSort(t, left, p-1); quickSort(t, p+1, right); } public static void quickSort(char[] t) { // méthode quickSort(t, 0, t.length-1); // d'amorce }

Principe Divide-and-Conquer • Ces algorithmes se composent de 2 parties :

- Divide : construire des sous-problèmes et les résoudre récursivement - Conquer : assembler les sous-résultats pour donner la solution

• Construire les sous-problèmes nécessite généralement un traitement • Fusionner les sous-résultats nécessite généralement un traitement • Le principe peut conduire à un algorithme efficace

- 54 -

Idée d'algorithme récursif Mergesort (variante 1) • trier un tableau vide, ou un tableau de 1 élément, c'est trivial; • pour trier un tableau non vide :

- le partager en 2 deux sous-tableaux A et B; - trier A et trier B; - fusionner A et B pour donner le résultat

• Fusionner deux listes triées peut se faire en 1 passage • Exemple : Complexité • Dans tous les cas O(n ln n) pour le temps de calcul • Nécessite O(n) cases mémoires supplémentaires (sinon le codage est ardu) • Algorithme peu utilisé pour le tri interne (trop de copies, trop de mémoire) • Il y a des variantes très utilisées pour le tri externe (voir plus bas)

2 5 7 8

3 4 6 9

2 3 4 5

min

4 9 6 3 7 5 8 2

4 9 6 3 7 5 8 2

3 4 6 9 2 5 7 8

2 3 4 5 6 7 8 9

mergeSort mergeSort

merge

... ...

2 3 4 5 6 7 8 9

copy tableau auxiliaire

- 55 -

Illustration du tri par fusion

A S O R T I N G E X A M P L E A S O R A O R S I T G N G I N T A G I N O R S T E X A M A E M X L P E L P A E E L M P X A A E E G I L M N O P R S T X

- 56 -

Codage en Java static char[] aux; // auxiliary memory used when merging public static void mergeSort1(char[] t) { aux = new char[t.length]; mergeSort1(t, 0, t.length-1); } static void mergeSort1(char[] t, int left,int right) { int mid; if (left>=right) return; mid = (left+right)/2; mergeSort1(t, left, mid); mergeSort1(t, mid+1, right); merge(t, left, mid, right);} // merges t[left..mid] with t[mid+1..right] back to t[] static void merge(char[] t, int left, int mid, int right) { ... // merge both intervals into aux[left..right] ... // then copy back aux[left..right] into t[left..right] }

Page 9: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 57 -

Tri par monotonies : Tri par fusion avec accès séquentiel • Le tri par fusion vu précédemment suppose qu'on traite un tableau puisqu'on

accède à des intervalles quelconques • Voici une variante non récursive qui tolère un accès séquentiel aux données

(liste, mais aussi adapté pour le tri externe, sur fichier). En O(n ln n) ! • Monotonie : sous-séquence triée de longueur maximale

Exemple : 3 monotonies dans cette séquence 3, 1, 4, 6, 2, 9 • Idées de base : on considère...

- la séquence d'éléments à trier...comme une succession de monotonies ! - 2 séquences à fusionner...comme une succession de couples de monotonies

• Algo en pseudo-code : proc monotonyMergeSort(données-résultat fileOrList A) { tant que la liste A n'est pas triée répartir les monotonies alternativement sur 2 nouvelles listes X et Y // puis fusionner X et Y vers A : tant que ni X ni Y ne sont vides fusionner les monotonies suivantes Xi et Yi vers A compléter A si nécessaire avec le reste de X ou Y

- 58 -

• Exemple :

a s o r t i n g e x a m p l e

a s i n e x l o r t g a m p e

a o r s t g i n a e m p x e l

a o r s a e m p x t g i n e l

split

merge

split

merge

a g i n o r s t a e e l m p x

a g i n o r s t a e e l m p x

split

merge

a a e e g i l m n o p r s t x

- 59 -

Structures de données récursives - Liste Récursive • Vue itérative du type abstrait liste : • Vue récursive du type abstrait liste : • Dans certains langages de programmation, c'est un type de base • Opérations élémentaires

- créer une liste vide - tester si une liste est vide - composer (ajouter une tête à une sous-liste) - décomposer (accéder à la tête et à la sous-liste)

• Spécification possible en Java public class RecursiveList { public RecursiveList() {...} public RecursiveList withHead(char e) {...} public char head() {...} public RecursiveList tail() {...} public boolean isEmpty() {...} }

• Dans cette spécification, les objets seront immuables (comme pour String)

= ...

= ∅ = ou

head tail

empty

- 60 -

Quelques algorithmes sur les listes récursives Exemple 1 : calculer la taille. Esquisse de l'algorithme :

Données Traitement Résultat sizeOf( ∅ ) 0 sizeOf( ) aux = sizeOf( ) 1 + aux

public static int sizeOf(RecursiveList l) { if (l.isEmpty()) return 0; return 1 + sizeOf(l.tail()); }

Exemple 2 : inverser une liste

Données Traitement Résultat inverse( ∅ ) ∅ inverse( ) = inverse( )

= append( , )

public static RecursiveList inverse(RecursiveList l) { if (l.isEmpty()) return l; return append(inverse(l.tail()), l.head()); }

Page 10: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 61 -

Exemple 3 : tester l'appartenance

Données Traitement Résultat isIn( , ∅ ) false isIn( , ) true isIn( , ) isIn( , )

public static boolean isIn(char e, RecursiveList l) { if (l.isEmpty()) return false; if (e == l.head()) return true; return isIn(e, l.tail()); }

Exemple 4 : ajouter un élément en fin de liste (cf. exercices) append Exemple 5 : concaténer 2 listes (cf. exercices) concat Exemple 6 : consulter le ième élément (cf. exercices) consultAt Exemple 7 : remplacer un élément par un autre (cf. exercices) replaceEach

- 62 -

Exemple 8 : partitionner en fonction d'une valeur

Données Traitement Résultat smaller( , ∅ ) ∅ smaller( , ) <

= smaller( , )

smaller( , ) >=

= smaller( , )

Exemple 9 : trier une liste

Données Traitement Résultat quickSort( ∅ ) ∅ quickSort( ) = smaller( , )

= greaterOrEqual( , ) = quickSort( ) = quickSort( ) = concat( , )

• Il y a d'autres structures de données qui s'expriment bien de façon récursive.

Exemple : l'arbre binaire.

A B

X Y

= ∅ ou =

X Y

A B

- 63 -

Traitement d'erreurs - Une affaire de conception • Traitement d'erreurs ≠ bug (faute de programmation) • Une méthode doit avoir une définition claire :

- pré-conditions : ce qui doit nécessairement être vérifié avant l'appel (p. ex. valeurs autorisées pour les paramètres); responsabilité de l'appelant

- post-conditions : ce que la méthode s'engage à établir, son effet, son résultat. Cas particulier (souvent implicite) : s'engager à laisser l'objet dans un état valide

• On parle de traitement d'erreurs lorsqu'on identifie des circonstances qui

empêchent une méthode d'accomplir correctement sa tâche (c.-à-d. établir ses propres post-conditions et garantir les pré-conditions des méthodes qu'elle devrait appeler)

• Un appel à une méthode sans respecter ses pré-conditions, c'est un bug • On a une responsabilité "vers le bas" : une procédure n'a pas à "guérir" des

mauvais paramètres reçus, mais doit garantir que les appels qu'elle engendre sont tous corrects

• En face d'une situation spéciale, il y donc une décision de conception : - soit on ajoute des pré-conditions qui excluent cette situation, et on offre à

l'appelant le moyen de vérifier ces pré-conditions

- soit on fait du traitement d'erreur et on rapporte le problème à l'appelant

- 64 -

Moyen 1 : Prévenir • Poser des PRE-conditions, et donner les moyens de les vérifier avant l'appel.

// PRE: !isEmpty(). Un pop sur une pile vide est interdit public char pop() {

• C'est analogue à un mode d'emploi avec des mises en garde • On ne peut pas toujours prédire si une erreur se produira !

isEnoughDiscSpace() avant de sauver un fichier - souvent impossible ! • On est donc limité aux situations prévisibles... • La méthode doit-elle détecter elle-même si ses PRE-conditions sont remplies ? Si la PRE-condition est violée, le comportement n'est pas défini Une telle spécification nous laisse donc libre... Qu'est-ce qui est le plus utile ? • Dans cette situation, les objectifs varient selon qu'on est en phase de :

(a) production : limiter les dégâts (sécurité, performances) (b) développement : aider à localiser le bug du code appelant

• (b) s'assurer que chaque PRE-condition violée se manifestera "bruyamment"

afin de favoriser la détection/correction du bug chez l'appelant : - lever une unchecked exception, p. ex. IllegalStateException("...") - p. ex. en posant des assertions : assert !isEmpty(); - faire un crash : System.err.println(...); System.exit(-1) - l'idéal serait de détecter le problème à la compilation déjà !

• Eviter tout malentendu : l'exception levée fait-elle ou non partie du "contrat" ?

Page 11: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 65 -

• Ne pas cacher le problème, rendre les données inconsistantes, retourner une valeur "spéciale" qui risque d'être confondue avec un output valide : il sera alors très difficile de détecter des bugs !

• Ne pas trafiquer une spécification sans réfléchir plus loin aux dangers :

- fonction "racine carrée de x" pré-condition : x>=0 - fonction "racine carrée de x" retourne (-x) si x < 0 ?!?!?!

Moyen 2 : Rapporter l'erreur (en Java, lever une exception) • La méthode s'engage à détecter le cas et informer l'appelant. Exemple :

// An exception is raised if the stack is empty public char pop() throws EmptyStackException;

• Indispensable pour toutes les situations imprévisibles (I/O) • Choisir qui va décider ce qu'il faut faire pour réparer l'erreur :

- StackIsFull : la classe Stack peut décider elle-même de doubler l'espace - FileNotFound : la classe qui trie ne peut pas inventer un nom de fichier...

• Influence des exceptions sur la clarté/qualité du code :

- les exceptions permettent de bien séparer les deux flux d'instructions : algorithme normal vs traitement des situations exceptionnelles

- moins de risque d'oublier une erreur rapportée (checked exceptions) - code appelant parfois encombré de try-catch purement formels

- 66 -

Traitement d'erreurs - La bonne démarche Obligations • Toute situation qui empêcherait la méthode de faire son travail doit être réglée

soit par des pré-conditions, soit en rapportant l'erreur • L'appelant est responsable de garantir les préconditions de l'appelé, et de

traiter (ou propager) les erreurs rapportées (ne jamais les "étouffer" !) • En cas d'erreur rapportée, garantir que les données restent dans un état valide

(basic guarantee) Conseils • Soigner le traitement d'erreurs, choisir le bon comportement en cas d'erreur

Annoncer clairement, dans la spécification, l'approche choisie (quelles erreurs rapportées dans quelles situations, quelles pré-conditions, ...)

• Privilégier une approche défensive (détecter/rapporter les situations spéciales) • En cas d'erreur rapportée, préférer revenir à l'état des données avant l'appel :

soit l'appel produit tout son effet, soit il n'en produit aucun et rapporte l'erreur (strong guarantee) (commit/rollback, transaction)

• Envisager s'il est possible d'avoir une spécification simple/claire sans erreur à rapporter ni pré-condition (comportement/output raisonnable en toute situation) (nofail guarantee)

• Séparer le rôle de chaque méthode/classe (p. ex. ne pas faire d'I/O n'importe où)

- 67 -

Exemple 1 (à discuter !)

public class SetOfInts { protected int[] buffer; protected int size; //------------------------------ private int eltPosition(int e) { // -1 if absent for (int i=0; i<size; i++) if (buffer[i] == e) return i; return -1; } //------------------------------ public SetOfInts(int maxSize) { buffer = new int[maxSize]; size = 0; } //------------------------------ public void add(int e) throws FullSetException { int p = eltPosition(e); if (p != -1) return; size ++; if (size == buffer.length) { size = 0; throw new FullSetException(); } buffer[size] = e; } //... }

- 68 -

class CharVectorA { // PRE: minIndex <= maxIndex CharVectorA(int minIndex, int maxIndex); // PRE: index is valid void set(int index, char elt); // PRE: index is valid char get(int index); int minIndex(); int maxIndex(); }

class CharVectorB { CharVectorB(int minIndex, int maxIndex) throws InvalidBoundsExc; void set(int index, char elt) throws InvalidIndexExc; char get(int index) throws InvalidIndexExc; }

class CharVectorC { CharVectorC(int minIndex, int length); // with automatic redimension void set(int index, char elt); // '\0' if not yet set char get(int index); }

Exemple 2 (à discuter) • Une classe pour simuler des tableaux où les indices commencent à X • Version A (préconditions) • Version B (erreurs rapportées) • Version C (nofail)

6 5 7 8 9 4 h e l l o !

Page 12: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 69 -

Assertions • Assertion : propriété (qui doit être) vérifiée à un endroit précis du programme,

du moins d'après la logique/intention du programmeur • Spécifier une méthode, c'est établir un contrat • PRE-conditions : propriétés qui doivent être vérifiées avant l'appel d'une

méthode, pour que celui-ci soit valide. • POST-conditions : propriétés qui seront vérifiées après l'exécution de la

méthode (effet de la méthode). • Changer les assertions de commentaire en code aide à débugger : la machine

peut alors vérifier que la condition est vraie à l'exécution • On aimerait les activer en phase de développement, mais pas forcément en

phase d'exploitation (pour ne pas dégrader inutilement les performances) On aimerait 2 versions du programme, mais si possible 1 seul fichier source...

• L'assertion doit rester neutre. • En Java, on les active à l'exécution : option '-ea' de la machine virtuelle

// PRE: the array is in growing order static boolean binarySearch(int[] t, int x) { assert isGrowing(t); ... }

assert (i++ > 0);

ici, l'approche par pré-condition est indispensable... voyez-vous pourquoi ?

- 70 -

Variantes moins utilisées pour rapporter les erreurs Retourner un code d’erreur

public int push(char c); // returns -1 if stack is full, 0 otherwise

• Problème de lisibilité (confusion entre vrai résultat et code d'erreur)

Consulter a posteriori un status (errno en C) public int wasOk(); // returns -1 if an error occurred in a // previous call of pop or push, 0 otherwise public void pop(); public void push(char c);

Passer une procédure d'erreur en paramètre

• Idée : la méthode "dangereuse" reçoit un paramètre supplémentaire : le code à exécuter en cas d'erreur ! Adapté à Pascal/Modula-2, moins à Java

class ErrHandler{ void handle(String s) {...} } void push(int elt, ErrHandler h){ if (top>maxLength) h.handle("stack is full");

... eh = new ErrHandler(); s.push(elt, eh);

Abonnement et notifications

class Stack { void addErrListener(ErrListener o); void push(int elt); // if error, for // each listener o, call o.handleEvent() }

class Prg implements ErrListener{ void handleEvent(...); ... s.addErrListener(this); s.push(19);

- 71 -

L'ingénieur et les tests • La rigueur est une qualité qu'on recherche chez un ingénieur (en informatique

comme dans les autres domaines) • La démarche de test est un point important de tout projet

Tester ≠ Essayer (niveau ingénieur) (niveau apprenti)

Propriétés d'un test : - conçu avec soin : objectifs clairs, souci d'exhaustivité... - reproductible : automatiser... - documenté : démarche et observations...

- 72 -

Bugs - Tests systématiques • "Code a little, test a little" "Work 50% on features, 50% on tests" Scénarios de test (test cases) • Pour tester, il faut d'abord décrire clairement le but de l'unité à tester. • Un scénario de test consiste à :

- faire des appels à l'unité qu'on veut tester (appeler des méthodes) - vérifier les propriétés qui auraient dû être satisfaites

• Un scénario de test est destiné à découvrir un bug • Le bug est découvert dans un des cas suivants :

- une des propriétés n'est pas vérifiée - le scénario provoque l'interruption du programme

(p. ex. NullPointerException) - le scénario provoque une boucle infinie

• Exemple de codage pour un scénario de test : // returns true if something wrong happens static boolean bugInStack01() { IntStack s = new IntStack(); s.push(10); int x = s.pop(); if (x != 10) return true; // bug detected if (! s.isEmpty()) return true; // bug detected return false; // test was negative : no bug detected }

remaining bugs

test effort

Page 13: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 73 -

• Il faut automatiser les tests. Imaginer un test où l'utilisateur ... - ... doit entrer au clavier les données d'un tableau de mille cases... - ... reçoit un listing de 100 pages de valeurs à contrôler à la main...

• Parfois on peut comparer avec une version "de référence" (p. ex. une autre

implémentation sûre mais peu efficace) • Un générateur aléatoire peut être utile pour "inventer" des jeux de données,

ou des séquences d'appels. Mais attention quand même à bien réfléchir :

- rnd.nextInt(100000) ne va pas générer un nombre négatif ! - rnd.nextInt() a peu de chance de générer la valeur 0

• Le but d'un test n'est jamais de prouver l'absence de bugs... • Apprendre à utiliser les outils du programmeur (approfondi en 2ème) :

- compilateur et ses options (activer les warnings, les assertions) - analyse statique (p. ex. jlint, escjava, findbugs, JML) - plateformes de tests (p. ex. junit) - debugger (p. ex. avec breakpoints conditionnels) - couverture de code (coverage) : quelles lignes n'ont pas été exécutées ? - runtime memory analyzer : détecter les accès à une zone non allouée, les

fuites de mémoire... (p. ex. valgrind, dmalloc, DUMA, purify) - profileur : où passe-t-on le plus de temps CPU, où passe la mémoire ?

• Bien programmer, c'est aussi prendre des bonnes habitudes...

- 74 -

Comment tester si un calcul de minimum fonctionne ? (test d'une fonction) • Inventer plusieurs jeux de données (aussi des cas "spéciaux" ou "limites") :

- taille du tableau : 1, 2, 10, 100, 1'000'000... - contenu : entiers positifs, négatifs, zéro, MIN_VALUE, MAX_VALUE minimum dans la première/dernière case ou ailleurs plusieurs occurrences des mêmes valeurs - une approche : générer des données aléatoires

• Propriétés du résultat : - le nombre est présent dans le tableau - aucun autre nombre du tableau n'est plus petit static boolean bugsInMin(int maxLength) { for (length=1; length < maxLength; length ++) { int[] t = createRandomIntArray(length); int result = min(t); // <--- tested method if (! (isIn(result, t) && isBelow(result, t)) System.out.println("Oups..." + toStr(t)); return true; } return false; }

// returns the minimum static int min(int[] tab);

- 75 -

Comment tester si une pile fonctionne ? (test d'un module entier) • On ne peut pas tester séparément push(), pop() et isEmpty() ! • Inventer des séquences d'appels...

static boolean bugsInStack02(int maxSize) { int i; IntStack s = new IntStack(); for (int i=0; i<maxSize; i++) s.push(i); for(i=maxSize-1; i>=0; i--) { if (s.isEmpty()) return true; // bug detected if (s.pop() != i) return true; // bug detected } if (!s.isEmpty()) return true; // bug detected return false; }

• On peut faire mieux... Par exemple, dans le scénario ci-dessus :

- on ne fait jamais un push après un pop()... - on remplit la pile avec des valeurs qui pourraient être confondues avec

des indices internes...

- 76 -

Bugs - Lutte systématique • Programmer est un métier dangereux... Comment prévenir les bugs ? → Améliorer la qualité du code • Etre convaincu du pseudo-code, puis de sa traduction en codage. • Ecrire le code pour qu'il soit facile à tester. • Faire l'inventaire de tous les cas possibles. Garantir qu’une boucle termine. • Activer tous les avertissements du compilateur (ou d'autres outils). • Suivre des conventions de codage. • Le programmeur doit viser le point où il sait que son programme est correct Comment détecter un bug (le plus tôt possible) ? → Augmenter la confiance dans le code • "Ramper" avec le debugger. • Ajouter des assertions, vérifier les PRE-conditions, etc. • Tester soigneusement, intensément, rigoureusement :

- Tests fonctionnels (blackbox) : uniquement d’après la spécification. Est-ce que l'effet du programme est conforme aux spécifications ?

- Tests structurels (whitebox) : d'après l'implémentation. Est-ce que chaque ligne du programme a été exécutée ?

• En C/C++ : Utiliser un "runtime memory analyzer"

Page 14: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 77 -

Comment localiser un bug ? • Isoler le problème. • Créer un scénario qui permet de reproduire le bug. • "Ramper" dans le code durant l'exécution, à l'aide d'un debugger. Mettre des

"breakpoints". Suivre les changements de chaque donnée. • Changer de point de vue : prendre le point de vue de chaque donnée

(variable), l'une après l'autre, et se concentrer sur son évolution. • Les bugs savent bien se cacher; ils ont beaucoup d'imagination. Comment éliminer un bug ? • Le comprendre complètement. • Chercher si ce bug se répète ailleurs. • Jamais "essaie de modifier, on verra bien" ! Démarche scientifique • Apprendre comment on aurait pu l'éviter. Imaginer quel moyen automatique

l'aurait immédiatement détecté. Ajuster sa façon de travailler pour éliminer le risque d'une nouvelle apparition...

• Jamais on ne programme que pour soi-même... • Tout fichier source doit mentionner son auteur (qui assume la responsabilité du

code et des commentaires) • Voir les bugs célèbres (explosion d'Ariane 5.1,...) (cf. page Web du cours)

"The rest is attitude..."

- 78 -

Introduction à la cryptographie • Comment communiquer de façon confidentielle • 2 phases : encrypter, décrypter Notion de clé • Exemple de transformation "sans clé": a→b, b→c, c→d, ... • Algorithmes à clés : paramétrer la transformation par un code propre aux

utilisateurs (p. ex. "nombre de caractères de décalage") • Hypothèses : les messages se résument à une séquence de nombres • La cryptographie est une application de la théorie des nombres • Avec les très grands nombres, certaines opérations sont rapides, d'autres pas :

- décomposer en facteurs premiers : inefficace (lent) - tester si un nombre est premier : efficace (rapide) - multiplier, calculer le modulo ou le pgdc : efficace (rapide)

Algos à clé unique (systèmes symétriques) • Le même code (clé) sert à encrypter et à décrypter • Problème : on doit s'échanger la clé (comment la communiquer sans risque ?) Algos à clés publique/privée (systèmes dissymétriques) • Chacun possède 2 clés : 1 secrète, et 1 connue de tous • Ce qui s'encrypte avec l'une se décrypte avec l'autre (elles sont donc liées !) • La clé secrète ne peut pas être calculée à partir des données connues

- 79 -

Principe des algo à clés publique/privée (envoi de msg confidentiels) 0. Alice doit envoyer un message confidentiel à Bob 1. Bob construit 2 clés K et K.

Bob montre son K à tout le monde 2. Alice encrypte son message original M avec la clé publique de Bob K

M = f(K, M) 3. Alice envoie M à Bob 4. Bob décrypte le message avec sa clé privée K

M = g(K, M) Une réalisation : l'algorithme RSA 1. Choisir au hasard 2 grands nombres premiers p, q

Calculer N = pq N'=(p-1)(q-1) Choisir au hasard un nombre e tel que e et N' sont premiers entre eux Calculer d à partir de e et N', tel que (e*d % N') == 1 Jeter N', p, q Clé publique : K = (e, N) Clé privée : K = (d, N)

2. M = f(e, N, M) = Me (mod N) 4. M = g(d, N, M) = Md (mod N)

- 80 -

Authentification • Comment s'assurer qu'un message a bien été écrit par son auteur présumé ? • Le principe des clés publique/privée fonctionne dans l'autre sens ! 1. Alice publie des informations non secrètes, mais veut éviter que d'autres

"publient sous son nom"; elle veut authentifier son message 2. Elle encrypte ses publications avec sa clé secrète K

M = g(K, M) 3. Bob, un de ses lecteurs, décrypte la publication avec la clé publique d'Alice

M = f(K, M) • Reste le problème de la certification par une institution "officielle"... Contrainte supplémentaire pour RSA • M < N (indispensable) (en général M << N) Opérations arithmétiques • Trouver d tel que e*d % n == 1 : tester pour toutes les valeurs depuis d=2 • Calculer xy mod z : voir plus bas • Choisir un nombre premier. Par exemple, trouver le nième nombre premier :

pour chaque nbre i, si aucun nbre entre 2 et (i-1) ne divise i, alors i est le prochain nbre premier.

• Il y a des algorithmes plus efficaces...

Page 15: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 81 -

Fiabilité en cryptographie • RSA est un algorithme solide, quasi universellement adopté • En fait, à partir de la clé publique, on peut calculer la clé privée !

(On factorise N en p et q, et on recalcule N' et d) • Mais factoriser un très grand nombre semble demander d'énormes ressources • L'histoire de la cryptographie est riche de codes (prétendument solides)

cassés. Prudence ! • 2 preuves manquent formellement à RSA :

- prouver que la factorisation est un problème "difficile" (complexité) - prouver que RSA dépend uniquement de la factorisation

• Rien ne garantit qu'une découverte importante soit dissimulée... • Les failles sont souvent dans ce qui entoure la méthode :

- "mauvais choix" des nombres premiers - accepter d'authentifier un message donné - laisser échapper le message original par un autre moyen - mauvaise série de messages originaux (il faudrait p. ex. éviter les longues

plages remplies d'espaces) - mauvaise implémentation (il faudrait p. ex. ajouter au hasard des

opérations bidon !!)

- 82 -

Calcul de xy mod z • Méthode naïve (attention au dépassement de capacité...) : (xy) mod z public static long powerModulo(long x, long y, long z) { long p = 1; while(y-- > 0) {p *= x;} return p % z; } • Idée d'algorithme robuste : (a*x) mod z = ((a mod z) * x) mod z public static long powerModulo(long x, long y, long z) { long res = 1; while(y-- > 0) {res = (res *(x % z) )% z;} return res; } • Idée d'algorithme robuste et efficace (et récursive) : x2y mod z = (x2 mod z)y mod z public static long powerModulo(long x, long y, long z) { if (y==0) return 1; long tmp = powerModulo((x*x)%z, y/2, z); if (y%2 != 0) tmp = (tmp * x) % z; return tmp; }

- 83 -

Manipuler des très grands entiers en Java : classe java.math.BigInteger • Java offre une classe pour représenter des entiers arbitrairement grands • On évite ainsi tout problème de dépassement de capacité (dans les limites de

la mémoire disponible...) • Exemple :

import java.math.*; BigInteger a = new BigInteger("123456712345671234567"); BigInteger b = new BigInteger("987659876598765987654"); BigInteger c = a.multiply(b); System.out.println(c);

• Cette classe contient déjà de nombreuses opérations utiles pour la

cryptographie

BigInteger(int bitLength, int certainty, Random rnd) // Constructs a randomly generated positive // BigInteger that is probably prime, with the // specified bitLength. Probability > 1-1/2certainty

BigInteger modPow(BigInteger exponent, BigInteger m) //Returns a BigInteger whose value is (thisexponent mod m)

• En passant, il y a aussi BigDecimal pour les nombres à virgule...

- 84 -

Amorce de la série d'exercice : réaliser RSA en Java Cahier des charges, interface utilisateur • une commande pour encrypter un fichier texte avec une clé. Spécification :

encrypt msgFile keyFile outFile • une commande pour décrypter un fichier texte avec une clé. Spécification :

decrypt msgFile keyFile outFile • une commande pour créer un couple de clés publiques/privées. Spécification :

create code1 code2 pubKeyFile privKeyFile • Choisir les code1ème et code2ème nombres premiers pour p et q

e sera toujours choisi de la même façon (p. ex. nbre premier qui suit N') Politique d'encodage/décodage • Encodage caractère par caractère (chaque car. considéré comme un nbre) • Version cryptée du message : un nombre sur chaque ligne • Fichier de clé : 1ère ligne : e ou d. 2e ligne : N • Pas de traitement d'erreur Choix d'implémentation de l'algo RSA • Utilisation du type long à la place de BigInteger, pour simplifier

Page 16: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 85 -

Identification des classes, ou des modules • Ici, on montre une version avec méthodes statiques (pas "orientée objet") Une découpe en méthodes

static void main(String[] args)

static void encrypt(String msgFile, String keyFile, String outFile) throws IOException static void decrypt(String msgFile, String keyFile, String outFile) throws IOException static void createKeys(int code1, int code2, String publicKeyFile, String privateKeyFile ) throws IOException

static long powerModulo(long base, long exp, long mod) static long multInverse(long e, long n) static long getKthPrimeNb(int kth) static boolean isPrime(long n)

static long encode(char m, long exp, long mod) static char decode(long m, long exp, long mod)

- 86 -

Type abstrait "File" (ou file d'attente) • Modèle d'organisation des données : First In First Out (FIFO) • Important : la file peut être vide • Applications : flux de symboles, jobs d'imprimantes... • Une spécification possible en Java :

public class IntQueue { public boolean isEmpty(); public void enqueue(int elt); public int consult(); public int dequeue(); }

Implémentation 1 : chaînage de noeuds

public class IntQueue { private QueueNode front; private QueueNode back; } class QueueNode { int elt; QueueNode next; QueueNode(int theElt, QueueNode theNext) {...} }

enqueue

dequeue

back enqueue dequeue

front

next

elt

- 87 -

• Opération enqueue(int x) - cas "normal" - cas où la file était vide

• Opération dequeue() - cas "normal" - cas où (front==back)

• Opération isEmpty() : tester si front désigne un objet ou vaut null

back

enqueue

front

x

back

enqueue

front

x

back dequeue

front

x

back

dequeue

front

x

- 88 -

Implémentation 2 : tampon circulaire • Idée : rendre un tableau "circulaire"

• Attributs : un tableau d'éléments; deux indices courants front et back • front désigne la case du plus ancien élément de la file back désigne la case du plus récent élément de la file

• Calcul d'indices "avec modulo" (après (n-1) on a 0 !)

0

1

2

...

n-2

n-1

front

back

0

1

2

...

n-2

n-1

front

back

0

1

2 ... n-2

n-1

front back

enqueue(x)

y=dequeue()

Page 17: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 89 -

• Utile : définir un attribut size

• Constructeur : créer le tableau; front = 1; back = 0; size=0

• isEmpty() : tester si size==0

• dequeue() : lire à la case front, puis incrémenter front

• enqueue(int x) : incrémenter back, puis déposer x dans la case back Si on remplit complètement le tableau :

création un tableau plus grand (attention à recopier correctement !) • Comment "incrémenter" back ou front ?

back = (back+1)%t.length ou bien : if (back == t.length-1) back = 0; else back++;

Comparaison des 2 implémentations • Chaînage : complexité en O(1)

- allocation dynamique de mémoire à chaque enqueue - stockage de n "pointeurs" supplémentaires

• Tableau : complexité en O(1)

- coûteux doublage d'espace mémoire - gaspillage pour les cases non occupées

• Comparaison de la difficulté de codage ? ... à vous de juger, dans la série !

- 90 -

Généricité • Généricité : faculté d'exprimer une logique indépendante du type de données • Un moyen de plus pour favoriser la réutilisabilité ! • Aux débuts de Java, le mécanisme manquait. Tenter de le simuler avec

l'héritage n'est pas "type-safe", et on doit donc s'encombrer avec : - des conversions explicites (type casting) Object ↔ MyClass - les types primitifs (Wrappers) int ↔ Integer

public class ObjStack { private Object[] buffer_; private int top = -1; public Object pop() { return buffer_[top--]; } public void push(Object x){ buffer_[++top] = x; } ... }

public class ObjStackTest { public static void main(String[] a){ ObjStack s = new ObjStack(); String w = "bonjour" s.push(w); w = (String)(s.pop()); int i = 5; s.push(new Integer(i)); i = ((Integer)(s.pop()).intValue()); } }

• D'autres langages supportent mieux la généricité que Java 1.4

- 91 -

x = new E(); x = (E) y; t = new E[10]; if(x instanceof E)

Généricité avec Java 1.5 [vu au cours Programmation] • Type safety. Faire détecter les incompatibilités de type par le compilateur. Utilisation d'une classe générique

Stack<Integer> s; s = new Stack<Integer>(); s.push(new Integer(43)); s.push(44); // autoboxing; int x=s.pop(); // unboxing;

Implémentation d'une classe générique • Le type générique s'utilise (presque !) comme un type normal

public class MyStack <E> { Vector<E> buf; int top = -1; public MyStack() { buf = new Vector<E>(); } public void push (E elt) { buf.add(elt); } public E pop () { return buf.get(top--); } }

• Type erasure : le type générique existe à la compilation,

pas à l'exécution. Certaines instructions n'ont pas de sens : • Utilisation avancée (wildcards, subtyping, generic methods) :

cf. "Generics in the Java Programming Language"

package java.util; public class Stack <E> {... public Stack(); public void push (E elt); public E pop (); }

- 92 -

Programme : davantage que du code... • Programme =

Spécification (signature des méthodes, PRE/POST claires, ...) + Implémentation(s) (avec bons commentaires) + Documentation (mode d'emploi, pages javadoc, limitations

document de conception, cahier des charges, découpe modulaire, diagrammes UML...)

+ Tout ce qui peut être utile pour la maintenance : pseudo-code esquisse des données analyse de complexité preuves/arguments de correction assertions tests de validation tests de performance et résultats documentation des mises à jour ou des alternatives possibles,

trace des réflexions sur le codage, optimisations possibles

Page 18: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 93 -

Calcul numérique Problème : les machines sont limitées • Dépassement de capacité int x=2*Integer.MAX_VALUE; • Aucun ordinateur ne traite de vrais nombres réels • Problèmes d'arrondis (1.0+(0.1+0.2)) != ((1.0+0.1)+0.2) (2.0E7) == (2.0E7 + 1)

• Algos numériques, instabilité : l'imprécision risque d'exploser au fil des calculs Nombres entiers en Java (byte, short, int, long) • La division par 0 lève une exception ArithmeticException • Mais pas les dépassements de capacité (overflow) !!

• Integer.MAX_VALUE + 1 == Integer.MIN_VALUE Nombres à virgule flottante en Java (float, double) • Jamais d'exception levée • Mais des "nombres spéciaux" : - Float.NEGATIVE_INFINITY

- Float.POSITIVE_INFINITY (Not A Number) - Float.NaN - Float.MAX_VALUE - Float.MIN_VALUE

- 94 -

Attitudes face aux problèmes numériques • Préférer les entiers aux nombres à virgule flottante.

Eviter le test d'égalité/différence sur les nombres à virgule flottante

• Remplacer "a==b" par "|a-b| < ε", avec par exemple ε = 0.0001 * |a| - problème : (a==b) && (b==c) && (a!=c)

• Utiliser un type abstrait "nombre à précision arbitrairement grande" : - autant de chiffres significatifs que nécessaire - classes existantes Java : BigInteger, BigDecimal

• Utiliser un type abstrait "nombre rationnel"

- relation d'ordre total - nbre = (int numérateur, int dénominateur)

• Utiliser un type abstrait "intervalle" - interval computation, reliable computing - nombre = (float low, float high) - relation d'ordre partiel seulement - on peut connaître en tout temps la marge d'imprécision

• Utiliser un type abstrait "expression symbolique" ; exemple : - nbre = arbre de dérivation "*"("/"("π", 3), "sqrt"(2)) - plus de relation d'ordre direct : évaluation seulement tout à la fin

• Etre astucieux... (en int, comment calculer (a*b/c), si (a*b) cause l'overflow ?)

• Cf. projet COJAC : https://github.com/Cojac/Cojac ☺

π3

2

- 95 -

Type abstrait ensemble Bitset • Hypothèses sur les éléments : nombres entiers sur un "petit" intervalle • Idée de base : prévoir une case mémoire pour chaque élément de l'intervalle,

qui indique son appartenance ou non. Accès direct en O(1) • Exemple : Pour représenter un ensemble sur l'intervalle [0..99] :

boolean[] isIn = new boolean[100]; • Pour tout tableau, donner un rôle clair aux indices, et un rôle clair au contenu

• En considérant la représentation interne binaire, un seul int (32 bits) suffit pour

représenter un ensemble de valeurs parmi [0..31]. // 31 30..6 5 4 3 2 1 0 int s=19; // {0,1,4} 0 0...0 0 1 0 0 1 1 // {- -...- - 4 - - 1 0 }

• Puis il faut utiliser les opérateurs binaires pour lire/écrire le jème bit d'un entier a :

int mask = 1<<j; a = a | mask; b = b & (~mask); if (c & mask != 0) {...

0 1 2 3 4 5 6 - - √ - √ √ -

{2, 4, 5}

- 96 -

• Choisir une implémentation efficace en temps et en mémoire dépend du langage de programmation :

- tableau d'entiers et calculs sur les bits (C) - tableau amalgamé (packed array) de booléens (Pascal) - type BITSET (limité à 32 bits) (Modula-2) - classe BitSet (Java)

La classe java.util.BitSet • BitSet sert à stocker une séquence de valeurs booléennes, de façon efficace

(en temps et espace mémoire) et dynamique (espace mémoire extensible) • Spécification (extraits) :

public class BitSet { public BitSet(); public BitSet(int capacity); public void set(int index, boolean value); // index >= 0 public boolean get(int index); public void and(BitSet b); // also or(), xor()... public int size (); // crt capacity=total nb of bits public int length(); // highest bit "on" + 1 public int nextSetBit(int fromIndex) // -1 if none public int cardinality() // nb of bits set to true }

Page 19: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 97 -

Type abstrait "ensemble d'entiers short" • Une spécification possible (rappel) :

public class SetOfShorts { public SetOfShorts (); public boolean isEmpty(); public int size (); public void add (short e); public void remove (short e); public boolean contains(short e); public void union (SetOfShorts s); public void intersection(SetOfShorts s); public SetOfShortsItr iterator() }

• Et une spécification possible d'un itérateur :

public interface SetOfShortsItr { public boolean hasMoreElements(); public short nextElement(); }

• On a déjà vu une implémentation non efficace en O(n)... • Complexité avec BitSet : add, remove, contains : en O(1)

(et les autres opérations ?)

- 98 -

-32768 ... -1 0 ... +32767

-32768 ... -1 0 ... +32767

0 -32768 ... -1 0 ... +32767

0

...

0

-1 0 -2 +1 -3 +2

...

éléments

indices

• Implémentation : utilisation d'un attribut de type BitSet (cf. exercices) • Comment mettre en correspondance les shorts X vers des entiers positifs Y ?

Plusieurs approches : • Il y a une bijection entre les éléments et les

indices du tableau (relation 1 à 1) • On parle aussi de hachage parfait Implémenter BitSet • Un tableau d'entiers int sur 32 bits (redimensionné si nécessaire) • Elément de valeur A : (A%32)ème bit de l'entier à l'indice (A/32) • Opérateurs binaires en Java, décalage et masquage

int a = 1 << 4 // décalage arithm. à gauche. // Résultat : 000...010000 int a = ~1 // NOT binaire (inversion) int c = a&b // ET binaire int d = a|b // OU binaire int e = a^b // XOR binaire (ou exclusif)

... ... ... 0 31 0 31 0 31

0 1 2 bit 38

- 99 -

Générateurs de nombres pseudo-aléatoires • Un nombre arbitraire. Une séquence de nombres aléatoire(s) • Un ordinateur ne génère pas du vrai hasard...

Séquence de nombres pseudo-aléatoire (qui semble aléatoire), avec un calcul, un algorithme qui vérifie certaines propriétés statistiques, comme :

- chaque nombre est tiré de façon équiprobable - probabilité pour 2 nbres consécutifs d'avoir une somme impaire = ½, etc.

• Spécification du générateur de nombres pseudo-aléatoires en Java :

• Notion de période d'un générateur (longueur d'un cycle complet) : toute

séquence pseudo-aléatoire est périodique. Généralement, la période est assez grande (p. ex. 232)

• Notion de graine (seed) : souvent, on veut pouvoir re-générer à volonté la même séquence. La graine est une sorte de numéro qui identifie chaque séquence possible

public class Random {... public Random (); public Random (long seed); public int nextInt (int n); // 0 <= value < n public double nextDouble (); // 0 <= value < 1 public boolean nextBoolean(); }

- 100 -

char specialRandomChar() { // tirer un nombre x entre 0 et 99 // if (x < 50) return 'y'; // 50 % de y // if (x < 80) return 'n'; // 30 % de n // return 'm'; // 20 % de m

void rndExperiment() { Random r; for (int i=0; i<1000; i++) { r = new Random(); System.out.println(r.nextInt(100)); } }

• Si on ne la choisit pas explicitement, certains systèmes prennent comme graine l'horloge interne (p. ex. System.currentTimeMillis())

• Voilà donc un piège typique dans l'usage des générateurs...

Permutations aléatoires d'un tableau • Algorithme en un seul passage : Distributions non uniformes • Loi uniforme pour simuler

d'autres lois de distribution. Exemple :

Algorithme probabiliste = qui prend certaines décisions de façon aléatoire • A quoi peut servir le hasard ?

- être efficace et espérer être correct (algo de Monte-Carlo) - espérer être efficace et être correct (algo de Las Vegas)

int rndDice() { Random r = new Random(); return 1+r.nextInt(6); }

pour chaque i entre 1 et length-1 { tirer un nombre x entre 0 et i échanger t[i] et t[x]

Page 20: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 101 -

Exemple : test de primalité (tester si N est un nombre premier) choisir un nombre (témoin) quelconque A entre 2 et N-2 calculer une certaine suite de nombres si l'un de ces nombres possède une certaine propriété, alors N n'est pas premier (c'est sûr !) sinon N est peut-être premier...

Dans ce dernier cas, l'algorithme "se trompe" une fois sur 4 !

Par contre, la complexité est O(ln n), contre O(n) pour l'algorithme déterministe • Pour avoir plus de confiance, on cherche d'autres "témoins" A ! • Avec 2 nombres A différents, le risque tombe à 1/4 * 1/4 = 1/16 • Pour un grand N, on tire au sort 20 nombres A et le risque est environ

de 1 sur 1012 • En gagnant beaucoup de temps de calcul, on a perdu la certitude d'avoir le

résultat correct... mais on atteint une (quasi-)certitude ! Un générateur par la méthode de congruence linéaire • Idée : Xi = (A * Xi-1) % M Paramètres : X0, et surtout A, M

• (Mauvais) Exemple : X0=2, A=5, M=7 → Xi = 3, 1, 5, 4, 6, 2, 3, 1, 5, ... • Choisir des bons paramètres, c'est tout un art...

(exemple : M = 231-1, A = 48271, X0 = 1)

NAxiN

i %)2/()1( −=

- 102 -

Tables de hachage • Une idée efficace pour représenter le type abstrait "ensemble" :

mettre les éléments en indice d'un tableau de booléens. • Avantage du tableau avec éléments en indice : accès direct en O(1) • Comment faire si les éléments ne sont pas d'un type entier ? • Idée : calculer un indice 0..k à partir de la valeur de l'élément • On parle de fonction de hachage, et de table de hachage • Exemple pour les String (une fonction de hachage médiocre)

// additionner les codes ASCII de chaque caractère // prendre la somme modulo k

donne, pour une table à 10 cases : "A" "ABC" "AA" "AI"

5 8 0 8 • En général, il y a risque de collision ("tri" et "tir") : plusieurs éléments peuvent

avoir la même valeur de hachage ! (donc aussi on ne peut pas calculer l'élément à partir de sa valeur de hachage).

• Une bonne fonction de hachage doit éparpiller au maximum les indices • On parle de hachage parfait s'il y a bijection entre éléments et indices (donc

jamais de collisions)

0 1 2 3 4 5 6 - - √ - √ √ -

{2, 4, 5}

- 103 -

• Il y a deux approches pour gérer les collisions Hachage ouvert (chaînage séparé) • Idée : une case du tableau ne contient pas un élément, mais une

collection d'éléments, typiquement une liste chaînée (cette collection n'a pas besoin d'être gérée efficacement)

• Au début, les k "listes" sont vides • Pour gérer un ensemble à n éléments, chaque "liste" devrait avoir en

moyenne n/k éléments (si c'est une bonne fonction de hachage) Hachage fermé (adressage ouvert !) • Idée : si la valeur de hachage du nouvel élément renvoie à une case déjà

occupée, il faut s'arranger pour trouver un autre indice d'une case libre ! • Chaque case doit indiquer si elle est occupée ou non • Mais il faut trouver un "truc" qui nous évite de parcourir toute la table pour

rechercher un élément qui ne s'y trouve pas ! Il y a plusieurs variantes. Hachage fermé, sondage linéaire • Idée : tester les cases H, H+1, H+2, ... (modulo k) • Problème : des "bouchons" ont tendance à se former (clustering) • Sondage quadratique : tester H, H+12, H+22, H+32 ...

Double hachage (fct subsidiaire H2) : tester H1, H1+H2, H1+2*H2, H1+3*H2 ...

0 1 2 3 ∅

r

x a g

h t

- 104 -

Une implémentation possible • Idée : compter combien d'éléments

ont la même valeur de hachage • Exemple : une table à 4 entrées : • Pour un dictionnaire, il y aurait des

colonnes "key" et "value" à la place de "element"

Index total busy element hashVal 0 0 no 1 0 no 2 0 no 3 0 no

Index total busy element hashVal 0 0 no 1 1 yes "bill" 1 2 0 no 3 0 no

Index total busy element hashVal 0 0 no 1 2 yes "bill" 1 2 0 yes "bob" 1 3 0 no

Index total busy element hashVal 0 0 no 1 1 no 2 0 yes "bob" 1 3 0 no

add("bill") h("bill")==1

add("bob") h("bob")==1

remove("bill")

Page 21: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 105 -

Implémentation du hachage fermé • Or donc, un type d'élément, une fonction de hachage, un tableau [0..K-1], ... • Informations à maintenir pour chaque case i : boolean busy : cellule occupée/vide (ou convenir un élt "marqueur", p. ex. null) EltType elt : l'élément éventuel int hashVal : peut aussi être re-calculé à partir de elt int total : nombre d'élts dans la table ayant i comme valeur de hachage

• Structure de données (ici éléments de type String) et méthodes :

class HashEntry{ boolean busy; String elt; int total; } class HashTable { HashEntry[] buffer; int crtSize; }

// ou tableaux parallèles: class HashTable { BitSet busy; String[] elt; int[] total; int crtSize; }

private int hashString (String s, int k); private int targetIndex(String e); // returns index where // e is, or can be put public void add (String e); public void remove (String e); public boolean contains (String e);

- 106 -

Hachage et complexité (minuscule aperçu...) • Soit λ=n/k le taux de charge (n/k) de la table

• Hachage ouvert : O(λ)

• Hachage fermé : O(1/(1-λ)) • Donc, moyennant une bonne fonction de hachage et une

table de taille adaptée, les opérations seront en O(1). Hachage et expansion dynamique • Souvent, on crée une fois pour toutes une table suffisamment grande • Si la table se remplit trop, les performances se dégradent, et on doit créer une

table plus grande • Ne pas attendre que les performances soient très mauvaises pour

redimensionner la table. Typiquement, on double la taille dès que : n > 2k pour le hachage ouvert et 2n > k pour le hachage fermé

• Mais attention, comme on change la taille du tableau, on change donc les résultats de la fonction de hachage !!!

• Quand on veut augmenter la taille d'une pile, il suffisait de recopier le tableau case par case. Pour la file aussi, c'est (assez) facile.

• Pour une table de hachage, il faut réinsérer chaque élément (rehachage) !

λ 1

time per access

- 107 -

Hachage dans la librairie standard Java • La classe Object de Java offre la méthode : int hashCode()

utilisée par les "java collections" HashSet et HashMap

int myHashObject(Object s, int k) { return Math.abs(s.hashCode()%k); }

• hashCode() calcule un entier qui dépend de l'adresse de l'objet en mémoire

(dans Object), ou bien du contenu (redéfinie dans les sous-classes) • On peut bien sûr redéfinir cette fonction pour nos classes.

Attention, hashCode() doit être cohérent avec equals() : (a.equals(b)) ⇒ (a.hashCode() == b.hashCode())

Une assez bonne fonction de hachage pour le calcul de hashCode() • Pour chaque champ significatif de valeur f, calculer un code de hachage c :

byte/char/short/int : c=(int)f boolean : c=(f?1:0) Objet : c=f.hashCode() tableaux : considérer chaque case long : c=(int)(f^(f>>>32)) comme un champ float (...double) : c=Float.floatToIntBits(f) (...puis comme un long)

puis combiner les différentes valeurs (on peut pré-calculer le résultat) :

int result = 19; // or any non-zero constant for each field with hash code c: result=result*31 + c; // 31 gives good results

- 108 -

Programmation dynamique • Certains algo récursifs conduisent à recalculer souvent une même expression • C'est donc du travail inutile; il suffirait d'effectuer une fois le calcul, et "de s'en

rappeler" • On cherche alors plutôt à mémoriser les résultats (des simples aux complexes) Exemple 1 : Problème du change • Soient N types de pièces (valeurs entières) C1..Cn • Quel est le minimum de pièces pour changer un montant K ? • Exemple : changer 63 avec des pièces {1, 2, 10, 21, 25} Fr. 63 = 25+25+10+1+1+1 6 pièces

Fr. 63 = 21+21+21 3 pièces • Idée (incorrecte) intuitive : l'algorithme glouton (greedy algorithm) :

- essayer de placer la plus grande pièce possible - recommencer avec le reste

• Principe des algorithmes gloutons : à chaque étape, faire le choix le plus "facile", celui qui semble le meilleur dans l'immédiat. Mais un optimum global ne découle pas toujours d'optimums locaux.

• Principe des algos de force brute : énumérer toutes les combinaisons et choisir

Page 22: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 109 -

63 ?

62+1 61+2 53+10 42+21 38+25 ou ou ou ou

• Idée d'algorithme récursif (et correct) : - si K = 0, on a la solution; - sinon, on prend la solution minimale des problèmes (K-Ci)

• Un algo de programmation dynamique : utiliser un tableau de solutions :

pour chaque valeur i entre 1 et k résoudre le problème i en lisant les solutions [0..i-1] stocker la solution dans Sol[i]

Principes de la programmation dynamique • Reprendre l'idée d'un algo récursif, mais :

- énumérer itérativement les problèmes, depuis les cas de base jusqu'aux cas complexes (souvent on remplace la récursivité par une boucle)

- remplacer le retour d'une valeur par une écriture dans le tableau - remplacer un appel récursif par une lecture au tableau

• En général, si la fonction récursive a X paramètres, le tableau aura X dimensions. Indices = données du problème. Contenu = solution.

- 110 -

public static int makeChangeRec(int k, int[] coins) { int minCoins = k+1; // greater than any valid result if (k==0) return 0; for (int c:coins) { if (k-c < 0) continue; int subResult = makeChangeRec(k-c, coins); minCoins = Math.min(minCoins, subResult); } return minCoins + 1; } public static int makeChangeDyn(int k0, int[] coins) { int[] makeChangeSol = new int[k0+1]; makeChangeSol[0] = 0; for (int k=1; k<=k0; k++) { int minCoins = k0+1; // greater than any valid result for (int c:coins) { if (k-c < 0) continue; int subResult = makeChangeSol[k-c]; minCoins = Math.min(minCoins, subResult); } makeChangeSol[k] = minCoins + 1; } return makeChangeSol[k0]; } // e.g. int[] coins = {1, 2, 10, 21, 25};

- 111 -

Exemple 2 : Cheminer dans un échiquier • On doit parcourir un échiquier d'un coin (0,0) jusqu'à l'autre (3,3), en se

déplaçant uniquement d'une case vers la droite ou vers le bas • Chaque case parcourue engendre un certain coût (>=0). Le coût total est la

somme de toutes les étapes • Quel est le coût du chemin le plus avantageux ? • Idée d'algo récursif :

- la solution jusqu'à (0, 0) est triviale - la solution jusqu'à (i, j) découle des solutions (i-1, j) et (i, j-1)

• Exemple :

si on sait que ça coûte 13 pour arriver jusqu'à (2,3) et 18 pour arriver jusqu'à (3,2), alors ça coûte 5+min(13,18) pour arriver jusqu'à (3,3)

• Cette fois, le "tableau des solutions" aura 2 dimensions. On doit le parcourir de telle façon que durant la visite de la case (i,j), les cases (i-1,j) et (i,j-1) auront déjà été traitées...

2 2 6 7

3 8 5 9 2 1 8 2

4 2 3 5

0 1 2 3 0 1 2 3

(i)

(j)

- 112 -

• Version récursive public static int minPath(int[][] t) { return minPath(t, t.length-1, t[0].length-1); } private static int minPath(int[][] t, int i, int j) { if (i <0 || j <0) return Integer.MAX_VALUE; if (i==0 && j==0) return t[0][0]; int a = minPath(t, i-1, j ); int b = minPath(t, i, j-1); return Math.min(a,b) + t[i][j]; }

• Version de programmation dynamique public static int minPath(int[][] t) { int n=t.length, m=t[0].length; int[][] minPathSol = new int[n][m]; minPathSol[0][0] = t[0][0]; for(int i=0; i<n; i++) ... for(int j=0; j<m; j++) ... ... return minPathSol[n-1][m-1]; }

• La correspondance entre version récursive et version par programmation

dynamique est parfois plus subtile...

2 4 10 17

5 12 15 24 7 8 16 18

11 10 13 18

0 1 2 3 0 1 2 3

Page 23: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 113 -

Exemple 3 : Problème de la somme d'un sous-ensemble • Soient un entier k, et n entiers positifs a1, a2, ...an. Y a-t-il un groupe de ces n

entiers dont la somme vaut k ? • Passons directement à la solution de programmation dynamique • Le tableau des solutions est un tableau de booléens • Idée : quel est l'effet d'un nombre supplémentaire A sur le tableau des

solutions ? Pour chaque somme X qu'on pouvait atteindre, X reste atteignable, et (X+A) devient atteignable !

• Exemple : peut-on faire la somme 17 avec {5, 8, 6, 2} ? Non Avec 00,01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17 {} 00,01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17 {5} 00,01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17 {5,8} 00,01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17 {5,8,6} 00,01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17 {5,8,6,2}

- 114 -

Structures d'arbre • Liens hiérarchiques entre éléments Terminologie • Arbre (avec racine), sommet (= noeud), lien (= arc) hiérarchique • Racine, sommet intermédiaire, feuille. Forêt = collection d'arbres • Ancêtres/descendants, père/fils, frère. Taille d'un arbre (nbre de sommets) • Sous-arbres. Arbre quelconque. Arbre M-aire (au plus M fils par sommet) • Chemin d'un sommet à la racine. Hauteur d'un arbre (nombre de "niveaux") • Profondeur d'un sommet (= longueur du chemin à la racine) • Arbre binaire. Fils gauche/droit Représentation d'arbres binaires • Chaînage de noeuds :

arbre = ∅ ou 1 élt + 1 père + 1 ss-arbre gauche + 1 droit • Dans un tableau (représentation implicite) :

- chaque case contient un élt ou indique son absence. Racine en 0 - fils gauche du sommet i dans la case 2i+1; fils droit dans la case 2i+2 - parent du sommet i dans la case (i-1)/2

• Parfois, un chaînage plus simple suffit (seulement vers le haut/bas)

0 5

hauteur=3 taille=7

prof=1

racine

feuilles

arc

- 115 -

1

2 3

tree

itr

Représentation d'arbres quelconques • Chaînage de noeud : arbre = ∅ ou 1 élt + 1 père + 1 collection de ss-arbres • Au moyen d'un arbre binaire !

- principe du "fils gauche, frère droit" - attention à ne pas confondre la structure "logique" et "physique"...

Spécifications Java • Cf. discussions sur les listes : approche récursive/itérative, plusieurs

spécifications possibles, sommet/arc courant, problèmes de symétrie des opérations, sentinelles, intérêt/danger des itérateurs multiples...

• On peut proposer une abstraction, par ex. basée sur ces principes :

arbre entier, et itérateurs sur un arc courant, opérations sur le "bas" de l'arc, insertion à la feuille (cf. 2ème année)

• Souvent, on travaille directement avec les noeuds du chaînage

left right

elt parent

BTNode(s) class BTNode { Object elt; BTNode left, right, parent; public BTNode(Object e, BTNode l, BTNode r, BTNode p) { elt = e;left = l; right = r; parent = p; } }

1 2 3 4

5 6 7 8 9

- 116 -

Domaines d'application (très nombreux !) • File system, document balisés (HTML) arbre de fouille heap ensembles d'éléments triés files de priorité... • Un exemple de type abstrait "arbre quelconque" : librairie DOM (Document

Object Model) pour les arbres XML/HTML Parcours d'arbres • Le parcours d'arbre est une opération de base pour plusieurs algos • Comment "linéariser" un arbre ? Deux approches principales • En profondeur d'abord : "traiter tout un sous-arbre avant le suivant" • Mais quand faut-il traiter le parent ? 3 variantes :

- avant les ss-arbres : pré-ordre a-b-e-f-c-d-g-h-i - après les ss-arbres : post-ordre e-f-b-c-g-i-h-d-a - entre les 2 ss-arbres : in-ordre (seulement pour arbre binaire !)

e-b-f-a-g-d-i-h

a b

e f c d

h i

g

1 2 3

a b

e f d h

i g

1

5 6 8 >=1 >=1

9 3 2

6

3 1 5 <6 >6

7 9 8

Page 24: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 117 -

Pseudo-code récursif (version itérative avec pile : cf. exercices) :

void depthFirst(Node root) { Node n; if (root == null) return; processPreOrder(root.elt); for each son n of root depthFirst(n); processPostOrder(root.elt); }

• En largeur d'abord :

- "traiter niveau par niveau" a-b-c-d-e-f-g-h-i • Pseudo-code itératif, utilisation d'une file FIFO

void breadthFirst (Node root) { Node crt, n; Queue<Node> q = new Queue<>(); q.enqueue(root); while(! q.isEmpty()) crt = q.dequeue(); process(crt.elt); for each son n of crt q.enqueue(n); }

a b

e f c d

h i

g

- 118 -

Parcours en largeur avec file, arbre binaire

static void breadthFirst (BTNode t) { BTNode crt; Queue q = new Queue(); q.enqueue(t); while(! q.isEmpty()) { crt = (BTNode) (q.dequeue()); if (crt==null) continue; System.out.print(" "+crt.elt); q.enqueue(crt.left ); q.enqueue(crt.right); } }

Parcours en profondeur préordre récursif

static void recDepthFirst(BTNode t) { if (t==null) return; System.out.print(" "+t.elt); recDepthFirst(t.left ); recDepthFirst(t.right); }

- 119 -

Type abstrait "File de priorité" • Modèle d'organisation des données : "le plus prioritaire d'abord" • On peut prévoir un ordre subsidiaire (en cas de priorités identiques) : FIFO • Applications : gestion de processus, jobs d'imprimante, protocoles réseau... • Une spécification possible en Java : public class IntPtyQueue { public boolean isEmpty(); public void enqueue(int elt, int pty); public int consult(); // elt with highest (smallest) pty public int consultPty();// its pty public int dequeue(); // elt with highest (smallest) pty }

Implémentation 1 : liste non ordonnée (tableau ou chaînage) • Enqueue : insérer (elt, pty) toujours en fin de liste O(1) • Dequeue : trouver le minimum et l'enlever O(n) Implémentation 2 : liste ordonnée (tableau ou chaînage) • Enqueue : insérer (elt, pty) au bon endroit O(n) • Dequeue : retirer le premier O(1)

- 120 -

Implémentation 3 : tableau de files • Idée : maintenir une file d'éléments pour chaque niveau de priorité • Enqueue : ajouter à la file t[pty] O(1) • Dequeue : trouver le premier t[i] non vide et retirer O(1) (quoique...) • Inconvénients :

- priorité entières (et pas seulement "comparables") - si possible, intervalle des priorités connus à l'avance

(sinon, on doit parfois doubler le tableau) - si possible, intervalle restreint (sinon, on gaspille de l'espace mémoire)

Implémentation 4 : tas (heap, monceau, maximier) • Enqueue : ... O(ln n) • Dequeue : ... O(ln n) • Le heap est une structure de données simple et élégante, basée sur la structure

d'arbre (et discutée en 2ème)

0 1 2 3 4

pty 8 5

4 7

3

enqueue(4, 2); enqueue(8, 0); enqueue(7, 2); enqueue(3, 4); enqueue(5, 0);

t

IntPtyQueue

FifoQueues

Page 25: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 121 -

Structure de graphe • Liens (arc, arête, edge) quelconques entre éléments (sommet, vertex) Terminologie • Notation : G = (V, E) e = |E| n = |V|

(un graphe G est un ensemble V de n sommets, et un ensemble E de e arêtes reliant deux sommets)

• Graphe, sommet (= noeud), arc (= arête). Sous-graphe. Graphe complet • Sommets adjacents (= voisin), degré (in/out) d'un sommet. • Sommets/arcs colorés. Graphe pondéré (weighted). Graphe orienté • Graphe connexe. Chemin entre 2 sommets. Cycle (= circuit) • Graphe acyclique (= simple = sans cycle). Graphes isomorphes Représentation de graphes • Un chaînage naïf ne suffit plus : pas de sommet de référence, de "point d'entrée" • On supposera ici que chaque sommet possède un numéro unique • 3 approches possibles, influençant la complexité des opérations :

- liste de sommets et liste d'arcs - matrice d'adjacence : à chaque couple de sommets, la description du lien - listes d'adjacence : à chaque sommet, la liste des voisins

4

- 122 -

Une spécification possible en Java • Pas de consensus sur les listes; encore moins pour les arbres et graphes... • Chaque sommet a un numéro (0<=vid<n).

Le nombre de sommets est fixé une fois pour toutes (constructeur) • Possibilité de stocker une couleur (Object) sur les sommets et les arcs

public class Graph { Graph(int nbOfVertices, boolean isDirected); int nbOfVertices(); // vertex IDs in [0..n-1] int nbOfEdges (); Object vertexColor (int vid); void setVertexColor(int vid, Object c); void addEdge (int fromVid, int toVid, Object col); void removeEdge(int fromVid, int toVid); boolean isEdge (int fromVid, int toVid); Object edgeColor (int fromVid, int toVid); int inDegree (int toVid); int outDegree(int fromVid); int[] neighboursFrom(int fromVid); } public class WeightedGraph extends Graph { WeightedGraph(int theMaxSize); void setEdgeWeight(int fromVid, int toVid, int cost); int getEdgeWeight(int fromVid, int toVid); ...}

• Domaines d'application : circuits imprimés, réseau de communication, réseau

géographique, documents hypertexte... et tellement d'autres !

- 123 -

Mot caché • Problème : Soient une matrice row x col de caractères et un ens. de n mots

Trouver des mots dans cette matrice, dans les 8 directions Algo 1 : Force brute • Idée : essayer de placer chaque mot partout et vérifier si ça marche

for each word W in the word list for each row R and column C for each direction D check if W exists at (R, C) in direction D

• Complexité : O(n * row * col * 8 * ??) Algo 2 : Recherche dichotomique • Idée : tester si chaque suite de caractères forme un mot existant

for each row R for each column C for each direction D for each word length L search the word list for that string

• Complexité : O(row * col * 8 * A * log n) A = longueurs possibles = (row+col)/2

- 124 -

Algo 3 : Détection des préfixes • Idée : ne tester que les suites qui commencent par un

préfixe valable • Si aucun mot anglais ne commence par "fg", pourquoi

essayer encore "fgd" et "fgdt" ? for each row R for each column C for each direction D for each word length L search the word list for that string if it is not even a valid prefix break the innermost loop (don't try longer L)

• Complexité : O(row * col * 8 * ??? * log n) Découpe en méthodes (découpe modulaire)

readWords(…) // from file to sorted array of strings readBoard(…) // from file to 2-dim array of char solveDirection(…)// given x, y, dir; output on stdout solvePuzzle(…) // code for algo 3 above prefixSearch(…) // binary search returning nearest pos

• Codage : sources de la série à disposition (inspiré de [Weiss98])

0 1 2 3 0 t h i s 1 w a t s 2 c a h g 3 f g d t

Page 26: Spécification 4 : arc courant Techniques de chaînagefrederic.bapst.home.hefr.ch/algo1/doc/c-algo1-solde-4up.pdf · 2019. 2. 4. · - 37 - • Pseudo-code principal du tri Shell

- 125 -

// Performs the binary search for word search using one // comparison per level. // Returns last position examined; // this position either matches x, or x is a prefix of the // mismatch, or there is no word for which x is a prefix. int prefixSearch(String[] a, String x) { int low = 0; // ∀i, a[i]<=a[i+1] int high = a.length - 1; while(low < high) { // P && Q int mid = (low+high) / 2; // low<= mid < high if(a[mid].compareTo(x) < 0) low = mid + 1; // P && Q else high = mid; // P && Q } return low; // low == high, donc (a[low] == x) OR (x ∉ a) } // P = "∀i<low, a[i]<x"; Q = "∀ i>high, a[i]>=x"

... // Usage example: int r = prefixSearch(theWords, theString); if (theWords[r].equals (theString)) // elt found else if (theWords[r].startsWith(theString)) // valid prefix else // not even !

- 126 -

Variante "Boggle/Ruzzle" (les mots peuvent "serpenter", pas de diagonale) • Il y a beaucoup de chemins... Précieuse optimisation par test de préfixe • Comment parcourir tous les chemins depuis une case (x,y) ? Deux idées :

- marquer les cases déjà visitées - tester récursivement les cases voisines (exemple de backtracking :

explorer récursivement toutes les combinaisons possibles) void testCell(int x, int y) { // isVisited = new matrix of boolean filled with false // testCellRec(x, y, isVisited, ""); } void testCellRec(int x, int y, bool[][] isVisited, String s) // if (x,y) has been visited, or is "out-of-bounds" // return // append to s the character at (x,y) // test if s is a valid word, // if yes print s // if s is not even a valid prefix // return // mark (x,y) as visited // testCellRec(x-1, y , isVisited, s) // testCellRec(x+1, y , isVisited, s) // testCellRec(x , y-1, isVisited, s) // testCellRec(x , y+1, isVisited, s) // mark (x,y) as unvisited

t h i w a t c a h

t h i w a t c a h

t h i w a t c a h

t h i w a t c a h

...

... ... ...

- 127 -

Aperçu des principaux types abstraits "collections d'éléments"

Opérations demandées Hypothèses s/éléments Nom du type abstrait

Accès par indice entier sur [0; n]

Last In First Out

First In First Out

Accès par valeur

Accès par valeur

Accès par valeur

Association clé-valeur

Classes d'équivalence

Accès au minimum (et fusions)

Gestion explicite de l'ordre

Gestion explicite de la hiérarchie

Gestion explicite de liens quelconques

Quelconques

Quelconques

Quelconques

Numérotés [0..petitNbre]

Fonction de hachage

Relation d'ordre

Numérotés [0..petitNbre]

Relation d'ordre

Quelconques

Quelconques

Quelconques

Vecteur (concret : tableau)

Pile

File

Ensemble - Bitset

Ensemble - Table de hachage

Ensemble - Arbre de fouille

Dictionnaire (cf. ensemble)

Ensembles disjoints

File de priorité (fusionnable)

Liste

Arbre (binaire)

Graphe