Issu de : https://algo.developpez.com/tutoriels/initiation/
Un algorithme est une procédure de calcul bien définie qui prend en entrée un ensemble de valeurs et qui délivre en sortie un ensemble de valeurs.
Le but de ce cours est de vous apprendre les bases de l'algorithmique.
L'auteur : M. Delest.
Définition 1.1. Un algorithme est une procédure de calcul bien définie qui prend en entrée un ensemble de valeurs et qui délivre en sortie un ensemble de valeurs.
Exemple 1.1
Problème : trier une suite de nombres entiers dans l'ordre croissant.
Entrée : suite de n nombres entiers (a1,a2… an)
Sortie : une permutation de la suite donnée en entrée (a′1,a′2… a′n) telle que a′1≤a′2≤⋯≤a′n
. À partir de la suite (6,9,2,4), un algorithme de tri fournira le résultat (2,4,6,9).
Définition 1.2. Une valeur particulière de l'ensemble des valeurs données en entrée est appelée instance du problème.
Exemple 1.1 (suite)
La valeur (6,9,2,4) est une instance du problème.
Définition 1.3. Un algorithme est correct, si pour toute instance du problème il se termine et produit une sortie correcte.
Les algorithmes peuvent être spécifiés en langage humain ou tout langage informatique. Dans ce qui suit, nous utiliserons un langage proche du langage naturel. Nous donnerons une implémentation en Python.
Définition 1.4. Une heuristique est une procédure de calcul correcte pour certaines instances du problème (c'est-à-dire se termine ou produit une sortie correcte).
Ce cours n'aborde pas les heuristiques.
Pour qu'un algorithme puisse être décrit et s'effectue, les données d'entrées doivent être organisées.
Définition 1.5. Une structure de données est un moyen de stocker et d'organiser des données pour faciliter leur stockage, leur utilisation et leur modification.
De nombreux problèmes nécessitent des algorithmes :
Définition 1.6. L'efficacité d'un algorithme est mesurée par son coût (complexité) en temps et en mémoire.
Un problème NP-complet est un problème pour lequel on ne connaît pas d'algorithme correct efficace, c'est-à-dire réalisable en temps et en mémoire. Le problème le plus célèbre est le problème du voyageur de commerce.
L'ensemble des problèmes NP-complets ont les propriétés suivantes :
L'efficacité d'un algorithme est fondamentale pour résoudre effectivement des problèmes.
Exemple1.2.
Supposons que l'on dispose de deux ordinateurs. L'ordinateur A est capable d'effectuer 109 instructions par seconde. L'ordinateur B est capable d'effectuer 107 instructions par seconde. Considérons un même problème (de tri par exemple) dont la taille des données d'entrées est n. Pour l'ordinateur A, on utilise un algorithme qui réalise 2n2 instructions. Pour l'ordinateur B, on utilise un algorithme qui réalise 50nlog(n)
instructions. Pour traiter une entrée de taille 106 : l'ordinateur A prendra 2000 s et l'ordinateur B prendra 100 s. Ainsi, même si la machine B est médiocre, elle résoudra le problème 20 fois plus vite que l'ordinateur A.
Définition 1.1. La complexité d'un algorithme est :
Dans ce cours, nous considérerons que la complexité des instructions élémentaires les plus courantes sur un ordinateur ont un temps d'exécution que l'on considérera dans ce cours comme constant égal à 1. Les instructions élémentaires sont : addition, multiplication, modulo et partie entière, affectation, instruction de contrôle. Ce qui intéresse fondamentalement l'algorithmique, c'est l'ordre de grandeur (au voisinage de l'infini) de la fonction qui exprime le nombre d'instructions. Les courbes de références sont ici.
Il est nécessaire de disposer d'un langage qui soit non lié à l'implémentation. Ceci permet une description plus précise des structures de données ainsi qu'une rédaction de l'algorithme plus souple et plus « lisible ». Le langage EXALGO est un exemple de ce qui peut être utilisé et qui sera utilisé dans ce cours. Il est composé de chaînes de caractères alphanumériques, de signes opératoires (+, -, *, /, <, <=, >=, >, <>, ==, =, ou, non, et), de mot-clés réservés, et de signes de ponctuation : ''=, ;, (, ), début, fin, //. Les balises début et fin peuvent être remplacées par { et }.
Python n'utilise pas de marqueurs de fin. Le caractère « : » est le marqueur de début et quand l'indentation cesse Python considère que c'est un marqueur de fin.
Définition 2.1. Un type abstrait est un triplet composé :
Les types abstraits de base de l'algorithmique sont :
entier, caractère, booléen, réel
que l'on écrit respectivement en EXALGO
entier, car, booléen, réel
Définition 2.2. Une variable est un triplet composé :
On écrit en EXALGO
var NomDeVariable : Type;
Type est à prendre pour l'instant dans l'ensemble {entier, car, booléen, réel}.
Définition 2.3. Les Expressions sont constituées à l'aide de variables déjà déclarées, de valeurs, de parenthèses et d'opérateurs du (des) type(s) de variables concernées.
Définition 2.4. L'affectation est l'instruction qui permet de stocker une valeur dans une variable.
On écrit :
NomDeVariable = ExressionDuTypeDeLaVariable;
Toute variable doit être déclarée et recevoir une valeur initiale.
II-B-1. Booléens
Une variable de type booléen prend comme valeur VRAI ou FAUX. Les opérations usuelles sont ET, OU et NON qui sont données dans les tables qui suivent :
II-B-2. Entiers
Une variable de type entier peut prendre comme valeur l'ensemble des nombres entiers signés. Les opérations associées sont les opérations usuelles +,-,*,/.
II-B-3. Réels
Une variable de type réel peut prendre comme valeur l'ensemble des nombres réels. Les opérations associées sont les opérations usuelles +,-,*,/.
II-B-4. Caractères
Une variable de type car peut prendre comme valeur l'ensemble des caractères imprimables. On notera les valeurs entre guillemets. On considère souvent que les caractères sont ordonnés dans l'ordre alphabétique.
II-B-5. Attention
Les valeurs :
sont différentes et ne seront pas codées de la même manière dans la mémoire de la machine.
II-B-6. Comparaison
Les opérateurs <, ≤, ==, !=, >, ≥ permettent de comparer les valeurs de type entier, réel et caractère. Le résultat de cette comparaison est une valeur booléenne.
Il y a trois structures principales de contrôle qui permettent de construire des algorithmes.
début
instruction1
instruction2
..........
fin
Traduction Python
if ExpressionBooléenne:
BlocInstruction1
else:
BlocInstruction2
si ExpressionBooléenne alors
BlocInstructions1
sinon
BlocInstructions2
finsi;
Traduction Python
if cas1:
BlocInstruction1
elif cas2:
BlocInstruction2
elif .....:
.....
......
else:
BlocInstruction
selon que
cas cas1 : BlocInstructions1
cas cas2 : BlocInstructions2
..........
autrement : BlocInstruction
finselonque
Traduction Python
while ExpressionBooléenne:
BlocInstruction
.......
tant que ExpressionBooléenne faire
BlocInstructions
fintantque;
Traduction Python
for VariableIndicatrice in range(ValeurInitiale,ValeurFinale,ValeurPas):
BlocInstruction
.............
pour VariableIndicatrice
allant de ValeurInitiale à ValeurFinale
par pas de ValeurPas faire
BlocInstructions
finpour;
répéter
BlocInstructions
jusqu'à ExpressionBooléenne finrépéter;
Une fonction est une section d'algorithme qui a un objectif bien défini et un nom. En général, elle communique avec l'extérieur par le biais de paramètres typés. Elle possède des variables locales qui ne sont pas visibles à l'extérieur de la fonction. Ces variables peuvent être des fonctions. Une fonction retourne une valeur par l'instruction simple retourne(Expression). L'expression peut être :
II-D-1. Syntaxe
fonction NomDeFonction (ListeParamètres) :TypeRésultat;
// déclarations des variables ou fonctions locales autres que les paramètres
début
// partie instruction qui contient l'appel à retourne
fin
finFonction
ref ListeVariable NomDeType
la fonction travaille directement dans la variable passée en paramètre ;
val ListeVariable:NomDeType
la fonction travaille sur une copie de la variable passée en paramètre.
II-D-2. Utilisation
Une fonction s'utilise en écrivant :
NomDeFonction(ListeInstanceParamètres)
II-D-3. Exemple
fonction exemple(val n :entier;ref m : entier) :vide;
début
n = 5;
m = 7;
fin
finFonction
Supposons que l'on ait la séquence suivante :
var p,q :entier;
début
p = 1;
q = 2;
exemple(p,q);
fin
Après exécution p contiendra 1 et q contiendra 7 (Animation ici).
EXALGO permet de fixer les quelques règles élémentaires permettant d'écrire des algorithmes en s'affranchissant l'implémentation.
Le langage EXALGO est composé de chaînes de caractères alphanumériques, de signes opératoires, de mot-clés réservés, et de signes de ponctuation : =, ;,(,), début, fin, //. Les marqueurs de fin, début et fin peuvent être remplacés par { et } lorsqu'il y a encombrement.
Définition de type
type NomDeType = TypePrédéfini;
Définition d'un tableau d'entiers
typeNomDeType = tableau 1..limite deTypePrédéfini;
var NomDeVariable: TypePrédéfini;
Constituées à l'aide de variables déjà déclarées, de parenthèses et d'opérateurs du (des) type(s) des variables concernées.
III-E. Instructions simples
NomDeVariable = ExressionDuTypeDeLavariable;
Bloc d'instruction
Instruction1
instruction2
.............
si ExpressionBooléenne alors
BlocInstruction1
sinon
BlocInstruction2
finsi;
selon que
cas cas1 : BlocInstruction1
cas cas2 : BlocInstruction2
.............
autrement : BlocInstruction
finselonque
tant que ExpressionBooléenne faire
BlocInstruction
fintantque;
pour VariableIndicatrice
allant de ValeurInitiale à ValeurFinale
par pas de ValeurPas faire
BlocInstruction
finpour;
répéter
BlocInstruction
jusqu'à ExpressionBooléenne finrépéter;
Une fonction retourne une valeur par l'instruction simple (retourne(Expression)). Une fonction s'utilise dans le calcul d'une expression ou comme instruction simple.
fonction NomDeFonction (ListeParamètres):TypeRésultat;
// déclarations des variables locales autres que les paramètres
début
// partie instruction qui contient l'appel à retourne()
fin
finFonction
ref ListeVariable:NomDeType
val ListeVariable:NomDeType
III-H-1. Type structuré
Un type structuré est constitué à partir de types de base ou d'autres types déclarés.
type NomDeType: structure
champ1:NomDeType1
champ2:NomDeType2
…
finstructure
Après la déclaration :
var E:NomDeTypeEnregistrement
on accède aux différents champs par le nom de la variable suivi d'un point suivi du nom de champ (E.champ1).
III-H-2. Type pointeur
Si O est un objet de type T, on accède à l'objet par O^. Si on déclare :
var P:^NomDeType
alors on peut obtenir un objet accessible par allouer(P). Lorsqu'on n'utilise plus l'objet, il faut libérer l'espace qu'il utilise par desallouer(P).
Définition 3.1. Une séquence sur un ensemble E est une suite d'éléments (e1,e2,…en) d'éléments de E.
Une séquence peut contenir des éléments identiques de l'ensemble E.
Exemple 3.1 (3,5,8,2,12,6) : est une séquence d'éléments de N, ensemble des entiers naturels. ("a","z","T","A","a") est une séquence sur l'ensemble des caractères imprimables (char).
Il existe plusieurs variantes de séquences suivant les opérations de manipulation autorisées : accès par l'indice de l'élément ou non, accès à la fin de la séquence ou non…
On utilisera en général des noms particuliers dépendant des caractéristiques de la séquence.
Exemple 3.2 : Un vecteur peut être défini par une séquence dans laquelle l'accès aux éléments se fait par son indice et la taille de la séquence dépend de l'espace dans lequel on se trouve. On dit aussi qu'on a un accès direct à l'élément. Dans la plupart des langages de programmation, le vecteur existe sous le nom d'array.
Exemple 3.3 : Soit la procédure calculant la factorielle :
fonction fac(val n :entier) :entier;
début
si n <= 1 alors
retourner(1)
sinon
retourner(n * fac(n-1))
finsi
fin
finfonction
Programme Python
def fac(n):
if (n<=1) :
return 1
else :
return n*fac(n-1)
print(fac(5))
La séquence des valeurs de n au cours des appels récursifs doit être mémorisée. Supposons l'appel fac(4) alors :
Soit F1, F2,…, Fp
des ensembles.
Définition 3.2. Une structure sur F1×F2×⋯×Fp
est une séquence (f1, f2,…, fk)
telle que
∀i∈[1..k],fi∈Fi.
Les structures sont des cas particuliers de séquences. En algorithmique, chaque ensemble Fi
peut être un type de base ou une structure. Ce mécanisme permet de définir de nouveaux types plus complexes que les types de base. En EXALGO, on écrit :
nom_du_type = structure
nom_champs_1 :type1;
nom_champs_2 :type2;
…
nom_champs_k :typek;
finstructure
Cela signifie que lorsqu'une variable est déclarée de ce type, elle référence k variables en même temps. Soit V une variable dont le type est une structure, on désigne un des champs par V. suivi du nom du champ.
Exemple 3.4 : Une date de naissance est un exemple de structure. On peut écrire :
dateDeNaissance = structure
jourDeNaissance: entier;
moisDeNaissance: entier;
annéeDeNaissance: entier;
finstructure
On peut définir une structure composée du sexe et de la date de naissance :
individu = structure
sexe :booléen
date :dateDeNaissance;
finstructure.
Soit la déclaration :
var I :individu
alors I.sexe sera un booléen et I.date.jourDeNaissance sera un entier. Ainsi les instructions suivantes ont un sens :
I.date.jour = 12;
I.sexe = faux;
Définition 3.3 : Soit F un ensemble. Une table d'association à clé unique est une séquence d'éléments de N×F
(N est l'ensemble des entiers naturels), ((c1,f1), (c2,f2),…, (ck,fk))
telle que :
∀i,j∈[1..k],i≠j,ci≠cj
Les tables d'association sont un cas particulier de séquences d'éléments structurés. La structure se décrit en EXALGO :
association = structure
cle :entier;
valeur :type_prédéfini;
finstructure
Exemple 3.5 : Lors de l'activation du compte électronique, l'étudiant de l'Université Bordeaux 1 fournit un numéro INE qui sera associé à un mot de passe. On a donc quelque part dans le système de gestion des comptes une table d'association à index unique dont l'élément de séquence est :
Etudiant = structure
INE :entier;
motDePasse :typeMotDePasse;
finstructure
Définition 4.1. (Notation de Landau). On dit que f=O(g) s'il existe deux nombres réels k,a>0 tels que ∀x>a,|f(x)|≤k|g(x)|.
Exemple 4.1. Si le nombre d'instructions est égal à f(n)=an2+bn+c avec a,b,c des constantes réelles, alors f(n)=O(n2).
Les figures permettent de comparer les fonctions usuelles utilisées pour décrire la complexité d'un algorithme en fonction de la taille n des données d'entrées. Parmi les fonctions usuelles, le log à base 2 de nlog2(n) joue un rôle important. Pour un algorithme A, notons CA(D), le coût de l'algorithme A pour une instance D.
Définition 4.2. On définit les trois complexités suivantes :
· · complexité dans le meilleur des cas :
C<A(n=min{CA(d),d donnée de taille n}
· · complexité en moyenne :
CA(n)=∑d instance de APr(d)CA(d)
où Pr(d) est la probabilité d'avoir en entrée une instance d parmi toutes les données de taille n.
Soit Dn l'ensemble des instances de taille n. Si toutes les instances sont équiprobables, on a :
CA(n)=1|Dn|∑d instance de ACA(d)
Parfois, il est nécessaire d'étudier la complexité en mémoire lorsque l'algorithme requiert de la mémoire supplémentaire (donnée auxiliaire de même taille que l'instance en entrée par exemple).
Les algorithmes font intervenir les opérations élémentaires suivantes :
Les complexités en temps des structures sont données ci-dessous :
(resp. BO(n)) la complexité en nombre de tests (resp. d'opérations élémentaires) de la suite d'instructions à itérer, et k
le nombre de fois où l'itération s'effectue alors la complexité sera de :
· pour le nombre de tests ;
· kBO(n)
· pour le nombre d'opérations du « tant que » et du « répéter » ;
· k(BO(n)+1)
· pour le nombre d'opérations du « pour ».
V-C-1. Somme des N premiers entiers
fonction suite(val n :entier) :entier;
var i,s :entier;
début
s = 0;
pour i allant de 1 à n faire
s = s + i;
finpour;
retourner(s)
fin
finfonction;
Source Python
#! /usr/bin/python
def somme(n):
s= 0
for i in [n]:
s = s+i
return s
print suite(4)
On a :
C>suite(n)=C<suite(n)=Csuite(n)=O(n)
V-C-2. Apparition d'une pile dans une suite de n lancers d'une pièce
Entrée : un entier n
Sortie : « vrai » si on rencontre une pile, « faux » sinon.
La fonction suivante retourne « vrai » lorsque l'un des lancers est égal à 6 et « faux » sinon.
fonction jeuDePile(val n :integer) :booléen;
var i : entier;
début
pour i allant de 1 à n faire
f = résultat_lancer_pièce()
si (f==pile) alors
retourner(vrai)
finsi
finpour
retourner(faux)
fin
finFonction
Source Python
#! /usr/bin/python
# Module pour calculer un nombre aléatoire
import whrandom
def rand():
return int(whrandom.random() * 32768.0) % 32768
# Lancer de pile
def resultat_lancer_piece():
return rand()*2/32767
def jeu_de_pile(n):
for i in [n]:
f = resultat_lancer_piece()
if (f == 1):
return 1
return 0
print jeu_de_pile(4)
· (On ne tire jamais de pile)
· C<pile(n)=O(1)
· (On tire une pile le premier coup)
· Les faces du dé apparaissent de manière équiprobable et les tirages sont indépendants. On peut montrer que le coût moyen de l'algorithme est : Cpile(n)=O(1)
V-D-1. n, log(n), nlog(n)
V-D-2. nlog(n), n2, n3
V-D-3. n2, 1.5n ,2n
V-D-4. 2n, nn n !
V-E. Formule de Stirling
n! = e-nnn(2.pi.n)0.5
Définition 5.1. Un tableau est une table d'association à clé unique telle que :
On écrit en EXALGO :
nom_tableau = tableau[min_indice..max_indice] de type_predefini;
ce qui signifie que :
La taille du tableau est donc max_indice - min_indice + 1. Pour accéder à un élément d'un tableau T d'indice I, on écrit T[I]. La complexité de l'accès à un élément du tableau est O(1).
Soit min_indice< i<j <max_indice, on notera T[i..j] la séquence des éléments de T (T[i],T[i+1],…,T[j]).
Beaucoup d'algorithmes peuvent être décrits sans préciser un type particulier. Dans ce cas, on écrira à la place de type_prédéfini le mot élément et on précisera les valeurs possibles pour élément.
Exemple 5.1. Soit deux tableaux :
L'algorithme qui permet de trier TC et TE est le même. Seul diffère le type de l'élément manipulé. On écrira dans ce cas un algorithme sur un tableau.
T = tableau[1..10] d'éléments;
et on précisera que l'élément est dans {car,entier}.
Les paramètres tableaux doivent, sauf raison majeure, être passés en paramètre par référence afin d'éviter la recopie.
VI-B-1. Initialisation d'un tableau
fonction init(ref T :tableau[min_indice..max_indice] d'éléments;
val valeurInitiale :élément) :vide;
var i :entier;
début
pour i allant de min_indice à max_indice faire
T[i] = valeurInitiale
finpour
fin
finfonction
Complexité : O(n)
VI-B-2. Taille d'un tableau
fonction taille(ref T :tableau[min_indice..max_indice] d'éléments) :entier;
début
retourner(max_indice - min_indice + 1)
fin
finfonction
Complexité : O(1).
VI-B-3. Échange d'éléments
fonction echange(ref T :tableau[min_indice..max_indice] d'éléments;
val indice1,indice2 : entier) :vide;
var e :élément;
début
e = T[indice1];
T[indice1] = T[indice2];
T[indice2] = e;
fin
finfonction
Complexité : O(1).
VI-B-4. Copie de tableau
fonction copie(ref T1,T2 :tableau[min_indice..max_indice] d'élément;
val indiceT1_1,indiceT1_2,indiceT2 : entier) :booléen;
var i :entier;
début
si indiceT2+indiceT1_2-indiceT1_1>max_indice alors
retourner(faux)
sinon
pour i allant de indiceT1_1 à indiceT1_2 faire
T2[indiceT2] = T1[i];
indiceT2 = indiceT2 + 1;
finpour
retourner(vrai)
fin
finfonction
Complexité :
Source Python
#! /usr/bin/python
# En Python, les tableaux peuvent être construits à partir des listes
# L'index varie de 0 à la taille du tableau -1
class Tableau:
def __init__(self,taille,valinit):
self.tableau = []
for i in range(taille):
self.tableau.append(valinit)
def taille(self):
return(len(self.tableau))
def echange(self,i,j):
tmp=self.tableau[i]
self.tableau[i]=self.tableau[j]
self.tableau[j]=tmp
return()
def copie(self,T1,indiceT_1,indiceT_2,indiceSelf):
if indiceSelf+indiceT_2-indiceT_1 >self.taille():
return(1)
else:
i=indiceT_1
for i in range(indiceT_1,indiceT_2+1):
self.tableau[indiceSelf]=T1.tableau[i]
indiceSelf=indiceSelf+1
return(0)
def ecrire(self):
for i in range(len(self.tableau)):
print self.tableau[i],
print
return()
VI-C-1. Somme des éléments d'un tableau d'entiers
fonction somme(ref T :tableau[min_indice..max_indice] d'entiers) :entier;
var s,i :entier;
début
s = 0;
pour i allant de min_indice à max_indice faire
s = s + T[i]
finpour
retourner(s)
fin
finfonction
Complexité : O(n)
VI-C-2. Recherche d'un élément
Propriété 5.2. Soit i,j deux entiers, i<=j. Soit T un tableau d'éléments d'indice variant entre i et j. Pour tout élément e, appartenant au tableau T, on a :
T[i] = e ou e est dans T[i+1..j]
fonction cherche(ref T :tableau[min_indice..max_indice] d'éléments;val e :élément) :entier;
var i :entier;
début
pour i allant de min_indice à max_indice faire
si T[i]==e alors
retourner(i)
finsi
finpour
retourner()
fin
finfonction
Complexité :
Source Python
from tableaux import Tableau
def chercheRecursif(T,e,min_indice,max_indice):
if T.tableau[min_indice] == e:
return(min_indice)
else :
if min_indice == max_indice :
return()
else :
return(chercheRecursif(T, e, min_indice+1, max_indice))
def chercheIteratif(T,e):
for i in range(T.taille()):
if T.tableau[i] == e :
return(i)
return()
VI-C-3. Recherche de l'indice du premier élément minimum
On suppose que le tableau contient des éléments comparables (l'ensemble des éléments est muni d'une relation d'ordre). Choisissons ici, pour simplifier les notations, des entiers.
Propriété 5.3. Soit i,j deux entiers, i<=j. Soit T un tableau d'entiers d'indice variant entre i et j. Soit m l'élément minimum du tableau, on a :
T[i] = m ou m est dans T[i+1..j]
fonction minimum(ref T :tableau[min_indice..max_indice] d'entier) :entier;
var i,sauv :entier;
début
sauv = min_indice;
pour i allant de min_indice+1 à max_indice faire
si T[i]<T[sauv] alors
sauv = i
finsi
finpour
retourner(sauv)
finfonction
Complexité : O(n)
VI-D-1. Déclaration
Une matrice M de dimension n×m est un tableau de dimension n dont chaque élément est un tableau de dimension m. On peut donc déclarer la matrice sous la forme suivante :
var M :tableau[1..n] de tableau [1..m] d'éléments;
VI-D-2. Initialisation
fonction initMatrice(ref M :tableau[1..n] de tableau [1..m] d'éléments;
val valeurInitiale :élément) :vide;
var i,j :entier;
début pour i allant de 1 à n faire
pour j allant de 1 à m faire
M[i][j] = valeurInitiale
finpour
finpour
retourner()
finfonction
Complexité : O(nm)
VI-D-3. Somme de deux matrices réelles
fonction sommeMatrice(ref M1,M2 :tableau[1..n] de tableau [1..m] de réels) :
tableau[1..n] de tableau [1..m] de réels;
var i,j :entier;
var M :tableau[1..n] de tableau [1..m] de réels;
début
pour i allant de 1 à n faire
pour j allant de 1 à m faire
M[i][j] = M1[i][j] + M2[i][j];
finpour
finpour
retourner(M)
finfonction
Complexité : O(nm)
On considérera dans tout ce chapitre que l'on manipule des entiers. L'objet du tri est d'ordonner une séquence de N entiers. On considérera que ces entiers sont rangés dans un tableau.
var T :tableau[1..N] d'entiers;
De plus, on considéra que l'ordre est croissant.
Ce tri est basé sur l'algorithme de recherche du minimum On adapte cet algorithme pour pouvoir effectuer la recherche dans un sous-tableau. On a le déroulement ici.
fonction minimumSoustableau(ref T :tableau[1..N] d'entiers, val Imin,Imax :entier) :entier;
var sauv :entier;
début
sauv = Imin;
pour i allant de Imin+1 à Imax faire
si T[i]<T[sauv] alors
sauv = i;
finsi
finpour
retourner(sauv);
fin
finfonction
fonction triSelection(ref T :tableau[1..N] d'entiers) :vide;
var i,j,indice_cle :entier;
début
pour i allant de 1 à N-1 faire
indice_cle = minimumSoustableau(T,i,N);
echange(T[i],T[indice_cle]);
finpour
fin
finfonction
Propriété 6.1. La complexité de l'algorithme triSelection sur une instance de taille N est O(n2)
Propriété 6.2. Soit T un tableau d'entiers trié d'indice variant entre i et j. Soit e un entier quelconque, alors on a l'une des propriétés suivantes :
On déduit de cette propriété deux algorithmes permettant de trier un tableau.
VII-B-1. Tri insertion
fonction triInsertion(ref T :tableau[1..N] d'entiers) :vide;
var i,j,cle :entier;
début
pour i allant de 2 à N faire
cle = T[i];
j = i-1;
tant que j>0 et T[j]>cle faire
T[j+1] = T[j];
j = j-1;
fintantque
T[j+1] = cle;
finpour
fin
finfonction
On a le déroulement ici.
Propriété 6.3. La complexité de l'algorithme triInsertion sur une instance de taille N est :
Idée de la démonstration
La boucle « pour » s'effectue systématiquement et demandera O(N) opérations.
La boucle « tant que » effectue au minimum O(1) opération (cas où les nombres sont déjà triés) et au maximum O(N).
La boucle « tant que » effectue en moyenne O(N/2) opérations.
fonction triBulle(ref T :tableau[1..N] d'entiers) :vide;
var i,j,cle :entier;
début
pour i allant de 1 à N-1 faire
pour j allant de N à i+1 par pas de -1 faire
si T[j] < T[j-1] alors
echange(T, j, j-1);
finsi
finpour
finpour
fin
finfonction
On a le déroulement ici.
Propriété 6.4. La complexité de l'algorithme triBulle sur une instance de taille N est O(N2).
Lorsque deux tableaux T1 et T2 sont triés, il est aisé de construire un nouveau tableau contenant la séquence triée regroupant les séquences correspondantes à T1 et T2.
PREMIERE VERSION
fonction fusion(ref T1 :tableau[1..N1] d'entier;
ref T2 :tableau[1..N2] d'entier
) :tableau[1..N1+N2] d'entier;
var I1,I2,i :entier;
var T :tableau[1..N1+N2] d'entier;
début
I1 = 1;
I2 = 1;
pour i allant de 1 à N1+N2 faire
si T1[I1]≤T2[I2] alors
T[i] = T1[I1];
I1 = I1+1;
sinon
T[i] = T2[I2];
I2 = I2+1;
finsi
finpour
retourner(T)
fin
finfonction
Complexité : O(n)
Cette version ne fonctionne pas toujours. Par exemple, si I1 a dépassé N1 et vaut par exemple N1+1, on comparera T1[N1+1] à T2[I2] ce qui n'a pas de sens. Il faut donc utiliser un algorithme exact. On a le déroulement ici.
Soit une séquence d'éléments de [0..k], il est alors possible de réaliser l'histogramme des valeurs. Par la suite le tri des éléments de la séquence se fait en temps linéaire O(n).
fonction triHisto(ref T :tableau[1..N] d'entiers) :vide;
var H :tableau[0..maximum(T)] d'entier;
var i,j,k,max : entier;
début
init(H,0);
pour i allant de 1 à N faire
H[T[i]] = H[T[i]] + 1;
finpour
i = 1;
max = maximum(T);
pour j allant de 0 à max faire
pour k allant de 1 à H[j] faire
T[i] = j;
i = i+1;
finpour
finpour
fin
finfonction
On a le déroulement ici.
VII-E-1. Aide
On considère une nouvelle fonction copie qui copie un tableau dans un autre même s'ils n'ont pas la même définition. L'en-tête de la fonction est :
fonction copie(ref T1:tableau[1..N1] d'élément;
ref T2:tableau[1..N2] d'élément;
val indiceT1_1,indiceT1_2,indiceT2: entier):vide;
Le schéma de la fonction fusion est alors le suivant :
fonction fusion(ref T1:tableau[1..N1] d'entier;
ref T2:tableau[1..N2] d'entier
):tableau[1..N1+N2] d'entier;
var I1,I2,i:entier;
var T:tableau[1..N1+N2] d'entier;
début
Initialiser I1,I2,i
tant que I1 ≤ N1 et I2 ≤ N2 faire
// ... On compare tant qu'il reste des éléments dans T1 et T2
fintantque
si I1 ≤ N1 alors
// il n'y a plus d'éléments dans T2 :copier(..........)
sinon
// il n'y a plus d'éléments dans T1 :copier(..........)
finsi
retourner(T)
fin
finfonction
VII-E-2. Algorithme de fusion
fonction fusion(ref T1:tableau[D1..N1] d'entier;
ref T2:tableau[D2..N2] d'entier
):tableau[1..N1+N2-D1-D2+2] d'entier;
var I1,I2,i: entier;
var T:tableau[1..N1+N2-D1-D2+2] d'entier;
début
i = 1;
I1 = D1;
I2 = D2;
tant que I1 ≤ N1 et I2 ≤ N2 faire
si T1[I1] ≤ T2[I2] alors
T[i] = T1[I1];
I1 = I1+1;
sinon
T[i] = T2[I2];
I2 = I2+1;
finsi
i = i+1
fintantque
si I1 ≤ N1 alors
copier(T1,T,I1,N1,i)
sinon
copier(T2,T,I2,N2,i)
finsi
retourner(T)
fin
finfonction
VII-E-3. Algorithme de fusion pour des morceaux de tableaux
fonction fusion(ref T1:tableau[D1..N1] d'entier;
ref T2:tableau[D2..N2] d'entier;
val DT1,FT1,DT2,FT2:entier):
tableau[1..FT1+FT2-DT1-DT2+2] d'entier;
var I1,I2,i:entier;
var T:tableau[1..FT1+FT2-DT1-DT2+2] d'entier;
début
i = 1;
I1 = DT1;
I2 = DT2;
tant que I1 ≤ FT1 et I2 ≤ FT2 faire
si T1[I1] ≤ T2[I2] alors
T[i] = T1[I1];
I1 = I1+1;
sinon
T[i] = T2[I2];
I2 = I2+1;
finsi
i = i+1
fintantque
si I1 ≤ FT1 alors
copier(T1,T,I1,FT1,i)
sinon
copier(T2,T,I2,FT2,i)
finsi
Retourner(T)
fin
finfonction
Comme vu au chapitre Codage et structures de contrôle, on peut déclarer dans une fonction des variables et des fonctions locales :
fonction NomDeFonction (ListeParamètres) :TypeRésultat;
// déclarations des variables ou fonctions locales
début
// partie instruction qui contient l'appel à retourner
fin
finFonction
La multi-imbrication possible des fonctions entraîne l'existence de problèmes de visibilité : entre les variables et entre les fonctions.
VIII-A-1. Visibilité d'une variable
VIII-A-2. Exemple
Soit la fonction P suivante :
fonction P (....) :....;
var x,y,z : entier ;
fonction R() :vide;
var z,u,v : entier ;
début
z = 0;
u = 6;
...
fin ;
finFonction
fonction Q(ref x :entier ) :....;
var u,y : entier ;
début
y = 4;
x = x+y;
u = 7
fin ;
finFonction
début
x = 1;
y = 2;
z = 3;
R() …
Q(z);
fin
finFonction
On a le déroulement ici.
VIII-A-3. Visibilité d'une fonction
Une fonction est visible depuis la fin de son entête jusqu'au finFonction de la fonction où elle a été déclarée. Cependant comme pour les variables, elle peut momentanément être cachée par une autre fonction ayant le même entête (surcharge).
VIII-A-3-a. Exemple
La fonction P suivante est annotée pour préciser la visibilité des fonctions Q,R,T.
fonction P(....) :....;
.....
fonction Q(....) :.....;
.....
fonction R(...) :.....;
....
début
....// on peut utiliser P,Q,R
fin
finFonction ;
début
....// on peut utiliser P,Q,R
fin
finFonction
fonction T(...) :...;
début
....// on peut utiliser P,Q,T mais pas R
fin
finFonction ;
début
... //// on peut utiliser P,Q,T mais pas R
fin
finFonction
La récursivité consiste à remplacer une boucle par un appel à la fonction elle-même. Considérons la suite factorielle, elle est définie par :
La fonction peut s'écrire simplement :
fonction factorielle(val n :entier) :entier;
début
si (n == 0)
retourne(1)
sinon
retourne(factorielle(n-1) * n)
finsi
fin
finfonction;
On a le déroulement ici. On peut décrire sur le papier les changements et les appels sous la forme suivante :
Plusieurs appels à la fonction peuvent être exécutés dans son corps. Soit la suite dite de Fibonacci définie par :
La fonction s'écrit tout aussi simplement :
fonction fibo(val n :entier) :entier;
début
si (n == 0) ou (n == 1) alors
retourne(1)
sinon
retourne(fibo(n-1) + fibo(n-2))
finsi
fin
finfonction;
On a le déroulement ici. On peut décrire sur le papier les changements et les appels sous la forme suivante :
Examinons la suite définie par :
Une fonction permettant le calcul de son ne terme est :
fonction suite(val n :entier) :entier;
var i,s :entier;
début
s = 0;
pour i allant de 1 à n faire
s = s+i;
finpour;
retourner(s)
fin
finfonction;
L'exemple ci-dessus devient en algorithme récursif :
fonction suiteR(val n :entier) :entier;
début
si n == 1 alors
retourne(1)
sinon
retourne(suiteR(n-1) + n)
finsi
fin
finfonction;
La complexité en nombre d'opérations de suite et suiteR est en O(n)
. On aurait donc tendance à préférer suiteR pour sa lisibilité. Cependant, si on examine la complexité en mémoire, suite est en O(1) alors que suiteR est en O(n).
La programmation non récursive est donc plus efficace. L'utilisation de la récursivité ne doit pas se faire au détriment de l'efficacité.
Chaque fois que l'on désire programmer une fonction récursive, on doit répondre aux questions suivantes :
VIII-D-1. Recherche d'un élément dans un tableau d'entiers
fonction cherche(ref T :tableau[min_indice..max_indice] d'éléments;
val e : élément) :entier;
début
si T[min_indice] == e alors
retourner(min_indice)
sinon
si min_indice == max_indice alors
retourner(NUL)
sinon
retourner(cherche(T[min_indice+1..max_indice], e))
finsi
finsi
fin
finfonction
VIII-D-2. Minimum dans un tableau d'entiers
fonction minimumTableau(ref T :tableau[1..N] d'entiers;
val Imin :entier) :entier;
var sauv :entier;
début
si Imin == N alors
retourner(T[N])
sinon
sauv = minimumTableau(T, Imin+1];
si T[Imin] < sauv alors
retourner(T[Imin])
sinon
retourner(sauv)
finsi
finsi
fin
finfonction
La dichotomie fait partie des méthodes dites « diviser pour régner ». Elle consiste pour un objet de taille N à exécuter un algorithme de façon à réduire le problème à un objet de taille N/2. On répète alors l'algorithme de réduction sur ce dernier objet. Ainsi, il suffit de connaître la résolution pour un problème de taille faible (typiquement N=1 ou N=2) pour obtenir la totalité de la résolution.Ce type d'algorithme est souvent implémenté de manière récursive. Lorsque cette technique est utilisable, elle conduit à un algorithme très efficace et très lisible.
Il est parfois nécessaire de traiter les données avant d'appeler la fonction récursive. La fonction récursive est alors une fonction locale à la fonction d'appel.
IX-B-1. Recherche du zéro d'une fonction croissante
Soit g une fonction croissante sur un intervalle [a,b] et telle que f(a)≤0 et f(b)≥0. L'algorithme ci-dessous permet de trouver la valeur x de [a,b] telle que f(x)=0 avec une précision e.
fonction zero(ref g(val n :réel) :fonction;val a,b,e :réel) :réel;
var M :réel;
début
M = g((a+b)/2);
si |M| < e alors
retourne((a+b)/2)
sinon
si M > 0 alors
zero(g, a, (a+b)/2, e)
sinon
zero(g, (a+b)/2, b, e)
finsi
finsi
fin
finfonction
IX-B-2. Trouver un élément dans un tableau ordonné
Nous avons déjà traité cet algorithme sous une autre forme au chapitre Tableaux.
Propriété 8.1. T un tableau d'entiers triés d'indice variant entre d et f. Posons m=⌊(d+f)/2⌋
. Soit e un entier appartenant à la séquence contenue dans T. On a l'une des propriétés suivantes :
fonction cherche(ref T :tableau[1..N] d'entiers; val e :entier) :entier;
var d,f :entier;
fonction chercheRec(ref T :tableau[1..N] d'entiers; val d,f,e :entier) :entier; var m;
début
si f == d alors
si T[d] == e alors
retourner(f)
sinon
retourner(NUL)
finsi
sinon
m = partieEntiere((d+f)/2);
si T[m] < e alors
retourner(chercheRec(T,m+1,f,e))
sinon
retourner(chercheRec(T,d,m,e))
finsi
finsi
fin
finfonction
début
d = 1;
f = N;
retourner(chercheRec(T,d,f,e))
fin
finfonction
Propriété 8.2. La complexité de la fonction cherche est O(log2(n)).
Idée de la preuve : la complexité de la fonction cherche est donnée par la complexité de chercheRec. Soit f(n)
le nombre de tests effectués par cette fonction.
On a :
f(n)f(1)=2+f(⌊n/2⌋)=2
Soit p tel que 2p≤n≤2p+1
On a donc p≤log2(n)≤p+1. De plus : f(n)=2∑i=0p1 et donc f(n)=2×(p+1).
IX-B-3. Remarque
L'algorithme de multiplication de deux matrices de dimension n×n
s'implémente facilement en O(n3). Strassen a montré qu'en utilisant une méthode « diviser pour régner », la multiplication peut s'effectuer en O(nln(7)/ln(2)).
La courbe se présente comme suit :
Un algorithme « diviser pour régner » a la structure suivante :
· Pour résoudre un problème de taille n>n0
La complexité en temps de l'algorithme est donc déterminée par une équation de récurrence de la forme :
C(n)=aC(n/b)+d(n)
qui après résolution permet de montrer que cette méthode conduit à des algorithmes plus efficaces en nombre d'opérations. Cependant, cela ne doit pas occulter l'aspect mémoire. La complexité en mémoire doit rester d'un ordre raisonnable. (cf. récursivité).
On considérera dans tout ce chapitre que l'on manipule des entiers. L'objet du tri est d'ordonner une séquence de N entiers. On considérera que ces entiers sont rangés dans un tableau :
var T :tableau[1..N] d'entiers;
De plus, on considéra que l'ordre est croissant.
Cet algorithme consiste à diviser la séquence d'entiers en deux sous-séquences, à les trier de manière récursive, puis à fusionner les deux sous-séquences triées. On utilise la fonction fusion vue au chapitre tris non récursifs.
fonction triFusion(ref T :tableau[1..N] d'entiers) :vide;
var d,f :entier;
fonction fusionLocal(ref T :tableau[1..N] d'entier;
val d,m,f :entier) :vide;
var C :tableau[1..f-d+1] d'entier;
début
C = fusion(T,T,d,m,m+1,f);
copie(C,T,d,f,d);
fin
finfonction
fonction triFusionRec(ref T :tableau[1..N] d'entiers; val d,f :entier) :vide;
début
si d<f alors
m = partieEntiere((d+f)/2);
triFusionRec(T,d,m);
triFusionRec(T,m+1,f);
fusionLocal(T,d,m,f);
finsi
fin
finfonction
début
trifusionRec(T,1,N)
fin
finfonction
On a le déroulement ici.
Propriété 9.1. La complexité du tri fusion pour une séquence de n éléments est O(nlog2(n)).
idée de la preuve : la complexité de la fonction triFusion est donnée par la complexité de triFusionRec. Soit f(n)
le nombre de tests effectués par cette fonction. On a :
f(n)f(1)=1+2f(⌊n/2⌋)+3n=0
Soit p tel que 2p≤n≤2p+1. On a donc p≤log2(n)≤p+1
On en déduit :
f(n)=∑i=0p2i+3pn
et
f(n)=2p+1+3pn−1
Cet algorithme consiste à utiliser une valeur x de la séquence pour diviser la séquence d'entiers en deux sous-séquences :
Puis la procédure s'effectue récursivement sur les deux sous-séquences :
fonction triRapide(ref T :tableau[1..N] d'entiers) :vide;
fonction diviserSequence(ref T :tableau[1..N] d'entier;
val d,f :entier) :entier;
var x,i :entier;
début
x = T[f];
i = d-1;
pour j allant de d à f-1 faire
si T[j] ≤ x alors
i = i+1;
echanger(T,i,j);
finsi
finpour
echanger(T,i+1,f);
retourner(i+1);
fin
finfonction
fonction triRapideRec(ref T :tableau[1..N] d'entiers; val d,f :entier) :vide;
var p :entier;
début
si d<f alors
p = diviserSequence(T,d,f);
triRapideRec(T,d,p-1);
triRapideRec(T,p+1,f);
finsi
fin
finfonction
début
triRapideRec(T,1,N)
fin
finfonction
On a le déroulement ici.
Propriété 9.2. La complexité du tri rapide pour une séquence de n éléments est :
Décrire une structure permettant de gérer des polynômes définis sur les réels. Écrire un ensemble de primitives associées permettant les principales opérations.
On note n le degré (resp. le degré maximum) du polynôme (resp. des polynômes).
Un polynôme peut être défini par son degré et un tableau contenant les coefficients. On définit également une primitive d'initialisation.
constante Taille = 200;
type polynome = structure
coeff:tableau[0..200] de réels;
degre: entier;
finstructure
fonction init(ref p:polynome):vide;
var i:entier;
début
p.degre = 0;
pour i allant de 0 à Taille faire
p.coeff[i] = 0;
finpour
fin
finfonction
XI-B-2. Addition
On notera l'analogie avec l'algorithme de fusion de tableau.
fonction ajoute(ref p1,p2:polynome):polynome;
var p:polynome;
var m,i:entier;
début
m = min(p1.degre,p2.degre);
pour i de 0 à m faire
p.coeff[i] = p1.coeff[i] + p2.coeff[i]
finpour;
si m < p1.degre alors
pour i de m+1 to p1[degre] do
p.coeff][i] = p1.coeff][i];
finpour:
p.degre = p1.degre;
sinon
pour i de m+1 to p2[degre] do
p.coeff[i] = p2.coeff[i];
finpour:
p.degre = p2.degre;
finsi
retourner(p);
fin;
finfonction;
Complexité : O(n)
XI-B-3. Multiplication
fonction multiplie(ref p1,p2:polynome):polynome;
var p:polynome;
var i,j:entier;
début
init(p);
p.degre=p1.degre + p2.degre;
pour i de 0 à p1.degre;
pour j de 0 à p2.degre faire
p.coeff[i+j] = p.coeff[i+j] + p1.coeff[i] * p2.coeff[j];
finpour
finpour
retourner(p);
fin
finfonction
Complexité : O(n2)
XI-B-4. Polynôme opposé
Soit :
P(x)=∑i=0naixi
Le polynôme opposé est :
Q(x)=∑i=0n−pixi
fonction moins(ref p:polynome):polynome;
var i:entier;
var m:polynome;
début
m.degre=p.degre;
pour i de 0 à p.degre faire
m.coeff[i] =- p.coeff[i];
finpour
retourner(m)
fin
finfonction
Complexité : O(n)
XI-B-5. Multiplication par xn
fonction decale(ref p:polynome;val n:entier):polynome
var i:entier;
var d:polynome;
début
d.degre = p.degre + n;
si n>0 then
pour i de 0 à n-1 FAIRE
d.coeff[i] = 0;
finpour
pour i de 0 à p.degre faire
d.coeff[i+n] = p.coeff[i];
finpour
sinon
pour i de -n à p.degre faire
d.coeff[i+n] = p.coeff[i];
finpour
finsi
retourner(d)
fin
finfonction
Complexité : O(n)
XI-B-6. Dérivée
fonction deriv(ref p1:polynome):polynome;
var p:polynome;
var i:entier;
début
si p1.degre == 0 alors
p.degre = 0;
p.coeff[0] = 0;
sinon
p.degre = p1.degre-1;
pour i de 1 à p1.degre faire
p.coeff[i-1] = p1.coeff[i]
finpour;
finsi;
retourner(p)
fin
finfonction
Complexité : O(n)
XI-B-7. Valeur en un point
fonction valeur(ref p:polynome;val x: réel):réel;
var f,s,i:réel;
début
s = 0;
f = 1;
pour i allant de 0 à p.degre faire
s = s + f * p.coeff[i];
f = f * x
finpour
retourner(s)
fin
finfonction
Complexité : O(n)
XI-B-8. Intégrale définie
fonction integraleDefinie(ref p1:polynome;val x,y: réel):réel;
var p:polynome;
var s:réel;
var i:entier;
début
p.degre = p1.degre+1;
p.coeff[0] = 0;
pour i allant de 0 à p1.degre faire
p.coeff[i+1] = p1.coeff[i] / (i+1);
finpour;
retourner(valeur(p,y) - valeur(p,x))
fin
finfonction
Même si en première approche, la complexité ne prend en compte que le nombre d'opérations (+, *), en seconde analyse, les multiplications sont beaucoup plus coûteuses que les additions. Cela est pris en compte dans les algorithmes ci-dessous.
XI-C-1. Valeur en un point
L'algorithme énoncé au paragraphe précédent effectue 2n multiplications. Le schéma d'Horner d'un polynôme permet d'effectuer n multiplications seulement. Le schéma d'Horner repose sur la propriété suivante :
Soit P(x) un polynôme de degré supérieur à 0 :
P(x)=∑i=0naixi
Alors, P(x) s'écrit :
P(x)=A(x)x+a0
On en déduit le schéma de Horner :
P(x)=((…((anx+an−1)x+an−2)…)x+a1)x+a0
fonction valeur(ref p:polynome;val x: réel):réel;
var s:réel;
début
s = p.coeff[p.degre];
pour i allant de p.degre-1 à 0 pas de -1 faire
s = s * x + p.coeff[i]
finpour
retourner(s)
fin
finfonction
Une méthode « diviser pour régner » permet d'améliorer cet algorithme. Elle est basée sur l'égalité suivante :
(ay+b)(cy+d)=acy2+((a+b)(c+d)−ac−bd)y+bd
Cette égalité signifie, entre autres, que si deux polynômes sont de degré 1, il suffit de trois multiplications de réels pour obtenir leur produit. Les quantités a, b, c, d, y étant quelconque, celles-ci peuvent être elles-mêmes des polynômes. Soit le polynôme :
P(x)=∑i=0npixi
On peut écrire P(x) sous la forme :
P(x)=A(x)x1+⌊n/2⌋+B(x)
avec :
A(x)=∑i=0n−1−⌊n/2⌋pixi
B(x)=∑i=0⌊n/2⌋pixi
De même, on a :
Q(x)=∑i=0nqixi
Q(x)=C(x)x1+⌊n/2⌋+D(x)
C(x)=∑i=0n−1−⌊n/2⌋qixi
D(x)=∑i=0⌊n/2⌋qixi
On peut donc utiliser l'équation de départ avec : a=A(x) , b=B(x), c=C(x), d=D(x) et y=x1+⌊n/2⌋.
De plus, on est amené à calculer des produits de polynômes de degré au plus n/2. Il s'agit donc d'une méthode : « diviser pour régner ». La complexité en nombre de multiplications est alors O(nlog23). Comme on notera ci-dessous, l'algorithme est plus complexe à écrire, mais il est bien plus efficace aussi.
Un prétraitement permet de considérer des polynômes de même degré. Il faut donc une fonction « chapeau ». On définit de plus deux autres fonctions utiles pour le calcul :
fonction multiplie(ref p,q:polynome):polynome;
var p1,p2:polynome;
fonction etend(ref p:polynome;val n:entier):polynome;
var i:entier;
var r:polynome;
début
r = decale(p,0);
r.degre = n;
pour i de p.degre+1 à n faire
r.coeff[i] = 0;
finpour;
retourner(r);
fin
finfonction
fonction tronque(ref p:polynome;val n:entier)
var c:polynome;
var i:entier;
début
c.degre = n;
pour i de 0 à n faire
c.coeff[i] = p.coeff[i];
finpour;
retourner(c);
fin
finfonction
fonction multirec(ref p,q:polynome)
var a,b,c,d:polynome;
var c0,c1,c2:réel;
var C0,C1,C2:réel;
var m:entier;
début
selon que
cas p.degre == 0
retourner(polynome([p.coeff[0]*q.coeff[0]]))
cas p.degre == 1
c0 = p.coeff[0] * q.coeff[0];
c2 = p.coeff[1] * q.coeff[1];
c1 = (p.coeff[0] + p.coeff[1]) * (q.coeff[0] + q.coeff[1]);
c1 = c1 - c0 - c2;
retourner(polynome([c0,c1,c2]));
autrement
m = partieEntière(p.degre/2);
a = decale(p, -(m+1));
b = tronque(p, m);
c = decale(q, -(m+1));
d = tronque(q, m);
C2 = multirec(a, c);
C0 = multirec(b, d);
C1 = multirec(ajout(a,b), ajout(c,d));
C1 = ajout(C1, ajout(moins(C0), moins(C2)));
C1 = decale(C1, 1+m);
C2 = decale(C2, 2+2*m);
C0 = ajout(C0, ajout(C1,C2));
retourner(C0);
finselonque:
fin
finfonction;
début
si p.degre > q.degre alors
p1 = p;
p2 = etend(q, p.degre);
sinon
p1 = q;
p2 = etend(p, q.degre);
finsi;
retourner(multirec(p1,p2));
fin
finfonction
Définition 6.1. Une liste est une table d'association à clé unique telle que :
La complexité de l'accès à un élément par son pointeur est O(1) . Si p est un pointeur vers un élément alors contenu(p) est l'élément lui-même. Un pointeur qui n'adresse aucun élément a pour valeur NIL. On écrit en EXALGO pour déclarer un pointeur :
nom_pointeur=^type_predefini;
On écrit en EXALGO pour déclarer une liste :
type_liste=liste de type_predefini;
La manipulation des éléments de la liste dépend des fonctions définies comme s'exécutant en temps O(1).
Définition 6.2. Une liste est dite simplement chainée si les opérations suivantes s'effectuent en O(1) :
fonction premier(val L :type_liste) :^type_predefini;
fonction suivant(val L :type_liste;
val P :^type_predefini) :^type_predefini;
fonction listeVide(val L :type_liste) :booléen;
fonction créer liste(ref L :type_liste) :vide;
fonction insérerAprès(val x :type_prédéfini;
ref L :type_liste;
P :^type_predefini) :vide;
fonction insérerEnTete(val x :type_prédéfini;
ref L :type_liste) :vide;
fonction supprimerAprès(ref L :type_liste;val P :^type_predefini) :vide;
fonction supprimerEnTete(ref L :type_liste) :vide;
On écrira en EXALGO listeSC pour préciser qu'il s'agit d'une liste simplement chaînée.
XII-B-1. Test de fin de liste
fonction estDernier(ref L :listeSC de type_prédéfini;
ref P :^type_prédéfini) :booléen;
début
retourner(suivant(L,P) == NIL)
fin
finfonction
XII-B-2. Chercher un élément dans une liste
fonction chercher(ref L :listeSC de type_prédéfini;
ref E :type_prédéfini) :^type_predefini;
début
var p :^type_prédéfini;
début
si listeVide(L) alors
retourner(NIL)
sinon
p = premier(L);
tant que non(estDernier(L,p)) et (contenu(p) != e) faire
p = suivant(L,p);
fintantque
si (contenu(p) != e) alors
retourner(NIL)
sinon
retourner(p)
finsi
finsi
fin
finfonction
Complexité :
XII-B-3. Trouver le dernier élément
fonction trouverDernier(ref L :listeSC de type_prédéfini) :^type_predefini;
var p :^type_prédéfini;
début
si listeVide(L) alors
retourner(NIL)
sinon
p = premier(L);
tant que non(estDernier(L,P)) faire
p = suivant(L,p);
fintantque
retourner(p)
finsi
fin
finfonction
Complexité : O(n).
Définition 6.3. Une liste doublement chaînée est une liste pour laquelle les opérations en temps O(1)
sont celles des listes simplement chaînées auxquelles on ajoute les fonctions d'accès.
fonction dernier(val L :type_liste) :^type_predefini;fonction précédent(val L :type_liste;
val P :^type_predefini) :^type_predefini;
On écrira en EXALGO listeDC pour préciser qu'il s'agit d'une liste doublement chaînée.
XII-C-1. Supprimer un élément
fonction supprimer(ref L :listeDC de type_predefini;
val e : type_prédéfini) :booléen;
var p,prec,suiv :^type_prédéfini;
début
p = chercher(L,e);
si p == NIL alors
retourner(FAUX)
sinon
si estPremier(L,p) alors
supprimerEnTete(L)
sinon
prec = précédent(L,p);
supprimerApres(L,prec);
finsi
retourner(VRAI)
finsi
fin
finfonction
Complexité :
XII-D-1. Taille
fonction taille(val L :type_liste) :entier;
var p :^type_prédéfini;
var t :entier;
début
si listeVide(L) alors
retourner(0)
sinon
retourner(1 + taille( suivant(L, premier(L)) ))
finsi
fin
finfonction
Complexité : O(n).
XII-D-2. Insérer dans une liste triée
On suppose la liste triée doublement chaînée dans l'ordre croissant :
fonction insertionTrie(ref L :listeDC de type_prédéfini;
val e : type_prédéfini) :vide;
var p :^type_prédéfini;
début
si listeVide(L) alors
insererEnTete(e,L)
sinon
si contenu(premier(L))>e alors
insererEnTete(e,L)
sinon
insererTrie(suivant(L,premier(L)), e)
finsi
finsi
fin
finfonction
Complexité moyenne : O(n).
On considérera dans tout ce chapitre que l'on a des valeurs qui correspondent à un caractère.
Pour certaines structures de données, l'ensemble des langages de programmation proposent une traduction immédiate. Pour d'autres, il n'existe pas de traduction immédiate. Il faut alors définir explicitement l'algorithme de chacune des primitives.
Exemple - les listes. On doit définir le stockage de la liste, et en fonction de ce stockage comment s'effectue par exemple l'adjonction.
L'implémentation doit respecter la complexité des primitives à part celle d'initialisation (celle-ci ne s'exécutera qu'une fois).
Exemple - les listes. On utilise souvent les fonctions ajouter et supprimer, mais une seule fois creerListe.
Ici nous allons choisir de ranger les éléments dans un tableau « suffisamment grand ». Chaque élément du tableau est une paire (valeurElement, pointeurSuivant). Un pointeur est la valeur d'un index du tableau ; ainsi l'accès au suivant est en complexité O(1). La zone de stockage peut donc être décrite par :
elementListe = structure
valeur :car;
suivant :entier;
finstructure;
stockListe = tableau[1..tailleStock] d'elementListe;
La valeur du pointeur (champ suivant) est donc un entier compris entre 0 et tailleStock. La valeur 0 correspondant à l'absence d'élément suivant. Le premier élément doit être accessible en O(1), il faut donc conserver son index. Si la liste est vide, par convention, l'index du premier sera 0. On peut donc représenter une liste par la structure suivante :
listeSC_Car = structure
tailleStock :entier;
premier :entier;
vListe :stockListe;
finstructure;
Le tableau de stockage étant grand, mais pas illimité, il faudra prévoir que l'espace de stockage puisse être saturé.
Ces fonctions sont immédiates.
fonction premier(val L :listeSC_Car) :entier;
début
retourner L.premier;
fin;
finfonction
fonction suivant(val L :listeSC_Car,P :entier) :entier;
début
retourner L.vListe[P].suivant;
fin
finfonction
fonction listeVide(val L :listeSC_Car) :booléen;
début
retourner L.premier == 0;
fin
finfonction
Pour ajouter un élément, il faut pouvoir trouver un élément « libre » dans le tableau. Une première solution consiste à marquer les éléments libres du tableau (par exemple champ suivant de l'élément a pour valeur -1). Dans ce cas, il faudra parcourir le tableau (complexité O(n/2) en moyenne). Par suite, la primitive insérerAprès ne sera plus en complexité O(1) puisqu'il faudra d'abord trouver un élément libre. Une solution compatible avec la complexité des primitives consiste à gérer cet espace de stockage en constituant la liste des cellules libres. On modifie donc en conséquence la description de listeSC_Car :
listeSC_Car = structure
tailleStock :entier;
premier :entier;
premierLibre :entier;
vListe :stockListe;
finstructure;
Par convention, l'espace de stockage sera saturé lorsque l'index premierLibre vaut 0 (la liste des cellules libres est vide). On définit donc la fonction de test :
fonction listeLibreVide(val L :listeSC_Car) :booléen;
début
retourner L.premierLibre == 0;
fin
finfonction
On définit deux primitives liées à la gestion du stockage :
Les opérations sont respectivement de type insererEnTete et supprimerEnTete. Préciser la liste sur laquelle s'effectue l'opération revient à préciser le pointeur de tête sur lequel on travaille.
fonction prendreCellule(ref L :listeSC_Car,ref tete :entier) :entier;
var nouv :entier;
début
nouv = tete;
tete = suivant(L,nouv);
retourner nouv;
fin
finfonction
fonction mettreCellule(ref L :listeSC_Car,val P :entier,ref tete :entier) :vide;
début
L.vListe[P].suivant = tete;
tete = P;
fin
finfonction
fonction créer_liste(ref L :listeSC_Car;val tailleMax :entier) :vide;
var i :entier;
début
L.tailleStock = tailleMax;
L.premier = 0;
L.premierLibre = 1;
pour i allant de 1 à L.tailleStock-1 faire
L.vListe[i].suivant = i+1;
finpour
L.vListe[tailleStock].suivant = 0;
fin
finfonction
fonction insérerAprès(val x :car; ref L :listeSC_Car; val P :entier) :booléen;
var nouv :entier;
début
si listeLibreVide(L) ou P == 0 alors
retourner faux;
sinon
nouv = prendreCellule(L,L.premierLibre);
L.vListe[nouv].valeur = x;
L.vListe[nouv].suivant = suivant(L,P);
L.vListe[P].suivant = nouv;
retourner vrai;
finsi
fin
finfonction
fonction insérerEnTete(val x :car;ref L :listeSC_Car) :booléen;
var nouv :entier;
début
si listeLibreVide(L) alors
retourner faux;
sinon
nouv = prendreCellule(L,L.premierLibre);
L.vListe[nouv].valeur = x;
mettreCellule(L,nouv, L.premier);
retourner vrai;
finsi
fin
finfonction
fonction supprimerAprès(ref L :listeSC_Car;val P :entier) :booléen;
var suivP :entier;
début
suivP = suivant(L,P);
si P == 0 ou suivP == 0 alors
retourner faux;
sinon
L.vListe[P].suivant = suivant(L,suivP);
mettreCellule(L,suivP, L.premierLibre);
retourner vrai;
finsi
fin
finfonction
fonction supprimerEnTete(ref L :listeSC_Car) :booléen;
var tete :entier;
début
si listeVide(L) alors
retourner faux;
sinon
tete = L.premier;
L.premier = suivant(L,tete);
mettreCellule(L,tete, L.premierLibre);
retourner vrai;
finsi
fin
finfonction