Vous êtes sur la page 1sur 170

Structures de Données et Algorithmes

CM1

Nadi Tomeh
tomeh@lipn.fr

Licence 2 Informatique
Institut Galilée, Université Sorbonne Paris Nord

15 octobre 2020
Point administratif

Information sur la page du cours sur Moodle :


https:
//moodlelms.univ-paris13.fr/course/view.php?id=2217
Le slides sont essentiellement dus à P. Geurts, C. Pivoteau, M.
Finiasz, parmi d’autres.
La présentation du contenu suit largement le livre de Cormen.
Plan

1. Algorithmes et structures de données

2. Récursivité

3. Complexité et notation asymptotiques

4. Types abstraits et structures de données

5. Pile et File

6. Liste, Vecteur, Séquence


Algorithmes

Un algorithme est une suite finie et non-ambiguë d’opérations ou


d’instructions permettant de résoudre un problème
Provient du nom du mathématicien persan Al-Khawarizmi (±820),
le père de l’algèbre
Un problème algorithmique est souvent formulé comme la
transformation d’un ensemble de valeurs, d’entrée, en un nouvel
ensemble de valeurs, de sortie.
Exemples d’algorithmes :
I Une recette de cuisine (ingrédients → plat préparé)
I La recherche dans un dictionnaire (mot → définition)
I La division entière (deux entiers → leur quotient)
I Le tri d’une séquence (séquence → séquence ordonnée)

Introduction 10
Algorithmes

On étudiera essentiellement les algorithmes corrects.


I Un algorithme est (totalement) correct lorsque pour chaque instance,
il se termine en produisant la bonne sortie.
I Il existe également des algorithmes partiellement corrects dont la
terminaison n’est pas assurée mais qui fournissent la bonne sortie
lorsqu’ils se terminent.
I Il existe également des algorithmes approximatifs qui fournissent une
sortie inexacte mais néanmoins proche de l’optimum.

Les algorithmes seront évalués en termes d’utilisation de ressources,


essentiellement par rapport aux temps de calcul mais aussi à
l’utilisation de la mémoire.

Introduction 11
Algorithmes

Un algorithme peut être spécifié de différentes manières :


en langage naturel,
graphiquement,
en pseudo-code,
par un programme écrit dans un langage informatique
...
La seule condition est que la description soit précise.

Introduction 12
Exemple : le tri

Le problème de tri :
I Entrée : une séquence de n nombres ha1 , a2 , . . . , an i
I Sortie : une permutation de la séquence de départ ha10 , a20 , . . . , an0 i
telle que a10 ≤ a20 ≤ . . . ≤ an0

Exemple :
I Entrée : h31, 41, 59, 26, 41, 58i
I Sortie : h26, 31, 41, 41, 58, 59i

Introduction 13
Tri par insertion

Description en langage naturel :

On parcourt la séquence de gauche à droite

Pour chaque élément aj :


On l’insère à sa position dans une nouvelle séquence ordonnée
contenant les éléments le précédant dans la séquence.
On s’arrête dès que le dernier élément a été inséré à sa place dans la
séquence.

Introduction 14
Tri par insertion
Insertion Sort
5 2 4 6 1 3

2 5 4 6 1 3

2 4 5 6 1 3

2 4 5 6 1 3

1 2 4 5 6 3

1 2 3 4 5 6

Intro to Data Structures and Algorithms © Introd


Introduction 15
Tri par insertion

Description en C (sur des tableaux d’entiers) :

void InsertionSort (int *a, int length) {


int key, i;
for(int j = 1; j < length; j++) {
key = a[j];
/* Insert a[j] into the sorted sequence a[0...j-1] */
i = j-1;
while (i>=0 && a[i]>key) {
a[i+1] = a[i];
i = i-1;
}
a[i+1] = key;
}
}

Introduction 16
Insertion sort

Description en pseudo-code (sur des tableaux d’entiers) :

Insertion-Sort(A)
1 for j = 2 to A.length
2 key = A[j]
3 // Insert A[j] into the sorted sequence A[1 . . j − 1].
4 i = j −1
5 while i > 0 and A[i] > key
6 A[i + 1] = A[i]
7 i = i −1
8 A[i + 1] = key

Introduction 17
Pseudo-code

Objectifs :
Décrire les algorithmes de manière à ce qu’ils soient compris par des
humains.

Rendre la description indépendante de l’implémentation

S’affranchir de détails tels que la gestion d’erreurs, les déclarations


de type, etc.

Très proche du C (langage procédural plutôt qu’orienté objet)

Peut contenir certaines instructions en langage naturel si nécessaire

Introduction 18
Pseudo-code

Quelques règles
Structures de blocs indiquées par l’indentation
Boucles (for, while, repeat ) et conditions (if, else, elseif) comme
en C.
Le compteur de boucle garde sa valeur à la sortie de la boucle
En sortie d’un for, le compteur a la valeur de la borne max+1.
i =1
for i = 1 to Max while i ≤ Max

Code Code
i = i +1
Commentaires indiqués par //
Affectation (=) et test d’égalité (==) comme en C.

Introduction 19
Pseudo-code

Les variables (i, j et key par exemple) sont locales à la fonction.


A[i] désigne l’élément i du tableau A. A[i..j] désigne un intervalle de
valeurs dans un tableau. A.length est la taille du tableau.
L’indexation des tableaux commence à 1.
Les types de données composés sont organisés en objets, qui sont
composés d’attributs. On accède à la valeur de l’attribut attr pour
un objet x par x.attr .
Un variable représentant un tableau ou un objet est considérée
comme un pointeur vers ce tableau ou cet objet.
Paramètres passés par valeur comme en C (mais tableaux et objets
sont passés par pointeur).
...

Introduction 20
Trois questions récurrentes face à un algorithme

1. Mon algorithme est-il correct, se termine-t-il ?


2. Quelle est sa vitesse d’exécution ?
3. Y-a-t’il moyen de faire mieux ?
Trois questions récurrentes face à un algorithme

1. Mon algorithme est-il correct, se termine-t-il ?


2. Quelle est sa vitesse d’exécution ?
3. Y-a-t’il moyen de faire mieux ?

Exemple du tri par insertion


1. Oui → technique des invariants (pas vu dans ce cours)
2. O(n2 ) → analyse de complexité (étudiée plus formellement plus tard)
3. Oui → il existe plusieurs algorithmes O(n log n) (tri fusion, ...)
Complexité de Insertion-Sort

Insertion-Sort(A)
1 for j = 2 to A.length
2 key = A[j]
3 i = j −1
4 while i > 0 and A[i] > key
5 A[i + 1] = A[i]
6 i = i −1
7 A[i + 1] = key

Nombre de comparaisons T (n) pour trier un tableau de taille n ?


Dans le pire des cas :
I La boucle for est exécutée n − 1 fois (n = A.length).
I La boucle while est exécutée j − 1 fois

Introduction 24
Complexité de Insertion-Sort

Le nombre de comparaisons est borné par :


n
X
T (n) ≤ (j − 1)
j=2
Pn
Puisque i=1 i = n(n + 1)/2, on a :

n(n − 1)
T (n) ≤
2
Finalement, T (n) = O(n2 )

(borne inférieure ?)

Introduction 25
Structures de données

Méthode pour stocker et organiser les données pour en faciliter


l’accès et la modification
Une structure de données regroupe :
I un certain nombre de données à gérer, et
I un ensemble d’opérations pouvant être appliquées à ces données
Dans la plupart des cas, il existe
I plusieurs manières de représenter les données et
I différents algorithmes de manipulation.
On distingue généralement l’interface des structures de leur
implémentation.

Introduction 26
Types de données abstraits

Un type de données abstrait (TDA) représente l’interface d’une


structure de données.
Un TDA spécifie précisément :
I la nature et les propriétés des données
I les modalités d’utilisation des opérations pouvant être effectuées
En général, un TDA admet différentes implémentations (plusieurs
représentations possibles des données, plusieurs algorithmes pour les
opérations).

Introduction 27
Exemple : file à priorités
Données gérées : des objets avec comme attributs :
I une clé, munie d’un opérateur de comparaison selon un ordre total
I une valeur quelconque

Opérations :
I Création d’une file vide
I Insert(S, x) : insère l’élément x dans la file S.
I Extract-Max(S) : retire et renvoie l’élément de S avec la clé la
plus grande.

Il existe de nombreuses façons d’implémenter ce TDA :


I Tableau non trié ;
I Liste triée ;
I Structure de tas ;
I ...
Chacune mène à des complexités différentes des opérations Insert
et Extract-Max

Introduction 28
Structures de données et algorithmes en pratique
La résolution de problème algorithmiques requiert presque toujours
la combinaison de structures de données et d’algorithmes
sophistiqués pour la gestion et la recherche dans ces structures.
D’autant plus vrai qu’on a à traiter des volumes de données
importants.
Quelques exemples de problèmes réels :
I Routage dans les réseaux informatiques
I Moteurs de recherche
I Alignement de séquences ADN en bio-informatique

Introduction 29
Un exemple (B. Boigelot)

Un laboratoire de génie génétique désire développer un programme


capable de repérer des répétitions de longueur M dans une séquence
de nucléotides S de longueur N (avec N >> M) :

ACTG CGAC GGTACGCTT CGAC TTAG ...(M = 4)

Première approche :
I Un indice i variant de 2 à N − M + 1
I Un indice j variant de 1 à i − 1
I Pour tout k ∈ [0, . . . , M − 1], on teste si S[i + k] = S[j + k].
Efficacité : le nombre de comparaisons à effectuer est égal à :

M(N − M + 1)(N − M)
M · (1 + 2 + . . . + (N − M)) =
2
≈ 4, 5.1021 pour N = 3 · 109 et M = 1000
≈ 143.000 ans au rythme de 109 opérations/s.

Introduction 30
Une meilleure solution
1. On construit une table à N − M + 1 lignes et M colonnes dont la
k-ème ligne contient la sous-séquence de longueur M commençant à
la position k dans S :
ACTG
CTGC
TGCG
GCGA
CGAC
..
.
2. On trie les lignes de cette table par ordre lexicographique ;
3. On parcourt la table triée afin de déterminer si elle contient deux
lignes consécutives identiques

Note : Lors de la comparaison de deux lignes, on s’arrête à la première


différence (⇒ moins de 4/3 comparaisons en moyenne).

Introduction 31
Efficacité
Construction de la table : M(N − M + 1) opérations de copie.
Tri par ordre lexicographique (algorithme de tri rapide, voir partie
3) :
8
≤ N ln N opérations de comparaison en moyenne
3
Détection de lignes consécutives :
4
≤ (N − M) opérations de comparaison en moyenne
3
En supposant des coûts identiques pour toutes les opérations, on obtient :
8 4 1
N(M + ln N + ) − M(M + )
3 3 3
≈ 3, 179.10 opérations pour N = 3.109 et M = 1000.
12

≈ 53 minutes au rythme de 109 opérations/s.

Introduction 32
Remarques

Utiliser un ordinateur plus puissant ne permet généralement pas de


résoudre les problèmes d’efficacité !
Avec un ordinateur 1000 fois plus puissant : 143 ans pour la
première approche, 3,2s pour la deuxième.
La deuxième solution est plus rapide mais elle est très gourmande en
espace mémoire (M fois plus que la première !)

Introduction 33
Plan

1. Algorithmes et structures de données

2. Récursivité

3. Complexité et notation asymptotiques

