Vous êtes sur la page 1sur 92

Programmation Python

Programme première année en classes


préparatoires aux études d'ingénieurs
Table des matières

Chapitre 1 : Introduction à Python


Chapitre 2 : Les types numériques en Python
Chapitre 3 : Programmation Python
Chapitre 4 : Les conteneurs à accès séquentiel (Tuples, listes et chaines)
Chapitre 5 : Les conteneurs à accès direct (Dictionnaires & ensembles)
Chapitre 6 : Programmer des fonctions sous Python
Chapitre 7 : La récursivité
Chapitre 8 : Analyse de complexité des fonctions
Chapitre 9 : Algorithme de recherche en Python
Chapitre 10: Algorithmes de tri en Python
Chapitre 11 : Les fichiers en python
Chapitre 12 : Les tableaux nupmy – simulation numérique en Python
Chapitre 1 : Introduction à Python
1.1. Introduction
Python est disponible sur son site officiel www.python.org :

Dans le cadre de notre cours, on va utiliser la version 3 du langage (bien que la version 2 est
encore utilisée mais elle présente certaines différences syntaxiques) qui est téléchargeable via
ce lien :

Il suffit de suivre les instructions pour installer correctement le système sur votre ordinateur et
Python est prêt à l’utilisation.

Belgacem Hajji ~1~


NB : Sachez qu’une version mobile est disponible pour androïd sous le nom de QPython :

Et une autre version disponible sur Apple store pour iOS intitulée Pythoni :

1.2. L’application IDLE de Python


On accède à Python soit en double cliquant sur l’icône nommée IDLE du bureau soit en en
utilisant le menu démarrer, la fenêtre suivante apparaît qui n’est autre que l’environnement de
développement intégré du langage :

Belgacem Hajji ~2~


Le symbole >>> est appelé l’invite de commandes ou le prompt, c’est l’endroit au niveau du
quel nous allons commencer à poser des questions à Python, on interagit avec l’environnement
en tapant des commandes :

Mais ce n’est pas l’unique manière de faire. Python permet aussi d’écrire des programmes
complets, on parle du mode script. Il suffit de tapez CTRL+N pour éditer un nouveau script :

Dans le chapitre 3, on détaillera d’avantage cette notion.

1.3. Le mode commande :


Python évalue différentes expressions introduites par l’utilisateur. Il suffit de taper une
commande suivie de la touche ENTREE pour obtenir le résultat de son évaluation.
NB : Tout ce qui suit le caractère # est considéré comme un commentaire et ne sera jamais
évalué.

Belgacem Hajji ~3~


1.4. Les variables en Python
On attribue une valeur à une variable en utilisant l’opérateur d’affectation =. Les variables sont
utilisées sans être déclarées et leurs types dépendent de leurs contenus. Une même variable peut
changer de type sans qu’il y ait d’erreurs :

Il est interdit d’utiliser une variable non initialisée (sans valeur de départ) dans une expression :

On peut effectuer des affectations simultanées (en même temps) de plusieurs variables :

Chaque variable possède un identifiant accessible via la commande id :

Pour tester si deux variables sont identiques (possédant le même identificateur), on utilise la
commande is :

Belgacem Hajji ~4~


1.5. Les fonctions intégrées
Pour interagir avec Python, on tape des commandes appelés aussi des builtins, ce sont des
commandes prêtes à l’utilisation et qui sont chargées d’une manière automatique en mémoire
de l’ordinateur (car on ne peut pas utiliser une commande qui n’est pas déjà en mémoire
centrale) :

• Valeur absolue :

• Conversion de types :

• Passage entre bases (binaires, hexa et octale) :

• Fonctions mathématiques :
 Puissance :

 Div et mod :

 Minimum et maximum :

 Arrondi :

Belgacem Hajji ~5~


 Nombres complexes :

• Codes ascii et caractères :

1.6. Le mode script


Taper des commandes ne suffit pas pour créer des programmes complets sous Python, c’est
pour cette raison, qu’on a recours au mode script. Pour ouvrir un nouveau script en Python, on
utilise CTRL+N ou le menu File>New File. Une page vierge apparait, où on peut éditer notre
programme. On va se contenter de créer un script sans trop comprendre l’utilité pour le
moment :

Une fois saisi, on utilise CTRL+S pour sauvegarder le script, il faut ne pas oublier de lui
attribuer un nom avec l’extension .py (par exemple script1.py). Le programme ainsi enregistré
est prêt à l’exécution, il suffit de taper F5 et on voit apparaître le résultat sur l’IDLE (et pas au
niveau du script) :

Belgacem Hajji ~6~


Chapitre 2: Les types numériques en Python

2.1. Introduction
Python permet de manipuler deux catégories de types :
- Les types simples : les entiers, les réels, les booléens, les nombres complexes
- Les types structurés appelés aussi conteneurs qui seront traités dans des chapitres
ultérieurs.
Dans ce chapitre, on va exposer les principales commandes applicables sur chacun des types
numériques cités précédemment ce qui nous mènera à aborder la notion de modules à savoir les
deux modules math et cmath.

2.2. Opérations communes :


L’ensemble des types numériques ont en commun un certain nombre d’opérateurs applicables
comme les opérateurs arithmétiques et aussi un certain nombre de commandes qu’on va exposer
dans la suite :

• Type d’une expression :


Pour déterminer le type d’une expression, on utilise la commande type :

• Opérateurs arithmétiques
 Addition +, soustraction -, multiplication *, division / et puissance ** :

Belgacem Hajji ~7~


 Quotient // et reste % d’une division euclidienne :

• Opérateurs avancés d’affectation : += ; -= ; *= ; /= ; //= ; %= ; **=

• Commandes sur les nombres complexes :


 Partie réelle

 Partie imaginaire

 Conjugué

Mais pas uniquement, on continuera à étudier d’autres commandes applicables sur le


corps C à la fin de ce chapitre.

Belgacem Hajji ~8~


2.4. Commandes du module math :
Dans le chapitre précédent, on a évoqué les builtins Python qui se chargent au moment du
démarrage de Python, mais ce ne sont pas les uniques commandes utilisables. Python
regroupent les commandes par thème dans des unités spéciales appelées modules. Un module
regroupe un ensemble de variables et de fonctions prêtes à l’utilisation. Il suffit d’importer le
module avec le mot clé import et d’appeler les fonctionnalités qu’il offre en utilisant une
notation préfixée de la forme nomModule.nomFonction ou nomModule.nomVariable.
Parmi les modules les plus importants, on note le module math qui regroupe un ensemble de
fonctions mathématiques définies sur le corps IR tels que la racine carré, les fonctions
trigonométriques, les fonctions log, exp…

Le contenu d’un module peut être affiché en utilisant la commande dir(nomModule) :

Et l’aide correspondant est accessible via la commande help(nomModule) :

…etc.

Belgacem Hajji ~9~


NB : on peut importer le contenu d’un module autrement :

 Dans ce cas, la notation préfixée n’est plus valide.


Parmi les fonctions les plus utiles du module math, on trouve :

• Racine carrée : sqrt


• Factorielle : factorial
• Fonctions trigonométriques : cos, sin, tan, asin, acos, atan
• Conversion des angles :degrees, radians
• log, exp : log,log10, log2
• fonctions d’arrondissement : floor, ceil
• troncature : trunc
NB : Le module math stocke aussi la valeur de π et de e.
Application : Essayer toutes les commandes du module math citées précédemment.

2.5. Commandes du module cmath :


D’une manière analogique, le module cmath offre toutes les fonctions mathématiques
applicables sur les nombres complexes :

Parmi les fonctions phares, on note :

• la forme polaire : polar

• l’argument : phase

• la forme algébrique à partir de la forme polaire : rect(module,argument)

2.6. Les booléens


C’est un type binaire comme on sait, les deux valeurs possibles sont True et False (attention à
la casse). Les opérateurs applicables sont and, or et not :

Belgacem Hajji ~ 10 ~
Chapitre 3 : Programmation Python
3.1. Introduction
Jusqu’à maintenant, on a utilisé Python en mode interpréteur, on tape une commande (une sorte
de requête) et on obtient le résultat correspondant. Cependant, Python permet comme les autres
langages de programmation d’éditer des scripts complets pouvant contenir des instructions
séquentielles, conditionnelles et des boucles.

3.2. Les structures séquentielles (simples)


• La saisie des données :
On utilise la commande input() qui quand rencontrée, le script attend jusqu’à ce que
l’utilisateur introduise une valeur qui peut importe son type sera comptée comme une
chaine de caractères. On peut documenter l’opération d’entrée en ajoutant un message
entre les () de input et qui sera affiché au moment de l’exécution de l’instruction :

La validation de la saisie est obtenue en tapant la touche ENTREE

On peut tout de même forcer la saisie d’un entier ou d’un réel directement en utilisant
respectivement les deux commandes int et float déjà rencontrées :

Parfois l’utilisateur se trompe sur la valeur introduite, l’instruction input déclenche une
erreur ou une exception, un message en rouge apparait indiquant la nature de l’erreur :

On parle d’erreurs d’exécution, on traitera les solutions à ce problème ultérieurement.

• L’affichage des résultats :


Pour afficher le contenu d’une variable, il suffit d’écrire son nom ; par contre, pour
éditer sur écran un message, on a recours à la commande print :

Belgacem Hajji ~ 11 ~
On peut afficher plusieurs messages sur une même ligne en choisissant le caractère de
séparation en utilisant la syntaxe suivante : print(mes1,mes2,…mesk,sep=caractere)

Pour choisir le caractère de fin de ligne, on utilise l’option end de la commande print (si
le caractère choisi est ‘’ alors il n’y aura pas de retour à la ligne) :

• L’affectation :
On affecte une valeur à une variable en utilisant l’opérateur =. La commande n’affiche
aucun résultat :

On n’a pas le droit d’utiliser des variables non initialisées dans une affectation :

Plusieurs variables peuvent être affectées en même temps, on parle d’affectation


simultanée :

3.3. Les structures conditionnelles


• schéma simple réduit :

Si condition alors
instruction
finsi

Belgacem Hajji ~ 12 ~
 On remarque que Python utilise la notion d’indentation (espacement) au niveau du bloc
d’instruction ce qui lui permet de distinguer quelles sont les instructions à exécuter si la
condition est vraie. Python gère automatiquement l’ensemble de ces espacements, il
suffit de mettre : et de taper ENTREE pour que le curseur soit placé au bon endroit
d’écriture.

 Le FinSi n’a pas d’équivalent en Python.


• schéma simple :

Si condition alors
Instruction1
Sinon
Instruction2
Finsi

• schéma imbriqué :

Si cond1 alors
ins1
sinon si cond2 alors
ins2
…..
Sinon insk
Finsi
Finsi

Finsi

Belgacem Hajji ~ 13 ~
• Application : Donner les instructions Python qui permettent d’afficher la solution de
l’équation ax+b=0 avec a et b deux réels introduits au clavier. Discuter tous les cas
possibles.

3.4. Les Boucles :


• while :
On utilise l’instruction while pour répéter un traitement tantque une condition est
vérifiée ou jusqu’à ce qu’une condition soit vraie. Notons que comme les structures
conditionnelles, l’instruction while réserve une indentation pour le bloc qui se répète.

- TANTQUE ….FAIRE :
Tantque condition faire
Instruction(s)
fintantque

 Exemple 1 : le nombre de fois est connu à l’avance :


On se propose de calculer la somme S=1+2+…+N avec N une donnée :

 Exemple 2 : Durée imprévisible du traitement :


On demande de saisir une variable x strictement positive et de proposer à
l’utilisateur la possibilité d’introduire une nouvelle valeur en cas où x ne vérifie
pas la condition :

- REPETER ….JUSQU’A :
Répéter
Instruction(s)
Jusqu’à condition

 Exemple 1 : Le contrôle de saisie

Belgacem Hajji ~ 14 ~
 Exemple 2 : le calcul de la somme

• boucle for :
- avec l’utilisation des intervalles :
Python offre un objet spécial appelé range qui représente un intervalle qui n’est
autre qu’une suite de valeurs discrètes (et pas réelles). La syntaxe générale de la
commande range est la suivante :
range(BI,BS,pas)
Avec :
➢ BI = borne inférieure par défaut égale à 0
➢ BS = borne supérieure jamais atteinte (on s’arrête toujours en BS-1)
➢ pas = Le pas qui est par défaut égal à 1 pouvant être un entier positif ou négatif :

NB : _ donne le résultat de la dernière commande exécutée.

Belgacem Hajji ~ 15 ~
La notion d’intervalle est utilisée pour déterminer la plage des valeurs que va prendre un
compteur dans une boucle pour. L’instruction Pour est matérialisée via le mot clé for de python
dont la syntaxe a la forme suivante :
 boucles croissantes :

Supposons qu’on veut utiliser une boucle for pour effectuer différents
affichages :
➢ affichage des entiers compris entre 0 et 3 :

➢ affichage des entiers compris entre 1 et 10 :

➢ affichage des entiers impairs compris entre 1 et 20 :

Belgacem Hajji ~ 16 ~
 boucles décroissantes :
Le pas doit être une valeur négative avec une borne inférieure > à la borne
supérieure :

NB : on peut utiliser une boucle for pour itérer sur les éléments d’un conteneur (notion qui sera
vue dans les séances prochaines). La forme générale de la boucle est la suivante :

Exemple :

3.5. continue – break – else :


• continue : Permet de sauter une boucle et de passer à la boucle suivante :

Belgacem Hajji ~ 17 ~
• break : permet de quitter la boucle quand une condition est vraie :

• else : traitement alternatif quand une boucle se termine :

3.6. Contrôle des erreurs lors de l’exécution:


Le boucle répéter jusqu’à permet de contrôler les erreurs de logique mais pas les erreurs lors de
l’exécution. Supposons qu’au moment de l’introduction d’une valeur entière l’utilisateur se
trompe et tape un caractère, le script s’interrompt automatiquement et un message en rouge sera
affiché. Pour remédier à ce genre de situations, le langage a programmé une instruction spéciale
intitulée try qui lors d’une erreur de ce genre permet à l’utilisateur de se rattraper. Le schéma
de try est le suivant :

Exemple 1: Saisie d’une variable entière strictement positive sans gestion d’erreurs :

Belgacem Hajji ~ 18 ~
Exemple 2 : Saisie d’une variable entière strictement positive avec gestion d’erreurs :

Applications:
1) Ecrire un script qui affiche tous les entiers premiers compris entre deux entiers a et
b (a<b) saisis au clavier. Faites tous les contrôles nécessaires.
2) Ecrire un script qui résout dans C une équation du second degré à coefficients réels
de la forme ax²+bx+c=0. Discuter tous les cas envisageables.

Belgacem Hajji ~ 19 ~
Chapitre 4 : Les conteneurs à accès séquentiel
(Tuples, listes et chaines)
4.1. Introduction
Python permet de regrouper un ensemble de données de même type ou de types différents à
l’intérieur de structures de données spéciales appelées conteneurs. Il existe deux catégories de
conteneur :
- Les conteneurs séquentiels : On note les listes, les tuples et les chaines de caractères. A
l’intérieur de ces structures de données, les éléments sont rangés en utilisant des index
(commençant de 0).
- Les conteneurs à accès directs qui seront abordés dans le chapitre suivant.
Dans la suite, on va exposer les propriétés communes aux conteneurs séquentiels, puis on
abordera le côté spécifique de chacun de ces derniers.

4.2. Propriétés communes aux conteneurs :


Hypothèses : On dispose d’un conteneur C qui peut être :

• Une liste : Une séquence d’éléments encadrés par des crochets []

• Une chaine : une séquence de caractères encadrés par des côtes ou des chevrons ‘’ ou
«»:

• Un tuples : une séquence d’éléments encadrés par des parenthèses

Les opérations communes sur un conteneur séquentiel sont :

• Taille : len(C)

• Accès à un élément : C[index de l’élement] : Les index permis sont compris entre 0 et
len(C )-1 pour un parcours de gauche à droite et entre -1 et –len(C) pour un parcours
de droite à gauche

Belgacem Hajji ~ 20 ~
• Accès à une partie des éléments : C[i :j] : renvoie un conteneur constitué des éléments
se trouvant entre la position i et la position j-1.
NB :
- C[ :i] : renvoie la sous liste constituée des éléments d’indices compris entre 0 et i-1
- C[i :] : renvoie la sous liste constituée des éléments d’indices compris entre i et len(C)-
1.
- C[ ::] :renvoie la totalité de la liste

- C[ ::-1] : inverse la liste des éléments

• Accès à une partie des éléments avec notion de pas : C[i :j :k] : renvoie un conteneur
stockant les éléments entre les deux positions i et j-1 en effectuant des sauts d’amplitude
k

• Test d’appartenance : x in C

• Test de non appartenance : x not in C

• Concaténation : l’opérateur + permet de coller les éléments de deux ou plusieurs


conteneurs

Belgacem Hajji ~ 21 ~
• Répétition : C*n : l’opérateur * permet de créer un conteneur contenant n copies de C

• Recherche de la position d’un élément : C.index(x)

• Calcul du nombre d’occurrence d’un élément : C.count(x)

• Minimum et maximum : min(C), max(C)

Application :
- Créer un tuple t=(1,2,4,-6) et une chaine ch=’abc cd ! ;:’
- Appliquer toutes les commandes vues précédemment pour t et ch.
NB : Les conteneurs ne peuvent pas être tous modifiés après leurs créations, seules les listes
sont des structures de données modifiables ou mutables ou non immuables, tuples et chaines
sont non mutables ou immuables.

4.3. Commandes spécifiques aux listes


• création :
 liste vide :

 commande list :

 avec les [] :

 liste en compréhension : La syntaxe est la suivante : [expression for indice in


conteneur/itérable if condition] :

Belgacem Hajji ~ 22 ~
Application : retrouver les listes en compréhension présentées en haut en utilisant les boucles.
 par conversion

• modification : on utilise l’opérateur d’affectation =


 d’un élément

 d’un slice

Application : créer une liste contenant les entiers compris entre 0 et 5 puis modifier tous les
éléments impairs par des 0 en utilisant la notion de pas.

• ajout
 d’un élément à la fin : on utilise la commande append

 d’un slice : avec la commande extend

 insertion : avec la commande insert

Belgacem Hajji ~ 23 ~
• suppression
 d’un élément : trois possibilités : del, remove et pop

 d’un slice

• copie :
 copie normale : avec la commande copy :

 copie profonde : avec la commande deepcopy

Belgacem Hajji ~ 24 ~
• tri : avec la commande sort

• inversion : avec la commande reverse

4.4. Commandes spécifiques aux tuples :


• création :
 tuples vides :

 création directe avec les () :

 avec la commande tuple :

 par conversion

 en compréhension

• modification :
Les tuples sont non modifiables, toute tentative de mise à jour déclenche une erreur :

Belgacem Hajji ~ 25 ~
• ajout d’un élément :

 On écrase l’ancien tuples avec le résultat de la concaténation de t avec le tuple (1,)


 (1,) est un tuples composé d’un seul élément, la virgule est obligatoire.

4.5. Commandes spécifiques aux chaines :


• test de la nature de la chaine : alphabétique(isalpha), numérique(isidigit) ,
vide(isspace) ou alphanumérique(isalnum) :

• conversion majuscule / minuscule : upper et lower :

• commence avec/ se termine avec : startswith, endswith :

• éliminer les espaces à droite et à gauche : avec la commande strip :

• décomposition : commande split : décompose une chaine en utilisant un caractère ou


une sous chaine comme séparateur. Le résultat est toujours une liste :

Belgacem Hajji ~ 26 ~
• coller plusieurs chaines : commande join : opération réciproque de split :

• Recherche d’une sous chaine dans une autre chaine : commande find :

• Remplacer une sous chaine par une autre : commande replace :

• Affichage formaté : commande format : Syntaxe : chaine.format(parametres) :

Belgacem Hajji ~ 27 ~
Chapitre 5 : Les conteneurs à accès direct
(Dictionnaires & ensembles)
5.1. Introduction
Dans le chapitre précédent, on a traité les conteneurs séquentiels de Python. Pour accéder à un
élément d’un conteneur, on utilise son index. Les valeurs autorisées des index sont
obligatoirement séquentielles. Python permet d’utiliser une autre catégorie de conteneurs
maniant des index organisés d’une manière non séquentielle. Le conteneur n’est plus une
structure ordonnée, il s’agit bien d’un conteneur à accès direct.

5.2. Les dictionnaires


Un dictionnaire est un conteneur modifiable non ordonné composé d’une ensemble de couples
index : valeur. Les index sont aussi appelés des clés et les valeurs des entrées. La création d’un
dictionnaire peut se faire de plusieurs manières :

• Dictionnaire vide :

• La commande dict (avec comme paramètre une liste de listes, chacun de la forme
[clé ,valeur]) :

• Les {} :

• En compréhension

Belgacem Hajji ~ 28 ~
• Directement (en utilisant la syntaxe NomDict[cle]=valeur) :

La taille d’un dictionnaire qui est le nombre d’entrées qu’il contient est accessible via la
commande len, pour afficher une valeur correspondante à une clé, on utilise la syntaxe
NomDict[clé], les deux opérateurs in et not in testent l’appartenance d’une clé à un
dictionnaire :

Python permet d’accéder aux informations stockées au niveau d’un dictionnaire de plusieurs
manières :

Belgacem Hajji ~ 29 ~
Une entrée peut être accédée par la commande get :

Par contre, la commande setdefault permet de :

• créer une entrée vide si on fournit un index non encore utilisé et pas de valeur en
deuxième paramètre :

• Ajouter une entrée :

• Récupérer la valeur d’une clé existante :

La mise à jour d’un dictionnaire passe par l’ajout, la modification et la suppression d’entrées :

Python offre un ensemble de commande de mise à jour pour les dictionnaires :

• update : La syntaxe est d1.update(d2) : Fusionne deux dictionnaires. Si la clé figure


dans les deux dictionnaires alors la valeur de l’entrée correspondante dans d1 sera mise
à jour, sinon, une nouvelle entrée est créée dans d1 :

Belgacem Hajji ~ 30 ~
• pop : la syntaxe est NomDict.pop(index) : supprime l’entrée dont la clé est index et la
retourne en résultat ; popitem : la syntaxe est NomDict.popitem() : supprime
aléatoirement une entrée et la retourne :

• clear : La syntaxe est NomDict.clear() : efface le contenu d’un dictionnaire

• copy : La syntaxe est d2=d1.copy() : copie un dictionnaire dans un autre après avoir
importer le module copy.

5.3. Les ensembles


Les ensembles Python modélisent les ensembles mathématiques. C’est une collection d’objets
non ordonnées sans redondance de même types ou de types différents. Les éléments d’un
ensemble ne doivent jamais être des listes, des dictionnaires ou d’autres ensembles.
Un ensemble peut être créé de plusieurs manières :

• ensemble vide :

• la commande set :

Belgacem Hajji ~ 31 ~
• les {}

• en compréhension

Un ensemble est une structure de données non ordonnée. La commande len donne la cardinalité
de l’ensemble. On peut tester l’appartenance d’un élément à un ensemble mais on ne peut pas
utiliser un indice :

La mise à jour d’un ensemble est possible vue que c’est un type mutable :

• ajout d’un élément

• Suppression d’un élément :


 remove : La syntaxe est s.remove(elt) : supprime un élément existant et déclenche une
exception dans le cas échéant :

Belgacem Hajji ~ 32 ~
 discard : La syntaxe est s.discard(elt) : supprime un élément s’il existe et ne déclenche
pas d’erreurs si elt ne fait pas part de s :

 pop : Le syntaxe est s.pop() : supprimé un élément aléatoire de s et déclenche une


exception si l’ensemble est vide :

Toutes les opérations ensemblistes sont possibles en Python, on peut teste si :

• deux ensembles sont identiques ou différents : Les opérateurs de comparaisons ==


et != :

• deux ensembles sont disjoints (aucun élément en commun) :

• un ensemble est un sous ensemble d’un autre

Belgacem Hajji ~ 33 ~
• un ensemble contient un autre ensemble

• L’union

• L’intersection

• La différence

• La différence symétrique

Les opérations de réunion, intersection, différence et différence symétrique se présentent sous


une 2ème forme en python qui autorise une sorte de mise à jour de l’ensemble en question :

Belgacem Hajji ~ 34 ~
Chapitre 6 : Programmer des fonctions sous Python
6.1. Introduction
Python offre une panoplie de fonctions prédéfinies prêtes à l’utilisation. On distingue
les fonctions qui se chargent au moment du démarrage de l’interpréteur comme la
fonction abs() ou la fonction len() …(il suffit d’appeler ses fonctions en précisant le(s)
paramètre(s) de chacune), d’autres qui se chargent au moment de l’importation de
modules (cas de sqrt du module math ou de polar du module cmath) … Ce chapitre a
pour but d’initier à la programmation des fonctions en Python.

6.2. Définition
Une fonction est un bloc d’instructions qui possède un nom, qui reçoit un certain nombre
de paramètres et renvoie un résultat. L’usage des fonctions permet :
• d’éviter la répétition
• de séparer les données des résultats
• de rendre le code réutilisable à travers l’importation de fichiers de fonctions…
• et de décomposer les tâches complexes en tâches simples plus abordables.

On doit définir une fonction alors à chaque fois qu’un bloc d’instructions se trouve
répété à plusieurs reprises. Il s’agit d’une factorisation du code.

6.3. Syntaxe
Une fonction Python à la syntaxe suivante :

• Le mot clé def sert à déclarer un objet de type fonction


• Il est suivi du nom de la fonction ainsi que de parenthèses contenant les paramètres
de cette dernière (Si la fonction a plusieurs paramètres, on utilise les virgules pour
les séparer ; en cas ou il n’y a pas de paramètres, on ouvre et on ferme les parenthèses
obligatoirement ; on précise jamais le type des paramètres ; une fonction peut
recevoir des paramètres de natures différentes entre deux jeux d’appels grâce à la
notion de typage dynamique=le type d’une variable dépend de son contenu)
• La chaine de documentation est optionnelle mais fortement recommandée.
• Le bloc d’instruction est obligatoire. L’instruction pass permet d’avoir un bloc
d’instruction vide (si on n’a pas encore désigné de quoi est chargé notre fonction).

Belgacem Hajji ~ 35 ~
6.4. Première fonction Python

On vient de créer une première fonction sous Python (La saisie peut être effectuée au
niveau du shell ou à l’intérieur d’une nouveau fichier .py, la différence entre les deux
cas sera traitée ultérieurement, mais pour le moment on opte pour la première option).
La fonction TableMul5 ne possède pas de paramètres, on lui a confié la responsabilité
d’afficher la table de multiplication de 5, ce qui explique la documentation entre triple
côtes… Une fois déclarée, la fonction peut être appelée comme le montre l’exemple
suivant :

Une fonction est définie pour assurer la factorisation du code et la réutilisabilité. Dans
l’exemple précédent, en dehors de la valeur 5, la fonction n’accepte aucune autre valeur.
D’où la pertinence de la notion de paramétrage. On redéfinit alors TableMul en réservant
un paramètre n qui représentera la valeur de l’entier dont on cherche à afficher sa table
de multiplication :

La fonction TableMul autorise des appels avec des paramètres de natures différentes
(Besoin de contrôle d’eventuelles erreurs de saisie à travers la gestion des exceptions…)
:

Belgacem Hajji ~ 36 ~
6.5. Le NonType & les fonctions avec retour

On peut aller un peu plus loin avec les fonctions Python et développer une vraie fonction
qui retourne un résultat récupérable par la fonction appelante (on traitera plus tard la
fonction __main__ qui représente la notion de programme principale). Le mot clé return
permet d’arrêter l’exécution d’une fonction et de renvoyer un résultat. Tous les types
vus en Python peuvent être utilisés comme un retour. Quand la fonction ne programme
pas d’instructions de type return, Python retourne tout de même un objet vide ayant
comme type NoneType. C’est un type réservé par le langage pour exprimer le vide, tel
est le cas d’une liste ou d’un tuple vide.

On va partir d’une fonction qui affiche la somme de deux entiers passés en paramètres :

En vérifiant le type du résultat obtenu, on tombe sur le cas d’un objet vide, très logique
print ne fait pas le travail de return :

Rectifions la fonction som de sorte qu’elle soit une vraie fonction avec un return :

Le type du résultat est bel et bien un entier. Le mot clé return peut figurer dans plusieurs
endroits au sein d’une fonction. La fonction signe retourne une chaine exprimant le signe
d’un argument x :

Belgacem Hajji ~ 37 ~
Quand une fonction programme plus d’un paramètre en retour, le type tuple s’avère très
utile. La fonction somdifprod calcule la somme, la différence et le produit de deux
entiers a et b passés en paramètres, un objet de type tuple constitué des trois résultats est
alors retourné. Au moment de l’appel, il faut prendre en considération la nature du
résultat. Soit qu’on utilise un objet qui stockera tout le tuple puis pour accéder aux
résultats on utilise les indices respectifs suivant la taille de ce dernier, soit qu’on effectue
un appel basé sur trois variables :

6.6. Mode de passage des paramètres


L’appel est une substitution (replacement) des paramètres formels par des paramètres
effectifs. Python différencie les données des résultats selon le type du paramètre :
• Les types non mutables (int, float, str, complex, bool, tuple…) sont passés par
valeur.
• Les types mutables (listes, ensembles, dictionnaires …) imitent le mode de passage
par variable (S et ES en algorithmique).

- Passage par valeur :

La première version de la fonction permut est supposée échanger le contenu des deux
variables a et b :

Belgacem Hajji ~ 38 ~
Sauf que le contenu des deux variables reste inchangé preuve que les types non mutables
ne peuvent pas être modifiés. Dans cette deuxième version, on effectue un affichage du
contenu de a et b à l’intérieur de la fonction avant la fin de son exécution :

On constate que la modification n’est visible qu’à l’intérieur de la fonction. Les


paramètres ont un comportement local (Pour python, paramètres passés par valeur et
variables locales sont traitées de la même manière).
Pour réussir la permutation, l’appel doit écraser l’ancien contenu de a et de b en utilisant
une affectation :

- Passage par variable : Les objets mutables sont modifiables par nature. La fonction
ajout ajoute à la fin d’une liste l un objet x.

Belgacem Hajji ~ 39 ~
Pour mieux assimiler la notion de mode de passage, on se base sur l’évolution de la
valeur des identifiants des paramètres avant l’appel, au cours et à la fin de l’exécution
d’une fonction :

La variable x contient initialement la valeur 1. Python crée un identifiant sur la valeur 1


qui n’est autre que 17622d44272. En appelant la fonction, la valeur de l’identifiant reste
inchangée jusqu’à l’exécution de l’incrémentation. Python alors stocke dans x un nouvel
identifiant. C’est logique, x continent l’identifiant de 2 et plus celui de 1. Sauf que ce
changement n’est pas communiqué à l’extérieur de la fonction. L’affichage de la valeur
de x et de son identifiant après l’appel donne les valeurs initiales de l’avant appel.

Vérifions les identifiants de la liste l avant, pendant et après l’exécution de ajout :

L’identifiant de la liste l est constant. La modification de l est visible à l’extérieur de la


fonction ajout. C’est le cas de tous les types mutables.

Belgacem Hajji ~ 40 ~
6.7. Paramètres avec valeur par défaut - Paramètres positionnels
Une fonction peut être déclarée avec des paramètres ayant une valeur des valeurs de
départ. Si l’utilisateur omet l’un des paramètres au moment de l’appel, la valeur par
défaut sera prise en considération tel est le cas de la fonction construire_point qui affiche
le point (0,0) si l’utilisateur ne communique aucun paramètre.

6.8. Paramètres positionnels


En plus de donner des valeurs de départ aux paramètres, python autorise d’appeler la
fonction sans trop respecter l’ordre des arguments au moment de la déclaration. Il suffit
d’utiliser le nom du paramètre au moment de l’appel et de préciser la valeur qu’il prend :

6.9. Nombre de paramètres variable


Python permet de définir une fonction avec un nombre arbitraire de paramètres.
Généralement ce paramètre est appelé args ou params et il est toujours précédé du
caractère *. Utiliser ce modèle de fonctions permet une flexibilité de programmation
quand on ne sait pas vraiment quel sera le nombre de paramètres au moment de l’appel.
L’appel de la fonction est basé sur un tuple de valeurs :

Belgacem Hajji ~ 41 ~
6.10. Les fonctions imbriquées
Il est possible de déclarer une fonction g à l’intérieur d’une fonction f. Seule la fonction
f est capable d’appeler g. Cette dernière a un comportement local au sein de f. Python
offre deux commandes qui permettent de retourner une liste composée chacune des
noms des objets considérés comme locaux à une fonction et de leurs équivalent
globaux : locals() et globals() :

Suite à l’appel de la fonction min3, on obtient la liste des objets locaux et globaux
pour chacune des fonctions min2 et min3. La fonction min2 figure comme un objet
local à la fonction min3.

Belgacem Hajji ~ 42 ~
6.11. Les variables globales
Pour forcer une fonction à considérer une variable comme globale, on utilise le mot clé
global suivi du nom de la variable en question. La fonction inc1 utilise la variable x sans
la déclarer ni comme paramètre ni comme variable globale. L’interpréteur ne reconnait
pas x et déclenche une exception :

