Vous êtes sur la page 1sur 34

Notes de cours

Algorithmes et structures de données


Draft v1.0, mars 2023
Rédigé par
Emmanuel CHIMI
echimi_udla@outlook.com

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 1


Contenu
1 Introduction: Rappels sur la programmation
2 Analyse asymptotique d’algorithmes
3 Types de données abstraits
4 Design d’algorithmes: Approche
5 Algorithmes de recherche
6 Algorithmes de tri
7 Listes chaînées

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 2


1 Introduction: Rappels sur la programmation

1.1 Variables
Les variables sont des désignateurs qui représentent simplement des emplacements
de stockage dans la mémoire principale utilisés pour stocker des données. De
l’espace est alloué en mémoire pour chaque variable déclarée dans le programme. La
taille de cet espace dépend du type de la variable. Par exemple, 2 octets sont alloués
pour le type integer, 4 octets sont alloués pour le type float, etc.

Programme 1.1
1 #include <stdio.h>
2
3 int main()
4 {
5 int var1,var2,var3;
6 var1=100;
7 var2=200;
8 var3=var1+var2;
9 printf("Additionner %d et %d donnera %d", var1,var2,var3);
10 return 0;
11 }

Analyse du programme
Ligne 5: La mémoire est allouée pour les variables var1, var2 et var3. Chaque fois
que nous déclarons une variable, de la mémoire est allouée pour stocker la valeur
affectée à la variable. Dans notre exemple, 2 octets sont alloués pour chacune des
variables qui sont de type int.
Lignes 6 et 7: La valeur 100 est affectée dans la variable var1 et la valeur 200 est
affectée à la variable var2.
Ligne 8: Les valeurs de var1 et var2 sont additionnées et le résultat est affecté à la
variable var3.
Ligne 9: Enfin, les trois valeurs de var1, var2 et var3 sont affichées à l'écran.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 3


Figure 1: Affichage (Output) du programme 1.1

1.2 Pointeurs
Les pointeurs ne sont rien de plus que des variables qui stockent les adresses en
mémoire d'autres variables et peuvent être utilisés pour accéder à la valeur stockée à
ces adresses. Divers opérateurs tels que *, & et [] nous permettent d'utiliser des
pointeurs.

Adresse: C'est le numéro de l'emplacement d'une cellule en mémoire. L'adresse