4. Types abstraits et structures de données

5. Pile et File

6. Liste, Vecteur, Séquence


Algorithmes récursifs
Un algorithme est récursif s’il s’invoque lui-même directement ou
indirectement.

Motivation : Simplicité d’expression de certains algorithmes

Exemple : Fonction factorielle :



1 si n = 0
n! =
n · (n − 1)! si n > 0

Factorial(n)
1 if n == 0
2 return 1
3 return n · Factorial(n − 1)

Introduction 35
Algorithmes récursifs

Factorial(n)
1 if n == 0
2 return 1
3 return n · Factorial(n − 1)

Règles pour développer une solution récursive :

On doit définir un cas de base (n == 0)


On doit diminuer la “taille” du problème à chaque étape (n → n − 1)
Quand les appels récursifs se partagent la même structure de
données, les sous-problèmes ne doivent pas se superposer (pour
éviter les effets de bord)

Introduction 36
Exemple de récursion multiple

Calcul du nième nombre de Fibonacci :

F0 = 0
F1 = 1
∀n ≥ 2 : Fn = Fn−2 + Fn−1

Algorithme :

Fibonacci(n)
1 if n ≤ 1
2 return n
3 return Fibonacci(n − 2) + Fibonacci(n − 1)

Introduction 37
Exemple de récursion multiple

Fibonacci(n)
1 if n ≤ 1
2 return n
3 return Fibonacci(n − 2) + Fibonacci(n − 1)

1. L’algorithme est-il correct ?


2. Quelle est sa vitesse d’exécution ?
3. Y-a-t’il moyen de faire mieux ?

Introduction 38
Exemple de récursion multiple

Fibonacci(n)
1 if n ≤ 1
2 return n
3 return Fibonacci(n − 2) + Fibonacci(n − 1)

1. L’algorithme est correct ?


I Clairement, l’algorithme est correct.
I En général, la correction d’un algorithme récursif se démontre par
induction.
2. Quelle est sa vitesse d’exécution ?
3. Y-a-t’il moyen de faire mieux ?

Introduction 39
Vitesse d’exécution
Nombre d’opérations pour calculer Fibonacci(n) en fonction de n
Results
Empiriquement :

60
Ruby
Python
50 Scheme
C
Time (seconds)

40 C-wiz
Java
C-gcc
30

20

10

0
20 25 30 35 40 45 50
n
© 2006 (Carzaniga)
Antonio Carzaniga

Toutes les implémentations atteignent leur limite, plus ou moins loin


Introduction 40
Trace d’exécution

Trace d’exécution :

fibonacci1(k)

fibonacci1(k 2) fibonacci1(k 1)

fibonacci1(k 4) fibonacci1(k 3) fibonacci1(k 3) fibonacci1(k 2)

(Boigelot)
p
1+ 5
Temps de calcul : O('n), où ' = .
2

Preuve ?

Introduction 32 41
Complexité

Fibonacci(n)
1 if n ≤ 1
2 return n
3 return Fibonacci(n − 2) + Fibonacci(n − 1)

Soit T (n) le nombre d’opérations de base pour calculer


Fibonacci(n) :

T (0) = 2, T (1) = 2
T (n) = T (n − 1) + T (n − 2) + 2

On a donc T (n) ≥ Fn (= le nème nombre de Fibonacci).

Introduction 42
Complexité
Comment croı̂t Fn avec n ?
T (n) ≥ Fn = Fn−1 + Fn−2
Puisque Fn ≥ Fn−1 ≥ Fn−2 ≥ . . ., on a :

n ( 2)n
Fn ≥ 2Fn−2 ≥ 2(2Fn−4 ) ≥ 2(2(2Fn−6 )) ≥ 2 2 −1 F2 =
2
si n ≥ 2 est pair et
√ √
n−1 ( 2)n ( 2)n
Fn ≥ 2Fn−2 ≥ 2(2Fn−4 ) ≥ 2(2(2Fn−6 )) ≥ 2 2 F1 = √ ≥
2 2
si n ≥ 1 est impair.
Et donc √
( 2)n (1.4)n
T (n) ≥ ≈
2 2
T (n) croı̂t exponentiellement avec n

Peut-on faire mieux ?


Introduction 43
Solution itérative

Fibonacci-Iter(n)
1 if n ≤ 1
2 return n
3 else
4 pprev = 0
5 prev = 1
6 for i = 2 to n
7 f = prev + pprev
8 pprev = prev
9 prev = f
10 return f

Introduction 44
Vitesse d’exécution
Complexité : O(n) Results

60
Ruby
Python
50 Scheme
C
Time (seconds)

40 C-wiz
Java
C-gcc
30 PythonSmart
20

10

0
0 20 40 60 80 100
n
© 2006 (Carzaniga)
Antonio Carzaniga

Introduction 45
Tri par fusion
Idée d’un tri basé sur la récursion :
on sépare le tableau en deux sous-tableaux de la même taille
on trie (récursivement) chacun des sous-tableaux
on fusionne les deux sous-tableaux triés en maintenant l’ordre
Le cas de base correspond à un tableau d’un seul élément.

merge-sort(A, p, r )
1 if p < r
2 q = b p+r
2 c
3 merge-sort(A, p, q)
4 merge-sort(A, q + 1, r )
5 merge(A, p, q, r )

Appel initial : merge-sort(A, 1, A.length)

Exemple d’application du principe général de “diviser pour régner”


Introduction 46
Bottom-up view for n D 8: [Heavy lines demarc
Tri par fusion : illustration
lems.]
sorted array
1 2 3 4 5 6 7 8
1 2 2 3 4 5 6 7

merge

2 4 5 7 1 2 3 6

merge

2 5 4 7 1 3 2 6

merge

5 2 4 7 1 3 2 6
1 2 3 4 5 6 7 8
initial array

Introduction
[Examples when n is a power of 2 are most stra
47
Fonction merge

Merge(A, p, q, r ) :
Entrée : tableau A et indice p, q, r tels que :
I p ≤ q < r (pas de tableaux vides)
I Les sous-tableaux A[p . . q] et A[q + 1 . . r ] sont ordonnés
Sortie : Les deux sous-tableaux sont fusionnés en un seul
sous-tableau ordonné dans A[p . . r ]

Idée :
Utiliser un pointeur vers le début de chacun des sous-tableaux ;
Déterminer le plus petit des deux éléments pointés ;
Déplacer cet élément vers le tableau fusionné ;
Avancer le pointeur correspondant

Introduction 48
Fusion : algorithme

Merge(A, p, q, r )
1 n1 = q − p + 1 ; n2 = r − q
2 Soit L[1..n1 + 1] et R[1..n2 + 1] deux nouveaux tableaux
3 for i = 1 to n1
4 L[i] = A[p + i − 1]
5 for j = 1 to n2
6 R[j] = A[q + j]
7 L[n1 + 1] = ∞ ; R[n2 + 1] = ∞ // Sentinels
8 i=1 ;j=1
9 for k = p to r
10 if L[i] ≤ R[j]
11 A[k] = L[i]
12 i = i +1
13 else
14 A[k] = R[j]
15 j = j +1

Introduction 49
Merge Sort
Fusion : illustration
Illustration of Merge

n1=q-p+1 elem. n2=r-q elem.


p q q+1 r

Abefore ... ...

L R
1 2 ... n1 n1+1 1 ... n2 n2+1
p q q+1 r

Aafter ... ...

n = r-p+1 elements Data Structures and Algorithms (89)

Complexité : O(n) (où n = r − p + 1)

Introduction 50
Vitesse d’exécution

Complexité de merge-sort : O(n log n) (voir partie 2)

5
x 10
2.5

Insertion Sort
Merge Sort
2
Complexity

1.5

0.5

0
0 50 100 150 200 250 300 350 400 450 500
n

Introduction 51
Remarques

La fonction merge nécessite d’allouer deux tableaux L et R (dont


la taille est O(n)).
On pourrait réécrire merge-sort de manière itérative (au prix de la
simplicité)
Version récursive du tri par insertion :
Insertion-Sort-rec(A, n)
1 if n > 1
2 Insertion-Sort-rec(A, n − 1)
3 Merge(A, 1, n − 1, n)

Introduction 52
Note sur l’implémentation de la récursivité

Trace d’exécution
Trace de: la factorielle
d’exécution

factorial(n)

factorial(n 1)

factorial(2)

factorial(1)

Chaque appel
Temps récursif
de calcul nécessite
: O(n) de mémoriser
(en supposant le contexte
des opérations arithmétiques en
temps constant!).
d’invocation
Espacemémoire
L’espace mémoire utilisé
utilisé :est donc O(n) (n appels récursifs)
O(n).

En e↵et, chaque appel récursif de factorial nécessite de mémoriser son


contexte d’invocation.
Introduction 53
Récursivité terminale

Définition : Une procédure est récursive terminale (“tail recursive”)


si elle n’effectue plus aucune opération après s’être invoquée
récursivement.

Avantages :
I Le contexte d’invocation ne doit pas être mémorisé et donc l’espace
mémoire nécessaire est réduit
I Les procédures récursives terminales peuvent facilement être
converties en procédures itératives

Introduction 54
Version récursive terminale de la factorielle

Factorial2(n)
1 return Factorial2-rec(n, 2, 1)

Factorial2-rec(n, i, f )
1 if i > n
2 return f
3 return Factorial2-rec(n, i + 1, f · i)

Espace mémoire utilisé : O(1) (si la récursion terminale est implémentée


efficacement)

Introduction 55
Récursivité en C
Types abstraits Implantation et utilisation Récursivité

Structures récursives

Il est possible de créer des structures de données récursives,


c’est-à-dire qu’elles sont définies en fonction d’elles-mêmes (c’est
le cas des listes, des arbres, ...).
! Il faut faire attention en définissant de tels types :
typedef struct{ typedef struct structRec{
int toto; int toto;
TR next; struct structRec *next;
}TR; /* FAUX ! */ }TR; /* OK ! */

On ne peut pas définir un type en fonction de lui-même ;


Il faut que l’on puisse déterminer l’espace mémoire
nécessaire à la structure.
Rmq : les fonctions récursives sont particulièrement adaptées
lorsque les structures qu’elles manipulent sont récursives.

Struct 15/24
Types abstraits Implantation et utilisation Récursivité

Exemple du type Person


typedef struct person{
char *firstName; /* prénom */
char *lastName; /* nom */
struct person *father;
struct person *mother;
}Person;
Person *createPerson(char *f, char *l){
Person *h;
if ((h=malloc(sizeof(Person)))==NULL) error("Allocation ratée!");
h->firstName=f; h->lastName=l; h->father=NULL; h->mother=NULL;
return h;
}
Person *createPersonWithParents(char *f, char *l, Person *dad, Person *mum){
Person *h=createPerson(f,l);
h->father=dad; h->mother=mum;
return h;
}
Person *createPersonWithParentsNames(char *f, char *l, char *dad, char *mum){
Person *d=createPerson(dad,"");
Person *m=createPerson(mum,"");
return createPersonWithParents(f,l,d,m);
}
Struct 16/24
Types abstraits Implantation et utilisation Récursivité

Exemple du type Person - suite


void printName(Person *h){
printf("%s %s", h->firstName, h->lastName);
}

void printPerson(Person *h){


printf("%s %s ( père: ", h->firstName, h->lastName);
(h->father==NULL)?printf("x"):printName(h->father);
printf("; mère: ");
(h->mother==NULL)?printf("x"):printName(h->mother);
printf(") ");
}

int main (int argc, char *argv[]){

Person *a = createPerson("Anabelle", "Curie");


printName(a); printf("\n");
Person *b = createPerson("Bernard", "Plank");
printName(b); printf("\n");
Person *c = createPersonWithParents("Clothilde", "Plank", b, a);
printPerson(c); printf("\n");
Person *d = createPersonWithParentsNames("Dany", "Newton", "Flint", "Fiona");
printPerson(d); printf("\n");
}
Struct 17/24
Types abstraits Implantation et utilisation Récursivité

Qu’est-ce qu’une fonction récursive ?

Une fonction qui s’appelle elle même... par exemple :


void toto(int i){
printf("%d ",i);
toto(i);
}

Struct 19/24
Types abstraits Implantation et utilisation Récursivité

Qu’est-ce qu’une fonction récursive ?

Une fonction qui s’appelle elle même... par exemple :


void toto(int i){
printf("%d ",i);
toto(i); toto(i-1);
}

Struct 19/24
Types abstraits Implantation et utilisation Récursivité

Qu’est-ce qu’une fonction récursive ?

Une fonction qui s’appelle elle même... par exemple :


void toto(int i){
printf("%d ",i);
toto(i-1);}
}
Il faut toujours s’assurer que la fonction termine !
void toto(int i){
if(i>0){
printf("%d ",i);
toto(i-1);
}
}

Struct 19/24
Types abstraits Implantation et utilisation Récursivité

Qu’est-ce qu’une fonction récursive ?

Une fonction qui s’appelle elle même... par exemple :


void toto(int i){
printf("%d ",i);
toto(i-1);}
}
Il faut toujours s’assurer que la fonction termine !
void toto(int i){
if(i>0){
printf("%d ",i);
toto(i-1);
}
}
Pour résoudre le problème P : si le problème est suffisamment
petit, on le résout directement, sinon on résout récursivement
un problème plus petit pour arriver à la solution de P.
Struct 19/24
Types abstraits Implantation et utilisation Récursivité