la deuxième version, inc2(), la variable x est déclarée globale d’une manière explicite.
La variable est initialisée à l’extérieur de la fonction et son contenu est modifiable par
les fonctions qui la déclarent comme avec le mot clé « global ».

Belgacem Hajji ~ 43 ~
6.12. Les fonctions lambda
En Python, une fonction lambda est une fonction anonyme (à laquelle on n’a pas donné
de nom), et qu’on peut appliquer “à la volée” dans une expression. La syntaxe est :
lambda paramètres : expression

NB : lambda est mot réservé du langage.

Les fonctions lambda sont réservées à des situations relativement simples. Leur
définition doit tenir sur une seule ligne, et elles ne peuvent pas contenir d’instructions
composées (pas d’affectation, pas de boucle, etc.). Elles consistent donc essentiellement
en la définition d’une expression calculée en fonction des paramètres qui lui sont passés.

Pour prendre un exemple simpliste (et pas très utile), les deux définitions suivantes de
la fonction f sont équivalentes :

Les fonctions anonymes permettent d’appliquer deux commandes intéressantes sur les
listes :
• map : Applique une fonction sur tous les éléments d’une liste pour former une
nouvelle liste distincte de l’initiale. La syntaxe de map est la suivante : list(map(f,l))

Supposons qu’on veut ajouter 1 à tous les éléments d’une liste l :

• filter : crée une liste contenant les éléments qui ne vérifient pas un critère spécifié
par une fonction f d’une liste initiale. La syntaxe de filter est la suivante :
list(filter(f,l))

Supposons qu’on veut supprimer tous les multiples de 3 d’une liste d’entiers :

Belgacem Hajji ~ 44 ~
6.13. Gestion des exceptions
Quand une erreur se produit dans un script, elle provoque l’arrêt du programme et
l’affichage d’un message d’erreur. Pour éviter cette interruption brutale, on peut
appliquer un traitement spécifique. Plus généralement, on peut traiter une situation
exceptionnelle dont on ne sait pas forcément où et quand elle va survenir à l’intérieur
d’un bloc donné. Plutôt que de parler d’erreur, on emploiera donc le terme “exception”
et prévoir une réaction adaptée à une exception, c’est la “rattraper”.

Pour le traitement des exceptions, Python offre la clause try: ... else.La forme la plus
simple est la suivante, où le bloc2 est parcouru si une exception (quelle qu’elle soit) est
rencontrée dans le bloc1 (cette exception provoque l’arrêt du bloc1 et le passage
immédiat au bloc2) :

On peut également prévoir un traitement particulier pour telle ou telle exception. Dans
ce cas, on utilisera une construction un peu plus élaborée, du genre :

On peut appliquer un même traitement à plusieurs exceptions. Pour celà, on écrira :


