Cours d’informatique pour tous (2013-2014) Stéphane Flon Table des matières Introduction 5 partie A. Algorithmique et programmation 7 Chapitre I. Programmation 1. Principes généraux de programmation 2. Le langage Python 3. Auto-documentation 4. Types 5. Les identificateurs 6. Structures de contrôle 7. Programmation fonctionnelle 8. Quelques approfondissements en Python 9. Présentation de quelques traits de programmation 9 9 11 12 12 16 17 19 20 25 Chapitre II. Algorithmique 1. Algorithmes : principes généraux 2. Algorithmes de recherche 3. Que peut-on espérer d’un algorithme ? 4. Complexité d’un algorithme 29 29 30 31 31 partie B. Architecture des ordinateurs et représentation des nombres Chapitre III. Représentation des nombres 1. Introduction : représentation humaine des nombres entiers naturels 2. Représentation de l’information dans un ordinateur 3. La représentation des nombres entiers en informatique 4. La représentation des nombres réels partie C. Feuilles de TD 35 37 37 39 39 40 43 Feuille de TD 1. Premiers pas en Python 45 Feuille de TD 2. Représentation des nombres 1. Représentation des entiers naturels 2. Représentation des réels 49 49 50 Feuille de TD 3. Arithmétique 1. Problèmes élémentaires 2. Problème classiques 3. Tests de primalité et méthodes de factorisation 4. Cryptographie 51 51 51 52 52 3 Introduction 5 Première partie Algorithmique et programmation CHAPITRE I Programmation 1. Principes généraux de programmation 1.1. Caractéristiques d’un langage de programmation Un algorithme consiste en la donnée d’une démarche organisée afin d’effectuer une tâche précise. Par exemple, cette recette de key lime pie 1, peut être considérée comme un algorithme. Temps de p r é p a r a t i o n : 20 minutes Temps de c u i s s o n : 25 minutes I n g r é d i e n t s ( pour 6 p e r s o n n e s ) : − 200 g de p e t i t s −b e u r r e s − 5 citrons verts − 75 g de b e u r r e − 1 b o i t e de l a i t c o n c e n t ré s u c r é − 3 oeufs − sel P ré p a r a t i o n de l a r e c e t t e : P r é c h a u f f e r l e f o u r à 180C ( t h e r m o s t a t 6 ) . É m i e t t e r l e s p e t i t s b e u r r e p u i s a j o u t e r l e b e u r r e fondu e t mélanger . D i s p o s e r c e t t e p r é p a r a t i o n dans un p l a t . F a i r e c u i r e e n v i r o n 10 minutes à 180C ( t h e r m o s t a t 6 ) j u s q u ’ à c e que l a c r o û t e a i t une c o u l e u r d o ré e . Pendant c e temps , p ré p a r e r l a g a r n i t u r e : b a t t r e l e s j a u n e s d ’ o e u f s avec l e l a i t c o n c e n t ré s u c ré , a j o u t e r l e j u s de c i t r o n e t mélanger a f i n d ’ o b t e n i r une c o n s i s t a n c e crémeuse . V e r s e r l a g a r n i t u r e s u r l a p r é p a r a t i o n de b i s c u i t s e t l a i s s e r c u i r e e n v i r o n 20 minutes à 180C ( t h e r m o s t a t 6 ) j u s q u ’ à c e que l a crème de c i t r o n s o i t b i e n p r i s e . B a t t r e l e s b l a n c s d ’ o e u f s avec une p i n cé e de s e l e t l e s u c r e , j u s q u ’ à c e qu ’ i l s s o i e n t b i e n f e r m e s e t b r i l l a n t s . É t a l e r s u r l e g â t e a u e t r e m e t t r e au f o u r à 180C ( t h e r m o s t a t 6 ) e n v i r o n 5 minutes pour que l a meringue s o i t d o ré e . Servir bien f r a i s . On peut définir un programme comme un texte ou un fichier (que l’on appelle souvent code) exprimant un algorithme, dans une version intelligible par un langage de programmation : ce langage va alors analyser le fichier reçu, et le compiler (i.e. le réécrire une fois pour toutes d’une façon adaptée au langage machine), ou l’interpréter (le réécrire temporairement pour l’occasion en langage machine). La plupart du temps, les langages compilés sont plus rapides que les langages interprétés. 1. piquée chez Marmiton et à peine remaniée 9 1. PRINCIPES GÉNÉRAUX DE PROGRAMMATION CHAPITRE I. PROGRAMMATION Transcrire un algorithme –décrit en langue naturelle par un pseudo-code– en un programme dépendra évidemment beaucoup du langage choisi. Certains langages sauront digérer un léger remaniement de l’algorithme initial : ils sont dits de haut niveau. D’autres exigeront une version beaucoup plus proche du langage machine, et nécessiteront donc un effort de conversion de la part du programmeur : ils sont dits de bas niveau. Par exemple, étant donné l’algorithme minimaliste A f f i c h e r ” H e l l o World ” une version Python peut être donnée par print ( ’ H e l l o World ’ ) alors qu’une version en assembleur (fournie par Wikipedia) pourrait être . model s m a l l . s t a c k 100h . data bonjour db ” H e l l o world ! $ ” . code main proc mov AX, @data mov DS , AX mov DX, o f f s e t b o n j o u r mov AX, 0 9 0 0 h i n t 21h mov AX, 4 C00h i n t 21h main endp end main $ tandis qu’en langage machine, on aurait, en binaire 10111010 00000001 00001001 00100001 11100100 00010110 00000000 11001101 01001000 01101100 01101111 01010111 01110010 01100100 00100100 00010000 10110100 11001101 00110000 11001101 10111000 01001100 00100001 01100101 01101100 00100000 01101111 01101100 00100001 ( he ) ( ll ) (o ) (wo) ( rl ) (d ! ) ($)$ On constate donc que, pour cet exemple au moins, Python est de haut niveau, et on se félicite déjà du choix de ce langage pour apprendre la programmation. 1.2. Expressions et instructions Une instruction est un ordre donné à l’ordinateur (par exemple, assigner une certaine valeur à un certain identificateur, exécuter telle fonction en tel point, etc.). Une expression est une phrase (une chaı̂ne de caractères) définissant une valeur. 10 Stéphane FLON CHAPITRE I. PROGRAMMATION 2. LE LANGAGE PYTHON 1.3. Variables, identificateurs et fonctions Une variable est un objet que l’ordinateur doit stocker en mémoire, comme un nombre, une liste, ou même une fonction. Cela peut se faire de manière explicite, lorsqu’on déclare une variable en la liant à un identificateur : x = 456 Ici, la variable est 456, et son identificateur est x. Nous avons affecté (ou assigné) la valeur 456 à l’identificateur x. Cela peut aussi se faire de manière implicite, par exemple lorsqu’on demande à Python d’effectuer un calcul >>> s i n ( 1 2 3 4 ) 0.60192765476249732 Ici, Python a stocké temporairement le nombre 1234, afin d’évaluer l’expression proposée. On parle alors de variable temporaire. Remarque : il arrive souvent que, par abus de langage, on confonde variables et identificateurs. Comme en mathématiques, on définit aussi la notion de fonction, qui, lorsqu’on l’applique à un ou plusieurs arguments, renvoie une valeur. Dans l’exemple ci-dessus, j’ai appelé la fonction sin de Python, et j’ai passé 1234 en argument. Python m’a renvoyé une valeur approchée de cette expression. Lorsqu’une fonction est appelée, elle suit les instructions, puis renvoie une valeur. Cela dit, l’appel de cette fonction a pu modifier beaucoup d’autres choses, par exemple en affichant des résultats dans la console, en produisant un graphique ou un son, en modifiant l’identificateur passé en argument, etc. On parle d’effet de bord. Par exemple, print ne fait qu’afficher son ou ses arguments, elle ne renvoie pas de valeur. Pour illustrer ceci, j’utiliserai l’underscore , qui permet de rappeler le dernier résultat renvoyé par Python : >>> x = 123 # Je d é c l a r e l ’ i d e n t i f i c a t e u r x >>> y = 234 # Je d é c l a r e l ’ i d e n t i f i c a t e u r y >>> x + y # E x p r e s s i o n à c a l c u l e r 357 >>> # D e r n i e r r é s u l t a t r e n v o yé 357 >>> print ( x ∗ y ) # Je demande d ’ a f f i c h e r une e x p r e s s i o n , non de l a r e n v o y e r 28782 >>> # D e r n i e r r é s u l t a t r e n v o yé 357 Dans cet exemple, print, qui n’a agi que par effet de bord, n’a pas renvoyé de valeur : la dernière valeur renvoyée est donc bien 357. 2. Le langage Python 2.1. Principes Python est un langage interprété, à la fois pédagogique et puissant. Il est très utilisé dans les milieux scientifiques (informatique, mathématiques, physique, médecine, imagerie, etc.) et dans l’industrie. Il est également très simple et clair. D’ailleurs, Python nous oblige presque à écrire des programmes clairs, puisque l’indentation y est obligatoire, c’est elle qui permet par exemple de définir le corps d’une boucle for ou while : toutes les instructions d’une même boucle seront indentées de la même façon. Le séparateur d’instruction standard est le point-virgule ; . 2.2. Quelques consignes de programmation – La première qualité d’un programme (qui renvoie le résultat attendu) est la lisibilité : ne cherchez pas à optimiser votre code par de petits arrangements cosmétiques. N’hésitez pas par exemple à rajouter des parenthèses facultatives, si elles permettent de faciliter la lecture. Remarque : passer d’un code très rapide à un code très lent peut être louable 2, mais cela ne se fera pas par de petits changements. Il faudra passer par des considérations assez théoriques de complexité. – Donnez des noms parlants à vos fonctions et variables., introduisez les au début. – Décomposez vos gros problèmes en petits, afin de gagner en lisibilité et en modularité. 2. et souvent nécessaire quand on travaille sur Project Euler . . . 11 Stéphane FLON 4. TYPES CHAPITRE I. PROGRAMMATION – Pour l’indentation, la coutume est d’utiliser quatre espaces par bloc, plutôt que des tabulations (mélanger les deux pourrait conduire à des erreurs de code invisibles ). – Ne surchargez pas programmes de tests inutiles : lorsqu’une fonction est censée prendre en argument un entier naturel par exemple, inutile de faire un test en entrée pour le vérifier, c’est vous qui testerez la fonction sur les bons objets (comme on dit we’re all consenting adults here , bien que je n’en sois pas sûr dans cette classe . . .). Quand vous développerez dans la vraie vie, ce sera une autre histoire. – De même, si votre fonction doit renvoyer un entier, faites en sorte qu’elle renvoie un entier, et non une chaı̂ne de caractère par exemple (du genre ’ le résultat cherché vaut 1345353’). – Factorisez vos démarches. Par exemple, vous ne devriez pas souvent employer un copier-coller au sein d’un même programme, écrivez plutôt une routine qui applique toutes les opérations aux différents objets. (Note pour moi-même : trouver un exemple simple) – Je ne sais pas encore à quoi ressembleront les sujets de concours, mais je suis sûr qu’ils ne mesureront pas la technicité en Python, ce dernier ne servant qu’à illustrer les concepts fondamentaux de l’informatique. Il n’est même pas sûr que vous ayez à écrire ne serait-ce qu’une ligne de Python. C’est pourquoi mon cours est volontairement très loin d’être exhaustif. Il est également évolutif (dites-moi si quelque chose vous semble inutile, mal expliqué, ou manquant). 3. Auto-documentation Pour obtenir de l’aide sur un objet, on peut exécuter la commande help ( nom de la fonction ) Par exemple, pour obtenir de l’aide sur la primitive 3 min, on peut effectuer : >>> h e l p ( min ) # Je demande l a do cumentation s u r l a f o n c t i o n min Help on b u i l t −in f u n c t i o n min in module builtin : min ( . . . ) min ( i t e r a b l e [ , key=f u n c ] ) −> v a l u e min ( a , b , c , . . . [ , key=f u n c ] ) −> v a l u e With a s i n g l e i t e r a b l e argument , return i t s s m a l l e s t item . With two or more arguments , return t h e s m a l l e s t argument . Bien sûr, Python nous aide si notre programme ne tourne pas : >>> def g ( x ) # J ’ a i o u b l i é l e ”: ” F i l e ”<s t d i n >” , l i n e 1 def g ( x ) ˆ Synta xError : i n v a l i d s y n t a x ou >>> 3 + [ 1 , 2 ] # On ne Traceback ( most r e c e n t F i l e ”<s t d i n >” , l i n e TypeError : unsupported p e u t pas a d d i t i o n n e r un e n t i e r a v e c une l i s t e call last ): 1 , in <module> operand type ( s ) f o r +: ’ i n t ’ and ’ l i s t ’ 4. Types En Python, le typage est implicite : il n’est pas nécessaire de déclarer le type des objets utilisés. Pour obtenir le type d’un objet, il suffit d’utiliser la fonction type : >>> type ( 4 2 ) <type ’ i n t ’> >>> type ( ” H e l l o World ”) <type ’ s t r ’> 3. Fonction intégrée dans le langage, built-in function en anglais. 12 Stéphane FLON CHAPITRE I. PROGRAMMATION 4. TYPES 4.1. Types élémentaires principaux 4.1.1. Le type entier. C’est le type ’ int ’, pour integer. Les principales fonctions associées aux entiers sont l’addition +, la multiplication ∗, la soustraction −, la division /, la division euclidienne // (donnant le quotient dans une division euclidienne), le reste % dans une division euclidienne, l’exponentiation ∗∗. En Python, la taille des entiers n’est pas limitée, dans la mesure où on ne travaille pas en précision 32 ou 64 bits. En Python 2 cela dit, un entier grand change de type, et devient un grand entier, de type ’long’. >>> type ( 1 2 3 4 ) <type ’ i n t ’> >>> type ( 1 2 3 4 ∗∗ 1 2 3 4 ) <type ’ l o n g ’> 4.1.2. Le type booléen. Un type essentiel, même si on ne se rend pas forcément compte qu’on l’utilise si souvent. Ce type ne possède que deux éléments, True et False. On dispose des connecteurs logiques usuels or, and, not, le ou exclusif ˆ. Remarque : le ou (resp. et) logique peut aussi s’écrire | (resp. &). Il y a en fait une différence subtile, testez True or (1 / 0 == 0) et True | (1 / 0 == 0). Remarque : le ou exclusif ˆ fonctionne pour les entiers (i.e. pour le type int), et fournit le ou exclusif bit à bit, mais il ne faut surtout pas le confondre avec l’exponentiation ! De même, les opérateurs & et | peuvent prendre en argument des entiers. Python dispose des opérateurs de comparaison suivants (dont les arguments ne sont pas nécessairement booléens, mais renvoyant des valeurs booléennes) : l’égalité == (à ne pas confondre avec l’affectation), l’inégalité !=, <, >, <= (inférieur ou égal), >= (supérieur ou égal). Remarque : ces opérateurs fonctionnent avec des objets de types variés, n’hésitez pas à les tester pour comprendre à quelles comparaisons ils correspondent. Remarque : il faut noter que, contrairement à beaucoup de langages, on peut regrouper plusieurs inégalités, et donc se passer de la conjonction dans certains cas : >>> 3 < 7 < 9 True >>> 3 < 8 > 2 True >>> 3 != 2 != 3 True Cependant, je ne suis pas sûr qu’utiliser cette syntaxe soit une bonne idée d’un point de vue pédagogique ... 4.1.3. Le type flottant. Le type float est celui des nombres à virgule flottante, que l’on peut considérer comme des approximations des réels. Comme d’habitude, en cas d’opérations mêlant flottants et entiers, le résultat renvoyé est de type flottant : >>> 3 . 5 + 3 . 5 7.0 >>> type ( ) # L ’ u n d e r s c o r e <type ’ f l o a t ’> r e n v o i e l e d e r n i e r r é s u l t a t Les opérateurs sont ceux donnés pour int >>> 1 2 . 5 // 2 . 3 5.0 >>> 1 2 . 5 % 2 . 3 1.0000000000000009 4.2. Les listes Nous abordons un type non élémentaire : les listes, ou listes chaı̂nées, sont obtenues à partir d’objets d’autres types. On peut par exemple créer une liste d’entiers >>> l i s t e = [ 1 , 3 , 6 , 9 ] >>> l i s t e [1 , 3 , 6 , 9] 13 Stéphane FLON 4. TYPES CHAPITRE I. PROGRAMMATION Remarque : on peut aussi créer les listes d’objets de types différents, et on peut même imbriquer des listes dans d’autres. Pour créer des intervalles d’entiers , on peut utiliser la primitive range : range(a, b, h) est la liste des entiers de a à b, dans l’ordre adéquat (indiqué par le signe de h), de pas h, et b exclu. Le terme initial a et le pas h sont optionnels 4, et valent respectivement 0 et 1 par défaut. >>> [1 , >>> [0 , >>> [] >>> [4 , range ( 1 , 4 ) 2 , 3] range (4) 1 , 2 , 3] range ( 4 , 1 ) r a n g e (4 ,1 , −1) 3 , 2] On obtient la longueur de la liste par la primitive len >>> l e n ( l i s t e ) 4 Dans notre exemple, la liste est de longueur 4 (i.e. a quatre termes). Ces termes sont indexés de 0 à len( liste ) − 1, et l’on y accède selon la syntaxe : l i s t e [ indice du terme ] >>> l i s t e [ 0 ] 1 >>> l i s t e [ l e n ( l i s t e ) − 1 ] 9 Cependant, ils sont aussi indexés par des indices négatifs, le terme d’indice −1 étant le dernier : >>> l i s t e [ −1] 9 >>> l i s t e [ −2] 6 Remarque : l’indexation ne se fait pas sur Z tout entier, liste [7] fournit par exemple une erreur dans notre cas. >>> l i s t e [ 7 ] Traceback ( most r e c e n t c a l l l a s t ) : F i l e ”<s t d i n >” , l i n e 1 , in <module> I n d e x E r r o r : l i s t i n d e x out o f r a n g e On peut attribuer une nouvelle valeur v à un terme d’indice i d’une liste liste selon la syntaxe liste [ i ] = v Remarque : nous verrons que cette modification se fait en place, voir le paragraphe 8.1. On peut aussi ajouter un nouveau terme nouveau terme à une liste, à sa droite, de la façon suivante : l i s t e . append ( nouveau terme ) En reprenant notre exemple : >>> l i s t e . append ( 1 6 ) >>> l i s t e [1 , 3 , 6 , 9 , 16] L’opérateur + concatène (ou accole ) les listes >>> l i s t e + [ 1 1 9 , 9 1 1 ] [ 1 , 3 , 6 , 9 , 16 , 119 , 911] 4. dans le cas de deux arguments donnés, ils sont interprétés comme a et b 14 Stéphane FLON CHAPITRE I. PROGRAMMATION 4. TYPES On peut trancher une liste entre deux indices i et j (ou i 6 j), en écrivant liste [ i : j ] : cela a pour effet de ne conserver que les termes d’indices i à j − 1. Illustration Dans le cas où on ne met pas d’indice i (resp. j), on commence à 0 (resp. on s’arrête à len( liste )). >>> >>> [3 , >>> [1 , >>> [6 , >>> [1 , l i s t e = [1 , 3 , 6 , 9 , 16] liste [1:3] 6] liste [0:3] 3 , 6] liste [2:] 9 , 16] liste [:] 3 , 6 , 9 , 16] Remarque : on peut même effectuer un tranchage (slicing en anglais) selon un certain pas k, en écrivant liste [ i : j :k] Il est même possible d’ajouter des termes dans une liste, tout en en supprimant d’autres, en utilisant le tranchage : >>> l i s t e [ 1 : 2 ] = [ 6 7 , 8 9 , 1 1 3 ] >>> l i s t e [ 1 , 67 , 89 , 113 , 6 , 9 , 16] Ici, j’ai remplacé la sous-liste liste [1:2] , c’est-à-dire [3] , par [67, 89, 113]. On peut tester l’appartenance d’un objet elt dans une liste par elt in liste , elt not in liste renvoyant le contraire. Remarque : on dispose de nombreuses autres fonctions, comme max, min, la suppression del, ou la multiplication d’une liste par un entier. 4.3. D’autres exemples de types 4.3.1. Le type complex. Les nombres complexes sont représentés en Python. Plus précisément, le nombre complexe a + ib (mis sous forme algébrique) peut s’écrire complex(a, b) ou a+bj. Les opérations standard +, ∗, −, / voire ∗∗ sont acceptées, le module est la primitive abs, et la conjugaison d’un complexe z peut s’écrire z.conjugate(). >>> z = 1 + 3 j # Notez l ’ a b s e n c e d ’ e s p a c e e n t r e 3 e t j >>> z . c o n j u g a t e ( ) (1−3 j ) >>> z (1+3 j ) 15 Stéphane FLON 5. LES IDENTIFICATEURS CHAPITRE I. PROGRAMMATION 4.3.2. Les uplets et les ensembles. Un uplet consiste en la donnée d’objets séparés par des virgules, (le plus souvent) encadrés par des parenthèses, et se comportent un peu comme les listes : >>> c o u p l e = ( 1 , 3 ) >>> t r i p l e t = ( 5 , 7 , 2 ) >>> c o u p l e + t r i p l e t (1 , 3 , 5 , 7 , 2) >>> c o u p l e [ 1 : 2 ] (3 ,) Un ensemble est la même chose, où l’on a remplacé les parenthèses par des accolades. Comme en mathématiques, l’ordre des éléments ne compte pas, ni leur éventuelle répétition >>> ensemble = { 2 , 3 , 5 , 7} >>> 4 in ensemble False >>> ensemble == { 5 , 3 , 3 , 5 , 2 , 7 , 7} True L’union, l’intersection, la différence s’écrivent respectivement |, & et −. La différence symétrique ∆ (donnée par A∆B = (A \ B) ∪ (B \ A)) s’écrit ˆ. 4.3.3. Les chaı̂nes de caractère. Les chaı̂nes de caractères sont délimités par des apostrophes ’ ou des guillemets”, et se comportent essentiellement comme les listes chaı̂nées. 4.4. Conversion de types En Python, pour convertir un objet en un objet d’un certain type, il suffit d’écrire nom du nouveau type ( o b j e t d e l a n c i e n t y p e ) >>> i n t ( 7 . 0 ) ; f l o a t ( 1 1 3 ) ; s t r ( 1 3 4 5 ) ; i n t ( ”5853 ” ) ; l i s t ( ’ Bonjour ’ ) 7 113.0 ’ 1345 ’ 5853 [ ’B ’ , ’ o ’ , ’ n ’ , ’ j ’ , ’ o ’ , ’ u ’ , ’ r ’ ] 5. Les identificateurs 5.1. Généralités Un identificateur est un nom donné à un objet. Pour déclarer une valeur à un identificateur, on utilise la syntaxe identificateur = valeur de la variable >>> a = 3 4 ; b = ”Au r e v o i r ” ; c = 3 . 1 4 # Je d é c l a r e l e s i d e n t i f i c a t e u r s a , b e t c >>> type ( a ) <type ’ i n t ’> >>> b ’Au r e v o i r ’ On ne confondra pas le symbole d’égalité, qui est une affectation (ou assignation), avec l’égalité mathématique : >>> a = 3 >>> 3 = b F i l e ”<s t d i n >” , l i n e 1 Synta xError : can ’ t a s s i g n t o l i t e r a l >>> a = 5 ; b = 7 ; a = b ; a , b (7 , 7) >>> a = 5 ; b = 7 ; b = a ; a , b (5 , 5) 16 Stéphane FLON CHAPITRE I. PROGRAMMATION 6. STRUCTURES DE CONTRÔLE Remarque : il n’est pas possible de déclarer un identificateur comme antécédent d’une certaine valeur par une fonction : >>> s i n ( a ) = 1 F i l e ”<s t d i n >” , l i n e 1 Synta xError : can ’ t a s s i g n t o f u n c t i o n c a l l Je ne m’étends pas sur les noms admissibles de variables : vous découvrirez à l’usage comment cela fonctionne, et cela suit des règles de bon sens. 5.2. Affectations conjointes Une spécificité de Python est la possibilité d’affectations simultanées de variables, sous l’une des formes suivantes : Assignation à plusieurs identificateurs d’une même valeur : >>> a = b = 57 >>> a ; b 57 57 Assignation en parallèle de valeurs à plusieurs identificateurs : >>> a , b = 5 7 , 68 >>> a ; b 57 68 Cette assignation en parallèle n’est pas si anecdotique que cela : elle permet par exemple l’échange de variables de manière élégante. >>> a = 1 2 3 ; b = 234 >>> a , b = b , a >>> a ; b 234 123 6. Structures de contrôle 6.1. La conditionnelle simple Elle admet la syntaxe suivante : i f condition : instruction 1 else : instruction 2 où condition est un booléen, instruction1 (resp. instruction2) une instruction effectuée si et seulement si condition est vraie (resp. fausse) >>> def f ( a ) : ... i f a >= 0 : ... print ( ”La r a c i n e c a r r é e de {} e s t {} ” . format ( a , s q r t ( a ) ) ) ... else : ... print ( ”Le nombre {} n ’ admet pas de r a c i n e c a r r é e r é e l l e ” . format ( a ) ) ... >>> f ( 3 ) ; f ( −1) La r a c i n e c a r r é e de 3 e s t 1 . 7 3 2 0 5 0 8 0 7 5 7 Le nombre −1 n ’ admet pas de r a c i n e c a r r é e r é e l l e Remarque : la partie en else est facultative. 17 Stéphane FLON 6. STRUCTURES DE CONTRÔLE CHAPITRE I. PROGRAMMATION Il est fréquent que l’on ait de plus nombreux cas à distinguer. On peut bien sûr utiliser des tests if emboı̂tés, mais on peut également utiliser une syntaxe permettant directement l’étude de plusieurs cas, grâce à elif , contraction de else if ( sinon, si ) : if condition 1 : instruction 1 e li f condition 2 : instruction 2 ... else : instruction n Il faut noter que si plusieurs des conditions sont satisfaites, seule l’instruction de la première à l’être sera exécutée 5 : >>> >>> ... ... ... ... ... ... ... ... A a = 3 if a > 1: print ( ”A”) elif a > 2: print ( ”B”) elif a > 0: print ( ”C”) else : print ( ”D”) 6.2. La boucle inconditionnelle : une première approche On présente ici une première approche de la boucle for . On se reportera à 8.3 pour découvrir des itérations plus sophistiquées. for i in r a n g e ( a , b ) : instruction 1 instruction 2 . . instruction n >>> f o r i in r a n g e ( 4 ) : ... print ( ” e n t i e r c o u r a n t ”) ... print ( i ) ... e n t i e r courant 0 e n t i e r courant 1 e n t i e r courant 2 e n t i e r courant 3 On peut par exemple programmer la suite de Fibonacci avec une boucle for, et grâce à une assignation parallèle : >>> def f i b o ( n ) : ... a, b = 0, 1 ... f o r i in r a n g e ( n ) : ... a , b = b , a+b ... return a 5. comme l’expression sinon, si le laisse entendre 18 Stéphane FLON CHAPITRE I. PROGRAMMATION ... >>> 5 >>> 8 >>> [0 , 7. PROGRAMMATION FONCTIONNELLE fibo (5) fibo (6) [ f i b o ( n ) f o r n in r a n g e ( 1 0 ) ] # Je demande l e s t e rm e s de f i b o d ’ i n d i c e s 0 à 9 , v o i r p l u s l 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34] 6.3. La boucle conditionnelle La boucle conditionnelle, ou boucle while, permet d’effectuer un passage en boucle tant que la condition d’entrée est satisfaite, selon la syntaxe suivante : while c o n d i t i o n : instructions >>> while i < 5 : ... print ( i ) ... i += 1 ... 1 2 3 4 7. Programmation fonctionnelle 7.1. Déclaration de fonctions La déclaration de fonctions suit la syntaxe suivante : def n o m d e l a f o n c t i o n ( argument 1 , . . . , argument n ) : instruction 1 . . . instruction k return v a l e u r a r e n v o y e r >>> def p y t h a g o r i c i e n ( a , b , c ) : # P ré d i c a t t e s t a n t s i un t r i p l e t e s t p y t h a g o r i c i e n ... return a ∗ a + b ∗ b == c ∗ c ... >>> p y t h a g o r i c i e n ( 3 , 4 , 5 ) True >>> p y t h a g o r i c i e n ( 4 , 9 , 1 2 ) False Remarque : une instruction à la suite de la ligne avec return ne sera pas exécutée lors de l’appel de la fonction : >>> def c a r r e 1 ( x ) : ... return x ∗ x ... print ( ” c a l c u l e f f e c t u é ”) ... >>> >>> def c a r r e 2 ( x ) : ... print ( ” c a l c u l non e n c o r e e f f e c t u é ”) ... return x ∗ x ... >>> c a r r e 1 ( 5 ) 19 Stéphane FLON 8. QUELQUES APPROFONDISSEMENTS EN PYTHON CHAPITRE I. PROGRAMMATION 25 >>> c a r r e 2 ( 5 ) c a l c u l non e n c o r e e f f e c t u é 25 7.2. Variables locales et globales 7.3. Le type NoneType et les procédures En réalité, il n’est pas nécessaire de finir la déclaration d’une fonction par un return >>> def b o n j o u r ( x ) : ... print ( ”Bonjour ” + format ( x ) ) ... >>> b o n j o u r ( ”Ada ”) Bonjour Ada Si l’on omet le return, la fonction est ce qu’on appelle une procédure, c’est-à-dire qu’elle ne renvoie rien de particulier, mais qu’elle agit uniquement par effet de bord. Pour des raisons de cohérence dans la définition des fonctions, les procédures sont malgré tout des fonctions, dont le résultat est un rien chosifié , appelé None, unique objet de type NoneType. Ainsi la fonction bonjour ci-dessus ne renvoie-t-elle pas une chaı̂ne de caractères, mais . . . rien ! >>> type ( b o n j o u r ( ”Alan ” ) ) Bonjour Alan <type ’ NoneType ’> L’exemple suivant illustre la nécessité de bien comprendre le type de la fonction considérée : >>> def f ( x ) : ... return s i n ( x ) ... >>> def g ( x ) : ... print ( s i n ( x ) ) ... >>> f ( 1 ) 0.8414709848078965 >>> f ( f ( 1 ) ) 0.7456241416655579 >>> g ( 1 ) 0.841470984808 >>> g ( g ( 1 ) ) 0.841470984808 Traceback ( most r e c e n t c a l l l a s t ) : F i l e ”<s t d i n >” , l i n e 1 , in <module> F i l e ”<s t d i n >” , l i n e 2 , in g AttributeError : sin Remarque : cela dit, une fonction qui n’est pas une procédure peut très bien agir par effet de bord, comme la fonction carre2 définie page 19. 8. Quelques approfondissements en Python 8.1. Égalité(s) de variables En Python, on peut effectuer essentiellement deux tests d’égalité : l’égalité structurelle et l’égalité physique. L’égalité structurelle est celle qui ressemble le plus à l’égalité au sens mathématique : elle compare des objets selon leur nature. L’opérateur correspondant est ==. >>> >>> >>> >>> a b c a = 512 = 456 = 512 == b 20 Stéphane FLON CHAPITRE I. PROGRAMMATION 8. QUELQUES APPROFONDISSEMENTS EN PYTHON False >>> a == c True >>> e n s e m b l e 1 = { 3 , 4 , 5} >>> e n s e m b l e 2 = { 5 , 3 , 3 , 3 , 4 , 4 , 5} >>> e n s e m b l e 1 == e n s e m b l e 2 True L’égalité physique est quant à elle beaucoup plus informatique : elle vérifie si deux identificateurs sont liés au même espace occupé en mémoire (pointent vers le même espace mémoire). Elle s’exprime à l’aide de is. >>> a >>> b >>> c >>> a False >>> a False = 512 = 456 = 512 is b is c Nous avons créé deux identificateurs a et c, dont les contenus sont mathématiquement égaux, mais qui pointent vers des emplacements mémoire différents. Bien entendu, l’égalité physique entraı̂ne l’égalité structurelle, mais la réciproque est fausse. Remarque : en créant séparément deux identificateurs égaux structurellement, on s’attend à ce qu’ils soient distincts physiquement, mais ce n’est pas nécessairement le cas : il ne faut pas le supposer a priori dans la programmation. >>> a >>> b >>> a True >>> a True = 42 = 42 == b # Réponse a t t e n d u e i s b # Réponse i n a t t e n d u e En réalité, on peut avoir accès à l’emplacement mémoire du contenu d’un identificateur avec la commande id(), qui renvoie un entier 6. >>> a = 387537743 >>> i d ( a ) # Je demande l ’ a d r e s s e mémoire v e r s l a q u e l l e a p o i n t e 60475352L >>> i d ( 3 8 7 5 3 7 7 4 3 ) # Je demande l ’ a d r e s s e mémoire d ’ un e n t i e r que Python doit stocker provisoirement 60475376L >>> i d ( 3 8 7 5 3 7 7 4 3 ) # Je r é i t è r e l e s o p é r a t i o n s 60475400L >>> i d ( a ) 60475352L Remarque : a is b et id(a) == id(b) sont logiquement équivalentes. Concernant le traitement des adresses mémoire, il convient de distinguer les objets mutables des autres : un objet est dit mutable 7 si on peut le modifier par certaines opérations tout en conservant son emplacement mémoire. Les listes et dictionnaires sont mutables, les entiers, booléens, flottants, uplets, chaı̂nes de caractères ne le sont pas. >>> a = 16585 >>> i d ( a ) 60477128L >>> a =986283 >>> i d ( a ) 60475352L 6. qui dépend évidemment du contexte physique, i.e. de l’ordinateur employé, et de son état lors de l’appel de la fonction 7. ou modifiable en place, ou simplement modifiable 21 Stéphane FLON 8. QUELQUES APPROFONDISSEMENTS EN PYTHON CHAPITRE I. PROGRAMMATION >>> l = [ 1 , 2 , 3 , 4 ] >>> i d ( l ) 103629512L >>> l [ 2 ] = 983636 # Je m o d i f i e l >>> i d ( l ) # l a c o n s e r vé l a même a d r e s s e mémoire 103629512L Attention cependant, un objet mutable ne conserve pas la même adresse mémoire quoi qu’il arrive : >>> l = [ 1 , 2 , 3 , 4 ] >>> i d ( l ) 103628936L >>> l . append ( 5 ) # J ’ a j o u t e 5 en queue de l >>> l , i d ( l ) # l a é t é m o d i f ié en p l a c e ( [ 1 , 2 , 3 , 4 , 5 ] , 103628936L) >>> l = l [ : ] # J ’ e f f e c t u e une c o p i e de l >>> l , i d ( l ) # l e s t s t r u c t u r e l l e m e n t inchangé , mais pas p h y s i q u e m e n t ( [ 1 , 2 , 3 , 4 , 5 ] , 103629320L) Dans cet exemple, la copie de l effectuée par l’instruction l=l [:]) est dite superficielle, car l’égalité n’est a priori que structurelle. Il faut cependant faire attention au cas où la liste comporte elle-même des éléments mutables, par exemple une liste ! >>> l 1 = [ 1 2 , 2 3 , ” H e l l o ” , [ 1 , 2 , 3 , 4 ] ] >>> l 2 = l 1 # Copie p h y s i q u e >>> l 3 = l 1 [ : ] # Copie s u p e r f i c i e l l e >>> l 1 [ 0 ] = 91 # M o d i f i c a t i o n d ’ un é lé m e n t non m u t a b l e de l 1 >>> l 1 [ 3 ] [ 1 ] = 7 # M o d i f i c a t i o n en p l a c e d ’ un é lé m e n t m u t a b l e de l 1 >>> print ( l 1 ) ; print ( l 2 ) ; print ( l 3 ) [ 9 1 , 23 , ’ Hello ’ , [ 1 , 7 , 3 , 4 ] ] [ 9 1 , 23 , ’ Hello ’ , [ 1 , 7 , 3 , 4 ] ] [ 1 2 , 23 , ’ Hello ’ , [ 1 , 7 , 3 , 4 ] ] La copie superficielle de l1 vers l3 est une copie par références (ou par pointeurs) : les éléments de l3 sont physiquement égaux à ceux de l1. C’est pourquoi la modification en place d’un élément de l1 a aussi modifié l’élément correspondant de l3. Pour effectuer une copie purement superficielle, on peut utiliser deepcopy 8 du module copy : >>> l 1 = [ 1 2 , 2 3 , ” H e l l o ” , [ 1 , 2 , 3 , 4 ] ] >>> from copy import deepcopy >>> l 4 = deepcopy ( l 1 ) >>> l 1 [ 0 ] = 91 # M o d i f i c a t i o n d ’ un é lé m e n t non m u t a b l e de l 1 >>> l 1 [ 3 ] [ 1 ] = 7 # M o d i f i c a t i o n en p l a c e d ’ un é lé m e n t m u t a b l e de l 1 >>> l 1 , l 4 ( [ 9 1 , 23 , ’ Hello ’ , [ 1 , 7 , 3 , 4 ] ] , [ 1 2 , 23 , ’ Hello ’ , [ 1 , 2 , 3 , 4 ] ] ) 8.2. Retour sur les fonctions On peut définir des arguments optionnels pour une fonction, en attribuant à certains arguments des valeurs par défaut. Pour ce faire, on utilise la syntaxe def f ( a r g 1 , . . . , arg n , o p t i o n 1 = v a l d e f a u t 1 , . . . , option m = v a l d e f a u t m ) ... >>> def f ( x , y , z = 0 ) : # z e s t o p t i o n n e l e t sa v a l e u r par d é f a u t e s t 0 ... return x + 2 ∗ y + 3 ∗ z ... >>> f ( 1 , 2 ) # z v a u t i c i 0 , sa v a l e u r par d é f a u t 5 >>> f ( 1 , 2 , 3 ) # z v a u t i c i 3 14 8. Cela sera très utile pour travailler sur les matrices 22 Stéphane FLON CHAPITRE I. PROGRAMMATION 8. QUELQUES APPROFONDISSEMENTS EN PYTHON Remarque : si possible, évitez d’utiliser des arguments par défaut mutables, car c’est un peu compliqué à gérer. On peut aussi appeler une fonction en passant les arguments par étiquettes, permettant de ne pas imposer un ordre de passage des arguments : >>> ... ... >>> ... ... >>> >>> 8 >>> 8 def e v a l u e f e n x ( f , x ) : return f ( x ) def cube ( x ) : return x ∗∗ 3 a = 2 e v a l u e f e n x ( f = cube , x = a ) e v a l u e f e n x ( x = a , f = cube ) Cela permet de ne pas avoir à se rappeler l’ordre de passage des arguments, mais impose de connaı̂tre leurs noms. 8.2.1. Définition d’une liste par compréhension. On peut se poser deux problématiques incontournables pour les listes : donner la liste des images d’une liste par une application, et filtrer une liste selon un prédicat (i.e. une fonction à valeurs booléennes). En Python, ces deux opérations sont extrêmement simples à écrire, et peuvent être regroupées selon la syntaxe [ f o n c t i o n ( e l t ) f o r e l t in l i s t e i f p r e d i c a t ( e l t ) ] Remarque : cela suit la définition par compréhension d’un ensemble 9 en mathématiques. Par exemple, si je pars de la liste [2, 3, 5, 7, 11, 13, 17], et que je veux l’élever au carré, j’écris >>> [ e l t ∗ e l t f o r e l t in l i s t e ] [ 4 , 9 , 25 , 49 , 121 , 169 , 289] Si je veux n’en conserver que les nombres congrus à 3 modulo 4, j’écris >>> [ e l t f o r e l t in l i s t e i f ( e l t \% 4 == 3 ) ] [3 , 7 , 11] Si je veux élever au carré ceux qui sont congrus à 3 modulo 4, j’écris 8.3. Retour sur les boucles inconditionnelles : itérateurs, itérables, et générateurs Nous avons vu un usage très limité de la boucle for, en se limitant à l’emploi de range. En fait, en Python 2, range produit une liste >>> l = r a n g e ( 1 0 ) >>> l [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9] La ligne for i in range(10) va indiquer à Python de donner pour valeur à i les termes successifs de cette liste range(10), dans l’ordre naturel. En réalité, cette façon de faire se généralise à n’importe quelle liste, pas seulement une liste constituée d’entiers successifs, selon la syntaxe attendue for i in l i s t e a e g r e n e r : instructions 9. ou plutôt d’une partie d’un ensemble donné 23 Stéphane FLON 8. QUELQUES APPROFONDISSEMENTS EN PYTHON CHAPITRE I. PROGRAMMATION >>> def somme1 ( l i s t e ) : ... s = 0 ... f o r i in r a n g e ( l e n ( l i s t e ) ) : ... s += l i s t e [ i ] ... return s ... >>> def somme2 ( l i s t e ) : ... s = 0 ... f o r e l t in l i s t e : ... s += e l t ... return s ... >>> l = [ 4 5 , 5 5 , 3 7 6 ] >>> somme1 ( l ) , somme2 ( l ) (476 , 476) Les deux fonctions somme1 et somme2 fournissent les mêmes résultats, mais la seconde est plus pythonique , et finalement plus naturelle une fois qu’on a compris le principe. Remarque : si on a besoin d’égrener la liste en sens inverse, on peut utiliser reverse. Si par exemple on veut évaluer le polynôme P = 3 + X + 2X 2 , représenté par la liste [3, 1, 2], en x = 2, on peut observer que P (x) = 3 + x(1 + 2x), et effectuer >>> >>> >>> ... ... >>> 13 v = 0 x = 2 f o r e l t in r e v e r s e d ( [ 3 , 1 , 2 ] ) : v = x ∗ v + elt v Remarque : Python permet d’itérer sur d’autres objets qu’une liste, par exemple une chaı̂ne de caractère : >>> >>> >>> ... ... ... L a >>> ... ... ... l a s = ”La malade p e d a l a mal ” f o r c in s : i f c != ” ” : print ( c ) , m a l a d e p e d a l a m a l f o r c in r e v e r s e d ( s ) : i f c != ” ” : print ( c ) , m a l a d e p e d a l a m a L Les objets sur lesquels Python peut itérer sont appelés itérateurs. Par souci de simplicité, nous ne nous étendrons pas sur leur usage. Remarque : s’agissant des itérateurs, Python 3 procède un peu différemment de Python 2. L’idée est grossièrement de ne pas stocker toute une liste en mémoire, mais plutôt ses éléments successivement. 24 Stéphane FLON CHAPITRE I. PROGRAMMATION 9. TRAITS DE PROGRAMMATION 9. Présentation de quelques traits de programmation 9.1. La programmation impérative C’est celle que tout le monde connaı̂t, qui donne des instuctions à l’ordinateur pour modifier son état. C’est le royaume des boucles conditionnelles (while), des boucles inconditionnelles (for), des embranchements (if), et des assignations. L’embranchement est utile si on sait distinguer les cas d’études possibles, et qu’il y en a un nombre limité. Exercice (Embranchement) 1 Écrire, à l’aide d’un test if, une fonction minimum renvoyant le minimum des deux entiers qu’elle prend en argument. 2 Écrire, à l’aide d’un test if, une procédure racines prenant en arguments trois réels a, b, c, et renvoyant une phrase donnant les racines complexes de aX 2 + bX + c. Remarque : on veut que tous les cas possibles pour le triplet (a, b, c) soient bien considérés. 1 La boucle inconditionnelle est à employer quand on a un nombre déterminé de calculs à faire, que l’on peut factoriser en une suite simple d’instructions. Exercice (Boucle inconditionnelle) 1 Définir, en utilisant une boucle for, une fonction prenant en arguments un réel a et un entier naturel n, et renvoyant an . 2 Définir, en utilisant une boucle for, une fonction fact prenant en argument un entier naturel, et renvoyant sa factorielle. 2 La boucle conditionnelle est utile quand on a un nombre indéterminé de calculs à faire, que l’on peut factoriser . Exercice (Boucle conditionnelle) 1 En utilisant une boucle while, écrire une fonction logarithme binaire, qui à un réel strictement positif a associe min{n ∈ N, 2n > a} 3 2 En utilisant une boucle while, écrire une fonction plus petit qui, à un entier naturel n > 2, associe son plus petit diviseur premier Remarque : on évitera d’employer une boucle conditionnelle quand on connaı̂t le nombre exact d’opérations à faire (i.e. quand on pourra utiliser une boucle inconditionnelle). 9.2. Programmation récursive De manière informelle, une fonction est dite récursive si elle s’appelle elle-même dans sa définition. Par exemple, on peut programmer la fonction factorielle de la façon suivante : >>> def f a c t ( n ) : ... i f n <= 0 : ... return 1 ... else : ... return n ∗ f a c t ( n − 1 ) >>> f a c t ( 5 ) 120 Pour calculer fact (5), Python appelle la fonction fact et lui passe 5 en argument. Il se trouve donc dans le second cas de l’embranchement, et doit donc renvoyer 5 · f act(4). Il fait donc passer 4 en argument à fact, etc. 25 Stéphane FLON 9. TRAITS DE PROGRAMMATION CHAPITRE I. PROGRAMMATION jusqu’à lui faire passer 0, qui le place dans le premier cas de l’embranchement, et permet de proche en proche de remonter à fact (5). Exercice (Récursivité) 1 Définir le minimum d’une liste de manière récursive. 2 Définir une fonction renvoyant la décomposition d’un entier n > 2 en produit de facteurs premiers sous forme de liste (on pourra utiliser la fonction plus petit ci-dessus). Remarque : on pourra aussi si on veut reprogrammer la fonction plus petit de manière récursive, mais c’est moins naturel. 4 9.3. Programmation fonctionnelle La programmation fonctionnelle, qui dans sa version la plus pure s’oppose à la programmation impérative, conçoit un programme comme une succession d’appels de fonctions. Python permettant tous ces styles de programmation, nous nous autoriserons de les mélanger. Cela dit, on peut retenir de l’approche fonctionnelle l’idée de concevoir des fonctions, et surtout des sous-fonctions pour répondre à nos problèmes. Plus concrètement, lorsque l’on fera face à un problème informatique relativement compliqué, on pourra commencer par écrire la fonction principale, censée répondre à la question, en s’autorisant en son sein l’utilisation de sous-fonctions, non encore définies, au nom clair et explicite. Ensuite seulement, on s’attachera à programmer ces sous-fonctions (éventuellement en créant de nouvelles sous-fonctions). Cette approche, dite descendante (top-down en anglais), a pour avantage de morceller le problème en sousproblèmes beaucoup plus simples, et facilite l’organisation de ces sous-problèmes. Elle permet aussi le partage des tâches, chacun des membres d’une équipe s’attelant à un sous-programme précis, et la réutilisation, puisque beaucoup des sous-fonctions seront communes à beaucoup de fonctions. Enfin, elle confine l’apsect bas niveau à certaines sous-fonctions seulement, ce qui rend le programme plus lisible et non tributaire, pour une large part, des spécifités du langage, des aspects bas niveau , ou du choix des structures de données. On dit que le programme a une plus grande portabilité. Remarque : poussé à l’extrême, ces principes de programmation conduisent à la programmation orientée objet. Elle a pour principal inconvénient de conduire à une inflation de fonctions. Par exemple, pour dresser la décomposition en produit de facteurs premiers d’un entier n > 2, on peut se demander comment la connaissance du plus petit d’entre eux nous permet de trouver cette décomposition (de manière récursive ou impérative), puis on cherche à programmer une fonction donnant ce plus petit facteur premier. Une particularité de la programmation fonctionnelle est qu’elle permet à des fonctions de prendre en argument des fonctions, ou de renvoyer des fonctions. Les fonctions de ce type sont appelées fonctionnelles. Exercice (Fonctionnelle) Programmer les fonctionnelles données par : 1 ajoute(f ) = (x 7→ f (x + 3) + 2). 2 compose(f, g) = f ◦ g. 5 9.4. Un exemple : l’ajout de zéros dans une liste On se propose d’écrire des fonctions qui prennent en argument une liste, et renvoient la liste obtenue en intercalant un 0 entre tous les termes consécutifs de la précédente. Programmation impérative, boucle inconditionnelle, création de la liste résultat par constructions progressives : >>> def a j o u t 1 ( l i s t e ) : ... resultat = [ ] ... f o r e l t in l i s t e : ... r e s u l t a t += [ e l t , 0 ] 26 Stéphane FLON CHAPITRE I. PROGRAMMATION ... 9. TRAITS DE PROGRAMMATION return r e s u l t a t [ : − 1 ] Programmation impérative, boucle inconditionnelle, création de la liste résultat de la bonne taille, puis modification par effets de bord de celle-ci : >>> def a j o u t 2 ( l i s t e ) : ... r e s u l t a t = [ 0 f o r e l t in r a n g e ( 0 , 2 ∗ l e n ( l i s t e ) − 1 ) ] ... f o r i in r a n g e ( l e n ( l i s t e ) ) : ... resultat [2 ∗ i ] = l i s t e [ i ] ... return r e s u l t a t Programmation impérative, boucle inconditionnelle, utilisation du slicing (version assez pythonique), modification de l’argument par effet de bord : >>> def a j o u t 3 ( l i s t e ) : ... f o r i in r a n g e ( l e n ( l i s t e ) − 1 , 0 , −1): ... liste [ i : i ] = [0] ... return l i s t e Programmation récursive : >>> def a j o u t 4 ( l i s t e ) : ... i f l e n ( l i s t e ) <= 1 : ... return l i s t e ... else : ... return a j o u t 4 ( l i s t e [ : − 1 ] ) + [ 0 , l i s t e [ − 1 ] ] Programmation impérative, boucle conditionnelle >>> def a j o u t 5 ( l i s t e ) : ... resultat = [ ] ... while l i s t e != [ ] : ... r e s u l t a t = [ l i s t e [ −1] , 0] + r e s u l t a t ... l i s t e = l i s t e [: −1] ... return r e s u l t a t [ : − 1 ] Résultats : >>> l i s t e = r a n g e ( 1 , 1 0 ) ; a j o u t 1 ( l i s t e ) ; l i s t e ; [1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 , 5 , 0 , 6 , 0 , 7 , 0 , 8 , 0 , 9] [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9] >>> l i s t e = r a n g e ( 1 , 1 0 ) ; a j o u t 2 ( l i s t e ) ; l i s t e ; [1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 , 5 , 0 , 6 , 0 , 7 , 0 , 8 , 0 , 9] [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9] >>> l i s t e = r a n g e ( 1 , 1 0 ) ; a j o u t 3 ( l i s t e ) ; l i s t e ; [1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 , 5 , 0 , 6 , 0 , 7 , 0 , 8 , 0 , 9] [1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 , 5 , 0 , 6 , 0 , 7 , 0 , 8 , 0 , 9] >>> l i s t e = r a n g e ( 1 , 1 0 ) ; a j o u t 4 ( l i s t e ) ; l i s t e ; [1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 , 5 , 0 , 6 , 0 , 7 , 0 , 8 , 0 , 9] [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9] >>> l i s t e = r a n g e ( 1 , 1 0 ) ; a j o u t 5 ( l i s t e ) ; l i s t e ; [1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 , 5 , 0 , 6 , 0 , 7 , 0 , 8 , 0 , 9] [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9] 27 Stéphane FLON CHAPITRE II Algorithmique 1. Algorithmes : principes généraux Lors d’une première approche, on peut penser que face à un problème donné, que l’on souhaite traiter informatiquement, se pose avant tout le choix du langage dans lequel on programmera, ainsi que celui de la machine avec laquelle on travaillera. Cependant, avant de passer du problème, posé dans le langage naturel (comme trier les notes des étudiants de manière croissante, rendre la monnaie, savoir dans quel ordre un voyageur va parcourir une liste de sites touristiques), au programme dans le langage choisi, il faudra décrire la manière dont nous le traiterons. Pour le tri de notes par exemple, on peut d’abord chercher la plus grande, puis, dans la liste restante, la suivante, etc. : c’est un algorithme de tri dit par sélection. On peut aussi créer une nouvelle liste, dans laquelle on insère progressivement les notes de la liste initiale, en respectant l’orde imposé : c’est le tri par insertion. Concernant le rendu de monnaie, vous avez déjà vu l’algorithme glouton. Un algorithme consiste donc en un mode de traitement non ambigü d’une information, donnée en entrée, produisant une certaine sortie, traitement que l’on décrira dans un langage à mi-chemin entre le naturel et celui choisi pour programmer (on parlera de pseudo-langage), par exemple, pour la recherche du plus grand élément d’une liste d’entiers Entrée : l i s t e d ’ e n t i e r s L S o r t i e : l e maximum de L V a r i a b l e s : maxi , i maxi <− L [ 1 ] Pour i a l l a n t de 2 à T a i l l e L i s t e (L) F a i r e : S i L [ i ] < maxi maxi <− L [ i ] Fin S i Fin Pour Renvoyer maxi Remarque : on peut facilement adapter cet algorithme afin qu’il renvoie le premier (resp. le dernier) indice pour lequel on atteint le maximum. Faites le en exercice. Remarque : j’ai fait exprès d’indexer mes tableaux en commençant à 1, et non à 0 comme en Python, et de ne pas mettre de : après le test Si. Cela n’a en fait aucune importance, l’écriture d’un algorithme tolère certaines imprécisions, puisque la syntaxe est ici secondaire. Cet algorithme est suffisamment précis pour décrire l’approche choisie pour aborder le problème, mais il ne rentre pas dans les détails techniques d’implémentation dans un langage donné. Cela a pour avantages de mieux saisir ce que l’on fait, et de pouvoir s’adapter à à peu près tous les langages de programmation. Par exemple, dans Python, on peut utiliser des techniques propres au langage, comme le slicing ou la définition en compréhension d’une liste, pour transcrire un algorithme, mais un néophyte dans ce langage ne comprendrait pas forcément la philosophie derrière notre programme. Remarque : si vous trouvez que l’algorithme proposé en exemple ressemble beaucoup au programme que nous écririons en Python, vous avez raison, et c’est dû à la grande lisibilité de Python, ainsi qu’à son aspect haut niveau. Je vous proposerai dans ce cours des algorithmes écrits tantôt en pseudo-langage, tantôt en Python : entraı̂nez-vous à passer de l’un à l’autre. 29 2. ALGORITHMES DE RECHERCHE CHAPITRE II. ALGORITHMIQUE Entraı̂nez-vous également à les modifier légèrement, par exemple pour qu’ils rendent un indice plutôt qu’un élément (d’une liste), ou en passant d’une boucle conditionnelle à une boucle inconditionnelle, etc. tout en vous demandant quels avantages et inconvénients ces changements produisent. Nous nous intéressons dans ce cours à des problèmes très courants et élémentaires. 2. Algorithmes de recherche 2.1. Recherche dans une liste et variantes On s’intéresse à la recherche d’un élément dans une liste : Entrée : L i s t e L , é lé m e n t e S o r t i e : un b o o lé e n dé t e r m i n a n t s i e e s t dans L Variable : i i <− 1 Tant que i <= T a i l l e L i s t e (L) e t L [ i ] != e F a i r e i <− i + 1 Fin Tant que Renvoyer i == T a i l l e L i s t e (L) + 1 Exercice (Calcul de la moyenne, de la variance, et de la médiane) Proposez des programmes Python (ou des algorithmes en pseudo-langage) de calcul de la moyenne, de la variance, et de la médiane d’une liste de flottants. 1 2.2. Recherche dans une liste triée Dans le cas où un tableau est trié, mettons dans l’ordre croissant, on peut très rapidement trouver si un élément donné se trouve dedans, en utilisant une recherche par dichotomie : Entrée : L i s t e L , é lé m e n t e S o r t i e : un b o o lé e n dé t e r m i n a n t s i e e s t dans L Variables : i , j i <− 1 j <− T a i l l e L i s t e (L) Tant que i != j F a i r e : S i L [ ( i + j ) // 2 ] < e F a i r e i <− ( i + j ) // 2 Sinon F a i r e j <− <− ( i + j ) // 2 Renvoyer L [ i ] == e Nous avons vu en préambule la recherche du maximum dans une liste de nombres Remarque : on peut naturellement adapter cet algorithme à la recherche par dichotomie du zéro d’une fonction continue et monotone (faites cependant attention à la contrainte de traiter avec des flottants). Faites le en exercice. 30 Stéphane FLON CHAPITRE II. ALGORITHMIQUE 4. COMPLEXITÉ D’UN ALGORITHME Exercice (Cas d’une chaı̂ne de caractères) Proposer un algorithme effectuant la recherche d’un mot dans une chaı̂ne de caractères. 2 3. Que peut-on espérer d’un algorithme ? Nous avons jusqu’à présent simplement proposé des algorithmes pour répondre à certains problèmes. On peut toutefois tenter de répondre à deux questions naturelles : – L’algorithme ainsi écrit fait-il vraiment ce qu’il est censé faire ? C’est le domaine de la preuve de programme (ou d’algorithme). – Cet algorithme est-il efficace, c’est-à-dire renvoie-t-il une réponse en temps raisonnable, et n’occupe-t-il pas trop de ressources ? C’est le domaine de la complexité (temporelle et spatiale). 3.1. Preuve d’un programme 3.1.1. Notion d’invariant de boucle. Cette notion permet de prouver la correction des segments itératifs, c’est-à-dire des boucles conditionnelles et inconditionnelles. L’idée consiste à chercher un prédicat Pk (i.e. une fonction à valeurs booléennes), fonction du nombre k de passages en boucle, que l’on prouverait (i.e. qui serait établi comme constant de valeur Vrai) par un raisonnement par récurrence : – (Initialisation) le résultat est vrai avant le premier passage en boucle (i.e. après zéro passage en boucle, soit encore en entrée de boucle). – (Hérédité) s’il est vrai après le k-ième passage en boucle, alors il est vrai au (k + 1)-ième. Le but étant bien sûr au final, en sortie de boucle, d’obtenir le résultat de correction du segment itératif. Dans le premier exemple d’algorithme donné ci-dessus, on peut proposer Pk : après le k-ième passage en boucle, maxi = max T [i], i ∈ [[1, (k + 1)]]. Exercice (Invariants de boucle) 1 Prouvez les segments itératifs des algorithmes précédents. 2 Écrire l’algorithme d’Euclide de manière impérative, et prouver cet algorithme à l’aide d’un invariant de boucle. 3 3.2. Comment prouver un programme récursif ? Nous n’entrerons pas dans les détails d’une preuve de programme récursif, mais on peut seulement essayer de comprendre les mathématiques sous-jacentes. Quand on écrit un programme récursif, comme celui permettant de programmer la factorielle, on peut effectuer un raisonnement par récurrence. Pour prouver la correction d’un algorithme d’Euclide récursif, on peut aussi réfléchir à une démonstration par récurrence, mais c’est plus délicat car cet algorithme prend en entrée deux entiers. Remarque : dans d’autres cas, pour des algorithmes utilisant des structures de données particulières (par exemple des listes, ou des arbres), on peut exploiter la récursivité de la structure elle-même. 4. Complexité d’un algorithme Nous avons proposé plusieurs algorithmes de recherche d’un élément dans une liste : l’un valable dans une liste quelconque, l’autre dans une liste triée. On peut donc appliquer ces deux algorithmes à une liste triée, et comparer leur efficacité. Intuitivement, le second semble plus rapide. Si c’est bien le cas, comment le prouver, et même, comment le formaliser ? Après tout, pour la recherche d’un élément dans un ensemble de 5 ou 10 notes, les deux algorithmes semblent répondre en des temps équivalents. Si on prend un tableau à 100, 1000, ou 10000 éléments, on commence à voir une différence de rapidité d’exécution. Si maintenant on prend un tableau à 109 éléments, le premier algorithme ne nous répondra pas alors que le second donnera une réponse instantanée. 31 Stéphane FLON 4. COMPLEXITÉ D’UN ALGORITHME CHAPITRE II. ALGORITHMIQUE Pour déterminer l’efficacité d’un algorithme, nous allons donc nous intéresser à son comportement asymptotique. De plus, nous ne nous intéressons pas vraiment au temps effectivement pris par une implémentation de l’algorithme sur une machine donnée dans un langage donné : en effet, ce temps ne donne d’indication que pour ce langage et cette machine. Nous voudrions conserver l’approche abstraite de l’algorithmique, i.e. du pseudo-langage dans lequel nous avons décrit l’algorithme. L’idée est donc de partir d’un paramètre n (éventuellement plusieurs), jaugeant la taille de l’entrée (par exemple le nombre de termes d’une liste ou d’une chaı̂ne de caractères, le nombre de bits d’un nombre entier, etc.), de compter un certain nombre d’opérations hn (par exemple le nombre d’ajouts (append) à une liste, le nombre de multiplications ou d’additions bit à bit, etc.), et d’estimer le comportement asymptotique de hn lorsque n tend vers l’infini : il est donc pertinent d’utiliser les relations de comparaison o, O et ∼. En fait, la relation la plus pertinente en est une autre, moins usitée en maths : la relation être de l’ordre de : Définition (Grand Theta) Soit u et v deux suites réelles, qui ne s’annulent pas à partir d’un certain rang. On dit que u et v ont même ordre et on note 4.a un = Θ(vn ) si un = O(vn ) et vn = O(un ). Il est clair qu’il sagit d’une relation d’équivalence, que un = Θ(v n ) si et seulement si les suites (vn /un ) et (un /vn ) sont bornées, si et seulement si la suite de terme général uvnn est majorée, et minorée par un réel strictement positif. Bien sûr, si un ∼ vn , alors un = Θ(vn ), et la réciproque est fausse. Remarque : pourquoi avoir préféré la relation Θ plutôt que ∼ ? – Tout d’abord, nous ne sommes intéressés que par l’ordre de grandeur : si pour un premier algo, hn ∼ n et si pour un second, hn ∼ 2n, le second ira asymptotiquement deux fois plus lentement que le premier, mais ce facteur deux n’est pas de nature à exclure le second au profit du premier, le gain n’est pas significatif. En revanche, si hn ∼ 10000n pour le premier, et hn ∼ n2 , le premier est asymptotiquement à privilégier. – Il y aurait quelque ridicule à faire un calcul extrêmement précis du nombre d’opérations effectuées : en effet, une multiplication et une addition ne coûtent pas nécessairement la même chose, donc nous ne devrions pas les compter avec le même poids. Il peut aussi y avoir des opérations que nous n’avons pas comptabilisées. Enfin, une implémentation un peu astucieuse peut faire passer de 3n à 2n opérations par exemple. – Il peut aussi être plus difficile de donner un équivalent, qui est une information plus fine qu’un ordre de grandeur. Selon la taille de données n, l’algorithme va effectuer un certain nombre de tâches, dont certaines auront un poids bien plus grand dans le temps d’exécution. Nous ne compterons que le nombre cn de ces opérations coûteuses. On dit qu’un algorithme est – logarithmique si cn est de l’ordre de log2 (n). – linéaire si cn est de l’ordre de n. – quasi-linéaire si cn est de l’ordre de n log n. – quadratique si cn est de l’ordre de n2 . – polynomial si cn est de l’ordre de nk , pour un entier non nul k. – exponentiel si cn est de l’ordre de an , où a > 1. Ces classes de complexité sont données en ordre croissant : les algorithmes exponentiels sont très peu utiles, les logarithmiques finissent en temps raisonnable pour n’importe quelle taille de l’entrée. Remarque : bien sûr, il faut tempérer ce jugement, puisqu’il peut y avoir des coûts occultes (si par exemple cn ∼ 10100 log2 (n), l’algorithme n’est pas si pratique que cela . . .). Remarque : en fait, on peut distinguer plusieurs types de complexité : – la complexité dans le meilleur des cas, i.e. la plus petite valeur possible de cn pour un objet de taille n. – la complexité dans le pire des cas, i.e. la plus grande valeur possible de cn pour un objet de taille n. – la complexité moyenne, i.e. la moyenne des valeurs de cn sur les différents objets de taille n (apparaissant selon une distribution de probabilité à préciser). 32 Stéphane FLON CHAPITRE II. ALGORITHMIQUE 4. COMPLEXITÉ D’UN ALGORITHME Exercice (Évaluations de complexité) Proposez des évaluations de complexité pour les algorithmes déjà étudiés (et éventuellement pour d’autres). 4 Pour vraiment bien comprendre la notion de complexité, il est recommandé de passer du temps sur le site project Euler, car souvent, l’algorithme naı̈f permettant de résoudre le problème met bien trop longtemps pour répondre à la question. L’ordinateur le plus puissant sur terre ne peut traiter un algorithme exponentiel (comme celui des tours de Hanoı̈) que pour une très petite taille de l’entrée, alors qu’un vieux PC traite un algorithme logarithmique instantanément : l’algorithmique, c’est de la technologie (et donc de l’argent, voir l’histoire de Google). Exercice (Un léger gain de temps) On cherche à donner un algorithme permettant de trouver à la fois le maximum et le minimum d’un tableau noté Tab de n éléments distincts. 1 Écrire un algorithme qui donne la place de l’élément minimum dans le tableau ainsi que sa valeur. 2 Combien de comparaisons effectue cet algorithme ? Quelle est sa complexité ? 3 Écrire ensuite un algorithme pour trouver les places et valeurs à la fois du minimum et du maximum dans ce tableau. 4 Pouvez-vous proposer un algorithme qui n’effectue que 3n 2 comparaisons ? 33 5 Stéphane FLON Deuxième partie Architecture des ordinateurs et représentation des nombres CHAPITRE III Représentation des nombres 1. Introduction : représentation humaine des nombres entiers naturels Avant d’aborder la représentation des nombres entiers dans un ordinateur, on peut réfléchir à notre propre représentation des nombres. On se pose la question du choix d’une représentation d’un entier naturel n au moyen de divers systèmes de numération. 1.1. Représentation par une infinité de symboles Supposons disposer d’une infinité de symboles ou d’objets différents, et qu’à chaque entier naturel corresponde un tel symbole, de façon injective. La représentation d’un nombre est alors donnée par le symbole correspondant. Ce système de numération est clairement inutilisable, car nous devrions nous entendre sur la correspondance entre les nombres et une infinité de symboles ! 1.2. Représentation par un symbole unique disponible en grande quantité Supposons disposer d’un symbole ou d’un objet u (des points, des cailloux, des boules, des barres verticales, etc.), présent en quantité infinie, et que nous voulions représenter un entier naturel n à l’aide de u : bien sûr, nous pourrions regrouper n exemplaires de u. C’est par exemple ce système de numération que nous employons sur les faces d’un dé (usuel), chaque face ayant pour valeur le nombre de points qu’on y trouve. Ce système est clairement très limité, et il devient très vite extrêmement difficile de bien faire la correspondance entre les nombres abstraits et leurs représentants, ou de comparer deux quantités (mettons 35 et 38 cailloux par exemple). Remarque : pour représenter 2n, on utilisera deux fois plus d’objets que pour représenter n. 1.3. La représentation décimale Le plus souvent, nous utilisons la représentation dite décimale des nombres. Par exemple, l’écriture 236 s’interprète comme le nombre 2 × 100 + 3 × 10 + 6, soit encore comme 2 × 102 + 3 × 101 + 6 × 100 . On observe notamment que dans cette représentation, à l’aide cette fois-ci d’un nombre limité de symboles (les dix chiffres), le poids d’un symbole dépend de sa position : dans le nombre 222, les trois chiffres 2 n’ont pas le même poids. L’idée est qu’un nombre n’aura pas un symbole propre, ce qui conduirait à une inflation de symboles et à une mémorisation impossible (cf. 1.1), mais une suite propre d’un ensemble restreint de symboles. Remarque : par commodité, on peut imaginer cetteX suite comme infinie (et donc tous les éléments sauf peutêtre un nombre fini sont nuls). Par exemple, 236 = dk 10k , où d0 = 6, d1 = 3, d2 = 2, et dk = 0 pour tout k>0 k > 3. Remarque : pour représenter 2n, on utilisera au plus un symbole de plus que pour représenter n. 1.4. La représentation fiduciaire La monnaie est un exemple très courant où l’on doit représenter un entier naturel par un ensemble de symboles ou objets (les pièces de monnaie et les billets de banque). Je parle d’entier naturel pour simplifier, en supposant que l’unité monétaire est l’objet de plus petite valeur, et que tous les autres en sont des multiples entiers. Par exemple, dans le cas de l’euro, l’unité est le centime d’euro, et toutes les pièces et billets ont pour valeur un multiple entier de cette unité. Je peux représenter 1236 en donnant un ensemble de pièces et billets dont la valeur totale sera de 1236 centimes d’euro. Pour ce faire, je peux choisir par exemple 37 1. INTRODUCTION CHAPITRE III. REPRÉSENTATION DES NOMBRES (1) de donner un billet de 10 euros, une pièce de deux euros, sept pièces de 5 centimes et une pièce d’un centime. (2) de donner 1236 pièces d’un centime. (3) de donner 618 pièces de deux centimes. Je peux encore le faire de bien d’autres manières (mais je n’ai qu’un nombre fini de manières de le faire). Cette non unicité est peu gênante pour l’échange pratique de monnaie, et est même plutôt un avanctage. Cependant, elle l’est beaucoup plus dans une perspective informatique : en effet, en informatique, il est très important de pouvoir tester l’égalité de deux objets. Pour cela, il faut s’entendre sur la signification de cette égalité : si deux billets de 10 euros et quatre billets de 5 euros ne sont pas les mêmes ensembles de billets, il représentent tous les deux la même valeur, à savoir 20 euros. Il nous faudrait donc dire qu’ils sont égaux de ce point de vue. Ces ensembles diffèrent par leur syntaxe (ils ne s’écrivent pas de la même façon), mais pas par leur sémantique (ils correspondent à la même valeur). Une façon de pallier ce problème est de choisir un représentant distingué d’un montant n, si possible de manière algorithmique, afin que tout le monde puisse appliquer la même méthode et obtienne ainsi le même représentant. On peut proposer d’utiliser un algorithme glouton. 1.5. L’algorithme glouton pour la représentation d’un entier par des pièces et billets Supposons qu’une monnaie M soit constituée de p pièces et billets, de montant v0 , v1 , . . ., vp−1 . On suppose que v0 = 1, que la suite (vi ) est strictement croissante, et que les pièces et billets de montant donné sont disponibles en quantités infinies. Exercice (Suite strictement croissante pour l’euro) 1 Donner cette suite dans le cas de la monnaie euro. Pour représenter un montant n, nous prenons le plus grand entier dp−1 tel que dp−1 vp−1 6 n : en termes mathématiques, dp−1 est le quotient (euclidien) de n par vp−1 . Nous sommes ramenés à représenter n0 = n − dp−1 vp−1 , qui est – par définition de dp−1 – compris entre 0 et vp−1 − 1. On applique à nouveau notre méthode, en cherchant le plus grand entier dp−2 tel que dp−2 vp−2 6 n0 , et on itère cette méthode jusqu’à arriver à v0 , qui permet d’arriver à 0. Par exemple, pour représenter 1236 en centimes d’euros avec l’algorithme glouton, on prendra un billet de 10 euros, une pièce de 2 euros, une pièce de 20 centimes, une de 10 centimes, une de 5 centimes, et enfin une d’un centime. Remarque : avec cet algorithme glouton, et dans le but de représenter un entier n, il n’est pas nécessaire d’avoir une infinité de chaque pièce ou billet de montant donné : en fait, il faut et il suffit d’avoir une infinité de représentants de vp−1 , et pour tout k ∈ [[0, p − 2]], d’avoir qk représentants de vk , où qk est le quotient (euclidien) de vk+1 − 1 par vk . Exercice (Quantité de pièces et billets pour l’euro) Donner, pour chaque valeur de pièce ou billet en euros, le nombre de ses représentants nécessaire et suffisant pour représenter n’importe quel entier naturel préalablement fourni. 2 1.6. Extension de la notion de base En réalité, la représentation décimale des entiers naturels suit exactement l’algorithme glouton : on dispose, pour tout k ∈ N, d’un objet de valeur 10k , ou plus précisément (vk )k∈N = (10k )k∈N . En suivant l’algorithme glouton, nous n’avons besoin que de 9 représentants de chaque valeur donnée 10k . Ce processus s’étend donc très largement, en prenant par exemple un entier a > 2 et la suite de ses puissances (chaque valeur ak devant être présente en a − 1 exemplaires) : c’est le système de numération en base a. Si (dk ) est la suite obtenue en appliquant l’algorithme glouton pour représenter n avec la suite (ak ), i.e. si s X X n= dk ak = dk ak , k>0 k=0 38 Stéphane FLON CHAPITRE III. REPRÉSENTATION DES NOMBRES 3. REPRÉSENTATION DES ENTIERS alors on notera a n = ds ds−1 . . . d0 . 2 Par exemple, 5 = 101 . Remarque : on pourrait même proposer des systèmes de numération avec une autre suite strictement croissante, comme (k!)k∈N . Vous pouvez vous amuser à représenter 555 par exemple avec ce système. Outre la base 10, les bases les plus utilisées sont – 2 (représentation binaire). – 8 (représentation octale). – 16 (représentation hexadécimale), les valeurs de 0 à 15 étant représentées par 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F . Exercice (Représentation dans diverses bases) Représenter 113 dans les base 2, 3, 5, 6, et 16. 3 2. Représentation de l’information dans un ordinateur En informatique, on privilégie la base 2 : un ordinateur est entre autres constitué d’une grande quantité de transistors, pour chacun desquels deux états individuels sont possibles : soit le courant passe, soit il ne passe pas. Cette limitation au binaire n’est en fait pas restrictive : pour représenter un état parmi N distincts, par exemple la note entière (et sur 20) d’un étudiant, il suffit de considérer m transistors, où 2m > N (dans notre exemple, cinq transistors suffisent). Une fois cette approche choisie, se pose la question de la représentation de l’information : en effet, toutes les informations stockées dans un ordinateur 1 le sont sous la forme d’une suite de transistors dans un certain état, allumés ou éteints. Il n’y a pas a priori de moyen de savoir la nature de l’information stockée. Chaque état d’un transistor est ce qu’on appelle un bit (contraction de binary digit) : il s’agit donc de l’unité élémentaire d’information en informatique, qui vaut par convention 1 si le courant passe, et 0 sinon. Ils sont souvent regroupés en octets (bytes en anglais), i.e. en séquences de 8 bits. Afin d’assurer une interopérabilité, il est donc essentiel de proposer des normes de représentation des informations. Nous nous intéressons à ces normes pour les entiers naturels, relatifs, et enfin pour les nombres réels. 3. La représentation des nombres entiers en informatique 3.1. Représentation des entiers naturels Évidemment, on ne peut espérer coder tous les entiers, en nombre infini, puisqu’un ordinateur ne comporte qu’un nombre fini de transistors. Plutôt que d’essayer d’en coder le plus possible, on préfère se limiter à une taille fixe d’octets pour représenter un nombre restreint d’entiers : en effet, si on autorisait une taille variable, il faudrait savoir où sont les séparations quand on étudie une suite d’octets représentant plusieurs entiers. De plus, le processeur de l’ordinateur est optimisé pour effectuer des opérations sur des entiers codés en taille fixe. Les entiers sont généralement codés en 16, 32 ou 64 bits : on s’attend à ce qu’un codage en n bits nous permette de représenter 2n entiers. C’est très facile si on souhaite coder les entiers de 0 à 2n − 1, en associant à un entier la liste de ses chiffres (digits en anglais) en binaire. Exercice (Opérateurs liés aux représentations binaires) Comprendre, à la lumière de ce choix de représentation, les résultats de a&b, a ˆ b, a | b, où a, b ∈ N. Si on n’a pas su effectuer la conversion binaire, on pourra utiliser la primitive bin de Python. 4 1. Par exemple des textes, des fichiers musicaux, des vidéos, des mots de passe, l’état de tel ou tel périphérique, etc. 39 Stéphane FLON 4. REPRÉSENTATION DES RÉELS CHAPITRE III. REPRÉSENTATION DES NOMBRES 3.2. Représentation des entiers négatifs Qu’en est-il pour les entiers négatifs ? On aimerait coder des entiers positifs et négatifs. Une première idée consisterait à isoler un bit pour qu’il détermine le signe, mettons 0 pour + et 1 pour −. Cette idée comporte plusieurs défauts : (1) 0 est codé par deux séquences distinctes (puisque +0 = −0 ). (2) On ne code que 2n − 1 entiers (puisque 0 est codé deux fois). (3) Le test d’égalité à 0 est bancal (puisque 0 n’a pas qu’un seul représentant). (4) L’addition ne se fait pas comme d’habitude, par addition bit à bit et retenues (le bit associé au signe a un comportement particulier). Une idée plus efficace consiste à coder un entier par son reste dans la division euclidienne par 2n : plus précisément, les entiers de 0 à 2n−1 − 1 sont codés par leur représentation usuelle, et un entier x compris entre −2n−1 et −1 est codé par la représentation de 2n + x. Remarque : avec cette représentation, il est facile de voir si on travaille avec un entier positif ou négatif, puisqu’il suffit de considérer son bit de plus grand poids (1 pour les négatifs, 0 pour les positifs). C’est d’ailleurs pour cela que nous avons choisi de représenter −2n−1 plutôt que 2n−1 . Cette représentation est appelée le complément à 2. Exercice (Complément à 2) Écrire les différentes représentations en complément à 2 sur 4 bits. 5 Exercice (Représentations d’entiers) Écrire un programme Python fournissant la représentation en complément à deux en 64 bits (on testera son programme sur des exemples variés). 6 Exercice (Passage à l’opposé) Pn−1 En observant que 2n = 1 + k=0 2k , expliquer une façon très simple d’obtenir la représentation de −x à partir de celle de x (on pourra utiliser l’opérateur ˆ). 7 Exercice (Limitation des entiers et nombre de bits) Écrire un programme permettant de déterminer l’architecture de votre machine (32 ou 64 bits), fondée sur la sortie d’entiers trop grands du type int en Python 2.x (en Python 3, on ne passe pas du type int au type long). Remarque : dans beaucoup d’autres langage de programmation, l’addition par exemple est une loi de composition interne sur les objets de type entier, les entiers étant vus comme des éléments de Z/(2n )Z. 8 4. La représentation des nombres réels De la même manière que pour les entiers, il serait illusoire de vouloir représenter tous les réels. En fait, la situation est bien pire, puisque R n’est même pas dénombrable (i.e. il n’est pas en bijection avec N). En réflechissant à l’avantage que nous pourrions avoir à utiliser des réels plutôt que des entiers, on arrive à deux types de représentations, avec leurs avantages et inconvénients : 40 Stéphane FLON CHAPITRE III. REPRÉSENTATION DES NOMBRES 4. REPRÉSENTATION DES RÉELS 4.1. Les nombres à virgule fixe Si nous voulons représenter des quantités physiques d’argent français, en prenant l’euro pour unité, nous sommes amenés à considérer des centièmes d’euros (i.e. des centimes), mais nous n’aurons pas à considérer des tiers ou des millièmes d’euros : deux chiffres significatifs (en base 10) nous donneront toujours la valeur exacte de l’argent dont nous disposons. On peut donc consacrer quelques bits à la partie décimale, les autres servant à déterminer (le signe et) la partie entière. On utilise encore un complément à deux pour la partie entière. Cette représentation, assez peu usitée car elle ne différe pas fondamentalement de la représentation des entiers. Elle est adaptée à un domaine où les calculs doivent être parfaitement exacts, en finance par exemple. 4.2. Les nombres à virgule flottante Plutôt que de vouloir représenter des réels régulièrement espacés, on peut s’inspirer de la notation scientifique, et représenter des réels à un nombre de chiffres significatifs près. Cela aura l’avantage de permettre de coder de très petits et de très grands réels au sein d’une même représentation. En simple précision (i.e. en 32 bits) selon le standard IEEE-754, on consacre un bit pour le signe s ∈ {−1, 1}, 23 bits pour la mantisse M ∈ [1, 2[ et 8 pour l’exposant E, afin de représenter (−1)s × M × 2E−127 Il y a donc environ 7 chiffres significatifs. Nous avons retranché 127 à l’exposant afin de représenter des réels très petits et d’autres très grands . Concrètement, pour coder un réel, le signe est le bit de gauche, on le convertit en binaire, on décale la virgule (d’où le nom de virgule flottante), on remplit par la gauche sur les 23 bits de droite par la partie fractionnaire de la mantisse 2, en rajoutant des 0 pour arriver à 23 s’il le faut. On décale l’exposant, le convertit en binaire, et on l’écrit sur les bits 2 à 8. Remarque : pour la précision en 64 bits (dite double), et toujours selon le standard IEEE-754, on consacre 11 bits à l’exposant, 52 à la mantisse, obtenant ainsi environ 16 chiffres significatifs. Exercice (Nombres à virgule flottante) Selon votre niveau, entraı̂nez-vous à représenter des nombres à virgule flottante, ou programmez la conversion d’un décimal en nombre à virgule flottante sous forme de liste de bits (et la conversion inverse). 9 4.3. Défauts de la représentation à virgule flottante Cette représentation étant fondée sur la notion de chiffres significatifs, elle représente les réels par des approximations : cela provoque des erreurs d’arrondis, rendant notamment les tests d’égalité et de comparaison imprécis. En fait, on peut retenir que pour les flottants, faire un test d’égalité n’a pas grand sens. Si on veut tester l’égalité de deux réels α et β représentés par des flottants a et b, on écrira quelque chose du genre |a − b| 6 ε pour une valeur adéquate liée à la précision (32 ou 64 bits). Il est donc clairement abusif de parler d’égalité. Les flottants sont le royaume de l’approximation, les entiers celui du calcul exact. Même pour les nombres décimaux, la représentation ne sera qu’approchée, car la représentation se fait en binaire (la partie fractionnaire est une somme de puissances de 2 d’exposants négatifs. >>> 0 . 1 + 0 . 2 0.30000000000000004 L’addition en flottant n’est pas associative ! >>> (0.000000000000001+1) −1 1 . 1 1 0 2 2 3 0 2 4 6 2 5 1 5 6 5 e −15 >>> 0.000000000000001+(1 −1) 1 e −15 2. la mantisse appartient à [1, 2[, et commence donc toujours par 1 : il est inutile de coder cette partie entière 41 Stéphane FLON 4. REPRÉSENTATION DES RÉELS CHAPITRE III. REPRÉSENTATION DES NOMBRES En première approche, on peut penser que ces erreurs d’arrondi ne posent pas de problème sérieux, mais dans certaines situations, elles peuvent s’accumuler tout en s’amplifiant, et finir par produire des résultats absurdes. Cela se produit par exemple quand on étudie un phénomène chaotique, très sensible aux conditions initiales (comme le fameux battement d’aile de papillon de Bornéo qui provoque un cyclone aux États-Unis). Remarque : certains calculs sur les flottants provoquent ce que l’on appelle un dépassement de capacité. Lorsque l’ordinateur ne sait pas quoi répondre, il répond NaN (Not a Number). Voici un exemple concret (emprunté à Sylvie Boldo) où les erreurs d’arrondi conduisent à un résultat éloigné de la véritable valeur (à savoir −0.827396) : >>> def f ( a , b ) : ... return 3 3 3 . 6 5 ∗ ( b ∗ ∗ 6 ) + ( a ∗ ∗ 2 ) ∗ ( 1 1 ∗ ( a ∗ ∗ 2 ) ∗ ( b ∗ ∗ 2 ) − ( b ∗ ∗ 6 ) − 121 ∗ ( b ∗ ∗ 4 ) − 2 ) + 5.5 ∗ (b∗∗8) + a / (2 ∗ b) ... >>> a = 7 7 6 1 7 . 0 ; b = 3 3 0 9 6 . 0 >>> f ( a , b ) −1.3141873685177936 e+26 Voici un autre exemple (encore emprunté à Sylvie Boldo), que vous comprendrez mathématiquement dans pas longtemps : Exercice (Un investissement rentable, ou pas) Votre banquier vous propose l’investissement suivant : – La première année, vous me donnez exp(1) − 1 euros. – L’année suivante, je prends un euro de frais, et je multiplie par deux. – L’année suivante, je prends un euro de frais, et je multiplie par trois. – ... – Après n années, je prends un euro de frais, et je multiplie par n. – Pour récupérer votre argent, il y a un euro de frais. Au bout de 50 ans d’investissement, combien d’argent cela vous fera-t-il gagner ? Testez plusieurs programmes sur plusieurs machines, avec des approximations plus ou moins fines de exp(1). 10 La représentation nécessairement imparfaite des nombres réels peut donc être source d’erreurs, qui ont parfois de lourdes répercussions concrètes : durant la première guerre du Golfe, un missile Patriot américain a décimé ses propres troupes à cause d’une approximation, bonne en première approche, mais qui s’est amplifiée. On comprend donc sans douleur que ce domaine constitue une branche très active de la recherche en informatique. 42 Stéphane FLON Troisième partie Feuilles de TD FEUILLE DE TD 1 Premiers pas en Python Exercice 1 (Simples calculs) Calculer les expressions suivantes : 0 p 1+ √ 17, 21 5, le reste de la division euclidienne de 31 2 par 17. Exercice 2 (Fonctions simples) 0 Programmer des fonctions carré et cube. Exercice 3 (Primitives Python) 0 Reprogrammer les primitives suivantes (sans les utiliser . . .) : 1 Valeur absolue 2 max, min de deux réels, d’une liste. Exercice 4 (Fonctionnelles) 1 evalue en 2(f)=f(2). 2 applique sur liste ( f )= ([l0 , . . . , ln ] 7→ [f (l0 ), . . . , f (ln )]). 3 maximum dans l intervalle(f)= (range(a, b) 7→ max(f (a), . . . , f (b − 1))). Exercice 5 (Filtre) 2 Écrire une fonction filtre, prenant en argument un prédicat et une liste, et renvoyant la liste ôtée de ses termes pour lesquels le prédicat est faux. Exercice 6 (Ajouts de zéros) 1 Écrire une fonction qui à une liste associe la liste dans laquelle on a intercalé un zéro entre tous les termes consécutifs. 2 Écrire une fonction qui à une liste associe la liste dans laquelle on a intercalé k zéros entre les termes d’indices k − 1 et k (pour tout k). CHAPITRE 1. TD 1 Exercice 7 (Parties d’un ensemble) Écrire une fonction parties qui à un ensemble fini associe l’ensemble de ses parties (on laisse le choix de la modélisation d’un ensemble fini). Exercice 8 (Lci sur les termes d’une liste) Écrire une fonction qui à une liste d’éléments d’un ensemble E, et à une loi de composition interne sur E, associe la composée des termes de la liste par cette loi (les parenthèses se mettant à gauche). Exercice 9 (Évaluation de polynômes) On modélise un polynôme par une liste de flottants, le terme d’indice k de la liste correspondant au coefficient d’indice k du polynôme. 1 Écrire une fonction evalue prenant en argument un couple P, x, et renvoyant la valeur (approchée) de P en x. 2 Écrire à nouveau une telle fonction, en suivant le schéma de Horner. Exercice 10 (Palindromes) Écrire une fonction palindrome prenant en argument une chaı̂ne de caractères, et déterminant s’il s’agit d’un palindrome (on supprimera les espaces et la ponctuation, et on ne tiendra pas compte de la casse). Exercice 11 (Tours de Hanoı̈) Tours de Hanoı̈ Exercice 12 (Suites récurrentes) 1 Programmer une fonction u, qui à n associe le terme un de la suite définie par le terme initial 1 et l’itératrice f : x 7→ ln(1 + x). 2 Programmer des fonctions u et v permettant de calculer les termes des suites u et v telles que u0 = v0 = 1, et pour tout n ∈ N : un+1 = un + 2vn et Exercice 13 (Approximation d’une suite par un réel) vn+1 = un ∗ vn 0 On considère la suite de terme général un = ln(n) n . Écrire une fonction rang qui prend en argument un réel strictement positif ε, et renvoie le plus petit indice n tel que |un | 6 ε. 46 Stéphane FLON CHAPITRE 2. TD 2 Exercice 14 (Somme maximale) Ecrire la fonction d’argument une liste de nombres L (d’au moins 3 nombres), qui retourne l’indice du premier élément du premier triplet consécutif de somme maximale dans L. Exercice 15 (Plateau maximal) Ecrire la fonction d’argument L une liste croissante au sens large de nombres entiers, et qui retourne l’indice du premier élément de la plus grande sous-suite constante. Exercice 16 (Suite ordonnée des nombres à petits diviseurs) Construire une fonction d’argument un entier naturel non nul, et qui retourne la suite ordonnée des n plus petits entiers de la forme 2p 3q 5r 47 Stéphane FLON FEUILLE DE TD 2 Représentation des nombres 1. Représentation des entiers naturels Exercice 1 (Algorithme glouton) 1 Programmer l’algorithme glouton, prenant en entrée un entier naturel et un système de numération sous forme de liste finie strictement décroissante d’entiers et terminant à 1. Exercice 2 (Conversion en base b) 0 Écrire une fonction convertir telle que convertir (a, b) écrive l’entier a (donné sous forme usuelle) en base b, sous forme de liste d’entiers. Exercice 3 (Nombre donné en base b) 0 Écrire une fonction valeur telle que valeur(L, b) renvoie la valeur de l’entier donné par la liste L dans la base b. Exercice 4 (Conversion entre bases) 0 Écrire une fonction changement de base prenant en argument une liste L, et deux entier b et c supérieurs ou égaux à 2, et renvoyant dans la base c l’entier donné par la liste L dans la base b. Exercice 5 (Opérations sur les nombres binaires) 2 Écrire des fonctions réalisant l’addition et la multiplication d’entiers naturels donnés en binaire. Exercice 6 (Représentation des entiers relatifs) 3 Reprendre les exercices précédents en travaillant sur des entiers relatifs (et leur représentation en complément à 2. 2. REPRÉSENTATION DES RÉELS CHAPITRE 3. TD 3 2. Représentation des réels Exercice 7 (Conversion de nombres en binaires) Écrire une fonction qui à un entier associe son écriture binaire (sous forme de liste par exemple). Écrire une fonction qui à un rationnel donné sous forme de couple d’entiers associe son écriture binaire en virgule flottante, à une précision donnée. Exercice 8 (Opérations sur les nombres binaires) Définir les opérations d’addition et de multiplication de nombres binaires, d’abord pour des entiers, puis pour des flottants. 50 Stéphane FLON FEUILLE DE TD 3 Arithmétique 1. Problèmes élémentaires Exercice 1 (Algorithmes élémentaires en arithmétique) 0 Reprogrammer 1 La division euclidienne. 2 Le calcul de pgcd et ppcm de deux entiers. 3 La recherche d’un couple de Bézout. Exercice 2 (Décomposition en produit de facteurs premiers) 0 Écrire une fonction premier diviseur telle que premier diviseur(n) renvoie le plus petit diviseur premier de n (où n > 2). Écrire une fonction donnant la décomposition d’un entier n > 2 en produit de facteurs premiers, sous forme de liste par exemple. 2. Problème classiques Exercice 3 (Crible d’Ératosthène) 2 Programmer une fonction qui à un entier n > 2 associe la liste des nombres premiers inférieurs ou égaux à n, à l’aide du crible d’Ératosthène. Exercice 4 (Nombres parfaits) 2 Un entier naturel n est dit parfait s’il est égal à la somme de ses diviseurs naturels stricts. Par exemple, 6 est parfait puisque 6 = 1 + 2 + 3. Écrire une fonction parfait testant si le nombre passé en argument est parfait. Trouver les nombres parfaits inférieurs à 106 . Exercice 5 (Nombres de Niven) Un entier positif divisible par la somme de ses chiffres est appelé nombre de Niven. Écrire une fonction niven déterminant si un nombre est de Niven. 3 4. CRYPTOGRAPHIE CHAPITRE 3. TD 3 Exercice 6 (Nombres complets) 3 Un nombre est dit complet si son carré utilise les 10 chiffres exactement une fois. Trouver les nombres complets. 3. Tests de primalité et méthodes de factorisation 0 Exercice 7 (Test de primalité de Fermat) 2 Exercice 8 (Test de primalité de Miller-Rabin) 2 Exercice 9 (La méthode p − 1) 2 Exercice 10 (La méthode ρ de Pollard) Test de primalité de Solovay-Strassen 4. Cryptographie 52 Stéphane FLON