Structure d’une fonction récursive

Comment écrire la fonction récursive int sumRec(int param)


pour calculer la somme des entiers de 1 à param ?
Il faut TOUJOURS un cas d’arrêt : dans quel cas (pour
quelle valeur de param) sait on résoudre le problème
directement ?

Pour résoudre récursivement, l’idée c’est d’imaginer que la


fonction que l’on est en train décrire existe déjà et qu’on
peut l’utiliser, à condition que le paramètre soit ”plus
petit” que param.

On utilise alors cette solution récursive pour résoudre le


problème principal (de paramètre param).

Struct 20/24
Types abstraits Implantation et utilisation Récursivité

Structure d’une fonction récursive

Comment écrire la fonction récursive int sumRec(int param)


pour calculer la somme des entiers de 1 à param ?
Il faut TOUJOURS un cas d’arrêt : dans quel cas (pour
quelle valeur de param) sait on résoudre le problème
directement ?
B if( n == 1 ) return 1;
Pour résoudre récursivement, l’idée c’est d’imaginer que la
fonction que l’on est en train décrire existe déjà et qu’on
peut l’utiliser, à condition que le paramètre soit ”plus
petit” que param.

On utilise alors cette solution récursive pour résoudre le


problème principal (de paramètre param).

Struct 20/24
Types abstraits Implantation et utilisation Récursivité

Structure d’une fonction récursive

Comment écrire la fonction récursive int sumRec(int param)


pour calculer la somme des entiers de 1 à param ?
Il faut TOUJOURS un cas d’arrêt : dans quel cas (pour
quelle valeur de param) sait on résoudre le problème
directement ?
B if( n == 1 ) return 1;
Pour résoudre récursivement, l’idée c’est d’imaginer que la
fonction que l’on est en train décrire existe déjà et qu’on
peut l’utiliser, à condition que le paramètre soit ”plus
petit” que param.
B int s = sumRec( param - 1 );
On utilise alors cette solution récursive pour résoudre le
problème principal (de paramètre param).

Struct 20/24
Types abstraits Implantation et utilisation Récursivité

Structure d’une fonction récursive

Comment écrire la fonction récursive int sumRec(int param)


pour calculer la somme des entiers de 1 à param ?
Il faut TOUJOURS un cas d’arrêt : dans quel cas (pour
quelle valeur de param) sait on résoudre le problème
directement ?
B if( n == 1 ) return 1;
Pour résoudre récursivement, l’idée c’est d’imaginer que la
fonction que l’on est en train décrire existe déjà et qu’on
peut l’utiliser, à condition que le paramètre soit ”plus
petit” que param.
B int s = sumRec( param - 1 );
On utilise alors cette solution récursive pour résoudre le
problème principal (de paramètre param).
B return param + s;

Struct 20/24
Types abstraits Implantation et utilisation Récursivité

Exemples de fonctions récursives


int sumRec(int param){ // somme des entiers de 1 à param
if( param == 1 )
return 1;
else{
int s = sumRec( param - 1 );
return param + s;
}
}

void printTab(int *tab, int lg){ // affiche un tab. d’entiers


if ( lg == 1 )
printf("%d\n",tab[0]);
else{
printf("%d ",tab[0]);
printTab(tab+1, lg-1); // et si on inverse?
}
}
Struct 21/24
Types abstraits Implantation et utilisation Récursivité

Récursif vs. itératif


Toutes les fonctions récursives peuvent s’écrire de façon
itérative et inversement :
int sumRec(int param){ // somme des entiers de 1 à param
if( param == 1 )
return 1;
else{
int s = sumRec( param - 1 );
return param + s;
}
}

int sumIte(int param){ // somme des entiers de 1 à param


int i, s=0;
for( i=1; i<=param; i++)
s+=i;
return s;
}

Struct 22/24
Types abstraits Implantation et utilisation Récursivité

Récursif vs. itératif nombres de Fibonacci

int fibo(int n){


int i, tmp, f1=1, f2=1;

for(i=3; i<=n; i++){


tmp=f2;
f2=tmp+f1;
f1=tmp;
}
return f2;
}

int fiboRec(int n){


if(n<=2)
return 1;
else return fiboRec(n-1)+fiboRec(n-2);
}

Struct 23/24
Types abstraits Implantation et utilisation Récursivité

Récursif vs. itératif nombres de Fibonacci

int fibo(int n){


int i, tmp, f1=1, f2=1;

for(i=3; i<=n; i++){


tmp=f2;
f2=tmp+f1;
f1=tmp;
}
return f2;
}
B Complexité : O(n)

int fiboRec(int n){


if(n<=2)
return 1;
else return fiboRec(n-1)+fiboRec(n-2);
}
B Complexité : O(2n )
Struct 23/24
Types abstraits Implantation et utilisation Récursivité

Fonctions récursives sur le type Person

void printMothers(Person *p){


if (p->mother!=NULL){
printName(p->mother);
printf("\n");
printMothers(p->mother);
}
}

void printAncestors(Person *p){


if (p->mother!=NULL){
printName(p->father); printf("\n");
printName(p->mother); printf("\n");
printAncestors(p->father);
printAncestors(p->mother);
}
}

Struct 24/24
Plan

1. Algorithmes et structures de données

2. Récursivité

3. Complexité et notation asymptotiques

4. Types abstraits et structures de données

5. Pile et File

6. Liste, Vecteur, Séquence


Performance d’un algorithme

Plusieurs métriques possibles :


I Longueur du programme (nombre de lignes)
I Simplicité du code
I Espace mémoire consommé
I Temps de calcul
I ...

Les temps de calcul sont la plupart du temps utilisés


I Ils peuvent être quantifiés et sont faciles à comparer
I Souvent ce qui compte réellement
Nous étudierons aussi l’espace mémoire consommé par nos
algorithmes

Outils d’analyse 84
Comment mesurer les temps d’exécution ?
Expérimentalement :
On écrit un programme qui implémente l’algorithme et on l’exécute
sur des données
Problèmes :
I Les temps de calcul vont dépendre de l’implémentation : CPU, OS,
langage, compilateur, charge de la machine, OS, etc.
I Sur quelles données tester l’algorithme ? Results

60
Ruby
Python
50 Scheme
C
Time (seconds)

40 C-wiz
Java
C-gcc
30

20

10

0
20 25 30 35 40 45 50
n
© 2006 Antonio Carzaniga (Carzaniga)
Outils d’analyse 85
Comment mesurer les temps d’exécution ?
Sur papier :
Développer un modèle de machine (“Random-access machine”,
RAM) :
I Opérations executées les unes après les autres (pas de parallélisme)
I Opérations de base (addition, affectation, branchement, etc.)
prennent un temps constant
I Appel de sous-routines : temps de l’appel (constant) + temps de
l’exécution de la sous-routine (calculé récursivement)
Calculer les temps de calcul = sommer le temps d’exécution associé
à chaque instruction du pseudo-code

Le temps dépend de l’entrée (l’instance particulière du problème)


On étudie généralement les temps de calcul en fonction de la
“taille” de l’entrée
I Généralement, le nombre de valeurs pour la décrire
I Mais ça peut être autre chose (Ex : n pour Fibonacci)

Outils d’analyse 86
Chapter 2 Getting Started
Analyse du tri par insertion
I NSERTION -S ORT .A/ cost times
1 for j D 2 to A:length c1 n
2 key D AŒj ! c2 n!1
3 // Insert AŒj ! into the sorted
sequence AŒ1 : : j ! 1!. 0 n!1
4 i D j !1 c4 P! 1
n
5 while i > 0 and AŒi! > key
n
c5 t
PjnD2 j
6 AŒi C 1! D AŒi! c6 .t ! 1/
PjnD2 j
7 i D i !1 c7 j D2 .tj ! 1/
8 AŒi C 1! D key c8 n!1

tj = The running
nombre de time of the
fois que la algorithm
conditionisdu thewhile
sum ofestrunning
testée.times for each sta
ment executed;
Temps exécutiona T
statement
(n) (pour thatun
takes ci steps
tableau detotaille
execute
n) and executes
donné par : n times w
contribute ci n to the total running time.6 To computen T .n/,
n the running time
I NSERTION
T (n) -S= ORTc1 n on
+ can
2 (ninput ofc4n(nvalues,
− 1) + we
c5 sum thec6 products of the cost
X X
− 1) + tj + (tj − 1)
times columns, obtaining j=2 j=2
n
n
X n
X
X
+c7 (tj − 1) + c8 (n − 1)
T .n/ D c1 n C c2j=2
.n ! 1/ C c4 .n ! 1/ C c5 tj C c6 .tj ! 1/
j D2 j D2
Outils d’analyse n 87
Qu’est ce que l’on compte ?

Souvent on ne compte que certain type d’opérations élémentaires


essentielles pour la nature du problème étudié.
Exemples :
Recherche d’un éléement dans un tableau, une liste, un arbre :
comparaisons
Tri d’une liste, d’un tableau, d’un fichier : comparaisons,
déplacements (affectations)
Multiplication de polynômes, matrices, grands entiers :
multiplications, additions
Pour simplifier, on peut faire l’hypothèse que toutes les opérations
élémentaires ont un coût uniforme.
Différents types de complexité

Même pour une taille fixée, la complexité peut dépendre de


