{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Algorithmes de tri (1) : tri par insertion\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Préambule\n", "Pourquoi étudier des algorithmes de tri ? \n", "Autant ne pas le cacher, ces algorithmes sont déjà implémentés (quelque soit le langage) dans des fonctions très performantes. \n", "\n", "En Python, on utilise la fonction `sort()`\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tab = [4, 8, 1, 2, 6]\n", "tab.sort()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tab" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Le meilleur de nos futurs algorithmes de tri sera moins efficace que celui de cette fonction `sort()`... \n", "Malgré cela, il est essentiel de se confronter à l'élaboration manuelle d'un algorithme de tri. \n", "Le tri par insertion est le premier des deux algorithmes de tri que nous allons étudier (nous étudierons aussi le tri par sélection). \n", "Ces deux algorithmes ont pour particularité de :\n", "- ne pas nécessiter la création d'une nouvelle liste. Ils modifient la liste à trier sur place.\n", "- ne pas faire intervenir de fonctions complexes (comme la recherche d'un minimum par exemple)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Tri par insertion (version la plus intuitive)\n", "Considérons la liste `[7, 5, 2, 8, 1, 4]` \n", "Voici le fonctionnement de l'algorithme : \n", "![](data/insertion1.gif)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Explications :**\n", "- on traite successivement toutes les valeurs à trier, en commençant par celle en deuxième position.\n", "- Traitement : tant que la valeur à traiter est inférieure à celle située à sa gauche, on échange ces deux valeurs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Codage de l'algorithme\n", "\n", "**algorithme :** \n", "\n", "Pour toutes les valeurs, en commençant par la deuxième :\n", "- tant qu'on trouve à gauche une valeur supérieure et qu'on n'est pas revenu à la première valeur, on échange ces deux valeurs." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def tri(lst):\n", " '''trie la liste lst donnée en paramètre'''\n", " for i in range(1, len(lst)):\n", " k = i\n", " while k > 0 and lst[k-1] > lst[k] :\n", " lst[k], lst[k-1] = lst[k-1], lst[k] \n", " k = k -1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Vérification :*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a = [7, 5, 2, 8, 1, 4]\n", "tri(a)\n", "print(a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Tri par insertion (version optimisée)\n", "Observez l'animation ci-dessous et comparer avec la version initiale. \n", "![](data/insertion2.gif)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Codage de l'algorithme\n", "\n", "Voici l'algorithme optimisé :" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def tri(lst) :\n", " for k in range(1,len(lst)):\n", " cle = lst[k]\n", " i = k-1\n", " while i>=0 and lst[i] > cle :\n", " lst[i+1] = lst[i]\n", " i = i -1\n", " lst[i+1] = cle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Vérification :*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a = [7, 5, 2, 8, 1, 4]\n", "tri(a)\n", "print(a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Complexité de l'algorithme\n", "\n", "Pour pouvoir utiliser la fonction `%timeit`, nous allons modifier légèrement notre algorithme de tri : comme la fonction `%timeit` effectue un grand nombre d'appel à la fonction `tri()`, la liste serait triée dès le premier appel et les autres appels essaieraient donc de tri une liste *déjà triée*. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def tri(L) :\n", " l = list(L) # pour ne pas modifier la liste passée en argument.\n", " for k ..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a = [k for k in range(100,0,-1)] #on se place dans le pire des cas : une liste triée dans l'ordre décroissant" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b = [k for k in range(200,0,-1)] #on se place dans le pire des cas : une liste triée dans l'ordre décroissant" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%timeit tri(a)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%timeit tri(b)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "En comparant les temps de tri des listes `a` et `b`, que pouvez-vous supposer sur la complexité du tri par insertion ?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Une liste à trier 2 fois plus longue prend 4 fois plus de temps : l'algorithme semble de complexité **quadratique**." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Démonstration\n", "Dénombrons le nombre d'opérations dans le pire des cas, pour une liste de taille $n$.\n", "- boucle for : elle s'exécute $n-1$ fois.\n", "- boucle while : dans le pire des cas, elle exécute d'abord 1 opération, puis 2, puis 3... jusqu'à $n-1$. Or \n", "$$1+2+3+\\dots+n-1=\\dfrac{n \\times (n-1)}{2}$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Si la liste est déjà triée, on ne rentre jamais dans la boucle `while` : le nombre d'opérations est dans ce cas égal à $n-1$, ce qui caractérise une complexité linéaire." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Résumé de la complexité \n", "- dans le meilleur des cas (liste déjà triée) : complexité **linéaire**\n", "- dans le pire des cas (liste triée dans l'ordre décroissant) : complexité **quadratique**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Preuve de la terminaison de l'algorithme\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Est-on sûr que notre algorithme va s'arrêter ? \n", "Le programme est constitué d'une boucle `while` imbriquée dans une boucle `for`. Seule la boucle `while` peut provoquer une non-terminaison de l'algorithme. Observons donc ses conditions de sortie : \n", "\n", "``` while i>=0 and l[i] > cle : ```\n", "\n", "La condition `l[i] > cle` ne peut pas être rendue fausse avec certitude. \n", "Par contre, la condition `i>=0` sera fausse dès que la variable `i` deviendra négative. Or la ligne \n", "`i = i - 1` nous assure que la variable `i` diminuera à chaque tour de boucle. La condition `i>=0` deviendra alors forcément fausse au bout d'un certain temps.\n", "\n", "Nous avonc donc prouvé la **terminaison** de l'algorithme.\n", "\n", "On appelle la valeur `i` un **variant de boucle**. C'est une notion théorique (ici illustrée de manière simple par `i` qui permet de prouver la bonne sortie d'une boucle et donc la terminaison d'un algorithme.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Preuve de la correction de l'algorithme\n", "Les preuves de correction sont des preuves théoriques. La preuve ici s'appuie sur le concept mathématique de **récurrence**. \n", "Principe du raisonnement par récurrence : \n", "une propriété $P(n)$ est vraie si :\n", "- $P(0)$ (par exemple) est vraie\n", "- Pour tout entier naturel $n$, si $P(n)$ est vraie alors $P(n+1)$ est vraie.\n", "\n", "Ici, la propriété serait : « Quand $k$ varie entre 0 et `longueur(liste) -1`, la sous-liste de longueur $k$ est triée dans l'ordre croissant.» On appelle cette propriété un **invariant de boucle** (sous-entendu : elle est vraie pour chaque boucle)\n", "\n", "- quand $k$ vaut 0, on place le minimum de la liste en l[0], la sous-liste l[0] est donc triée.\n", "- si la sous-liste de $k$ éléments est triée, l'algorithme rajoute en dernière position de la liste le minimum de la sous-liste restante, dont tous les éléments sont supérieurs au maximum de la sous-liste de $k$ éléments. La sous-liste de $k+1$ éléments est donc aussi triée.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "---\n", "## Bibliographie\n", "- Wikipedia, https://en.wikipedia.org/wiki/Sorting_algorithm\n", "\n", "\n", "\n", "---\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.6" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }