« Introduction à la complexité et sa formalisation » : différence entre les versions
Aucun résumé des modifications |
Aucun résumé des modifications |
||
| Ligne 5 : | Ligne 5 : | ||
== Introduction == |
== Introduction == |
||
Pour introduire notre sujet nous allons partir d'un exemple, |
Pour introduire notre sujet nous allons partir d'un exemple, imaginez que vous vouliez trier des fichiers dans votre ordinateur (dans notre cas des fichiers numérotés et par exemple nous souhaitons les trier par ordre croissant). Pour effectuer cette tache vous pouvez donc créer un programme qui le ferra pour vous. Malheureusement, votre programme prend beaucoup de temps à s'exécuter. Pour résoudre ce problème on se tourne vers la complexité. |
||
== Complexité == |
== Complexité == |
||
La complexité permet de mesurer <b>l'efficacité des fonctions</b>. Elle dépend de différents critères |
La complexité permet de mesurer <b>l'efficacité des fonctions</b>. Elle dépend de différents critères tels que : |
||
* le temps de <b>calcul séquentiel</b> , c'est |
* le temps de <b>calcul séquentiel</b> , c'est à dire le temps qui est mis pour exécuter toutes les instructions une par une. |
||
* le temps de <b>calcul parallèle</b> , c'est |
* le temps de <b>calcul parallèle</b> , c'est à dire le temps qui est mis pour exécuter toutes les instructions avec plusieurs processeurs donc avec des opérations exécutées en simultané. |
||
* <b>l' |
* <b>l'espace disque</b> nécessaire à ces opérations qui peuvent donc rallonger le temps d'exécution d'une opération. |
||
* ainsi que |
* ainsi que d'autres paramètres… |
||
Pour illustrer cela nous allons utiliser deux fonctions « identité » comme exemple. |
Pour illustrer cela nous allons utiliser deux fonctions « identité » comme exemple. |
||
On commence donc par définir un type d'entier |
On commence donc par définir un type d'entier unaire. |
||
<pre> |
<pre> |
||
| Ligne 48 : | Ligne 48 : | ||
On s'intéresse au nombre d'étape de calcul en fonction de la taille de l'argument. |
On s'intéresse au nombre d'étape de calcul en fonction de la taille de l'argument. |
||
On fait souvent des hypothèses simplificatrices pour calculer une complexité, |
On fait souvent des hypothèses simplificatrices pour calculer une complexité. Toutefois, celles-ci doivent rester vraies. |
||
Ici nous comptons le nombre de tests <b style="color:#ff8308">match</b> ... <b style="color:#ff8308">with</b>. |
Ici nous comptons le nombre de tests <b style="color:#ff8308">match</b> ... <b style="color:#ff8308">with</b>. |
||
| Ligne 57 : | Ligne 57 : | ||
<b> Notation <em>O</em> </b> |
<b> Notation <em>O</em> </b> |
||
Nous allons |
Nous allons ensuite introduire la notation O (dite « grand o »). |
||
Pour cela fixons des types <em>A</em> et <em>B</em> et une « manière de compter ». |
Pour cela, fixons des types <em>A</em> et <em>B</em> et une « manière de compter ». |
||
On |
On définit donc une fonction f: A -> B. |
||
Cette fonction f nous permet de prendre la taille de a et de trouver le temps avec f(a) soit : |
Cette fonction f nous permet de prendre la taille de a et de trouver le temps avec f(a) soit : |
||
| Ligne 78 : | Ligne 78 : | ||
* Complexité de id2 : O(n) |
* Complexité de id2 : O(n) |
||
Si l'on souhaite développer |
Si l'on souhaite développer cette notation, nous pouvons prendre : |
||
f ∈ O(g) qui signifie f(n) ≤ c * g(n) |
f ∈ O(g) qui signifie f(n) ≤ c * g(n) |
||
| Ligne 91 : | Ligne 91 : | ||
== Langages utilisés == |
== Langages utilisés == |
||
Pour créer nos différentes fonctions et comprendre leur complexité nous avons utiliser deux |
Pour créer nos différentes fonctions et comprendre leur complexité nous avons utiliser deux langages différents : |
||
* Le langage OCaml, un langage fonctionnel et fortement typé. |
* Le langage OCaml, un langage fonctionnel et fortement typé. |
||
* Le langage Agda, qui est un langage fonctionnel, typé et qui sert en partie programmer des preuves mathématiques. |
* Le langage Agda, qui est un langage fonctionnel, typé et qui sert en partie à programmer des preuves mathématiques. |
||
* On utilise aussi une extension d'Agda qui se nomme Calf et qui sert |
* On utilise aussi une extension d'Agda qui se nomme Calf et qui sert à calculer la complexité d'une fonction sous forme de coûts. |
||
== Tri en OCaml == |
== Tri en OCaml == |
||
Nous allons donc écrire différentes |
Nous allons donc écrire différentes fonctions de tri dans le langage OCaml. |
||
=== Tri par insertion en OCaml === |
=== Tri par insertion en OCaml === |
||
| Ligne 107 : | Ligne 107 : | ||
* Prendre en argument une liste à trier. |
* Prendre en argument une liste à trier. |
||
* |
* Prendre une liste vide. |
||
* Prendre un élément de la liste non |
* Prendre un élément de la liste non trié. |
||
* Insérer |
* Insérer cet élément au bon endroit dans la liste vide (il faut donc parcourir la liste). |
||
* Puis on recommence le processus de sorte |
* Puis on recommence le processus de sorte à trier cette liste et à renvoyer celle-ci. |
||
Il faut donc avoir en tête que nous manipulons bien deux listes différentes pendant l'exécution. |
Il faut donc avoir en tête que nous manipulons bien deux listes différentes pendant l'exécution. |
||
| Ligne 125 : | Ligne 125 : | ||
</pre> |
</pre> |
||
Cette fonction est une fonction récursive qui s'appelle donc elle |
Cette fonction est une fonction récursive qui s'appelle donc elle-même. On peut donc voir que si la liste fournie est vide, alors elle renvoie l'élément <b>e</b>, sinon elle insert un élément au bon endroit. |
||
| Ligne 136 : | Ligne 136 : | ||
</pre> |
</pre> |
||
Cette fonction est aussi une fonction récursive qui va appeler notre fonction d'insertion pour trier la liste |
Cette fonction est aussi une fonction récursive qui va appeler notre fonction d'insertion pour trier la liste fournie en argument. |
||
| Ligne 142 : | Ligne 142 : | ||
On trouve que pour chaque élément de la liste l' le coût augmente de 1. |
On trouve que pour chaque élément de la liste l' le coût augmente de 1. |
||
On se retrouve donc avec une complexité |
On se retrouve donc avec une complexité totale qui correspond a la somme des éléments de 1 à n-1 soit (n(n-1))/2 ce qui représente une complexité O(n²). |
||
=== Tri par fusion en OCaml === |
=== Tri par fusion en OCaml === |
||
Nous nous intéressons donc maintenant |
Nous nous intéressons donc maintenant à une fonction de tri par fusion. Le principe de cette fonction est le suivant : |
||
* On part d'une liste à trier. |
* On part d'une liste à trier. |
||
* On effectue une suite de |
* On effectue une suite de divisions récursives en deux parties jusqu'à obtenir des listes de taille 1. |
||
* On |
* On trie ensuite chaque liste. |
||
* Puis on les |
* Puis on les fusionne entre elles tout en les triant. |
||
== Complexité en Agda/Calf == |
== Complexité en Agda/Calf == |
||
Version du 10 mai 2025 à 15:05
Étudiant : ALBRECHT Maël
Tuteur : HIRSCHOWITZ Tom
Introduction
Pour introduire notre sujet nous allons partir d'un exemple, imaginez que vous vouliez trier des fichiers dans votre ordinateur (dans notre cas des fichiers numérotés et par exemple nous souhaitons les trier par ordre croissant). Pour effectuer cette tache vous pouvez donc créer un programme qui le ferra pour vous. Malheureusement, votre programme prend beaucoup de temps à s'exécuter. Pour résoudre ce problème on se tourne vers la complexité.
Complexité
La complexité permet de mesurer l'efficacité des fonctions. Elle dépend de différents critères tels que :
- le temps de calcul séquentiel , c'est à dire le temps qui est mis pour exécuter toutes les instructions une par une.
- le temps de calcul parallèle , c'est à dire le temps qui est mis pour exécuter toutes les instructions avec plusieurs processeurs donc avec des opérations exécutées en simultané.
- l'espace disque nécessaire à ces opérations qui peuvent donc rallonger le temps d'exécution d'une opération.
- ainsi que d'autres paramètres…
Pour illustrer cela nous allons utiliser deux fonctions « identité » comme exemple.
On commence donc par définir un type d'entier unaire.
1. type nat = Z | S of nat 2. let one = S Z 3. let two = S (S Z) (* etc.. *)
On programme ensuite une fonction identité simple :
1. let id1 n = n
Et une fonction identité qui inspecte son argument, puis le reconstruit :
1. let rec id2 n = match n with 2. | Z -> Z 3. | S p -> S (id2 p)
Complexité en temps
On s'intéresse au nombre d'étape de calcul en fonction de la taille de l'argument.
On fait souvent des hypothèses simplificatrices pour calculer une complexité. Toutefois, celles-ci doivent rester vraies. Ici nous comptons le nombre de tests match ... with.
- Pour id1 : 0.
- Pour id2 : autant que la taille de l'argument.
Notation O
Nous allons ensuite introduire la notation O (dite « grand o »).
Pour cela, fixons des types A et B et une « manière de compter ».
On définit donc une fonction f: A -> B.
Cette fonction f nous permet de prendre la taille de a et de trouver le temps avec f(a) soit :
taille(a) -> temps(f(a))
Cela nous permet donc de trouver :
id1: n -> 1
id2: n -> n
Pour présenter cela nous utilisons la notation suivante :
- Complexité de id1 : O(1)
- Complexité de id2 : O(n)
Si l'on souhaite développer cette notation, nous pouvons prendre :
f ∈ O(g) qui signifie f(n) ≤ c * g(n)
Avec :
- c une constante bien choisie
- et pour tout n ≥ n_0
Langages utilisés
Pour créer nos différentes fonctions et comprendre leur complexité nous avons utiliser deux langages différents :
- Le langage OCaml, un langage fonctionnel et fortement typé.
- Le langage Agda, qui est un langage fonctionnel, typé et qui sert en partie à programmer des preuves mathématiques.
- On utilise aussi une extension d'Agda qui se nomme Calf et qui sert à calculer la complexité d'une fonction sous forme de coûts.
Tri en OCaml
Nous allons donc écrire différentes fonctions de tri dans le langage OCaml.
Tri par insertion en OCaml
Tout d'abord, le tri par insertion consiste :
- Prendre en argument une liste à trier.
- Prendre une liste vide.
- Prendre un élément de la liste non trié.
- Insérer cet élément au bon endroit dans la liste vide (il faut donc parcourir la liste).
- Puis on recommence le processus de sorte à trier cette liste et à renvoyer celle-ci.
Il faut donc avoir en tête que nous manipulons bien deux listes différentes pendant l'exécution.
Pour faire cela en OCaml nous avons besoin de la fonction d'insertion :
let rec insert e l = match l with [] -> [e] | x :: l' -> if e <= x then e :: l else x :: (insert e l')
Cette fonction est une fonction récursive qui s'appelle donc elle-même. On peut donc voir que si la liste fournie est vide, alors elle renvoie l'élément e, sinon elle insert un élément au bon endroit.
Nous avons ensuite besoin de la fonction de tri :
let rec trie l = match l with [] -> [] | e::l' -> insert e (trie l')
Cette fonction est aussi une fonction récursive qui va appeler notre fonction d'insertion pour trier la liste fournie en argument.
Pour la complexité de cette fonction nous allons compter le maximum de tests que peut faire trie l.
On trouve que pour chaque élément de la liste l' le coût augmente de 1.
On se retrouve donc avec une complexité totale qui correspond a la somme des éléments de 1 à n-1 soit (n(n-1))/2 ce qui représente une complexité O(n²).
Tri par fusion en OCaml
Nous nous intéressons donc maintenant à une fonction de tri par fusion. Le principe de cette fonction est le suivant :
- On part d'une liste à trier.
- On effectue une suite de divisions récursives en deux parties jusqu'à obtenir des listes de taille 1.
- On trie ensuite chaque liste.
- Puis on les fusionne entre elles tout en les triant.