l’instance particulière
Soit Dn l’ensemble des instances de taille n d’un problème et T (in )
le temps de calcul pour une instance in ∈ Dn .
Sur quelles instances les performances d’un algorithme devraient
être jugées :
I Cas le plus favorable (best case) : T (n) = min{T (in )|in ∈ Dn }
I Cas le plus défavorable (worst case) : T (n) = max{T (in )|in ∈ Dn }
P
I Cas moyen (average case) : T (n) = i ∈D Pr (in )T (in ) où Pr (in ) est
n n
la probabilité de rencontrer in
On se focalise généralement sur le cas le plus défavorable
I Donne une borne supérieure sur le temps d’exécution.
I Le meilleur cas n’est pas représentatif et le cas moyen est difficile à
calculer.

Outils d’analyse 88
Analyse du tri par insertion

Meilleur cas :
le tableau est trié ⇒ tj = 1.
Le temps de calcul devient :

T (n) = c1 n + c2 (n − 1) + c4 (n − 1) + c5 (n − 1) + c8 (n − 1)
= (c1 + c2 + c4 + c5 + c8 )n − (c2 + c4 + c5 + c8 )

T (n) = an + b ⇒ T (n) est une fonction linéaire de n

Outils d’analyse 89
Analyse du tri par insertion

Pire cas :
le tableau est trié par ordre décroissant ⇒ tj = j.
Le temps de calcul devient :
 
n(n + 1)
T (n) = c1 n + c2 (n − 1) + c4 (n − 1) + c5 −1
2
   
n(n − 1) n(n − 1)
+c6 + c7 + c8 (n − 1)
2 2
c5 c6 c7 c5 c6 c7
= ( + + )n2 + (c1 + c2 + c4 + − − + c8 )n
2 2 2 2 2 2
−(c2 + c4 + c5 + c8 )

T (n) = an2 + bn + c ⇒ T (n) est une fonction quadratique de n

Outils d’analyse 90
Analyse de la recherche d’un élément dans un tableau trié

La taille du problème : le nombre de cases du tableau


Le temps de la recherche dépend de l’algorithme choisi, mais aussi des
paramètres (le tableau et l’élément à trouver)
Exemples :
Algorithme : recherche séquentielle

Algorithme : recherche dichotomique


Analyse asymptotique
On s’intéresse à la vitesse de croissance (“order of growth”) de T (n)
lorsque n croı̂t.
I Tous les algorithmes sont rapides pour des petites valeurs de n
On simplifie généralement T (n) :
I en ne gardant que le terme dominant
I Exemple : T (n) = 10n3 + n2 + 40n + 800
I T(1000)=100001040800, 10 · 10003 = 100000000000
I en ignorant le coefficient du terme dominant
I Asymptotiquement, ça n’affecte pas l’ordre relatif
6
x 10
10

1000*N
9
4*N2
8
N3
0.01*2N
7

0
100 200 300 400 500 600 700 800 900 1000
N

Exemple : Tri par insertion : T (n) = an2 + bn + c → n2 .


Outils d’analyse 91
Pourquoi est-ce important ?

Supposons qu’on puisse traiter une opération de base en 1µs.


Temps d’exécution pour différentes valeurs de n

T(n) n = 10 n = 100 n = 1000 n = 10000


n 10µs 0.1ms 1ms 10ms
400n 4ms 40ms 0.4s 4s
2n2 200µs 20ms 2s 3.3m
n4 10ms 100s ∼ 11.5 jours 317 années
2n 1ms 4 × 1016 années 3.4 × 10287 années ...
(Dupont)

Outils d’analyse 92
Pourquoi est-ce important ?
Taille maximale du problème qu’on peut traiter en un temps donné :
T(n) en 1 seconde en 1 minute en 1 heure
n 1 × 106 6 × 107 3.6 × 109
400n 2500 150000 9 × 106
2n2 707 5477 42426
n4 31 88 244
2n 19 25 31

Si m est la taille maximale que l’on peut traiter en un temps donné,


que devient cette valeur si on reçoit une machine 256 fois plus
puissante ?
T(n) Temps
n 256m
400n 256m
2n2 16m
n4 4m
2n m+8
(Dupont)
Outils d’analyse 93
Notations asymptotiques

Permettent de caractériser le taux de croissance de fonctions


f : N → R+

Trois notations :
I Grand-O : f (n) ∈ O(g (n)) ≈ f (n) ≤ g (n)
I Grand-Omega : f (n) ∈ Ω(g (n)) ≈ f (n) ≥ g (n)
I Grand-Theta : f (n) ∈ Θ(g (n)) ≈ f (n) = g (n)

Outils d’analyse 94
Notation grand-O
Asymptotic notation

O-notation
O(g (n)) = {f (n)|∃c > 0, ∃n0 ≥ 1 tels que 0 ≤ f (n) ≤ cg (n), ∀n ≥ n0 }
O.g.n// D ff .n/ W there exist positive constants c and n0 su
0 " f .n/ " cg.n/ for all n # n0 g :
cg(n)

f(n)

n
n0

g.n/ is an asymptotic upper bound for f .n/.


f (n) ∈ O(g (n)) ⇒ g If
(n) est2une
f .n/ bornewesupérieure
O.g.n//, write f .n/ asymptotique
D O.g.n// (willpour
precisely
f (n).
Par abus de notation, on écrira aussi : f (n) = O(g (n)).
Outils d’analyse 95
n1:99999
Notation grand-Omega
n2 = lg lg lg n

!-notation
Ω(g (n)) = {f (n)|∃c > 0, ∃n0 ≥ 1 tels que 0 ≤ cg (n) ≤ f (n), ∀n ≥ n0 }
!.g.n// D ff .n/ W there exist positive constants c and n0 such
0 ! cg.n/ ! f .n/ for all n " n0 g :

f(n)

cg(n)

n
n0

g.n/ is an asymptotic lower bound for f .n/.


f (n) ∈ Ω(g (n)) ⇒ g (n) est une borne inférieure asymptotique pour
f (n). Example
Par abus de notation,pnonDécrira aussi
!.lg n/, with :c fD(n) = nΩ(g
1 and (n)).
0 D 16.

Outils d’analyse Examples of functions in !.n2 /: 96


Notation grand-Theta
Lecture Notes for Chapter 3: Growth of Functions

Θ(g (n)) = {f (n)|∃c 1 , c2 > 0, ∃n0 ≥ 1


‚-notation
tels que 0 ≤ c1 g (n) ≤ f (n) ≤ c2 g (n), ∀n ≥ n0 }
‚.g.n// D ff .n/ W there exist positive constants c1 , c2 , and n
0 ! c1 g.n/ ! f .n/ ! c2 g.n/ for all n "
c2g(n)

f(n)

c1g(n)

n
n0

is an asymptotically tight bound for f .n/.


f (n) ∈ Θ(g (n)) ⇒ gg.n/
(n) est une borne serrée (“tight”) asymptotique
pour f (n).
Par abus de notation,Example
on écrira aussi : f (n) = Θ(g (n)).
n2 =2 # 2n D ‚.n2 /, with c1 D 1=4, c2 D 1=2, and n0 D 8.
Outils d’analyse 97
Exemples
3n5 − 16n + 2 ∈ O(n5 ) ? ∈ O(n) ? ∈ O(n17 ) ?
3n5 − 16n + 2 ∈ Ω(n5 ) ? ∈ Ω(n) ? ∈ Ω(n17 ) ?
3n5 − 16n + 2 ∈ Θ(n5 ) ? ∈ Θ(n) ? ∈ Θ(n17 ) ?
2n + 100n6 + n ∈ O(2n ) ? ∈ Θ(3n ) ? ∈ Ω(n7 ) ?
Classes de complexité :
O(1) ⊂ O(log n) ⊂ O(n) ⊂ O(n log n) ⊂ O(na>1 ) ⊂ O(2n )
500

450
2n
n3
400
n2
350
n.log(n)
300 n
250
log(n)

200

150

100

50

0
5 10 15 20 25 30 35 40 45 50

Outils d’analyse 98
Quelques propriétés

f (n) ∈ Ω(g (n)) ⇔ g (n) ∈ O(f (n))


f (n) ∈ Θ(g (n)) ⇔ f (n) ∈ O(g (n)) et f (n) ∈ Ω(g (n))
f (n) ∈ Θ(g (n)) ⇔ g (n) ∈ Θ(f (n))

Si f (n) ∈ O(g (n)), alors pour tout k ∈ N, on a k · f (n) ∈ O(g (n))


I Exemple : loga (n) ∈ O(logb (n)), an+b ∈ O(an )
Si f1 (n) ∈ O(g1 (n)) et f2 (n) ∈ O(g2 (n)), alors
f1 (n) + f2 (n) ∈ O(g1 (n) + g2 (n)) et f1 (n) + f2 (n) ∈ O(max{g1 (n), g2 (n)})
P
I Exemple : m i
i=1 ai n ∈ O(n )
m

Si f1 (n) ∈ O(g1 (n)) et f2 (n) ∈ O(g2 (n)), alors


f1 (n) · f2 (n) ∈ O(g1 (n) · g2 (n))

Outils d’analyse 99
Comment calculer la complexité en pratique ?

Quelques règles pour les algorithmes itératifs :


Affectation, accès à un tableau, opérations arithmétiques, appel de
fonction : O(1)
Instruction If-Then-Else : O(complexité max des deux branches)
Séquence d’opérations : l’opération la plus couteuse domine (règle
de la somme)
Boucle simple : O(nf (n)) si le corps de la boucle est O(f (n))

Outils d’analyse 105


Comment calculer la complexité en pratique ?

Double boucle complète : O(n2 f (n)) où f (n) est la complexité du


corps de la boucle
Boucles incrémentales : O(n2 ) (si corps O(1))
for i = 1 to n
for j = 1 to i
...

Boucles avec un incrément exponentiel : O(log n) (si corps O(1))


i =1
while i ≤ n
...
i = 2i

Outils d’analyse 106


Exemple :

prefixAverages(X ) :
Entrée : tableau X de taille n
Pi
j=1 X [j]
Sortie : tableau A de taille n tel que A[i] = i

prefixAverages(X )
prefixAverages2(X )
1 for i = 1 to X . length
1 s =0
2 a=0
2 for i = 1 to X . length
3 for j = 1 to i
3 s = s + X [i]
4 a = a + X [j]
4 A[i] = s/i
5 A[i] = a/i
5 return A
6 return A
Complexité : Θ(n)
Complexité : Θ(n2 )

Outils d’analyse 107


Complexité d’algorithmes récursifs
La complexité d’algorithme récursif mène généralement à une
équation de récurrence

Factorial(n)
1 if n = = 0
2 return 1
3 return n · Factorial(n − 1)

T (0) = c0
T (n) = T (n − 1) + c1
= c1 n + c0

⇒ T (n) ∈ Θ(n)

La résolution de cette équation n’est pas toujours triviale

Outils d’analyse 108


Autres exemples

Fibonacci :

T (1) = c0
T (n) = T (n − 1) + T (n − 2) + c1 pour n > 1

Tri par fusion :

T (1) = c0
T (n) = T (dn/2e) + T (bn/2c) + c1 n + c2 pour n > 1

Tour de Hanoï :

T (1) = c0
T (n) = 2T (n − 1) + c1 pour n > 1

Outils d’analyse 109


Quelques techniques de résolution de récurrences
Pas dans ce cours

Méthodes “manuelles” :
I Téléscopage (plug-and-chug)
I Arbre de récursion
I ...
Méthodes systématiques :
I Le théorème maître (récurrences diviser-pour-régner)
I Equations caractéristiques (récurrences linéaires)
I ...
Synthèse : calcul de complexité en pratique