d'une mémoire est utilisée par l'ordinateur (système d'exploitation) pour identifier la
cellule et accéder directement aux données qui y sont stockées. Tout comme
l'adresse postale d'une maison est utilisée pour identifier une maison par l’agent
postal.

Référence: Un pointeur stocke une adresse d'un emplacement dans la mémoire.


Pour obtenir la valeur stockée à cette adresse, nous devons déréférencer1 le
pointeur, ce qui signifie que nous devons nous rendre à l'emplacement indiqué par le
pointeur (à travers l'adresse qu'il détient) et obtenir la valeur stockée là-bas.

Programme 1.2
1 int main()
2 {
3 int var;
4 int* ptr;
5 var = 10;
6 ptr = &var;
7
8 printf("Valeur stockée pour la variable var est %d\n",var);
9 printf("Valeur stockée pour la variable var est %d\n", *ptr);
10
11 printf("L’adresse de la variable var est %p \n", &var);
12 printf("L’adresse de la variable var est %p \n", ptr);
13 return 0;

1
Obtenir à partir d'un pointeur l'adresse d'un élément de données qui se trouve dans un autre emplacement.
©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 4
14 }

Analyse du programme
Ligne 3: Une variable de type integer var est déclarée.
Ligne 4: Un pointeur de nom ptr sur une variable de type int est déclaré.
Ligne 6: L'adresse de var est stockée à l’emplacement sur lequel pointe ptr.
Lignes 8 et 9: La valeur stockée dans la variable var est affichée à l'écran.
L'opérateur * est utilisé pour obtenir la valeur stockée à l'emplacement du pointeur.
Lignes 11 & 12: L'adresse mémoire de var est affichée à l'écran. L'opérateur
d'adresse & est aussi utilisé pour obtenir l'adresse d'une variable.

Figure 2: Affichage (Output) du programme 1.2

Il faut constater que "é" ne fait pas partie du jeu de caractères utilisé par le compilateur, d'où
l'affichage "stockÚe".

Points à retenir
• On déclare un pointeur en incluant un * devant le nom de la variable qui doit être
un pointeur. (Déclaration de pointeur)
• On peut obtenir la valeur stockée à l'adresse en ajoutant * avant le nom de du
pointeur. (Utilisation du pointeur)
• On peut obtenir l'adresse d'une variable en utilisant l'opérateur &.

1.3 Tableaux
Un tableau est une structure de données utilisée pour stocker plusieurs éléments de
données du même type. Toutes les données sont stockées séquentiellement, c'est-à-
dire sur des emplacements consécutifs en mémoire. La valeur stockée à n'importe
quel index est accessible directement et en temps constant. Les tableaux font partie
des structures de données statiques.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 5


Programme 1.3
1 int main()
2 {
3 int tab[10];
4 for (int i = 0; i < 10; i++)
5 {
6 tab[i] = i;
7 }
8 printArray(tab, 10);
9 }

Analyse du programme
Ligne 3: Déclare un tableau d'entiers de nom tab. Le tableau est de taille 10, ce qui
signifie qu'il peut stocker 10 entiers, les indices allant de 0 à 9.
Ligne 6: Les éléments du tableau sont accessibles à l'aide de l'opérateur d'indice [].
L'indice le plus bas est 0 et l'indice le plus élevé est (taille du tableau − 1). Les
valeurs 0 à 9 sont stockées dans le tableau aux indices respectifs de 0 à 9.
Ligne 8: Le tableau et sa taille sont passés à la fonction printArray().
Il faut remarquer que le programme 1.3 n'est pas complet parce qu'il utilise une fonction qui
n'est ni une fonction importée ni implémentée dans le programme directement.

Programme 1.4
1 void printArray(int arr[], int count)
2 {
3 printf("Valeurs stockées dans le tableau sont : ");
4 for (int i = 0; i < count; i++)
5 {
6 printf(" [ %d ] ", arr[i]);
7 }
8 }

Analyse du programme
Ligne 1: La variable de tableau arr et son nombre d'éléments sont passés comme
arguments à la fonction printArray().
Ligne 4-7: Enfin, les valeurs du tableau sont affichées à l'écran à l'aide de la fonction
printf dans une boucle.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 6


Le programme 1.4 implémente la fonction appelée dans le corps (ou programme
principal) du programme 1.3. La fusion des deux codes donne un programme
complet qui produit l'affichage ci-dessous.

Figure 3: Affichage produit par la fusion des programmes 1.3 et 1.4

Point à retenir:
1. L'indice de tableau commence toujours à partir de l'indice 0 et l'indice le plus
élevé est de (taille du tableau) − 1.
2. L'opérateur d'indice a la priorité la plus élevée si on écrit arr[2]++. Ensuite, la
valeur de arr[2] sera incrémentée.
3. Chaque élément du tableau a une adresse mémoire.

Le code suivant affiche les valeurs enregistrées dans un tableau accompagnées de


l'adresse de chaque valeur.

Programme 1.5
1 void printArrayAddress(int arr[], int count)
2 {
3 printf("Les valeurs enregistrées sont : ");
4 for (int i = 0; i < count; i++)
5 {
6 printf("La valeur: [%d] a pour adresse: [%p] \n", arr[i], arr+ i);
7 }
8 }

Analyse du programme
Ligne 6: La valeur stockée dans un tableau est affichée. L'adresse des différents
éléments du tableau est également affichée.

Figure 4: Affichage produit par la fusion des programmes 1.3 et 1.5

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 7


Point à retenir:
Des emplacements de mémoire consécutifs sont alloués aux éléments d’un tableau.

Programme 1.6
1 void printArrayUsingPointer(int arr[], int count)
2 {
3 printf("Les valeurs stockées sont : ");
4 int* ptr = arr;
5 for (int i = 0; i < count; i++)
6 {
7 printf("La valeur: [%d] a pour adresse: [%p] \n", *ptr, ptr);
8 ptr++;
9 }
10 }

La fusion de ce code Programme 1.6 avec Programme 1.3 produit exactement le


même affichage que ce qui est visible sur la Figure 4.

Analyse du programme
Ligne 4: Un pointeur sur une variable de type int est déclaré et il pointera sur le
tableau (premier élément).
Ligne 7: La valeur stockée dans le pointeur est affichée à l'écran.
Ligne 8: Le pointeur est incrémenté.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 8


1.4 Tableaux à deux dimensions
Dans les langages de programmation on peut déclarer des tableaux
unidimensionnels, bidimensionnels ou multidimensionnels. Un tableau
multidimensionnel est un tableau de tableaux.

Programme 1.7
1 int main()
2 {
3 int arr[4][2];
4 int count = 0;
5 for (int i = 0; i < 4; i++)
6 for (int j = 0; j < 2; j++)
7 arr[i][j] = count++;
8
9 print2DArray((int**)arr, 4, 2);
10 print2DArrayAddress((int**)arr, 4, 2);
11 }

1 void print2DArray(int* arr[], int row, int col)


2 {
3 for (int i = 0; i < row; i++)
4 for (int j = 0; j < col; j++)
5 printf("[ %d ]", *(arr + i * col + j );
6
7 }
1 void print2DArrayAddress(int* arr[], int row, int col)
2 {
3 for (int i = 0; i < row; i++)
4 for (int j = 0; j < col; j++)
5 printf("Value: %d, Address: %p\n", *(arr+i*col+j), (arr+i*col+j));
6 }

Analyse du programme
• Le corps du programme commence par la déclaration d'un tableau de dimension
4 x 2. Le tableau aura 4 lignes et 2 colonnes.
• Les valeurs sont enregistrées dans le tableau dans une boucle imbriquée.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 9


• Enfin, les valeurs stockées dans le tableau sont affichées à l'écran et les adresses
auxquelles sont stockées les valeurs sont affichées à l'aide des fonctions
print2DArray() et print2DarrayAddress().

Nous pouvons déclarer un tableau de pointeurs similaire à un tableau d'entiers. Un


élément d’un tel tableau stockera un pointeur.

Programme 1.8
1 void printArray(int* arr[], int count)
2 {
3 int *ptr;
4 for (int i = 0; i < count; i++)
5 {
6 ptr = arr[i];
7 printf("[ %d ]", *ptr);
8 }
9 }

1 void printArrayAddress(int* arr[], int count)


2 {
3 int *ptr;
4 for (int i = 0; i < count; i++)
5 {
6 ptr = arr[i];
7 printf("La valeur %d, se trouve à l’adresse: %p\n", *ptr, ptr);
8 }
9 }

1 int main()
2 {
3 int one = 1, two = 2, three = 3;
4 int* arr[3];
5 arr[0] = &one;
6 arr[1] = &two;
7 arr[2] = &three;
8 printArray(arr, 3);
9 printArrayAddress(arr, 3);
10 }

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 10


Analyse du programme
• Trois variables, one, two et three sont déclarées.
• Le tableau de pointeur arr est déclaré.
• L'adresse de one, two et three, respectivement, est stockée à l'intérieur du
tableau arr.
• Les fonctions PrintArray() et printArayAddress() sont utilisées pour afficher
les valeurs stockées dans le tableau.

Figure 5: Affichage produit par le programme 1.8

1.5 Tableaux: Exercices et savoir-faire


La présente section va discuter des différents algorithmes applicables aux tableaux et
sera suivie par une liste de problèmes pratiques avec des approches de solutions
similaires. Le travail du lecteur pour chaque exercice consiste à compléter le code
donné pour avoir un programme exécutable en C et qui permet de tester le code.

Exercice 1: Sommation des éléments d’un tableau


Écrire une fonction qui renvoie la somme de tous les éléments d’un tableau d'entiers
donné en argument. La taille du tableau est aussi passée comme paramètre à la
fonction.

Programme 1.9
int SumArray(int arr[], int size)
{
int total=0;
int index=0;
for(index=0;index<size;index++)
{
total = total + arr[index];
}
return total;
}

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 11


Conseil: Renseignez-vous sur la génération des nombres aléatoires (fonctions rand() et
srand()) et ajoutez un code qui remplit un tableau d'entiers avec des nombres aléatoires au
début du corps du programme. Appelez ensuite la fonction donnée pour calculer la somme
des éléments du tableau et afficher le résultat. La Figure 6 montre un exemple d'affichage
produit par un programme complété comme il est demandé ici.

Figure 6: Exemple sur l'exercice 1

Exercice 2: Recherche séquentielle


Écrire une fonction qui recherche dans un tableau si une valeur (clé) est présente
dans le tableau ou non.

Programme 1.10
int SequentialSearch(int arr[], int size, int value)
{
int i = 0;
for(i = 0; i < size; i++)
{
if(value == arr[i] )
return i;
}
return -1;
}

• Puisque nous n'avons aucune idée des données stockées dans le tableau ou si les
données ne sont pas triées, nous devons rechercher sur les éléments du tableau
de manière séquentielle, un par un.
• Si nous trouvons la valeur que nous recherchons la fonction renvoie l’index
correspondant.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 12


• Sinon, elle retourne l'indice -1, pour signifier que nous n'avons pas trouvé la
valeur que nous recherchons.

Figure 7: Exemple sur l'exercice 2

Conseil: Complétez le code du programme 1.10 de la manière sugérée à l'exercice 1. La


Figure 7 montre un exemple d'affichage ainsi obtenu.

Dans l'exemple ci-dessus, les données ne sont pas triées. Si les données sont triées,
une recherche binaire peut être effectuée. Nous examinons la position médiane à
chaque étape. Selon la clé que nous recherchons, elle est supérieure ou inférieure à
la valeur médiane. Nous continuons la recherche soit dans la partie gauche, soit dans
la partie droite du tableau. A chaque étape, on y élimine la moitié de l'espace de
recherche, ce qui rend cet algorithme très efficace.

Exercice 3: Recherche binaire


Recherche binaire ou dichotomique dans un tableau trié.

Programme 1.11
/* Binary Search Algorithm – Iterative Way */
int BinarySearch (int arr[], int size, int value)
{
int low = 0, mid;
int high = size-1;
while (low <= high)
{
mid = low + (high-low)/2; /* To avoid the overflow */
if (arr[mid] == value)
return mid
else

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 13


if (arr[mid] < value)
low = mid + 1;
else
high = mid - 1;
}
return -1;
}

Analyse du programme
• Puisque nous avons des données triées par ordre croissant/décroissant, nous
pouvons appliquer une recherche binaire plus efficace. A chaque étape, nous
réduisons de moitié notre espace de recherche.
• A chaque étape, nous comparons la valeur médiane avec la valeur que nous
recherchons. Si la valeur médiane est égale à la valeur que nous recherchons,
nous renvoyons l'index du milieu.
• Si la valeur est inférieure à la valeur médiane, nous recherchons dans la moitié
gauche du tableau.
• Si la valeur est supérieure à la valeur médiane, nous recherchons dans la moitié
droite du tableau.
• Dans l’ensemble, si nous trouvons la valeur que nous recherchons, son index est
renvoyé ou, sinon, -1 est renvoyé par la fonction.

Exercice 4: Rotation d’un tableau de k positions


Par exemple, lorsque le tableau [10,20,30,40,50,60] rote de 2 positions on obtient le
résultat [30,40,50,60,10,20].

Programme 1.12
void rotateArray(int *a,int n,int k)
{
reverseArray(a,k);
reverseArray(&a[k],n-k);
reverseArray(a,n);
}
void reverseArray(int *a,int n)
{
for(int i=0,j=n-1;i<j;i++,j--)
{
a[i]^=a[j]^=a[i]^=a[j];
}
}

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 14


1,2,3,4,5,6,7,8,9,10 => 5,6,7,8,9,10,1,2,3,4
1,2,3,4,5,6,7,8,9,10 => 4,3,2,1,10,9,8,7,6,5 => 5,6,7,8,9,10,1,2,3,4

Analyse du programme
• La rotation du tableau se fait en deux parties. Dans la première partie, nous
inversons d'abord les éléments de la première moitié du tableau, puis seconde
moitié.
• Ensuite, nous inversons tout le tableau en complétant toute la rotation.

Exercice 5:
Détermination de la plus grande somme de sous-tableau contigu.
Étant donné un tableau d'entiers positifs et négatifs, il faut trouver un sous-tableau
contigu dont la somme (somme des éléments) est maximisée.

Programme 1.13
int maxSubArraySum(int a[], int size)
{
int maxSoFar = 0, maxEndingHere = 0;
for (int i = 0; i < size; i++)
{
maxEndingHere = maxEndingHere + a[i];
if (maxEndingHere < 0)
maxEndingHere = 0;
if (maxSoFar < maxEndingHere)
maxSoFar = maxEndingHere;
}
return maxSoFar;
}

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 15


1.6 Structures
Les structures sont utilisées lorsque nous voulons traiter les données de plusieurs
types de données comme une entité unique. Le programme suivant illustre
l’utilisation des structures.

Programme 1.14
1 struct coord {
2 int x;
3 int y;
4 };
5 int main()
6 {
7 struct coord point;
8 point.x=10;
9 point.y=10;
10 printf("Valeur de la coordonnée de l’axe des x est %d \n", point.x);
11 printf("Valeur de la coordonnée de l’axe des y est %d \n", point.y);
12 printf("Taille de la structure est de %d bytes\n", sizeof(point));
13 return 0;
14 }

Affichage du programme

Figure 8: Affichage du programme 1.14

Analyse du programme
Ligne 1 - 4: Nous avons déclaré la structure de nom coord, qui regroupe deux
éléments en son sein. Les deux éléments x et y correspondant aux coordonnées de
l'axe des x et de l'axe des y.
Ligne 7: Nous avons déclaré une variable de nom point comme étant de type
struct coord.
Ligne 8 - 9: Nous avons attribué des coordonnées (10, 10), aux composants x et y
de la variable point. Les différents éléments d'une structure sont accessibles à l'aide
de l'opérateur DOT (.).

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 16


Ligne 10 - 11: Nous affichons la valeur stockée dans les éléments x et y,
respectivement, du point de type struct coord.
Ligne 12: Nous affichons la taille du point de type structure. Étant donné que la
structure se compose de plus d'un élément, la taille d'une structure est la somme des
tailles de tous les éléments à l'intérieur.

1.7 Pointeur sur une structure


On peut utiliser les pointeurs pour accéder aux différents éléments d'une structure.
Pour cela on se sert de l’opérateur ->.

Programme 1.15
1 #include<stdio.h>
2 struct student {
3 int rollNo;
4 char* firstName;
5 char* lastName;
6 };
7 int main()
8 {
9 int i=0;
10 struct student stud;
11 struct student* ptrStud;
12 ptrStud= &stud;
13 ptrStud->rollNo=1;
14 ptrStud->firstName ="john";
15 ptrStud->lastName ="smith";
16 printf("Roll No: %d Student Name: %s %s ", ptrStud->rollNo,
ptrStud->firstName, ptrStud->lastName);
17
18 return 0;
19 }

Analyse du programme
Ligne 2 - 6: Nous avons déclaré une structure student (étudiant) contenant le
numéro de rôle, le prénom et le nom d'un étudiant.
Ligne 12: Nous avons déclaré un pointeur sur la structure student.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 17


Lignes 13-15: Le pointeur ptrStud pointe sur le stud. Nous avons utilisé ptrStud pour
attribuer une valeur à la structure stud. Nous avons utilisé l'opérateur -> pour
accéder aux différents éléments de la structure sur laquelle ptrStud pointe.

Remarque: Si nous avions utilisé le nom de la variable stud pour l'allocation de


valeurs aux composants de la structure, nous aurions utilisé l’opérateur ".". Pour la
même structure lors de l'accès à l'aide du pointeur, nous utilisons l'opérateur
d'indirection "->".

Ligne 16 : Nous avons enfin affiché tous les différents éléments de la variable de la
structure stud.
Remarque: De la même manière, on peut utiliser l'opérateur -> pour accéder aux
éléments de l'Union.

1.8 Allocation dynamique de la mémoire


Dans le langage C, la mémoire dynamique est allouée à l'aide des fonctions malloc(),
calloc() et realloc(). La mémoire dynamique devant être libérée à l'aide de la fonction
free().

Fonction malloc
La définition de la fonction malloc est la suivante :
void *malloc (size_t size);

La fonction alloue un bloc mémoire de taille size octets et renvoie un pointeur vers
le bloc. Elle retourne NULL si le système n'a pas assez de mémoire ou si l’opération
d’allocation a échoué pour une autre raison.

La norme C définit void* comme un pointeur générique qui doit être casté dans le
type requis. La plupart des compilateurs C ont besoin de ce casting. Cependant, la
dernière norme ANSI C ne l'exige pas. Par exemple.
int* p = (int *) malloc (sizeof(int));

Fonction calloc
La définition de la fonction calloc est la suivante.
void *calloc (size_t num, size_t size);

Elle alloue un bloc mémoire de taille num x size octets et renvoie un pointeur sur le
bloc. La fonction retourne NULL si le système n'a pas assez de mémoire. Une chose
supplémentaire qu’elle fait c’est qu’elle initialise chaque octet à zéro.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 18


Fonction realloc
La définition de la fonction realloc est la suivante.
void *realloc (void *ptr, size_t newSize);

Cette fonction est utilisée pour modifier la taille du bloc de mémoire d'un bloc de
mémoire précédemment alloué et sur lequel pointe ptr. Elle renvoie un bloc mémoire
de taille newSize. Si la taille de bloc est augmentée, alors le contenu de l'ancien
bloc de mémoire est copié dans une région nouvellement allouée. Si le pointeur
renvoyé par la fonction est différent de l'ancien pointeur ptr, alors ptr ne pointera
plus sur un emplacement valide. Donc, en général, on ne doit pas utiliser ptr une fois
qu'il est passé à la fonction realloc() sans lui avoir attribué le nouveau pointeur
renvoyé. Si ptr est NULL, realloc fonctionne comme malloc().

Remarque: Encore une fois, on doit convertir la valeur de retour de


malloc/calloc/realloc avant de l'utiliser. int *i = (int *) malloc(size);

Fonction free
La mémoire allouée à l'aide de malloc/calloc/realloc doit être libérée à l'aide d'une
fonction free(). La syntaxe de la fonction free() est la suivante.
void free(void *pointer);

Un pointeur sur la mémoire précédemment allouée est passé à la fonction free(). La


fonction free() va restituer le bloc de mémoire alloué à la section du tas (heap).

1.9 Fonctions
Les fonctions dans le langage C et bien d’autres sont des sous-programmes tels que
introduits par la programmation structurée. Elles sont utilisées pour apporter de la
modularité au programme. En utilisant la fonction, nous pouvons diviser des tâches
complexes en tâches gérables plus petites. L'utilisation des fonctions permet
également d'éviter le code en double. Par exemple, nous pouvons définir une
fonction sum() qui prend deux entiers et renvoie leur somme. Ensuite, nous
pouvons utiliser cette fonction plusieurs fois chaque fois que nous voulons la somme
de deux entiers. Le programme suivant illustre l’appel d’une fonction.

Programme 1.16
1 #include <stdio.h>
2 /* Déclaration de la fonction */
3 int sum(int num1, int num2);
4 int main()
5 {
6 /* Déclaration de variables locales */

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 19


7 int x = 10;
8 int y = 20;
9 int result;
10 /* Appel d’une fonction pour trouver une somme */
11 result = sum(x, y);
12 printf( "La somme est de : %d\n", result );
13 return 0;
14 }
15 /* fonction renvoyant la somme de deux nombres */
16 int sum(int num1, int num2)
17 {
18 /* Déclaration d’une variable locale */
19 int result1;
20 result1= num1+num2;
21 return result1;
22 }

Affichage :
La somme est de: 30

Analyse du programme
Ligne 3: Déclaration de la fonction sum().
Ligne 11: La fonction sum est appelée à partir de main (Programme principal) en lui
passant les variables x et y avec les valeurs 10 et 20. A ce point le flux de contrôle
ira à la ligne 16.
Ligne 16: Les variables passées à la fonction sum sont copiées dans les variables
locales num1 et num2.
Ligne 20 & 21: La somme est calculée et enregistrée dans la variable result1. Et le
résultat est renvoyé. Le flux de contrôle revient à la ligne numéro 11.
Ligne 11-12: La valeur de retour de la fonction somme est enregistrée dans une
variable locale result et affichée à l'écran.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 20


1.10 Concept de pile
Une pile (stack) est une mémoire dans laquelle les valeurs sont stockées et
récupérées de manière "dernier entré, premier sorti" (Last In, First Out, LIFO). Les
données sont ajoutées à la pile à l'aide de l'opération push et les données sont
retirées de la pile à l'aide de l'opération pop.

Figure 9

1. Initialement, la pile était vide. Ensuite, nous avons ajouté la valeur 1 à la pile en
utilisant l'opérateur push(1).
2. De même, push(2) et push(3)
3. L'opération pop prend le haut de la pile. Les données de la pile sont ajoutées et
supprimées de la manière "dernier entré, premier sorti".
4. La première opération pop() va retirer 3 de la pile.
5. De même, une autre opération pop prendra 2 puis 1 de la pile.
6. Au final, la pile est vide lorsque tous les éléments sont sortis de la pile.

Pile système et appels de fonctions


Lorsque la fonction est appelée, l'exécution en cours est arrêtée et le contrôle passe
à la fonction appelée. Après la sortie/le retour de la fonction appelée, l'exécution
reprend à partir du point où l'exécution a été arrêtée.

Pour obtenir le point exact auquel l'exécution doit reprendre, l'adresse de l'instruction
suivante est stockée dans la pile. Lorsque l'appel de la fonction est terminé, l'adresse
en haut de la pile est supprimée.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 21


Programme 1.17
1 void fun2()
2 {
3 printf("fun2 line 1\n");
4 }
5
6 void fun1()
7 {
8 printf("fun1 line 1\n");
9 fun2();
10 printf("fun1 line 2\n");
11 }
12
13 int main()
14 {
15 printf("main line 1\n");
16 fun1();
17 printf("main line 2\n");
18 }

Affichage
main line 1
fun1 line 1
fun2 line 1
fun1 line 2
main line 2

Analyse du programme
Ligne 13: Chaque programme commence par la fonction main().
Ligne 15: C'est la première instruction qui sera exécutée. Et nous obtiendrons la
"main line 1" en sortie.
Ligne 16: fun1() est appelée. Avant que le contrôle ne passe à fun1(), l'instruction
suivante qui est l'adresse de la ligne 17 est stockée dans la pile système.
Ligne 6: Le contrôle passe à la fonction fun1().
Ligne 8: C'est la première déclaration à l'intérieur de fun1(), qui va afficher "fun1
ligne 1" en sortie.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 22


Ligne 9: fun2() est appelé depuis fun1(). Avant que le contrôle ne passe à fun2(),
l'adresse de l'instruction suivante qui est l'adresse de la ligne 10 est ajoutée à la pile
système.
Ligne 1: Le contrôle passe à la fonction fun2().
Ligne 3: "fun2 line 1" est affiché à l'écran.
Ligne 10: Lorsque fun2 se termine, le contrôle revient à fun1. De plus, le programme
lit l'instruction suivante dans la pile et la ligne 10 est exécutée et affiche "fun1 ligne
2" à l'écran.
Ligne 17: Lorsque fun1 se termine, le contrôle revient à la fonction principale (main).
Le programme lit l'instruction suivante à partir de la pile, la ligne numéro 17 est
exécutée et enfin la "main lin 2" est affichée à l'écran.

Points à retenir:
1. Les fonctions sont implémentées à l'aide d'une pile.
2. Lorsqu'une fonction est appelée, l'adresse de l'instruction suivante est poussée
(pushed) dans la pile.
3. Lorsqu'une fonction est terminée, l'adresse de l'exécution est retirée (poped) de
la pile.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 23


1.11 Passage de paramètre: Appel par valeur
Les arguments peuvent être passés à une fonction par le code appelant (programme
principal ou une autre fonction) à l'aide de paramètres. Par défaut, tous les
paramètres sont passés par valeur (Call by value). Cela signifie qu'une copie
séparée est créée à l'intérieur de la fonction appelée et que la variable dans la
fonction appelante reste inchangée.

Programme 1.18
1 void increment(int var)
2 {
3 var++;
4 }
5
6 int main()
7 {
8 int i = 10;
9 printf("Valeur de i avant incrémentation est : %d \n", i);
10 increment(i);
11 printf("Valeur de i après incrémentation est : %d \n", i);
12 }

Affichage
Valeur de i avant incrémentation est : 10
Valeur de i après incrémentation est : 10

Analyse du programme
Ligne 8: La variable "i" est déclarée et initialisée avec la valeur 10.
Ligne 9: Valeur de "i" est affichée.
Ligne 10: La fonction d'incrémentation est appelée. Lorsqu'une fonction est appelée
par valeur, la valeur de l'argument est copiée dans une autre variable de la fonction
appelée. Le flux de contrôle va à la ligne n° 1.
Ligne 3: La valeur de var est incrémentée de 1. Cependant, il faut se rappeler qu'il
ne s'agit que d'une copie à l'intérieur de la fonction d'incrémentation.
Ligne 11: Lorsque la fonction se termine, la valeur de "i" est toujours 10.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 24


1.12 Passage de paramètre: Appel par référence
Si la valeur de la variable passée en argument à une fonction doit être modifiée à
l'intérieur de la fonction, alors on doit utiliser l'appel par référence (Call by
reference) pour le passage. Le langage C par défaut passe par valeur. Par
conséquent, pour réaliser un appel par référence, on doit passer plutôt l'adresse
d'une variable à la fonction, ce qui va permettre à l'intérieur de la fonction appelée
d'accéder à l'emplacement de la variable en mémoire pour modifier sa valeur.

Programme 1.19
1 void increment(int *ptr)
2 {
3 (*ptr)++;
4 }
5 int main()
6 {
7 int i = 10;
8 printf("Valeur de i avant incrémentation est : %d \n", i);
9 increment(&i);
10 printf("Valeur de i après incrémentation est : %d \n", i);
11 }

Affichage
Valeur de i avant incrémentation est : 10
Valeur de i après incrémentation est : 11

Analyse du programme
Ligne 9: L'adresse de "i" est transmise à la fonction increment. Celle-ci prend, selon
la déclaration, un pointeur à int comme argument.
Ligne 3: On accède à la variable à l'adresse ptr et sa valeur est incrémentée.
Ligne 10: Enfin, la valeur incrémentée est affichée à l'écran.

Points à retenir:
L'appel par référence est implémenté indirectement en passant l'adresse d'une
variable à la fonction.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 25


1.13 Fonction récursive
Une fonction récursive est une fonction qui s'appelle elle-même, directement ou
indirectement. Une fonction récursive se compose de deux parties: la condition de
terminaison et le corps (qui incluent l'expression récursive).
1. Condition de terminaison: Une fonction récursive contient toujours une ou
plusieurs conditions de terminaison. Une condition dans laquelle la fonction
récursive traite un cas simple et ne s'appelle pas elle-même: Partie non récursive.
2. Corps (y compris l'expression ou expansion récursive): La logique principale de la
fonction récursive contenue dans le corps de la fonction. Il contient également
l'instruction d'expression de récursivité qui appelle à son tour la fonction elle-
même.

Les trois propriétés importantes de l'algorithme récursif sont:


• Un algorithme récursif doit avoir une condition de terminaison.
• Un algorithme récursif doit changer d'état, et se diriger vers la condition de
terminaison (Condition pour l'algorithme se termine).
• Un algorithme récursif doit s'appeler lui-même.

Remarque: La vitesse d'exécution d'un programme récursif est plus fiable en raison
des surcharges de la pile. Si la même tâche peut être effectuée en utilisant une
solution itérative (boucles), nous devrions préférer une solution itérative (boucles) à
la place de la récursivité pour éviter la surcharge de la pile.

Remarque: Sans condition de terminaison, la fonction récursive peut s'exécuter


indéfiniment et consommera finalement toute la mémoire de la pile (crash!).

Programme 1.20 Calcul de la factorielle N ! = N* (N-1)…. 2*1.


1 int factorial(unsigned int i)
2 {
3 /* Condition de terminaison */
4 if(i <= 1)
5 return 1;
6 /* Corps, Expression récursive */
7 return i * factorial(i - 1);
8 }

Analyse du programme
Chaque fois que la fonction factorial appelle factorial c'est avec une valeur réduite
de l'argument, i - 1. La complexité temporelle est O(N).

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 26


Programme 1.21 Affichage des nombres en base 10.
1 void printInt(unsigned int number)
2 {
3 char digit = number % 10 + '0';
4 if (number /= 10)
5 printInt(number/10);
6 printf("%c", digit);
7 }

Analyse du programme
Ligne 3: Chaque reste de la division est calculé et stocké comme chiffre dans son
équivalent digit de type char.
Ligne 4-5: Si le nombre est supérieur à 10, le nombre divisé par 10 est transmis à la
fonction printInt().
Ligne 6: Le nombre sera affiché avec le plus grand ordre d'abord, puis les chiffres
d'ordre inférieur.
La complexité temporelle est O(N).

Programme 1.22 Affichage des nombres en base 16.


1 void printInt(unsigned int number, const int base)
2 {
3 char* conversion = "0123456789ABCDEF";
4 char digit = number % base ;
5 if (number /= base)
6 printInt(number,base);
7 printf("%c", conversion[digit]);
8 }

Analyse du programme
Ligne 1: La valeur de base est également fournie avec le nombre.
Ligne 4: Le reste du nombre est calculé et stocké en chiffres.
Ligne 5-6: Si le nombre est supérieur à la base, alors le nombre divisé par la base est
passé en argument à la fonction printInt() de manière récursive.
Ligne 7: Le nombre sera affiché avec le plus grand ordre d'abord, puis les chiffres
d'ordre inférieur.
La complexité temporelle est O(N).

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 27


Programme 1.23 Conversion d’entiers aux chaînes de caractères.
1 char * intToStr(char *p, unsigned int number)
2 {
3 char digit = number % 10 + '0';
4 if (number /= 10)
5 p = intToStr(p, number);
6 *p++ = digit;
7 return (p);
8 }

Analyse du programme
Ligne 1: Le tampon de caractères p est passé en argument et le nombre qui doit être
converti en chaîne est aussi passé comme argument.
Ligne 3: Le chiffre le moins significatif du nombre est converti en caractère
correspondant.
Ligne 4-5: Si le nombre est supérieur à 10, le nombre divisé par 10 est passé en
argument à la fonction intToStr() de manière récursive.
Ligne 6: Le caractère est stocké dans le tampon p d'ordre supérieur en premier, puis
les chiffres d'ordre inférieur.
La complexité temporelle est O(N).

Tour de Hanoi
La Tour de Hanoï (appelée aussi la Tour de Brahma). On nous donne trois tiges
et un nombre N de disques, initialement tous les disques sont empilés autour de la
première tige (la plus à gauche) par ordre décroissant de taille. L'objectif est de
transférer toute la pile de disques de la première tour à la troisième tour (la plus à
droite), en déplaçant un seul disque à la fois et jamais un plus grand sur un plus
petit. Il est permis de se servir de la tige intermédiaire pour un stockage provisoire,
tout en respectant l'interdiction d'avoir un disque sur un autre plus petit.

Programme 1.24
1 void towerOfHanoi(int num, char src, char dst, char temp)
2 {
3 if (num < 1)
4 return;
5
6 towerOfHanoi(num - 1, src, temp, dst);
7 printf("\n Move disk %d from peg %c to peg %c", num, src, dst);

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 28


8 towerOfHanoi(num - 1, temp, dst, src);
9 }
1 int main()
2 {
3 int num = 4;
4 printf("The sequence of moves involved in the Tower of Hanoi are :\n");
5 towerOfHanoi(num, 'A', 'C', 'B');
6 return 0;
7 }

Figure 10 : Tour de Hanoi

Analyse du programme
Problème de TowerOfHanoi. Si nous voulons déplacer N disques de la source vers la
destination, nous déplaçons d'abord N-1 disques de la source vers la tige
intermédiaire (temp), puis déplaçons le Nième disque le plus bas de la source vers la
destination. Ensuite, les disques N-1 seront déplacés de temp vers la destination.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 29


Plus grand commun diviseur
Programme 1.25 Greatest Common Divisor (GCD).
1 int GCD(int m, int n)
2 {
3 if(m<n)
4 return (GCD(n, m));
5 if(m%n == 0)
6 return (n);
7 return(GCD(n, m%n));
8 }

Analyse du programme
L'algorithme d'Euclide est utilisé pour trouver pgcd :
PGCD(n, m) == PGCD(m, n mod m)

Nombres de Fibonacci
Programme 1.26
1 int fibonacci(int n)
2 {
3 if (n <= 1)
4 return n;
5 return fibonacci(n - 1) + fibonacci(n - 2);
6 }

Analyse du programme
Les nombres de Fibonacci sont calculés en faisant la somme des deux nombres
précédents pour créer le prochain nombre. Il y a une inefficacité dans la solution,
nous chercherons une meilleure solution dans les prochains chapitres.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 30


Détermination de toutes les permutations d’un tableau d’entiers
Programme 1.27
1 void printArray(int arr[], int count)
2 {
3 printf("Valeurs stockées dans le tableau sont : ");
4 for (int i = 0; i < count; i++)
5 {
6 printf(" %d ", arr[i]);
7 }
8 printf("\n");
9 }
10 void swap(int* arr, int x, int y){
11 int temp = arr[x];
12 arr[x] = arr[y];
13 arr[y] = temp;
14 return;
15 }
16 void permutation(int *arr, int i, int length) {
17 if (length == i){
18 printArray(arr, length);
19 return;
20 }
21 int j = i;
22 for (j = i; j < length; j++) {
23 swap(arr, i, j);
24 permutation(arr, i + 1, length);
25 swap(arr, i, j);
26 }
27 return;
28 }
29 int main()
30 {
31 int arr[5];
32 for (int i = 0; i < 5; i++)
33 {
34 arr[i] = i;

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 31


35 }
36 permutation(arr, 0, 5);
37 }

Analyse du programme
Dans la fonction de permutation, à chaque appel récursif le nombre à l'index "i" est
permuté avec tous les numéros qui sont à sa droite. Puisque le nombre est échangé
avec tous les nombres à sa droite un par un, il produira toutes les permutations
possibles.

Recherche binaire à l’aide de la récursion


Programme 1.28
1 /* Binary Search Algorithm – Recursive Way */
2 int BinarySearchRecursive(int arr[ ], int low, int high, int value)
3 {
4 if(low > high)
5 return -1;
6 int mid = low + (high-low)/2; /* To avoid the overflow */
7 if (arr[mid] == value)
8 return mid;
9 else if (arr[mid] < value)
10 return BinarySearchRecursive (arr, mid + 1, high, value);
11 else
12 return BinarySearchRecursive (arr, low, mid - 1 , value);
13 }

Analyse du programme
La solution itérative au problème de recherche binaire a été déjà vue. Examinons
maintenant la solution récursive du même problème dans cette solution également,
nous plongeons l'espace de recherche en deux et faisons la même chose que nous
avions faite dans la solution itérative.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 32


1.14 Exercices
1. Écrire un programme qui calcule la moyenne de tous les éléments d'un tableau.

2. Écrire un programme qui trouve la somme de tous les éléments d'un tableau à
deux dimensions.

3. Écrire un programme qui détermine le plus grand élément du tableau.

4. Écrire un programme qui détermine le plus petit élément du tableau.

5. Écrire un programme qui détermine le deuxième plus grand nombre dans le


tableau.

6. Écrire un programme qui affiche tous les maxima d'un tableau. (Une valeur est un
maximum si la valeur avant et après son index est plus petite qu'elle ne l'est ou
n'existe pas.)
Conseil :
• Commencez à parcourir le tableau à partir de la fin et gardez une trace de
l'élément max.
• Si nous rencontrons un élément > max, on affiche l'élément et on met à jour
max.

7. Étant donné un tableau contenant les valeurs 0 ou 1 comme éléments, écrire un


programme pour séparer 0 sur le côté gauche et 1 sur le côté droit.

8. Étant donné une liste d'intervalles, écrire un programme qui fusionne tous les
intervalles qui se chevauchent. Par exemple:
Entrée: {[1, 4], [3, 6], [8, 10]}
Sortie: {[1, 6], [8, 10]}

9. Écrire une fonction qui prend des intervalles en entrée et effectue la fusion des
intervalles qui se chevauchent.

10. Écrire un programme qui inverse un tableau sur place. (On n’utilise pas un
tableau supplémentaire, ce qui va conduire à une complexité spatiale de O (1).)
Astuce: Utiliser deux variables, début et fin. Début est initialisé à 0 tandis que
fin est initialisé à (n-1). Incrémenter début et décrémenter fin. Permuter les
valeurs stockées à arr[début] et arr[fin]. On arrête le processus lorsque début
est égal fin ou lorsque début est supérieur fin.

11. Écrire un programme qui trie un tableau ne contenant que les valeurs 0 et 1 de
sorte que tous les 0 viennent avant tous les 1.
Astuce: Utiliser deux variables, début et fin. Début est initialisé à 0 tandis que
fin est initialisé à (n-1). Incrémenter début et décrémenter fin. Permuter les
valeurs stockées à arr[début] et arr[fin] si et seulement si arr[début]==1 et

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 33


arr[fin]==0. On arrête le processus lorsque début est égal à fin ou lorsque
début est supérieur fin.

12. On donne un tableau qui ne contient que les valeurs 0, 1 et 2. Écrire un code qui
trie le tableau de sorte que tous les 0 soient avant tous les 1 et tous les 1 avant
les 2.
Astuce: Comme dans un exercice précédent avec un tableau contenant les deux
valeurs 0 et 1 uniquement, il faut penser d'abord aux 0 et aux 1 comme un seul
groupe et déplacer tous les 2 sur le côté droit. On fait ensuite un deuxième
passage sur le tableau pour trier les 0 et les 1.

13. Écrire un code qui retrouve les éléments en double dans un tableau de taille n où
la valeur de chaque élément est comprise entre 0 et n-1.
Astuce:
Approche 1: On compare chaque élément avec tous les éléments du tableau (en
utilisant deux boucles). La complexité de cette solution est de O(n2)
Approche 2: On maintient une table de hachage. On fixe la valeur de hachage à 1
si nous rencontrons l'élément pour la première fois. Lorsque nous avons à
nouveau la même valeur, nous pouvons voir que la valeur de hachage est déjà 1,
nous pouvons donc afficher cette valeur comme étant un doublon. La complexité
de cette solution est réduite à O(n), mais elle requiert de l’espace
supplémentaire.
Approche 3: Nous allons exploiter la contrainte "La valeur de tout élément est
comprise entre 0 et n-1". On peut prendre un tableau arr[] de taille n et définir
tous les éléments sur 0. Chaque fois que nous obtenons une valeur, par exemple
val1. On va incrémenter la valeur à l'index arr[val1] de 1. À la fin, on peut
parcourir le tableau arr et afficher les valeurs répétées (arr[] > 0). La complexité
spatiale supplémentaire sera de O(n), ce qui sera inférieure à la même complexité
dans l'approche de la table de hachage.

14. Écrire une fonction qui permet de trouver la somme des chiffres da chaque
nombre de type entier. Par exemple: Pour le nombre 1984 donné en entrée, la
fonction renvoie 32 (1+9+8+4).

15. Écrire une fonction qui permet de calculer Sum(N) = 1+2+3+…+N.

©Emmanuel Chimi, 03/2023 | Algorithmes et structures de données 34

Vous aimerez peut-être aussi