except (err1, err2, . . .) La construction except peut être complétée par une clause else,
exécutée si aucune exception n’a été levée/rattrapée. Python possède beaucoup
d’exceptions prédéfinies (cf http://docs.python.org/3.3/library/exceptions.html) En
voici juste quelques-unes :

• IndexError : Accès à un élément non existant d’une liste


• NameError : Utilisation d’un objet non existant
• SyntaxError : Erreur de syntaxe dans une instruction
• TypeError : Erreur de type
• ValueError : Erreur de valeur
• ZeroDivisionError : Division par 0

Belgacem Hajji ~ 45 ~
La gestion des exceptions favorise le contrôle des données. Au moment de la saisie,
l’utilisateur peut se tromper sur une valeur introduite. La fonction saisie par exemple
doit allouer la saisie de nouveau d’une nouvelle valeur en cas d’erreur en spécifiant la
nature du bug détecté avec un message explicatif :

6.14. Les modules personnels


Python permet de créer des modules personnels qui regroupent un ensemble de
fonctions et de variables (voir de classes, notion qui sera vue en deuxième année) à
l’intérieur d’un même fichier .py et de les utiliser en cas besoin. Le fichier possède un
nom défini par le programmeur. Avant de penser développer un module, vérifier que
python n’offre pas déjà une alternative.

• L’instruction import nom_module permet de charger la totalité du module en


mémoire. Les fonctions et les variables du module sont accessibles de la façon
suivante : nom_module.nom_element_du_module.
• Pour choisir les éléments à charger d’un module et gagner en mémoire, la syntaxe
suivante est sollicitée : from nom_module import fonction1,fonction2,var1 … Dans
ce cas, les éléments importés sont accessibles sans le prefixé nom_module.
• Le contenu d’un module est affiché par la commande dir(nom_module)
• L’aide est fournie par la commande help(nom_module)

Belgacem Hajji ~ 46 ~
Le regroupement de plusieurs modules dans un dossier donne naissance à un paquetage
(package en anglais).

On souhaite développer un module intitulé pile qui permet de créer une nouvelle
structure de donnée LIFO (Last In First Out). Cette structure repose sur deux
opérations :

• Empiler : ajouter un nouvel élément à la fin de la pile si cette dernière n’est pas
saturée
• Dépiler : supprimer toujours l’élément récemment ajouté si la pile n’est pas vide.

Sous Python, on ouvre un nouveau script avec CTRL+N, pour éditer notre script :

Belgacem Hajji ~ 47 ~
On enregistre ce script sous le nom de « pile.py ». Le module est prêt à l’utilisation :

En algorithmique, on appelle les fonctions et les procédures déjà déclarées au niveau de


l’algorithme principal ce qui est à l’encontre de Python, qui n’offre pas une fonction
principale comme en C ou en java (la fonction main()). Pour choisir les instructions qui
seront exécutées comme programme principal dans un script, on utilise une variable
spéciale __name__. Cette variable est créée par python partout et renseigne, comme son
nom l’indique sur le nom de tout objet utilisé (script, fonction, variable…). Le shell est
une fonction python qui porte le nom ‘__main__’. Ainsi, pour forcer des fonctions à
s’exéctuer comme programme principal, on teste si la variable __name__ contient la
valeur ‘__main__’.

Le script suivant propose la saisie et le tri d’une liste :

Belgacem Hajji ~ 48 ~
L’appel du script précédent donne :

Belgacem Hajji ~ 49 ~
Chapitre 7 : La récursivité
7.1. Introduction
Pour minimiser la complexité d’un problème informatique, on opte pour sa décomposition en
sous problèmes de complexités moindres. Certains problèmes spéciaux sont vues comme étant
un ensemble de sous problèmes similaires au problème original mais qui opèrent sur des
données de tailles plus petites. Résoudre le grand problème revient alors à résoudre les sous
problèmes, qui s’avèrent parfois facile à faire. L’algorithme de résolution du problème initial
va s’appeler lui-même pour résoudre les sous problèmes semblables. On parle de la notion de
récursivité des algorithmes. Une illustration de cette dernière est le calcul de la factorielle d’un
entier positif ou nul n. On sait que n !=n*(n-1)*…*1, il suffit de remarquer que calculer n !
revient à multiplier n par (n-1) ! que le problème soit résolu. Dans la suite, on présente une
fonction python chargée de calculer la factorielle de n d’une manière récursive :

….

Un appel récursif induit lui-même d’autres appels récursifs, ce qui peut mener à un nombre
infini d’appels. Il faut arrêter la suite des appels quand on obtient le résultat. Dans notre cas, on
sait que 0 ! = 1, une fois n atteint la valeur 0, le processus d’appel doit s’arrêter :

Belgacem Hajji ~ 50 ~
7.2. Définition
Une fonction est dite récursive si elle s’appelle elle-même. Il faut prévoir toujours une condition
d’arrêt pour stopper les appels récursifs sinon on obtient un traitement similaire à une boucle
infinie.

Python utilise une pile d’exécution pour enregistrer les appels et leurs contextes (paramètres
utilisés, résultats rendus, variables locales déclarées par chacun des appels …), mais la taille de
cette dernière n’est pas illimitée. Un chevauchement des appels mène au déclenchement d’une
exception (débordement de la taille de la pile).

La pile d’exécution de l’appel de fact avec n=3 a l’allure suivante :

• Appel de fact(3) :

fact(3) :
Retourner 3*fact(2)

• Appel de fact(2) :

fact(3) : fact(2) :
Retourner 3*fact(2) Retourner 2*fact(1)

• Appel de fact(1) :

fact(3) : fact(2) : fact(1)


Retourner 3*fact(2) Retourner 2*fact(1) Retourner 1*fact(0)

• Appel de fact(0) :

fact(3) : fact(2) : fact(1) fact(0)


Retourner 3*fact(2) Retourner 2*fact(1) Retourner 1*fact(0) Retourner 1

La condition d’arrêt est atteinte, on procède par la suite à la récupération du résultat :

• fact(0) = 1
• fact(1) = 1*fact(0)=1 * 1 = 1
• fact(2) = 2*fact(1)=2 * 1 = 2
• fact(3) = 3*fact(2)=3 * 2 = 6

D’une manière générale, pour obtenir une solution récursive à un problème :

- On paramètre le problème ;
- On décompose le cas général de façon à réduire la taille des données ;
- On détermine la condition d’arrêt qui arrête les énoncés récursifs.

Dans le cas de la factorielle, la relation entre le cas simple et le cas général est la réduction des
données n !=n*(n-1) !, mais ce n’est pas l’unique champ d’application de la récursivité. Parfois

Belgacem Hajji ~ 51 ~
la fonction ne retourne aucune valeur, mais effectue des actions récursives, tel est le cas de
l’affichage d’une liste ou son inversion :

7.3. Itératif VS Récursif


Tout problème ayant une solution récursive peut avoir une solution itérative équivalente. Il est
toujours préférable d’utiliser une version itérative pour plusieurs raisons :

- Clarté de la solution : on décrit les différentes étapes élémentaires pour résoudre un


problème
- Trace de l’exécution : on obtient une sauvegarde de l’évolution des variables
manipulées au fur et à mesure du calcul de la solution
- Economie de l’espace mémoire : La pile d’exécution, bien que elle alloue
dynamiquement de l’espace mémoire (réserver de la mémoire en cas de besoin) est
débordée rapidement, la taille des enregistrements qu’elle contient (les appels à la
fonction récursive et leurs contextes) est assez élevée comparée à l’espace utilisé par de
simples variables.
- Rapidité : Manipuler des variables c’est mieux qu’utiliser des appels de fonctions.

Considérons la suite de Fibonacci définie par la relation suivante :


1 𝑠𝑖 𝑛 = 0 𝑜𝑢 𝑛 = 1
𝐹(𝑛) = {
𝐹(𝑛 − 1) + 𝐹(𝑛 − 2) 𝑠𝑖𝑛𝑜𝑛

Belgacem Hajji ~ 52 ~
• Solution itérative :

• Solution récursive :

• Etude comparative en termes de temps d’exécution avec différentes valeurs de n :

On procède à un jeu d’appels avec différentes valeurs de n, on remarque que quand les
valeurs sont petites, y a pas de différence notable entre les deux méthodes sur la base du
temps de calcul nécessaire, mais l’écart devient flagrant dès qu’on accroît un peu n :

Cependant, l’avantage de la récursivité réside au niveau de l’abstraction : Il suffit de décrire la


solution au problème à partir d’un cas simple. Ce style de programmation est appelé déclaratif.

Belgacem Hajji ~ 53 ~
Le système (Python dans notre cas), réalise les actions nécessaires pour obtenir le résultat sans
qu’on lui donne l’algorithme sous forme d’actions pour résoudre le problème. On peut utiliser
la récursivité si :

• Le raisonnement sur le problème est lui-même récursif


• La structure de données utilisée est récursive (par exemple une liste peut être vue comme
un élément suivi d’une autre liste)

7.4. Exemple de raisonnement récursif : Les tours de Hanoi


La figure suivante résume les données du problème :

Hypothèses :
- n disques troués au centre qu’on peut placer sur 3 piliers : A, B et C respectivement le
pilier à gauche, le pilier du milieu et le pilier à droite.
- Au départ, tous les disques sont placés au niveau du pilier A triés d’une manière
décroissante (le plus petit disque est en haut, le plus grand en bas)
But :
- Déplacer les disques du pilier A au pilier C en gardant la même disposition du départ
Règles du jeu :
- On ne peut déplacer qu’un seul disque à la fois et il doit être non couvert par un autre
- Un disque ne doit jamais être placé sur un autre plus petit que lui
Solution :

• Cas de n=2 :
- On déplace le petit disque de A vers B
- On déplace le grand disque de A vers C
- On déplace le petit disque de B vers C
 On a utilisé 3 déplacements pour résoudre le problème = 2² -1

• Cas général :
- Déplacer les n-1 premiers disques (D1,D2,…,Dn-1) du pilier gauche vers le pilier
intermédiaire
- Déplacer le grand disque Dn du pilier gauche au pilier droit
- Et déplacer les n-1 disques du pilier intermédiaire vers le pilier droit.

Belgacem Hajji ~ 54 ~
Formulons le problème mathématiquement :
 Si n=0 alors on a 0 déplacement à effectuer
 Si n>0 alors Xk= 2Xk-1+ 1 avec Xk = le nombre de déplacements à effectuer à l’étape
k.
La résolution de cette relation donne l’expression de Xn en fonction de n donne :

Xn=2n-1
On vient de définir un algorithme récursif qui est basé sur un simple affichage (instruction
numéro 2) et qui effectue un appel à lui-même avec un nombre de données inférieur (n-1
disques). On est sure qu’il va se terminer à un moment donné (quand n atteint la valeur 0)
et on effectuera un nombre limité de déplacement géré par la formule précédente.
La fonction python Hanoï est basée sur le principe décrit précédemment aura l’allure
suivante :

On peut exécuter la fonction avec différentes valeurs de n (attention à ne pas s’amuser à


entrer des valeur élevées, le processeur risque de chauffer…) :

7.5. Types de récursivité


• Simple : La fonction récursive s’appelle elle-même une seule fois (calcul de la
factorielle)

• Multiple : La fonction s’appelle elle-même plusieurs fois dans un même appel récursif
(calcul d’une combinaison)

Belgacem Hajji ~ 55 ~
1 𝑠𝑖 𝑛 = 𝑝 𝑜𝑢 𝑝 = 0
𝐶𝑛𝑝 = { 𝑝 𝑝−1
𝐶𝑛−1 + 𝐶𝑛−1 𝑠𝑖 𝑛 > 𝑝

• Croisée : Deux fonctions récursives chacune appelant l’autre (nombres pairs et impairs)

• Imbriquée : L’appel est une imbrication d’appels (suite Ackermann)


𝑛+1 𝑠𝑖 𝑚 = 0
𝐴(𝑚 − 1,1) 𝑠𝑖 𝑚 > 0 𝑒𝑡 𝑛 = 0
𝐴(𝑚, 𝑛) = {
𝐴(𝑚 − 1, 𝐴(𝑚, 𝑛 − 1)) 𝑠𝑖 𝑚 > 0 𝑒𝑡 𝑛 > 0

• Terminale VS non terminale :


Quand l’appel récursif nécessite des instructions de calcul autre que l’appel de la
fonction elle-même, on parle de récursivité non terminale. C’est la cas de la fonction
fact définie précédemment : return n*fact(n-1) , on effectue une multiplication à côté de
l’appel de fact. On peut transformer cette solution pour supprimer tout calcul
accompagnant, on parle de récursivité terminale. C’est une manière d’accélérer les
algorithmes récursifs.

Belgacem Hajji ~ 56 ~
Chapitre 8 : Analyse de complexité des fonctions
8.1. Introduction
Jusqu’à maintenant, on n’a fait qu’écrire des algorithmes, des scripts, des fonctions avec
l’unique objectif d’avoir le bon résultat à partir des données fournies sans jamais s’intéresser à
l’aspect performance. Qui dit performance, dit espace mémoire utilisé et temps processeur, on
parle bien de complexité spatiale et de complexité temporelle. Dans la suite, on va se focaliser
sur l’aspect complexité temporelle. La durée de l’exécution d’une fonction python est facile à
obtenir avec le module time. Mais ce n’est pas ce qui est intéressant dans le cadre de notre
étude, parce qu’il suffit de changer de machine que le temps d’exécution change. Pensons alors
chercher à décrire le comportement asymptotique d’un algorithme quand la taille des données
augmente. Ceci est réalisable grâce à des fonctions qui décrivent l’ordre de grandeur du temps
d’exécution.

8.1. Calcul de coûts des fonctions python


Le calcul de cout consiste à déterminer le temps nécessaire à l’exécution des différentes
instructions d’un programme en fonction de la taille des données. Les règles suivantes sont à
prendre en considération :

Instruction Désignation Notations Formule de calcul


du coût
Instruction simples input, print, =, nécessitent une unité 1
opérations de de temps à leur
comparaisons exécution
(==, !=,<=…),
opérations
arithmétiques (+,-
,* ,/,//,%,**) , break,
continue, return

Structures if condition : On note c1, c2, c3 les c1+max (c2, c3)


conditionnelles i1 temps d’exécutions
else : respectifs nécessaires
i2 à la condition, i1 et
i2.
Boucles for for i in range(N) : On note ci le temps
ins d’exécution du bloc
d’instruction ins.
Deux cas se
présentent :
• Le bloc ins
dépend de la ∑ci
valeur de i :
• Le bloc ins
est n*ci
indépendant
de la valeur
de i :

Belgacem Hajji ~ 57 ~
Boucles while while condition : On note m,c1 et c2 m*(c1+c2)
ins respectivement le
nombre de boucles
espéré, le cout de la
condition et le cout de
ins

Considérons les deux scripts suivants et essayons d’analyser leurs coûts :

• Script 1 :

• Script 2 :

8.3. Mesure de la complexité


Le calcul de coût d’une fonction renseigne sur le temps nécessaire à son exécution, mais il suffit
de changer de machine que la valeur obtenue change avec des données identiques. Il sera utile
de construire une fonction qui décrit le comportement de l’algorithme quand :

• La taille des données s’accroit : on parle de comportement asymptotique des


algorithmes ;
• Et le traitement nécessite le maximum de temps : on parle de complexité dans le pire
des cas.
Mesurer la complexité d’un algorithme revient alors à trouver une fonction qui informe sur la
nature de ce comportement asymptotique dans le pire des cas. On parle de fonctions d’ordre de
grandeurs notées généralement O dont l’avantage est de chercher à borner la fonction de coût.
Le tableau suivant résume les fonctions O des différentes structures algorithmiques
rencontrées :

Instruction(s) Notation Fonction de complexité O


input, print, =, opérations de O(1)
comparaisons (==, !=,<=…),
opérations arithmétiques (+,-
,* ,/,//,%,**) , return, break,
continue

Belgacem Hajji ~ 58 ~
Un bloc d’instructions On suppose que les O(max(f1(n),f2(n),…,fk(n))
simples de la forme : complexités respectives de
i1 i1, i2,…ik sont O(f1(n)),
i2 O(f2(n)),…O(fk(n))

ik
if condition : On suppose que les O(max(g(n),f1(n),f2(n))
i1 complexités respectives de la
else : condition, de i1 et de i2 sont
i2 données par : O(g(n)),
O(f1(n)) et O(f2(n))

for i in range(N) : La complexité de ins est O(N*f(n))


ins notée O(f(n))

while condition : On suppose que la boucle se O(m*max(O(g(n)),O(f(n)))


ins répète m fois, que la
complexité de la condition
est O(g(n)) et la complexité
de l’instruction O(f(n))

8.4. Complexité des boucles multiplicatives


Certaines boucles partent d’une variable ayant une valeur petite, multiplient cette dernière par
une constante jusqu’à ce que la variable contient une valeur plus grande. D’autres boucles
divisent une variable ayant une grande valeur jusqu’à l’obtention d’une petite valeur. On parle
bien de boucles multiplicatives. La complexité de ce genre de structure est donnée par la
formule suivante :

O(logbase du multiplicateur ou du diviseur (n))


Exemple de script de boucles multiplicatives :

Démonstration :

• Initialisation : i(0)=1
• On suppose que la boucle while s’exécute q fois, alors i(q)=2*2*…*2 q fois
• i(q) = 2q , on applique la fonction log des deux côtés, on obtient log(i)=q*log(2)
• le nombre de boucle q est estimé alors à log(i)/log(2)=log2(i)
• Or la valeur de i tend vers N (100 dans notre cas), on conclut alors que la complexité de
cette boucle est O(log2(N))

Belgacem Hajji ~ 59 ~
Si une boucle multiplicative se situe à l’intérieur d’une boucle for alors la complexité de la
double boucle est donnée par :
O(nlog(n))

8.5. Complexité des problèmes récursifs


• Tours de Hanoï
Reprenons le problème des tours de Hanoï, l’algorithme de la solution est basé sur des appels
récursifs comme suit :

On remarque le temps nécessaire à la résolution du problème avec n disques dépend des temps
de résolution des sous problèmes à n-1 disques, lui-même dépendant du temps de résolution
des problèmes à n-2 disques… on note la présence d’une récurrence dans le calcul du coût et
par la suite dans la détermination de la fonction O associée.
Posons T(n) le temps nécessaire à la résolution du problème initial, on aura besoin de T(n-1)
unités de temps pour les deux appels récursifs et d’une unité de temps pour le déplacement d’un
disque. On rappelle que si n=0, on n’a aucun déplacement à faire. Le temps total est formulé
par la relation de récurrence suivante :
𝑇(0) = 0
{ (1)
𝑇(𝑛) = 2𝑇(𝑛 − 1) + 1
C’est une équation de récurrence linéaire de la forme suivante :

𝑇(𝑛) = 𝑎𝑇(𝑛 − 1) + 𝑓(𝑛)


Dont la solution est donnée par :
𝑛
𝑓(𝑖)
𝑇(𝑛) = 𝑎𝑛 (𝑇(0) + ∑ )
𝑎𝑖
𝑖=1

En appliquant ce modèle à notre cas, on détermine la solution du système (1) :

Belgacem Hajji ~ 60 ~
𝑛
𝑛
1
𝑇(𝑛) = 2 (𝑇(0) + ∑ )
2𝑖
𝑖=1

𝑓(𝑖)
Or le terme ∑𝑛𝑖=1 𝑎𝑖 est la somme de n termes d’une suite géométrique de premier terme ½ et
de premier terme ½ d’où :

𝑛
1 (1 − 1/2𝑛 )
𝑇(𝑛) = 2 (0 +
2 1 − 1/2
La simplification de cette expression donne :

𝑇(𝑛) = 2𝑛 − 1
Quand n augmente, T(n) devient exponentiel, il s’agit bien d’un algorithme en O(exp(n))

• La suite Fibonacci :

La fonction de coût est donnée par la formule suivante :

𝑇(𝑛) = 𝑇(𝑛 − 1) + 𝑇(𝑛 − 2)


Il s’agit d’une relation de récurrence linéaire sans second membre de la forme :

𝑇(𝑛) − 𝑎1 𝑇(𝑛 − 1) − 𝑎2 𝑇(𝑛 − 2) … . . −𝑎𝑘 = cste


Dont la solution est toujours exponentielle.
La résolution d’une telle équation nécessite l’association d’un polynôme caractéristique donné
par :

𝑃(𝑥) = 𝑥 𝑘 − 𝑎1 𝑥 𝑘−1 − 𝑎2 𝑥 𝑘−2 − ⋯ − 𝑎𝑘


Posons r1 et r2 les deux racines de P(x), ce dernier s’écrit :

𝑃(𝑥) = 𝑎𝑟1𝑛 + 𝑏𝑟2𝑛


Pour notre cas : T(n)-T(n-1)-T(n-2) = T(n)-1*T(n-1)-1*T(n-2)  k=2, a1=1, a2=1
 P(x) = x²-x-1

(1+√5) (1−√5)
La résolution de P(x)=0 donne deux racines r1= et r2= :
2 2

1 + √5 𝑛 1 − √5 𝑛
𝑃(𝑥) = 𝑎( ) + 𝑏( )
2 2
La solution est exponentielle.

Belgacem Hajji ~ 61 ~
• Recherche du maximum d’une liste :
La récursivité permet de chercher le maximum d’une liste en la subdivisant en deux sous listes
de tailles égales et en retournant le maximum entre les maximums respectifs des deux sous
listes. Pour trouver une solution, on a divisé le problème en deux sous problèmes chacun
opérant sur la moitié des données, on a résolu chacun des sous problèmes et on a combiné les
solutions trouvées pour aboutir à la solution finale. On parle du paradigme diviser pour
régner :

Les problèmes récursifs basés sur ce paradigme ont des fonctions de coût de la forme suivante :
𝑂(1) 𝑠𝑖 𝑛 ≤ 𝑐
𝑇(𝑛) = { 𝑛
𝑎𝑇 ( ) + 𝑐𝑛𝑘 𝑠𝑖𝑛𝑜𝑛
𝑏
Avec :
a : le nombre de sous problèmes
n : taille du problème initial
c : constante sur la taille du problème qui rend sa résolution facile (en O(1))
b : constante intervenant dans la répartition des données du problème inital
k : constante intervenant dans le calcul du temps de division du problème initial en sous
problèmes et de la combinaison des solutions résultantes pour construire la solution du
problème initial.

Le théorème suivant permet de déterminer la solution à cette relation récurrence :

• Si a>bk alors T(n)=O(nlogb(a))


• Si a=bk alors T(n)=O(nklogb(n))
• Si a<bk alors T(n)=O(nk)

Revenons à notre problème :

• a=2
• b=2
• k=0
 2>20 : 2>1  O(nLog2(2))=O(n) : La recherche est linéaire.

Belgacem Hajji ~ 62 ~
8.6. Classification des problèmes
Dans la littérature, les problèmes algorithmiques sont classés selon leurs complexités comme
indique le tableau suivant :

Classes de complexité désignations Exemple de problèmes


O(1) Complexité constante : le - Affectation
temps d’exécution n’augmente - Calcul arithmétique
pas avec l’accroissement de la - Comparaison
taille des données - Accès au premier
élément d’un
ensemble de
données
O(log(n)) Complexité logarithmique : - Recherche
augmentation très faible du dichotomique
temps d’exécution quand la - Conversion d’un
taille des données s’accroit. décimal en binaire
- Couper un
ensemble de
données en deux
sous ensembles
O(n) Complexité linéaire : le temps - Parcours d’une liste
d’exécution s’accroit d’une - Recherche linéaire
manière linéaire avec la taille dans une liste
des données
O(nlog(n)) Complexité quasi-linéaire : le - Couper un
temps d’exécution s’accroit ensemble de
d’une manière quasi linéaire données en deux
avec la taille des données sous ensembles
d’une manière
répétitive et
combiner les sous
ensembles pour
obtenir la solution
finale (les
problèmes diviser
pour régner…)
O(n²) Complexité quadratique : le - Double boucle for
temps d’exécution est le carré imbriquées
de la taille des données
O(n3) Complexité cubique : le temps - Trois boucles for
d’exécution est le cube de la imbriquées
taille des données
O(np) Complexité polynomiale : le - p boucles for
temps d’exécution est imbriquées
polynomial par rapport à la
taille des données
O(2n) ou O(exp(n)) Complexité exponentielle : le - générer tous les
temps d’exécution est sous ensembles
possibles d’un

Belgacem Hajji ~ 63 ~
exponentiel par rapport à la ensemble de
taille des données données.

Pour être mieux être sensibilisé à l’écart entre ces différentes classes de complexité, on va
considérer un processeur pouvant exécuter 109 opérations par seconde et on va essayer de
calculer le temps nécessaire à l’exécution d’un script avec différents jeux de données de tailles
croissantes comme montre le tableau suivant :

Reprenons le script qui calcule la double somme vu précédemment. La fonction de cout qui en
découle est f(n)=3n²+2. Supposons que la valeur de n=200, on peut déterminer le poids de
chaque terme de cette somme dans le temps d’exécution :

• f(200)=3*200²+2 = 120000+2=120002
• la contribution du premier terme 3n² dans le temps de calcul est évaluée à :
120000/120002 = 99,8%
• par contre la contribution du deuxième terme 2 = 2/120002 = 0,2
On peut alors garder le terme qui contribue le plus qui n’est autre que 3n². La conclusion est
que le script a une complexité quadratique ce qui justifie la notation O (la notion d’ordre de
grandeur).

Belgacem Hajji ~ 64 ~
Chapitre 9 : Algorithme de recherche en Python
8.1. Problématique
On a besoin de modifier la valeur d’un objet x par une nouvelle valeur y dans un conteneur C.
Il faut tout d’abord procéder à la vérification de l’appartenance de x à C. Python permet de
vérifier si un objet x appartient à un conteneur C avec l’opérateur in : x in C ou l’opérateur not
in : x not in C. La nature de la réponse dans ce cas est booléenne (True ou False).
Parfois la réponse booléenne ne suffit pas, le programmeur a besoin de savoir où se situe x dans
C. Ceci est réalisable grâce à la commande index (C.index(x)). Le problème peut être étendu
pour déterminer tous les i tels que C[i]=x avec i =0..len(C). On rappelle que la commande
C.count(x) calcule le nombre d’occurrences. La recherche diffère selon la nature du conteneur :

• Accès séquentiel (Séquences (listes, chaines, tuples) ou direct (Ensembles,


dictionnaires))
• Trié : recherche guidée
• Non trié : recherche aveugle

8.2. Recherche dans un conteneur non trié :


Hypothèses : C = un conteneur non trié et x un objet. Le problème est : x appartient à C ?
Solution 1 :

Rech1 effectue un parcours séquentiel (du 1er au dernier élément) de C. Elle itère sur les
éléments de C et pas sur les indices. A chaque fois on vérifié si i=x, dans ce cas, on retourne
vrai et on quitte la fonction. Si tous les objets de C sont parcourus et on n’a jamais obtenu la
valeur vrai pour la condition i=x, alors on est sure que l’objet n’existe pas, dans ce cas on doit
retourner la valeur faux.

Rech1 peut être appelée avec tous les conteneurs qu’on a rencontrés :

Belgacem Hajji ~ 65 ~
Bien que cette recherche séquentielle n’est pas vraiment adaptée aux dictionnaires... (À
développer dans la dernière partie du cours).

Solution 2 :

Rech2 utilise une boucle tantque basée sur les indices et pas les objets. La détermination du
résultat de la recherche est basée sur la valeur de i avec laquelle on quitte la boucle tantque. Si
i atteint len(C) c’est qu’on a parcouru tout le conteneur sans tomber sur x.

Attention à ce genre d’appels, les ensembles ne sont pas des séquences, donc la notion d’indice
n’existe pas.

• Analyse de la complexité de Rech1 et Rech2 :

Au maximum, on effectuera len(C) comparaisons dans les deux cas. Si on pose n=len(C), on
peut dire que le nombre de comparaisons augmente d’une manière parallèle avec la taille du
conteneur. D’où la complexité des deux solutions est linéaire notée O(n).

Solution 3 :

La question maintenant est ou se situe le premier x dans C ?

Belgacem Hajji ~ 66 ~
Rech3 parcourt les indices de C qui sont compris entre 0 et len(C)-1. Si on ne trouve pas
l’élément x d’habitude on retourne un indice non existant (comme -1) ; hors Python utilise aussi
un système d’indexation négatif, la valeur None du type NoneType permet de résoudre ce
problème.

Application : Modifier Rech3 pour obtenir la liste des indices de l’élément x dans C ainsi que
le nombre de ses occurrences.

8.3. Recherche dans une séquence triée


On suppose que le conteneur C est trié d’une manière croissante. La recherche est guidée par la
valeur de x :
Solution 1 :

Solution 2 :
Quand le conteneur est trié on a la possibilité de procéder par dichotomie, le principe est le
suivant :

- On calcule m le milieu du conteneur


- On compare C[m] à x :
• Si C[m]=x : c’est trouvé
• Si C[m]>x : on continue la recherche dans la partition C[0 :m]
• Si C[m]<c :on continue la recherche dans la partition C[m+1 :]
- Ce principe est répété jusqu’à ce que le conteneur C ne contienne plus d’éléments.

Belgacem Hajji ~ 67 ~
On va modifier la fonction pour afficher les partitions de recherche à chaque étape :

Application : Modifier Rech2 de sorte qu’elle ait deux paramètres uniquement C et x.

• Analyse de complexité de Rech2 :

Posons n=len(C) ; supposons que la boucle while s’exécute p fois alors à chaque boucle on a p
comparaisons au pire des cas étant donné qu’à chaque boucle on effectue une comparaison :

 Initialement : n éléments
 Boucle n°1 : n/2 éléments
 Boucle n°2 : n/4 éléments

….

 Boucle n°p : n/2p=1 seul élément.

Belgacem Hajji ~ 68 ~
La valeur de p est donnée par :

log(𝑛)
𝑝= = 𝑙𝑜𝑔2 (𝑛)
log(2)

Le nombre de comparaisons est une fonction logarithmique du nombre d’éléments : La


recherche dichotomique a une complexité en O(log2(n)), donc plus rapide que la recherche
linaire comme le montre le tableau suivant :

n Recherche linéaire (O(n)) Recherche dichotomique


(O(log(n))
128 128 7
1024 1024 11
106 106 21

- Solution 3 : version récursive de Rech2 :

Au lieu de mettre à jour les bornes de l’intervalle de recherche, on peut appeler la fonction de
recherche en lui passant directement le nouvel intervalle en question. Le calcul du cout de la
fonction Rech3 est un peu spécial, étant donne qu’à l’intérieur de la fonction Rech3, on a un
appel à elle-même :

T(n)=T(n/2)+5

Le temps d’exécution est donné par une relation de récurrence non linéaire ayant généralement
la forme suivante :

T(n)=aT(n/b)+cnk

La solution de cette équation dépend des valeurs de a, b et k. Trois cas se présentent :

• Si a>bk alors T(n)=O(nlogb(a))


• Si a=bk alors T(n)=O(nklogb(n))
• Si a<bk alors T(n)=O(nk)
Dans notre cas, on a :

Belgacem Hajji ~ 69 ~
• a=1
• b=2
• k=0 (Le terme n ne figure pas dans l’expression de T(n))
bk=20=1 = a : T(n)= O (n0 log2(n))=O (Log2(n)) : On conclut que la version
récursive a la même complexité que celle itérative.

Application : redéfinir Rech3 en utilisant uniquement C et x comme paramètres.

8.4. Recherche dans un dictionnaire


Un dictionnaire est ensemble de couples d’associations de la forme (clé,valeur) . Les clés d’un
dictionnaire sont uniques. Pour accéder à une valeur, on a besoin de sa clé et pas de son index
ce qui rend l’algorithme de recherche différent de celui des conteneurs déjà abordés.

La position d’une entrée dans un dictionnaire est calculée à partir de sa clé. Une fonction
spéciale est utilisée pour ce faire, appelée fonction de hachage. Dans la littérature, on croise pas
mal de fonctions de hachage, on va considérer une fonction qui se base sur les codes ascii des
caractères constituants d’une clé.

La construction d’un dictionnaire se fait en deux étapes :

- création d’un dictionnaire vide :

- ajout des différentes entrées de la forme (clé,valeur) sans dépasser la taille maximale
(20 dans notre cas) :

Supposons qu’on veut ajouter les trois entrées suivantes dans un dictionnaire d : (‘orange’,.9),
(‘dattes’,6.), (‘citron’,.7) ; on procède comme suit :

Belgacem Hajji ~ 70 ~
• Recherche dans un dictionnaire :

En disposant de la clé d’une entrée, on utilise la fonction de hachage pour déterminer la position
de la valeur correspondante dans le dictionnaire :

• Analyse de complexité de rech_dico :

T(n)=1 retour + 1 accès à d + 1 appel de hache = Constante

O(T(n))=1 : Les dictionnaires sont les conteneurs qui assurent la recherche la plus
rapide.

Belgacem Hajji ~ 71 ~
Chapitre 10 : Algorithmes de tri en Python
9.1. Problématique
Dans ce cours, on va se restreindre au tri des éléments d’une liste, mais rien n’empêche
l’application des fonctions de tri qui seront présentées dans la suite aux autres conteneurs
séquentiels. Nous allons supposer que :

• Les listes sont homogènes (contenant toutes des éléments de même nature, par exemple
des listes d’entiers uniquement ou de réels uniquement…)
• Les éléments de la liste peuvent être ordonnés en utilisant une relation d’ordre par
exemple l’opérateur de comparaison ≤.

Trier une liste c’est ordonner ses éléments par ordre croissant ou décroissant. La liste sera elle-
même une donnée et un résultat. Toutes les modifications opérées lors du processus du
traitement seront enregistrées sur la liste L et pas sur une copie d’elle-même.

Dans la littérature, plusieurs algorithmes de tri existent. Trancher entre un algorithme et un


autre fait intervenir la notion de complexité algorithmique : Bien évidemment, on a besoin de
classer les éléments en un minimum de temps. Une fois triée, une liste peut servir pour d’autres
traitements comme :

• L’insertion d’une nouvelle donnée à sa bonne position en prenant en considération


l’ordre établi ;
• Les algorithmes dichotomiques qui utilisent les listes triées pour accélérer la
recherche…
On distingue deux grandes catégories de tri : Tris lents (par sélection, à bulles, par insertion…)
et les tris rapides (par fusion, quicksort,…)
Hypothèses : L est une liste d’entier de taille n=len(L).

9.2. Tri par sélection


• Principe :
- Commencer à chercher l’indice du minimum de la liste L ;
- Permuter le 1er élément de la liste avec l’élément minimum trouvé ;
- Chercher l’indice du minimum des éléments de la sous liste L[1 :] et le permuter avec
le 2ème élément…
- Continuer ce principe jusqu’à ce que la liste devienne triée.
 D’une manière générale, on échange l’élément à la position i avec le minimum de
la sous liste L[i+1 :]. L’algorithme se termine au bout de n-1 boucles quand on a
trouvé les n-1 minimums successifs.
Exemple : L=[4,5,1,-6,2]
- Etape N°1 : le minimum de L est -6, on le permute avec 4 : L=[-6,5,1,4,2] ;
- Etape N°2 : le minimum de [5,1,4,2] est 1, on le permute avec 5 : L=[-6,1, 5,4,2]
- Etape N°3 : le minimum de [5,4,2] est 2, on le permute avec 5 : L=[-6,1,2,4,5]
- Etape N°4 : le minimum de [4,5] est 4, il est à sa bonne place.

Belgacem Hajji ~ 72 ~
• Implémentation :

Application :

1) Modifier la fonction précédente pour éviter les permutations inutiles.


2) Utiliser les fonctions prédéfinies pour retourner directement l’indice du minimum
3) Afficher l’état de la liste après chaque permutation.

• Analyse de la complexité :
Pour faciliter le calcul du temps d’exécution, nous allons considérer la version suivante
de triselect :

Algorithme Unités de temps


def triselect(L): T(n)
n=len(L) 1+1
for i in range(n-1): n-2 boucles
imin=i 1
for j in range(i+1,n): (n-i-1) boucles
if L[j]<L[imin]: 1
imin=j 1
if i!=imin: 1
L[i],L[imin]=L[imin],L[i] 2

 Le nombre de comparaison est donné par (n-i-1) pour l’élément à la position i d’où :

Belgacem Hajji ~ 73 ~
𝑛−2

𝑇(𝑛) = (𝑛 − 1) + (𝑛 − 2) + ⋯ + 2 + 1 = ∑(𝑛 − 𝑖 − 1)
𝑖=0

T(n) peut être vu comme la somme de n-1 termes d’une suite arithmétique de premier terme 1
et de raison 1 :

(𝑛 − 1)(1 + (𝑛 − 1)) (𝑛 − 1)𝑛 𝑛2 𝑛


𝑇(𝑛) = = = −
2 2 2 2
 O(T(n))=O(n²) : L’algorithme du tri est quadratique, pas très pratique quand la taille des
données s’accroit.

9.3. Tri à bulles :


• Principe : Parcourir la liste en comparant à chaque fois deux éléments successifs(L[i]
et L[i+1]), si l’ordre croissant n’est pas respecté (L[i]>L[i+1]) alors on permute les
deux éléments. On répète le parcours de la liste jusqu’à ce qu’on ne permute plus. On
est sûre d’obtenir une liste triée au bout de n-1 parcours mais parfois le processus
s’arrête un peu plus tôt.

• Implémentation :

• Analyse de complexité :
Pour simplifier l’étude on va se baser sur la 2ème version de tribulles :
Algorithme Unités de temps
def tribulles(L): T(n)
n=len(L) 2
for i in range(n-1): n-2 boucles
for j in range(n-i-1): n-i-1 boucles
if L[j]>L[j+1]: 1
L[j],L[j+1]=L[j+1],L[j] 2

Belgacem Hajji ~ 74 ~
D’où :
𝑛−2
𝑛2 𝑛
𝑇(𝑛) = ∑(𝑛 − 𝑖 − 1) + 𝑐𝑠𝑡𝑒 = − + 𝑐𝑠𝑡𝑒
2 2
𝑖=0

 O(T(n))=O(n²) : La complexité du tri à bulle est aussi quadratique.

9.4. Tri par insertion :


• Principe : A chaque étape, on cherche la bonne position d’insertion d’un élément se
trouvant à la position i dans la séquence d’éléments qui le précède (L[ :i]). On compare
l’élément L[i] avec L[i-1], s’il est plus petit alors on décale L[i-1](L[i]L[i-1]) et on
fait reculer la valeur du compteur i. Bien évidemment :
 On ne peut pas décrémenter le compteur d’une manière infinie
 Le premier élément à considérer dans l’insertion est le 2ème pas le premier.
On note qu’à chaque étape i, la sous liste L[ :i] est déjà triée pour i=1..len(L).

• Implémentation :

• Analyse de complexité :

Algorithme Unités de temps


def tri(L): T(n)
n=len(L) 2
for i in range(1,n): n-1 boucles
pos=i 1
x=L[i] 1
while pos>0 and L[pos-1]>x: i boucles au pire des cas
L[pos]=L[pos-1] 1
pos=pos-1 2
if pos!=i:L[pos]=x 1+1

Belgacem Hajji ~ 75 ~
Le temps d’exécution est donné par :
𝑛−1
𝑛2 𝑛
𝑇(𝑛) = ∑ 𝑖 + 𝑐𝑠𝑡𝑒 = 1 + 2 + ⋯ + 𝑛 − 1 = + + 𝑐𝑠𝑡𝑒
2 2
𝑖=1

 O(T(n))=O(n²)

9.4. Tri par fusion:


• Principe :
C’est un algorithme de tri basé sur le principe « Diviser pour régner ». On subdivise la liste
en deux listes de tailles presque égales ; on tri (on règne) ces dernières puis on les fusionne
(on combine) pour obtenir une liste triée.
L’algorithme utilise deux fonctions :
- Fonction fusion : Fusionne deux listes supposées triées en une seule. Par exemple
fusion([1,2,4],[2,3,8]) renvoie [1,2,2,3,4,8]. L’algorithme de la fusion procède comme
suit :
 Comparer les premiers éléments des deux sous listes
 Ajouter le plus petit d’entre eux dans une liste résultat
 Avancer dans la liste d’où est ce que l’élément a été pris
 Si on termine tous les éléments d’une liste et qu’il reste encore des éléments dans la
seconde, on effectue une copie des éléments restants directement

- Fonction tri : C’est une fonction récursive. Elle consiste à :

 Couper en deux la liste initiale


 Trier par appels récursifs les deux sous listes
 Fusionner les deux listes triées en utilisant la fonction fusion.

• Implémentation :

Belgacem Hajji ~ 76 ~
• Analyse de complexité :

Algorithme Unités de temps


def fusion(A,B): T(n)
C=[] 1
i,j=0,0 2
n1,n2=len(A),len(B) 4
while i<n1 and j<n2: 2*min(n1,n2)
if A[i]<B[j]: 1
C.append(A[i]) 1
i+=1 2
else:
C.append(B[j]) 1
j+=1 2
if i==n1:C.extend(B[j:]) n2-j+1+1
if j==n2:C.extend(A[i:]) n1-i+1+1
return C 1
 O(T(n))=O(n)
def trifusion(L): T(n)
if L==[] or len(L)==1:return L 4
n=len(L) 2
m=n//2 2
A=L[:m] n/2
B=L[m:] n/2
A1=trifusion(A) T(n/2)
B1=trifusion(B) T(n/2)
C=fusion(A1,B1) En O(n)
return C 1

Belgacem Hajji ~ 77 ~
Le temps d’exécution du tri est donné par :
𝑛
𝑇(𝑛) = 2𝑇 ( ) + 𝑛 + 𝑐𝑠𝑡𝑒
2
On en déduit que : a=2, b=2 et k=1. Or bk=21 = a : O(T(n))=O(nkLogb(n)) = O(nlog2(n)). Le tri
par fusion est un algorithme quasi linéaire plus rapide que les tris abordés précédemment.

n n² nlogn
100 10000 665
1000 1000000 9966
100000 10000000000 1660964

9.4. Tri rapide ou quicksort:


• Principe :
C’est aussi un algorithme basé sur le paradigme « diviser pour régner » :
- Diviser : Il s’agit de choisir un élément appelé pivot p et de subdiviser la liste initiale
L en deux sous listes A et B contenant respectivement les éléments de L qui sont
inférieurs à p et ceux qui lui sont supérieurs ou égaux.
- Régner : Le même principe est appliqué pour tirer les deux sous-listes A et B, jusqu’à
obtention de sous listes de tailles ≤1.
- Combiner : Aucun effort de combinaison n’est nécessaire étant donné que les sous
listes sont déjà triées.

• Implémentation :
- Fonction de partitionnement : On va choisir le 1er élément de la liste L comme pivot.

- La fonction du tri :

Belgacem Hajji ~ 78 ~
• Analyse de complexité :

Algorithme Unités de temps


def part(L): T(n)
p=L[0] 1
A=[] 1
B=[] 1
for i in range(1,len(L)): n-1
if L[i]<p:A.append(L[i]) 2
else:B.append(L[i]) 1
return A+[p]+B 3
T(n)=n+8
O(T(n))=O(n)
def QS(L): T(n)
if len(L)==1 or L==[]:return L 4
x=L[0] 1
L=part(L) n+8
pos=L.index(x) n+1
return T(n/2)+1+T(n/2)
QS(L[:pos])+[x]+QS(L[pos+1:])

D’où :
𝑛
𝑇(𝑛) = 2𝑇 ( ) + 2𝑛 + 𝑐𝑠𝑡𝑒
2
 O(T(n))=O(nlog2(n)) : Le tri rapide est un tri quasi linéaire.

Belgacem Hajji ~ 79 ~
Chapitre 11 : Les fichiers en python
10.1. Introduction
On appellera fichier toute collection de données (textuelles ou binaires) enregistrée sur un
support physique (un disque dur, une clé usb, etc). Un fichier peut être accessible en lecture
seule, ou en lecture/écriture. Pour pouvoir être consulté et/ou modifié, il doit être ouvert
d’abord, puis fermé ensuite (mais Python se charge souvent de fermer le fichier lui-même quand
tout est terminé).

Python propose la classe file pour modéliser la notion de fichier, et pour faciliter les
interactions. Tout dépend beaucoup du système d’exploitation (Windows, Linux, Mac OSX,
etc) mais la classe file offre des méthodes qui permettent de masquer les différences. Dans tous
les cas, un fichier possède un nom et réside (ou est créé) dans un dossier. Pour accéder au
fichier, on a besoin de son nom et du chemin d’accès à son dossier. Par défaut, il s’agit du
“dossier courant”, c’est-à-dire celui où réside le script en cours d’exécution (ou alors le dernier
script utilisé, ou celui de l’application IDLE si on travaille en mode interactif).

Dans toute la suite de cette section, on suppose qu’on se place uniquement dans le “dossier
courant”. Les fichiers seront donc désignés par leur nom, simplement, sans indication d’un
chemin d’accès. Mais si on veut un contrôle précis sur les dossiers, on importera le module os
(pour “operating system”) qui fournit quelques fonctions utiles :

• os.getcwd() renvoie le dossier de travail actuel (“get current directory”) :

• os.chdir("chemin") désigne un nouveau dossier de travail :

On ne confondra pas les objets de type file tels qu’ils sont créés et manipulés par Python (voir
plus loin) avec la concrétisation “physique” de ces fichiers sur le disque. En fait, les objets file
sont des abstractions permettant de désigner commodément ces fichiers “physiques”.

10.2. Manipulation des fichiers textes:


• ouverture :
Syntaxe : VarFichier=open(NomFichier,ModeTypAcces)
La commande open nécessite deux paramètres pour créer une variable en mémoire qui
représente le fichier physique nommé NomFichier :
 Le nom physique du fichier sur le support qui est généralement stocké dans le
répertoire courant de Python
 Le mode d’accès : Trois modes sont possibles
- ‘w’ :writting : Si le fichier existe dèjà son contenu sera remplacé par un nouveau
contenu, dans le cas contraire, on crée un fichier en mode écriture pour y
enregistrer du texte.
- ‘r’ : reading : Le mode lecture : le fichier est ouvert pour qu’il soit consulté

Belgacem Hajji ~ 80 ~
- ‘a’ : append : C’est un mode semblable au mode ‘w’ sauf qu’on n’écrase pas
l’ancien contenu du fichier, mais on ajoute à la fin du fichier de chaines de
caractères nouvelles.
 Le type d’accèes :
- ‘t’ : le fichier est considéré comme une suite de caractères ascii : c’est le mode
texte
- ‘b’ : le fichier est considéré comme une suite d’octets : c’est le mode binaire.

Dans la suite, on va créer un nouveau fichier intitulé « essai.txt » :

• Ecriture :
Syntaxe : varFichier.write(chaine_de_caracteres)
La commande write enregistre la chaine dans le fichier et retourne la longueur de cette
dernière.

Il faut toujours penser fermer le fichier à la fin de son utilisation surtout s’il est ouvert
en mode ‘w’ ou ‘a’ pour que les modifications soient enregistrées.

La commande closed permet de tester si le fichier est fermé avec succès, son résultat est
booléen.

Une fois créée, on peut aller consulter le contenu de notre fichier dans le répertoire de
Python :

Belgacem Hajji ~ 81 ~
Le contenu du fichier est le suivant :

 Les deux chaines sont écrites l’une à côté de l’autre sans retour à la ligne.

• Ecriture de plusieurs lignes


Pour remédier à ce problème de retour à la ligne, on rajoute le caractère d’échappement
‘\n’ à la fin de chaque chaine au niveau de la commande write :

Le contenu du fichier est mieux arrangé :

NB : on peut utiliser un conteneur pour enregistrer les lignes qu’on désire stocker dans
un fichier :

L’ancien contenu est perdu et remplacé par le contenu de la liste L :

Belgacem Hajji ~ 82 ~
• Lecture :
Avant de pouvoir lire le contenu d’un fichier, on doit l’ouvrir en mode lecture texte avec
la commande open(nomFichier,’rt’) . La consultation du contenu peut s’effectuer de
trois manières :
- VarFichier.read() : Affiche le contenu intégral du fichier :

- VarFichier.readline() : lit le fichier ligne par ligne, à chaque fois qu’on


rencontre le caractère ‘\n’, on s’arrête :

On peut utiliser une boucle pour améliorer l’affichage :

Belgacem Hajji ~ 83 ~
- VarFichier.readlines() : retourne une liste constituée des lignes du fichier :

• Ajout à la fin :
Syntaxe : VarFichier=open(NomFichier,’a’)

Le contenu du fichier n’est pas perdu mais augmenté :

10.3. Manipulation des fichiers binaires :


Supposons qu’on veut sauvegarder une liste obtenue après l’appel d’une fonction de tri. Ceci
parait illogique vu que dès qu’on quittera Python, le contenu de toutes les variables manipulées
sera perdu. Grâce au module pickle de Python, la sérialisation (l’enregistrement) des variables
de tout type en Python est possible. On parle plus de fichiers texte, mais de fichiers de flux
binaires, leur manipulation est similaire à celle des fichiers textes, avec quelques changement
au niveau de la lecture et de l’écriture.

• Ouverture d’un fichier binaire


Syntaxe :
- VarFichier=open( NomFichier,’wb’) : ouvre un fichier binaire en mode
écriture
- VarFichier=open(NomFichier,’rb’) : ouvre un fichier binaire en mode lecture

• Ecriture dans un fichier binaire :


Pour enregistrer un objet Python dans un fichier binaire, on utilise la commande dump
du module pickle avec la syntaxe suivante : pickle.dump(objet,varFichier) à
condition qu’on a déjà chargé le module pickle, l’objet python est déjà crée et le fichier
est déjà ouvert en mode écriture binaire :

Belgacem Hajji ~ 84 ~
• Lecture d’un fichier binaire :
On utilise toujours le module pickle et plus précisément sa commande load avec la
syntaxe suivante : pickle.load(varFichier). Cette commande retourne le 1er objet du
fichier binaire. On peut l’utiliser pour lire autant d’objet qu’on le souhaite à condition
de ne pas dépasser la taille du fichier, sinon une exception EOFError sera levée :

Pour éviter ce genre d’erreurs, on peut utilise la structure try …except :

Belgacem Hajji ~ 85 ~
Chapitre 12 : Les tableaux nupmy – simulation
numérique en Python

11.1. Introduction
Les tableaux sont des conteneurs spéciaux en Python, qui ne peuvent être utilisés que si le
module numpy est installé et chargé. On distingue les tableaux à une seule dimension et les
tableaux à deux dimensions appelés aussi matrices. Python autorise un certain nombre de
types pour les tableaux à savoir : les entiers, les booléens, les réels et les nombres
complexes. Les tableaux sont des conteneurs séquentiels (ordonnés) et modifiables
(mutables).

11.2. Les tableaux à une dimension


• Création : La commande array du module numpy permet de créer un tableau à une
dimension à partir d’une liste d’éléments :

• Dimension : on utilise nom_tableau.ndim ou ndim(nom_tableau) :

• Nombre d’éléments : on utilise nom_tableau.size ou size(nom_tableau) :

• Type des éléments : L’attribut dtype permet de renvoyer le type des éléments

• Tableaux spéciaux :
 Vides

 Des zéros partout

 Des uns partout

Belgacem Hajji ~ 86 ~
 A partir d’un intervalle :

• Accès : Le même système d’indexation des listes est réservé pour les tableaux :

• Ajout d’un élément

• Modification d’un élément

Belgacem Hajji ~ 87 ~
11.3. Les tableaux à deux dimensions
Chaque élément d’un tableau à deux dimensions est accessible à partir de deux indices, le
premier est l’indice sur les lignes et le deuxième sur les colonnes.

• Création : La syntaxe est la suivante : nom_tableau=array([[ligne1],[ligne2],…]). Bien


évidemment, la ligne1, ligne2...sont des listes de tailles égales.

• Tableaux de 0 ou de 1 :

• Nombre d’éléments

• Dimension

• Nombre de lignes et de colonnes

• Type des éléments

• Accès :
 Un élément

 Une ligne

 Une colonne

Belgacem Hajji ~ 88 ~
• Ajout d’une ligne

• Ajout d’une colonne

• Modification

NB : On ne peut pas supprimer des éléments d’un tableau (à 1D ou à 2D) :

11.4. Opération communes aux tableaux (1D et 2D) :


• Minimum et maximum des éléments :

Belgacem Hajji ~ 89 ~
• Somme, produit et moyenne des éléments :

• Multiplication par un scalaire, somme et produit de tableaux :

Belgacem Hajji ~ 90 ~

Vous aimerez peut-être aussi