Déterminer la taille n du problème


Choisir les opérations à compter
Déterminer le pire cas pour l’algorithme
I Ne pas oublier que n est très grand
Compter (approximativement) le nombre d’opérations élémentaires
en fonction de n
I Donner une borne supérieure la plus précise possible

Pour tous les algorithmes étudiés : donner la complexité !


Plan

1. Algorithmes et structures de données

2. Récursivité

3. Complexité et notation asymptotiques

4. Types abstraits et structures de données

5. Pile et File

6. Liste, Vecteur, Séquence


Concept

Une structure de données est une manière d’organiser et de stocker


l’information
I Pour en faciliter l’accès ou dans d’autres buts
Une structure de données a une interface qui consiste en un
ensemble de procédures pour ajouter, effacer, accéder, réorganiser,
etc. les données.
Une structure de données conserve des données et éventuellement
des méta-données
I Par exemple : un tas utilise un tableau pour stocker les clés et une
variable A.heap-size pour retenir le nombre d’éléments qui sont dans
le tas.
Un type de données abstrait (TDA) = définition des propriétés de la
structure et de son interface (“cahier des charges”)

Structures de données 186


Structures de données

Dans ce cours :
Principalement des ensembles dynamiques (dynamic sets), amenés à
croı̂tre, se rétrécir et à changer au cours du temps.
Les objets de ces ensembles comportent des attributs.
Un de ces attributs est une clé qui permet d’identifier l’objet, les
autres attributs sont la plupart du temps non pertinents pour
l’implémentation de la structure.
Certains ensembles supposent qu’il existe un ordre total entre les
clés.

Structures de données 187


Opérations standards sur les structures

Deux types : opérations de recherche/accès aux données et


opérations de modifications
Recherche : exemples :
I Search(S, k) : retourne un pointeur x vers un élément dans S tel
que x. key = k, ou NIL si un tel élément n’appartient pas à S.
I Minimum(S), Maximum(S) : retourne un pointeur vers l’élément
avec la plus petite (resp. grande) clé.
I Successor(S, x),Predecessor(S, x) retourne un pointeur vers
l’élément tout juste plus grand (resp. petit) que x dans S, NIL si x
est le maximum (resp. minimum).
Modification : exemples :
I Insert(S, x) : insère l’élément x dans S.
I Delete(S, x) : retire l’élément x de S.

Structures de données 188


Implémentation d’une structure de données

Etant donné un TDA (interface), plusieurs implémentations sont


généralement possibles
La complexité des opérations dépend de l’implémentation, pas du
TDA.

Les briques de base pour implémenter une structure de données


dépendent du langage d’implémentation
I Dans ce cours, les principaux outils du C : tableaux, structures à la C
(objets avec attributs), liste liées (simples, doubles, circulaires), etc.
Une structure de données peut être implémentée à l’aide d’une autre
structure de données (de base ou non)

Structures de données 189


Quelques structures de données standards

Pile : collection d’objets accessibles selon une politique LIFO


File : collection d’objets accessibles selon une politique FIFO
File double : combine accès LIFO et FIFO
Liste : collection d’objets ordonnés accessibles à partir de leur
position
Vecteur : collection d’objets ordonnés accessibles à partir de leur
rang
Arbre : collection d’objets organisés en une structure d’arbre
File à priorité : accès uniquement à l’élément de clé (priorité)
maximale

Dictionnaire : structure qui implémente les 3 opérations recherche,


insertion, suppression (cf. partie 5)

Structures de données 190


Exemple d’implémentation d’un types abstrait
en C
Types abstraits Implantation et utilisation Récursivité

Exemple du type Color

Fichier color.h : Programme principal :


#include "color.h"

typedef ... Color; int main (int argc, char *argv[]){


Color c1 = randomColor();
Color randomColor(void); Color c2 = newColorRGB(255,0,0);
Color c3;
Color newColorRGB(int r, int g, int b);
printRGB(c1);
void printRGB(Color c); printRGB(grayScale(c1));
printHexa(c2);
void printHexa(Color c);
c3=mixColors(c1,c2);
Color grayScale(Color c); printRGB(c3);

Color mixColors(Color c1, Color c2); inverseColor(&c3);


printRGB(c3);
void inverseColor(Color *c); }

Struct 5/24
Types abstraits Implantation et utilisation Récursivité

Struct
Les types structurés sont un moyen privilégié de définir des
types abstraits : ce sont des types complexes construits à partir
de types “plus simples” :
struct nomStructure { typedef struct{
type champ1 nomChamp1; type champ1 nomChamp1;
type champ2 nomChamp2; type champ2 nomChamp2;
... ...
}; } nomType ;
typedef nomStructure nomType ;

Si on définit un pointeur sur une structure, la notation -> est


un raccourci pour accéder aux champs :
typedef struct{ Complex *c = &(Complex){0.3,1.2};
double Re;
double Im; printf("%f+i*%fc", c->Re, c->Im);
}Complex; Complex conjug = {c->Re,-c->Im};
Struct 7/24
Types abstraits Implantation et utilisation Récursivité

Le type Color - 1ère version color.c


typedef struct { int hexa; }Color;

Color randomColor(void){
return (Color){myRand(0xffffff+1)};
}

Color newColorRGB(int r, int g, int b){


return (Color){(r<<16)+(g<<8)+b};
}

void printRGB(Color c){


printf("[%d,%d,%d] ",c.hexa>>16, (c.hexa & 0x00ff00)>>8 , (c.hexa & 0x0000ff));
}

void printHexa(Color c){


printf("#%x ", c.hexa);
}

Color grayScale(Color c){


int s=((c.hexa>>16) + ((c.hexa & 0x00ff00)>>8) + (c.hexa & 0x0000ff))/3;
Color res = {(s<<16)+(s<<8)+s};
return res;
}

Color mixColors(Color c1, Color c2){


return (Color){(c1.hexa+c2.hexa)/2};
}

void inverseColor(Color *c){


c->hexa=0xffffff-c->hexa;
}

Struct 8/24
Types abstraits Implantation et utilisation Récursivité

Le type Color - 2ème version color.c

typedef struct {
Color grayScale(Color c){
int red; int green; int blue;
int s;
}Color;
s=(c.red+c.blue+c.green)/3;
Color res = {s,s,s};
Color randomColor(void){
return res;
Color c={myRand(256), myRand(256), myRand(256)};
}
return c;
}
Color mixColors(Color c1, Color c2){
Color res;
Color newColorRGB(int r, int g, int b){
res.red=(c1.red+c2.red)/2;
Color c={r,g,b};
res.green=(c1.green+c2.green)/2;
return c;
res.blue=(c1.blue+c2.blue)/2;
}
return res;
}
void printRGB(Color c){
printf("[%d,%d,%d] ",c.red, c.green, c.blue);
void inverseColor(Color *c){
}
c->red=255-c->red;
c->green=255-c->green;
void printHexa(Color c){
c->blue=255-c->blue;
printf("\#%x ", (c.red<<16) + (c.green<<8) + c.blue);
}
}

Le programme principal (main) reste inchangé... l’intérêt d’un


type abstrait est que l’on peut l’utiliser, quelle que soit son
implémentation, sans modifier les programmes qui y font appel.
Struct 9/24
Types abstraits Implantation et utilisation Récursivité

Exemple du type SArray (tableaux triés)


typedef ... SArray;
SArray newEmptySArray(void);
void printSArray(SArray t);
int getLength(SArray t);
int isEmpty(SArray t);
int isFull(SArray t);
int get(int i, SArray t);
int searchPosition(int elem, SArray t);
int search(int elem, SArray t);
void insertPosition(int elem, SArray *t, int i);
void delete(int i, SArray *t);
/***********************************************************/
void insert(int elem, SArray *t){
insertPosition(elem, t, searchPosition(elem,*t));
}

void randomSArray(int nb, SArray *t, int up){


int i;
for(i=0;i<nb;i++)
insert(myRand(up),t);
}
Struct 10/24
Types abstraits Implantation et utilisation Récursivité

Exemple du type SArray - suite


int main (int argc, char *argv[]){

SArray t = newEmptySArray();
int i,j, up=20, taille=100;

srandom(time(NULL));

printSArray(t);
printf("t vide? %d, t plein? %d\n",isEmpty(t),isFull(t));
randomSArray(taille, &t, up);
printf("t vide? %d, t plein? %d\n",isEmpty(t),isFull(t));
printSArray(t);
printf("indice de %d dans t: %d\n", 1, search(1,t));

for(i=0;i<up;i++){
j=search(i,t);
printf("verification: get(search(%d,t),t): %d\n", i,j==-1?-1:get(j,t) );
}
while( (i=search(5, t)) != -1 )
delete(i, &t);
printf("longueur de t: %d\n", getLength(t));
printSArray(t);
}

Struct 11/24
Types abstraits Implantation et utilisation Récursivité

Exemple du type SArray - fin


typedef struct{ int lg; int tab[MAX SIZE]; }SArray;

SArray newEmptySArray(void){ int get(int i, SArray t){


SArray res; if (i<0 || i>=t.lg)
res.lg=0; erreur("L’indice %d est incorrect!", i);
return res; return t.tab[i];
} }

void printSArray(SArray t){ void insertPosition(int elem, SArray *t, int i){
printTab(t.tab, t.lg); ...
} }

int getLength(SArray t){ void delete(int i, SArray *t){


return t.lg; ...
} }

int isEmpty(SArray t){ int searchPosition(int e, SArray t){


return (t.lg==0); return isEmpty(t)?0:searchPos(e,t.tab,0,t.lg-1);
} }

int isFull(SArray t){ int search(int elem, SArray t){


return (t.lg==MAX_SIZE); return dichoSearch(elem, t.tab, 0, t.lg-1);
} }

Struct 12/24
Types abstraits Implantation et utilisation Récursivité

Exemple du type SArray - 2e fin

Ou alors...

typedef struct{ int lg; int *tab; }SArray;

SArray newEmptySArray(void){
SArray res;
res.lg=0;
if( (res.tab=malloc(MAX SIZE*sizeof(int))) == NULL )
erreur("Allocation ratée!");
return res;
}

...

Struct 13/24
Plan

1. Algorithmes et structures de données

2. Récursivité

3. Complexité et notation asymptotiques

4. Types abstraits et structures de données

5. Pile et File

6. Liste, Vecteur, Séquence


Pile

Ensemble dynamique d’objets accessibles selon une discipline LIFO


(“Last-in first-out”).
Interface
I Stack-Empty(S) renvoie vrai si et seulement si la pile est vide
I Push(S, x) pousse la valeur x sur la pile S
I Pop(S) extrait et renvoie la valeur sur le sommet de la pile S
Applications :
I Option ’undo’ dans un traitement de texte
I Langage postscript
I Appel de fonctions dans un compilateur
I ...
Implémentations :
I avec un tableau (taille fixée a priori)
I au moyen d’une liste liée (allouée de manière dynamique)
I ...

Structures de données 192


Implémentation par un tableau

S est un tableau qui contient les éléments de la pile


S.top est la position courante de l’élément au sommet de S
Implémentation par un tableau
4 9 5
S est un tableau qui contientStack-Empty(S)
les éléments de la pile
S.top est la position courante return
1 de S.top ==
l’élément au 0sommet de

Pop(S)
Stack-Empty(S)
Push(S, x)
1 1 ififS.top
Stack-Empty(S)
== 0
1 if S.top == S. length 2 error “underflow”
2 error “overflow” 2 return true
elsereturn
3 3 else S. top = S. top − 1
false
3 S. top = S.top + 1 4 return S[S. top + 1]
4 S[S. top] = x
ComplexitéPush(S,
en temps x) et en espace : O(1) Pop(S)
(Inconvénient : L’espace occupé
1 S.top = S.top + 1 ne dépend pas du nombre d’objets)
1 if Stack-Emp
2 S[S.top] = x 2 error “und
3 else S.top = S
Structures de données 193
10.2 Linked lists 237
Rappel : liste simplement et doublement liée
prev key next

(a) L:head 9 16 4 1

(b) L:head 25 9 16 4 1
Structure de données composée d’une séquence d’éléments de liste.
(c)
Chaque élément x de25 la liste est9 composé16:
L:head 1

I d’un contenu utile x.data de type arbitraire (par exemple une clé),
I d’un pointeur
Figure 10.3 x. (a)next
A doubly linked
vers list L representing
l’élément the dynamic
suivant dansset la 9; 16g. Each element in
séquence
f1; 4;
the list is an object with attributes for the key and pointers (shown by arrows) to the next and previous
I Doublement liée : d’un pointeur x. prev vers l’élément précédent dans
objects. The next attribute of the tail and the pre! attribute of the head are NIL , indicated by a diagonal
slash. The attribute L: head points to the head. (b) Following the execution of L IST-I NSERT.L; x/,
la séquence
where x: key D 25, the linked list has a new object with key 25 as the new head. This new object
Soit L unepoints
listeto the
liée
old head with key 9. (c) The result of the subsequent call L IST-D ELETE.L; x/, where x
I L. headpoints to the object with key 4.
pointe vers le premier élément de la liste
I Doublement liée : L.tail pointe vers le dernier élément de la liste
elements. In the remainder of this section, we assume that the lists with which we
Le dernier are working are
élément unsorted un
possède and doubly linked.x.next vide (noté NIL)
pointeur
Searching
Doublement liée : aLe
linked list
premier élément possède un pointeur x.prev
vide The procedure L IST-S EARCH .L; k/ finds the first element with key k in list L
by a simple linear search, returning a pointer to this element. If no object with
key k appears in the list, then the procedure returns NIL. For the linked list in
Structures de données Figure 10.3(a), the call L IST-S EARCH .L; 4/ returns a pointer to the third element, 194
ecution of L IST-I NSERT.L; x/,
heImplémentation
new head. This newd’uneobject pile à l’aide d’une liste liée
L IST-D ELETE.L; x/, where x
S est une liste simplement liée (S.head pointe vers le premier
à l’aide d’une liste liée
élément de la liste)
ément dewith
at the lists la which
liste we Stack-Empty(S)
liée (S.head pointe
9 vers le14premier 1 if S. head = = NIL
de (noté NIL) 2 return true
3 else return false
pointeur x.prev
ack-Empty(S)
4
Pop(S)
ment with key
if S.head k in list L
== NIL 1 if Stack-Empty(S)
ement. If notrue
object with
Push(S,
return x) 2 error “underflow”

. For the false


linked list in
1 x.next = S.head 3 else x = S.head
IL
else return 4 S. head = S. head.next
2 S.head = x
inter to the third element, 65 5 return x

Complexité en temps O(1), complexité en espace O(n) pour n


Pop(S)
opérations
1 if Stack-Empty(S)
2
Structures de données
error “underflow” 195
Application
Vérifier l’appariement de parenthèses ([],() ou {}) dans une chaı̂ne
de caractères
I Exemples : ((x) + (y )]/2 → non, [−(b) + sqrt(4 ∗ (a) ∗ c)]/(2 ∗ a) →
oui
Solution basée sur une pile :
ParenthesesMatch(A)
1 S = pile vide
2 for i = 1 to A. length
3 if A[i] est une parenthèse gauche
4 Push(S, A[i])
5 elseif A[i] est une parenthèse droite
6 if Stack-Empty(S)
7 return False
8 elseif Pop(S) n’est pas du même type que A[i]
9 return False
10 return Stack-Empty(S)

Structures de données 196


File

Ensemble dynamique d’objets accessibles selon une discipline FIFO


(“First-in first-out”).
Interface
I Enqueue(Q, s) ajoute l’élément x à la fin de la file Q
I Dequeue(Q) retire l’élément à la tête de la file Q

Implémentation à l’aide d’un tableau circulaire


I Q est un tableau de taille fixe Q. length
I Mettre plus de Q. length éléments dans la file provoque une erreur de
dépassement
I Q. head est la position à la tête de la file
I Q. tail est la première position vide à la fin de la file
I Initialement : Q.head = Q.tail = 1

Structures de données 197


234
Enqueue Chapter
etChapter 10 Elementary Data Structures
Dequeue
234 10 Elementary Data Structures
234
Etat initial : Chapter 10 Elementary Data Structures
1 2 3 4 5 6 7 8 9 10 11 12
(a) Q 1 2 3 4 5 6 157 6 8 9 9 8 104 11 12
(a) Q 1 2 3 4 5 6 7 8 9 10 11 12
15 6 9 8 4
(a) Q Q:head D 7
15 6 9 8 4
Q:tail D 12
Q:head D 7 Q:tail D 12
1 2 3 4 5 6 7 8 9 10 11 12
Enqueue(Q, 17), Enqueue(Q, 3), Enqueue(Q,
Q:head D 7 5)Q:tail D 12
(b) Q 31 52 3 4 5 6
157 6 8 9 9 8 104 11
17 12

(b) Q 13 25 3 4 5 6 15
7 68 99 810 411 17
12
(b) Q 3 Q:tail
5 D3 Q:head15D 76 9 8 4 17

1 2 3 D
Q:tail 4 3 5 Q:head
6 7 8 D 97 10 11 12
(c) Q 3 Q:tail
5 D3 15 6 D97 8 4 17
Q:head
1 2 3 4 5 6 7 8 9 10 11 12
Dequeue(Q) → 15
(c) Q 13 25 3 4 5 6 15
7 68 99 810 411 17
12
Q:tail D 3 Q:head D 8
(c) Q 3 5 15 6 9 8 4 17
Q:tail D 3 Q:head D 8
Figure 10.2 A queue implemented using an array QŒ1 : : 12!. Queue elements appear o
lightly shaded D 3 (a) TheQ:head
positions.
Q:tail queue hasD5 8elements, in locations QŒ7 : : 11!. (b) The con
of the queue after the calls E NQUEUE.Q; 17/, E NQUEUE.Q; 3/, and E NQUEUE.Q; 5/
Figure 10.2 ofA the
configuration queue implemented
queue using
after the call an array
D EQUEUE .Q/QŒ1returns theQueue
: : 12!. elements
key value ap
15 forme
Structures de données lightly shaded positions. (a) The queue has 5 elements, in locations QŒ7 : : 11!. (b)
198 T
Enqueue et Dequeue

Enqueue(Q,x) Dequeue(Q)
1 Q[Q. tail] = x 1 x = Q[Q. head]
2 if Q. tail = = Q. length 2 if Q.head == Q. length
3 Q. tail = 1 3 Q. head = 1
4 else Q. tail = Q.tail + 1 4 else Q. head = Q. head + 1
5 return x

Complexité en temps O(1), complexité en espace O(1).


Exercice : ajouter la gestion d’erreur

Structures de données 199


Soit la file double Q :
Implémentation à l’aide d’une liste liée
I Q.head pointe vers un
I Q.tail pointe vers un
Soit la file double Q :
I
Q.head pointe
9 vers un14 4
I
Q.tail pointe vers un
Q est une liste simplement liée
Q.head (resp. Q.tail) pointe vers la tête (resp. la queue) de la liste
Enqueue(Q,x) Dequeue(Q)
1 x. next = NIL 1 if Q.head = = NIL
2 if Q.head == NIL 2 error “underflow”
3 Q. head = x 3 x = Q. head
4 else Q. tail.next = x 4 Q. head = Q. head.next
5 Q. tail = x 5 if Q.head = = NIL
6 de données
Structures Q. tail = NIL
élémentaires
7 return x
Structures de données élémentaires
Complexité en temps O(1), complexité en espace O(n) pour n
opérations

Structures de données 200


File double

Double ended-queue (deque)

Généralisation de la pile et de la file


Collection ordonnée d’objets offrant la possibilité
I d’insérer un nouvel objet avant le premier ou après le dernier
I d’extraire le premier ou le dernier objet
Interface :
I insert-first(Q, x) : ajoute x au début de la file double
I insert-last(Q, x) : ajoute x à la fin de la file double
I remove-first(Q) : extrait l’objet situé en première position
I remove-last(Q) : extrait l’objet situé en dernière position
I ...
Application : équilibrage de la charge d’un serveur

Structures de données 201


Implémentation par u
Implémentation à l’aide d’une liste doublement liée
mplémentation par une liste doublement liée
A l’aide d’une liste doublement liée
Soit la file double Q :
I Q.head pointe vers un élément sentinelle en début de liste
I Q.tail pointe vers un élément sentinelle en fin de Soit
liste la file double Q
I Q.size est la taille courante de la liste I Q.head pointe ver
I Q.tail pointe vers
Soit la file double Q :
I Q.head pointe vers un 9 14 4
I Q.tail pointe vers un
Les sentinelles ne contiennent pas de données. Elles permettent de
simplifier le code (pour un coût en espace constant).

Exercice : implémentation de la file double sans sentinelles,


implémentation de la file simple avec sentinelle

Structures de données Structures de données élémentaires 202


Implémentation à l’aide d’une liste doublement liée
insert-first(Q, x) insert-last(Q, x)
1 x. prev = Q. head 1 x.prev = Q. tail.prev
2 x. next = Q.head.next 2 x.next = Q. tail
3 Q. head.next.prev = x 3 Q. tail.prev .next = x
4 Q. head.next = x 4 Q. head.prev = x
5 Q. size = Q. size + 1 5 Q. size = Q. size + 1

remove-first(Q) remove-last(Q)
1 if (Q. size == 0) 1 if (Q. size == 0)
2 error 2 error
3 x = Q. head.next 3 x = Q.tail.prev
4 Q. head.next = Q.head.next.next 4 Q. tail.prev = Q. tail.prev .prev
5 Q. head.next.prev = Q. head 5 Q. tail.prev .next = Q. head
6 Q. size = Q. size − 1 6 Q. size = Q. size − 1
7 return x 7 return x

Complexité O(1) en temps et O(n) en espace pour n opérations.


Structures de données 203
Implémentations (contigu et chaînée) de

Pile et File en C
Implémentation d’une pile
Représentation contiguë d’une pile Avec un tableau

On considère ici une pile avec une capacité maximale


on peut utiliser un tableau dynamique pour enlever cette
contrainte.
1 typedef struct {
2 unsigned int max; // capacité max
3 unsigned int cur; // nombre d’éléments
4 int * tab;
5 } stack ;

La pile est donc juste un tableau tab :


max est la capacité maximale,
cur est le nombre d’éléments stockés (à mettre à jour).
En permanence les éléments sont au début du tableau :
on ajoute et on retire par la fin,
c’est tout simple !
13
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas

??? ??? ??? ??? ???

max 5
P cur 0
tab

La pile est vide au départ : cur vaut 0.

14
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
push(7)

7 ??? ??? ??? ???

max 5
P cur 1
tab

Chaque push ajoute un élément dans le tableau et incrémente cur.

15
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
push(3)

7 3 ??? ??? ???

max 5
P cur 2
tab

Chaque push ajoute un élément dans le tableau et incrémente cur.

16
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
push(8)

7 3 8 ??? ???

max 5
P cur 3
tab

Chaque push ajoute un élément dans le tableau et incrémente cur.

17
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
pop()

7 3 ??? ??? ???

max 5 8
P cur 2
tab

Chaque pop extrait le dernier élément du tableau et décrémente


cur.

18
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
push(5)

7 3 5 ??? ???

max 5
P cur 3
tab

19
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
pop()

7 3 ??? ??? ???

max 5 5
P cur 2
tab

20
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
pop()

7 ??? ??? ??? ???

max 5 3
P cur 1
tab

21
Fonctionnement d’une pile
Représentation contiguë d’une pile Avec un tableau

Pile Tas
pop()

??? ??? ??? ??? ???

max 5 7
P cur 0
tab

Le fonctionnement est donc très simple,


on peut aussi remplacer tab par un tableau dynamique
_ plus de limite de capacité.
22
Implémentation d’une pile en C
Représentation contiguë d’une pile Avec un tableau

1 int pop(stack* P) {
2 if (P->cur == 0) {
3 printf("Pile vide !\n");
4 return -1;
5 }
6 P->cur--;
7 return P->tab[P->cur];
8 }
9

10 void push(stack* P, int val) {


11 if (P->cur == P->max) {
12 printf("Pile pleine !\n");
13 return;
14 }
15 P->tab[P->cur] = val;
16 P->cur++;
17 }

23
Implémentation d’une pile en C
Représentation contiguë d’une pile Avec un tableau dynamique

1 int pop(stack* P) {
2 if (P->cur == 0) {
3 printf("Pile vide !\n");
4 return -1;
5 }
6 P->cur--;
7 return P->tab[P->cur];
8 }
9

10 void push(stack* P, int val) {


11 if (P->cur == P->max) {
12 P->max *= 2;
13 P->tab = (int * ) realloc(P->tab, P->max);
14 }
15 P->tab[P->cur] = val;
16 P->cur++;
17 }

24
Implémentation d’une file
Représentation contiguë d’une file Avec un tableau

On implémente la file de façon similaire à la pile


toujours un tableau avec capacité maximale,
on ajoute encore les éléments à la fin du tableau.
Il faut en revanche enlever les éléments par le début :
on ne peut pas décaller tout le tableau (ne se fait pas en Θ (1)),
on utilise le tableau de façon cyclique
_ quand on arrive au bout du tableau on recommence du début.
On doit donc aussi garder l’indice du premier élément (le plus ancien
encore présent).

1 typedef struct {
2 unsigned int max; // capacité max
3 unsigned int cur; // nombre d’éléments
4 unsigned int first; // premier élément
5 int * tab;
6 } queue ;

25
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas

??? ??? ??? ??? ???

max 5
cur 0
F first 0
tab

La structure est similaire à celle d’une pile : cur et first sont


initialisés à 0.

26
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
push(7)

7 ??? ??? ??? ???

max 5
cur 1
F first 0
tab

Les push fonctionnent comme avec la pile.

27
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
push(5)

7 5 ??? ??? ???

max 5
cur 2
F first 0
tab

28
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
push(8)

7 5 8 ??? ???

max 5
cur 3
F first 0
tab

29
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
pop()

??? 5 8 ??? ???

max 5
cur 2 7
F first 1
tab

Les pop sont en revanche différents de ceux de la pile :


on extrait l’élément le plus ancien (à la position first),
on incrément first,
on décrémente cur.
30
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
push(6)

??? 5 8 6 ???

max 5
cur 3
F first 1
tab

31
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
push(2)

??? 5 8 6 2

max 5
cur 4
F first 1
tab

32
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
pop()

??? ??? 8 6 2

max 5
cur 3 5
F first 2
tab

33
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
push(9)

9 ??? 8 6 2

max 5
cur 4
F first 2
tab

Quand on arrive au bout du tableau, la file n’est pas forcément


pleine :
les premiers éléments sont “vides”,
on peut donc faire un cycle et réinsérer au début.
34
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
pop()

9 ??? ??? 6 2

max 5
cur 3 8
F first 3
tab

35
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
pop()

9 ??? ??? ??? 2

max 5
cur 2 6
F first 4
tab

36
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
pop()

9 ??? ??? ??? ???

max 5
cur 1 2
F first 0
tab

37
Fonctionnement d’une file
Représentation contiguë d’une file Avec un tableau

Pile Tas
pop()

??? ??? ??? ??? ???

max 5
cur 0 9
F first 1
tab

L’extraction aussi s’effectue de façon cyclique :


il suffit de calculer les indices d’insertion et d’extraction modulo
max.

38
Implémentation d’une file en C
Représentation contiguë d’une file Avec un tableau

1 int pop(queue* F) {
2 int res;
3 if (F->cur == 0) {
4 printf("File vide !\n");
5 return -1;
6 }
7 res = F->tab[F->first];
8 F->first = (F->first+1) % F->max; // incrémentation "cyclique"
9 F->cur--;
10 return res;
11 }
12 void push(queue* F, int val) {
13 if (F->cur == F->max) {
14 printf("File pleine !\n");
15 return;
16 }
17 F->tab[(F->first+F->cur) % F->max] = val;
18 F->cur++;
19 }

39
Implémentation d’une file en C
Représentation contiguë d’une file Avec un tableau dynamique

1 void push(queue* F, int val) {


2 int * new_tab;
3 if (F->cur == F->max) { // realloc ne marche pas ici...
4 new_tab = (int * ) malloc(2*F->max*sizeof(int ));
5 // on utilise : memcpy(dest, source, taille);
6 // on doit le faire en 2 blocs
7 memcpy(new_tab, &(F->tab[F->first]), (F->max-F->first)*sizeof(int ));
8 memcpy(&(new_tab[F->max-F->first]), F->tab, F->first*sizeof(int ));
9 free(F->tab); // on libère l’ancien tab
10 F->tab = new_tab;
11 F->first = 0;
12 F->max *= 2;
13 }
14 F->tab[(F->first+F->cur) % F->max] = val;
15 F->cur++;
16 }

Ça marche bien, mais c’est un peu compliqué à gérer...

40
Piles et files avec des listes chaı̂nées
Représentation chaînée des piles et des files
Il est aussi possible d’implémenter efficacement une pile ou une file
avec une liste chaı̂née
il n’y a pas de contraintes de taille comme avec un tableau.
La pile est le plus simple :
push consiste à insérer un élément en début de liste,
pop lit le contenu du premier élément et le supprime,
_ les opérations se font bien en Θ (1).
Pour la file c’est un peu plus compliqué :
il faut insérer les éléments à un bout et les supprimer à l’autre,
on ne sait pas reculer d’un élément, juste passer au suivant.
_ on insère à la fin (nécessite un pointeur supplémentaire) et
on retire du début.

61
Piles et files avec des listes chaı̂nées
Représentation chaînée des piles et des files
Implémentation d’un file

queue.c
1 typedef struct { // nouvelle structure de file
2 cell* beg; // pointeur sur la première case
3 cell* end; // pointeur sur la dernière case
4 } queue ;
5
6 void push(queue* F, int v) {
7 cell* new = (cell* ) malloc(sizeof(cell ));
8 new->val = v;
9 new->next = NULL;
10 if (F->end == NULL) { // si la file est vide
11 F->beg = new; // le dernier est aussi le premier élément,
12 F->end = new;
13 } else { // sinon :
14 F->end->next = new; // - ajout de l’élément à la fin
15 F->end = new; // - on met à jour la fin
16 }
17 }

62
Piles et files avec des listes chaı̂nées
Représentation chaînée des piles et des files
Implémentation d’un file

queue.c
1 int pop(queue* F) {
2 int res;
3 cell* tmp;
4 if (F->beg == NULL) {
5 printf("File vide !\n");
6 return -1;
7 } else if (F->beg == F->end) { // s’il n’y a qu’une case
8 F->end = NULL; // la file sera vide à la fin
9 }
10 res = F->beg->val; // on sauvegarde la valeur
11 tmp = F->beg; // on sauvegarde le pointeur
12 F->beg = F->beg->next; // on avance le début d’une case
13 free(tmp); // on libère la case extraite
14 return res;
15 }

Le code parait compliqué, mais les opérations sont très simples


les listes sont très efficaces pour implémenter des files.

63
Plan

1. Algorithmes et structures de données

2. Récursivité

3. Complexité et notation asymptotiques

4. Types abstraits et structures de données

5. Pile et File

6. Liste, Vecteur, Séquence


Liste
Ensemble dynamique d’objets ordonnés accessibles relativement les
uns aux autres, sur base de leur position
Généralise toutes les structures vues précédemment
Interface :
I Les fonctions d’une liste double (insertion et retrait en début et fin de
liste)
I Insert-Before(L, p, x) : insére x avant p dans la liste
I Insert-After(L, p, x) : insère x après p dans la liste
I Remove(L, p) : retire l’élement à la position p
I replace(L, p, x) : remplace par l’objet x l’objet situé à la position p
I first(L), last(L) : renvoie la première, resp. dernière position dans
la liste
I prev(L, p), next(L, p) : renvoie la position précédant (resp. suivant)
p dans la liste

Implémentation similaire à la file double, à l’aide d’une liste


doublement liée (avec sentinelles)
Structures de données 205
Implémentation à l’aide d’une liste doublement liée
mplémentation à l’aide d’une liste doublement liée
p.prev p.next p x 6 remove(L, p)
1 p.prev .next = p.next
Implémentation
Implémentation ààl’aide
p, x) d’une
l’aide
Implémentation d’une liste
liste2doublement
à l’aide doublement
d’une liste liée
p, x)doublement
insert-before(L,
9 14 4
insert-after(L,
p.next.prev = p.prev liée
1 x.prev = p.prev 1 x.prev = p = L.size − 1
3 L.size
p.prev
p.prev2p.next
x.nextp p=
p.next xp.prev
xp p.next p x 2 x.next = p.next
4 return p
3
p.prev .next = x 3 p.next.prev = x
insert-before(L,
4insert-before(L,
p.prev =x insert-before(L,
p,p,x)x) p, x) = x
insert-after(L,
4 p.nextinsert-after(L, insert-after(L,
p, x) p, x)
5
1L.size = L.size + 1
1 x. prev = p. prev 1 x. prev = p
x.x.
prev ==p.p.
1 insert-before(L,
prev prev
prev p, x) 5 L.size 11 x. x.=prev
L.size
prev
insert-after(L, =+
= pp 1 p, x)
2 x. next = p 2 x. next = p. next
2 2 x. next
x. next = =p p
1 x. prev = p.prev 22 x.
x. next
next =
= p.
p.next
3 p. prev .next = 1 x x.prev = p 3 p. next.prev = x
3 3 p.
2 p. prev
prev
x. ==p=4xx p. prev
.next
.next
next 33 p. p.next.prev
next.prev = x
remove(L, p) = x 4 p. next = x
4 4 p.
3 p. prev
prev
p. prev x x 5 = L.x size = L. size24+
==.next 4 1x.next
p.next
p. next === xxp.next
5 L. size = L. size + 1
5 5 L. 1L.size
p.prev
++1.next = p.next355 p.next.prev =+ x1
4 L. size
size
p. ==L.
prev = size
x 1
2 p.next.prev = p.prev
L.size
L. size =
= L.L.size
5 L. size 3= L.size 4 p.next = x
L.size + = 1L.size remove(L,
1
remove(L,
5 L.sizep)= L.size + 1
p) 1 p. prev .next = p. next
4 return
remove(L, p p)
Complexité O(1) en temps 11 p.et O(n)
p.prev
prev en=
.next
.next espace
2= nextpour n= opérations.
p.next
p. next.prev p. prev
Complexité O(1) en temps et O(n) en espace pour n opérations.1
2 2 p.p. next.prev
next.prev =3= L.
p.
p. size
prev
prev = L. size
33 L.L.size 4 return
size ==L.L.size
size 11 p
Structures de données 206
Vecteur

Ensemble dynamique d’objets occupant des rangs entiers successifs,


permettant la consultation, le remplacement, l’insertion et la
suppression d’éléments à des rangs arbitraires
Interface
I Elem-At-Rank(V , r ) retourne l’élément au rang r dans V .
I Replace-At-Rank(V , r , x) remplace l’élément situé au rang r par
x et retourne cet objet.
I Insert-At-Rank(V , r , x) insère l’élément x au rang r , en
augmentant le rang des objets suivants.
I Remove-At-Rank(V , r ) extrait l’élément situé au rang r et le
retire de r , en diminuant le rang des objets suivants.
I Vector-Size(V ) renvoie la taille du vecteur.
Applications : tableau dynamique, gestion des éléments d’un
menu,. . .
Implémentation : liste liée, tableau extensible. . .

Structures de données 207


Implémentation par un tableau extensible
Les éléments sont stockés dans un tableau extensible V .A de taille
initiale V .c.
En cas de dépassement, la capacité du tableau est doublée.
V .n retient le nombre de composantes.
Insertion et suppression :
Insert-At-Rank(V , r , x) Remove-At-Rank(V , r )
1 if V . n = = V . c 1 tmp = V . A[r ]
2 V .c = 2 · V .c 2 for i = r to V . n − 1
3 W = “Tableau de taille V . c” 3 V . A[i] = V . A[i + 1]
4 for i = 1 to n 4 V .n = V .n − 1
5 W [i] = V .A[i] 5 return tmp
6 V .A = W
7 for i = V . n downto r
8 V . A[i + 1] = V . A[i]
9 V . A[r ] = x
10 V . n = V . n + 1

Structures de données 208


Complexité en temps
Insert-At-Rank :
I O(n) pour une opération individuelle, où n est le nombre de
composantes du vecteur
I Θ(n2 ) pour n opérations d’insertion en début de vecteur
I Θ(n) pour n opérations d’insertion en fin de vecteur
Justification :
I Si la capacité du tableau passe de c0 à 2k c0 au cours des n
opérations, alors le coût des transferts entre tableaux s’élève à

c0 + 2c0 + . . . + 2k−1 c0 = (2k − 1)c0 .

Puisque 2k−1 c0 < n ≤ 2k c0 , ce coût est Θ(n).


I On dit que le coût amorti par opération est O(1)
I Si on avait élargi le tableau avec un incrément constant m, le coût
aurait été
k(k − 1)
c0 + (c0 + m) + (c0 + 2m) + . . . + (c0 + (k − 1)m) = kc0 + m.
2
Puisque c0 + (k − 1)m < n ≤ c0 + km, ce coût aurait donc été Θ(n2 ).
Structures de données 209
Complexité en temps

Remove-At-Rank :
I O(n) pour une opération individuelle, où n est le nombre de
composantes du vecteur
I Θ(n2 ) pour n opérations de retrait en début de vecteur
I Θ(n) pour n opérations de retrait en fin de vecteur

Remarque : Un tableau circulaire permettrait d’améliorer l’efficacité


des opérations d’ajout et de retrait en début de vecteur.

Structures de données 210


Séquence

Ensemble dynamique d’objets ordonnés combinant les propriétés


d’une liste et d’un vecteur, c’est-à-dire dont les objets sont
accessibles tant sur base de leur position absolue que relative
Interface :
I Toutes les opérations d’un vecteur
I Toutes les opérations d’une liste
I atRank(S, r ) : retourne la position de l’élément possédant le rang r .
I rankOf(S, p) : retourne le rang de l’élément situé à la position p.

Implémentation à l’aide d’une liste doublement liée (laissée comme


exercice)
I atRank,rankOf, Elem-At-Rank, Remove-At-Rank,
Replace-At-Rank : O(n), où n est le nombre d’éléments
appartenant à la séquence.
I Autres opérations : O(1).

Structures de données 211


Exemple d’implémentation d’un type abstrait en C

Liste Récursive (Rlist)


Listes Listes chaı̂nées en C Piles

Type abstrait Rlist


typedef ... Element;
typedef ... Rlist;

// renvoie une liste vide


Rlist newEmptyRlist(void);

// teste si la liste est vide (renvoie 1 si elle est vide, et 0 sinon)


int isEmpty(Rlist l);

// crée une liste dont le 1er élément est e et dont la suite est nxt;
// erreur si l’allocation mémoire de la nouvelle cellule échoue.
Rlist cons(Element e, Rlist nxt);

// renvoie le 1er élément de la Rliste l, si celle-ci n’est pas vide;


// provoque une erreur si la liste est vide.
Element car(Rlist l);

// renvoie la suite de la Rliste l, si celle-ci n’est pas vide;


// provoque une erreur si la liste est vide.
Rlist cdr(Rlist l);
Struct
Listes Listes chaı̂nées en C Piles

Exemple d’utilisation d’une liste


void printRlist(Rlist l, char *fmt);

int main(void){
Rlist rl = newEmptyRlist();
printRlist(rl,"%d");
rl = cons(1,rl);
rl = cons(2,cons(3,rl));
printRlist(rl,"%d");
printf("%d\n",car(rl));
printf("%d\n",car(cdr(rl)));
printRlist(cdr(rl),"%d");
printRlist(cdr(cdr(rl)),"%d");
rl = cdr(rl);
printRlist(rl,"%d");
printf("%d\n",car(cdr(cdr(rl))));

return 0;
}
Struct
Listes Listes chaı̂nées en C Piles

Exemple d’utilisation d’une liste


void printRlist(Rlist l, char *fmt);

int main(void){
Rlist rl = newEmptyRlist();
printRlist(rl,"%d"); ---------------> [ ]
rl = cons(1,rl);
rl = cons(2,cons(3,rl));
printRlist(rl,"%d"); ---------------> [ 2 3 1 ]
printf("%d\n",car(rl)); ------------> 2
printf("%d\n",car(cdr(rl))); -------> 3
printRlist(cdr(rl),"%d"); ----------> [ 3 1 ]
printRlist(cdr(cdr(rl)),"%d"); -----> [ 1 ]
rl = cdr(rl);
printRlist(rl,"%d"); ---------------> [ 3 1 ]
printf("%d\n",car(cdr(cdr(rl)))); --> erreur!

return 0;
}
Struct
Listes Listes chaı̂nées en C Piles

Manipulation récursive des listes


2 (principaux) types de fonctions récursives sur les listes :
B Les fonctions qui parcourent une liste :
/* type */ scanRec(Rlist l){
if( isEmpty(l) ) /* ou presque vide */
return /* valeur du cas d’arr^
et */
else
return /* une combinaison de car(l) et de scanRec( cdr(l) )*/
}

B Les fonctions qui construisent une liste :


Rlist buildRec( /* param */ ){
if( /* cas d’arr^
et */ )
return /* liste vide ou presque vide */
else
return cons( /* 1er */ , buildRec( /* liste + petite */ ) );
}

Struct
Listes Listes chaı̂nées en C Piles

Exemples de fonctions (récursives) sur les listes


int sumRlist(Rlist l); /* somme des éléments de l*/

int nbOcc(Element e, Rlist l); /* nombre d’occurrences de e dans l */

Rlist member(Element e, Rlist l); /* teste si e appartient à l */

Rlist copyRlist(Rlist l); /* copie la liste l* /

Rlist copyAndAppend(Rlist l1, Rlist l2);


/* renvoie une copie de la conatenation de l1 et l2 */

Rlist arrayToRlist(Element *tab, int lg);


/* transforme un tableau en liste */

Rlist arrayToInvRlist(Element *tab, int lg);


/* transforme un tableau en liste inversée */

Struct
Listes Listes chaı̂nées en C Piles

Fonctions qui prennent des listes en paramètre


int sumRlist(Rlist l){ /* somme des éléments de l*/
if(isEmpty(l))
return 0;
else return car(l) + sumRlist(cdr(l));
}

int nbOcc(Element e, Rlist l){ /* nombre d’occurrences de e dans l*/


if(isEmpty(l))
return 0;
else if( car(l)==e )
return 1 + nbOcc(e,cdr(l));
else return nbOcc(e,cdr(l));
}

Rlist member(Element e, Rlist l){ /* teste si e appartient à l*/


if(isEmpty(l))
return newEmptyRlist();
else if( car(l)==e )
return l;
else return member(e,cdr(l));
}
Struct
Listes Listes chaı̂nées en C Piles

Fonctions qui renvoient des listes


Rlist copyRlist(Rlist l){
if(isEmpty(l)) return newEmptyRlist();
else return cons(car(l),copyRlist(cdr(l)));
}

Rlist copyAndAppend(Rlist l1, Rlist l2){


if(isEmpty(l1)) return copyRlist(l2);
else return cons(car(l1),copyAndAppend(cdr(l1),l2));
}

Rlist arrayToRlist(Element *tab, int lg){


if(lg==0) return newEmptyRlist();
else return cons(tab[0],arrayToRlist(tab+1,lg-1));
}

Rlist arrayToInvRlist(Element *tab, int lg){


if(lg==0) return newEmptyRlist();
else return cons(tab[lg-1],arrayToInvRlist(tab,lg-1));
}

Struct
Listes Listes chaı̂nées en C Piles

Structure d’une liste chaı̂née


Une liste chaı̂née est constitué de cellules qui contiennent une
donnée et un pointeur vers la suite de la liste.
liste vide

liste contenant 1 élément

liste contenant 3 éléments

4 17 1

Struct
Listes Listes chaı̂nées en C Piles

Définition d’une liste chaı̂née d’entiers en C

typedef ... Element; // type des éléments de la liste

typedef struct cell{ // type des maillons de la liste


Element elem;
struct cell *next;
}Cell;

typedef Cell *Rlist; // type d’une liste (pointeur sur maillon)

Rlist newEmptyRlist(void){ // créé une nouvelle liste vide


return (Rlist)NULL;
}

int isEmpty(Rlist l){ // teste si la liste est vide


return (l==NULL);
}

Struct
Listes Listes chaı̂nées en C Piles

Manipulation d’une liste chaı̂née d’entiers en C


Rlist cons(Element e, Rlist nxt){ // constructeur
Cell *c;
if ( (c=(Cell *)malloc(sizeof(Cell))) == NULL ) error("...");
c->elem=e;
c->next=nxt;
return c;
}

Element car(Rlist l){ // 1er élement


if(isEmpty(l))
error("Pas d’éléments dans une liste vide!!!");
return l->elem;
}

Rlist cdr(Rlist l){ // fin de la liste


if(isEmpty(l))
error("Pas d’éléments dans une liste vide!!!");
return l->next;
}

Struct

Vous aimerez peut-être aussi