DS NOM, Prénom : Brillant Dany NOTE : 20/20 Youhou !! DS d’informatique n°2 (Corrigé) EXERCICE 1 : FONCTION MYSTERE Soit le programme suivant : def tri_mystere(L): # L une liste d’entiers et n = len(L) for i in range(1, n): j = i x = L[i] while 0 < j and x < L[j-1]: L[j] = L[j-1] j = j-1 de flottants L[j] = x Question 1 : (2 pt) Lors de l'appel tri_mystere(L), lorsque L est la liste [5, 2, 3, 1, 4], donner le contenu de la liste L à la fin de chaque itération de la boucle for. A la fin de l’itération Contenu de L 0 (avant les itérations) [5, 2, 3, 1, 4] 1 [2, 5, 3, 1, 4] 2 [2, 3, 5, 1, 4] 3 [1, 2, 3, 5, 4] 4 [1, 2, 3, 4, 5] Question 2 : (1 pt) Quel type de tri est utilisé dans la fonction tri_mystere(L) ? Justifier votre réponse. C’est du tri par insertion, car à chaque itération de la boucle for, l’élément d’indice i , est inséré au bon endroit dans la sous-liste L[0:i] . Question 3 : (3 pt) Montrer, par l’invariant de boucle, que la fonction permet bien de trier la liste. Invariant de boucle : Inv(i) = « à la fin d’une l’itération de la boucle for, la sous-liste L[0:i+1] est trié ». Démo : • • Initialisation : Avant de rentrer dans la boucle, la sous-liste L[0:1]= L[0] est forcément triée car composée d’un seul élément. Hérédité : supposons Inv(i) vrai. A l’itération suivante, la boucle while ne s’arrête que si Lycée Buffon 1/6 DS 2 (1h15) § x >= L[j-1]: et x s’insère entre deux éléments inférieurs et supérieurs, donc bien triée § soit jatteint 0 et x est placé en tout début de liste car inférieur à tous les éléments qui le précèdent. Donc la sous-liste L[0:i+2]est triée, donc Inv(i+1) vrai. • • Terminaison : Un boucle for s’arrête forcement, et la boucle while s’arrête également car j diminue à chaque passage, et donc va forcément atteindre 0, dans le pire des cas. Bonne terminaison : Les itérations s’arrêtent pour i=n-1, donc Inv(n-1) vrai, soit la sous-liste L[0:n] , c’est à dire la liste L est triée, CQFD. Question 4 : (3 pt) Soit n, le nombre d’éléments dans la liste L. Donner, en le démontrant, et en précisant à quelle forme de L cela correspond, la complexité algorithmique de la fonction tri_mystere(L) dans le meilleur et le pire des cas : Meilleur des cas : - L est déjà triée Complexité (à démontrer): Prennons comme critère de calcul de la complexité, le nombre de comparaisons. Chaque test de la boucle while est faux, donc il n’y a globalement qu’une boucle for, il y a donc n-1 itérations, donc 2(n-1) comparaisons, la complexité algorithmique est donc en O(n). Pire des cas : - L est triée par ordre décroissant Complexité (à démontrer): Chaque test de la boucle while est vrai, jusqu’à ce que j atteigne 0. Pour i= 1 : 1 passage dans les instructions de la boucle while (2 comparaisons) Pour i= 2 : 2 passages dans les instructions de la boucle while (4 comparaisons) … Pour i = n-1 : n-1 passage dans les instructions de la boucle while (2(n-1) comparaisons) Donc le nombre de passage total, noté Nit , dans les instructions de la boucle while est de : !!! 𝑁!" = 1 + 2 + ⋯ + 𝑛 − 1 = 𝑖= !!! (𝑛 − 1)𝑛 2 Il y a donc (𝑛 − 1)𝑛 comparaisons, donc une complexité algorithmique en O(n2). Question 5 (question de cours) (3 pt): Pour les trois algorithmes de tri suivants, donner la complexité temporelle pour les deux types de listes proposées. Lycée Buffon 2/6 DS 2 (1h15) Tri par Insertion Tri Rapide Tri fusion Liste quasiment triée O(n) O(n2) O(n.log(n)) Liste quelconque O(n2). O(n.log(n)) ou O(n2) O(n.log(n)) EXERCICE 2 : OPTIMISATION DU TRI PAR INSERTION On peut optimiser l’algorithme du tri par insertion, en recherchant par dichotomie la position d’insertion. Question 6 : (4 pt) Écrire une fonction ajouter(L,x) qui étant donnés une liste triée L et un élément x, modifie L en insérant x dans la liste L à la bonne place (de sorte d’obtenir une liste triée dans l’ordre croissant). On recherchera la position d’insertion par dichotomie. Exemple : l’instruction ajouter([1,3,4],2) doit renvoyer la liste [1,2,3,4] Remarque : on pourra utiliser la méthode L.insert(i,x) qui permet d’insérer l’élément x dans la liste L dans la position i. def ajouter(L,x): a = 0 b = len(L) if x>L[-1]: L.append(x) else: while a<b: m=(a+b)//2 if L[m]<=x: a=m+1 else: b=m L.insert(a,x) return L Question 7 : (1 pt) Estimer, en justifiant brièvement, la complexité algorithmique de la fonction ajouter() si on suppose que la fonction insert() a une complexité algorithmique en O(1). C’est une algorithme de dichotomie, donc l’intervalle de recherche est divisé par 2 à chaque itération, ce qui est typique d’une complexité algorithmique en O(log2(n)) ou O(log(n)). A savoir redémontrer ! On donne la fonction Tri_Insertion(L) qui permet de trier une liste L par l’algorithme du tri par insertion et qui utilise la fonction ajouter(L,x) définie précédemment. def Tri_Insertion(L): for i in range(1,len(L)): SL = L[0:i] # sous-liste triee x=L.pop(i) ajouter(SL,x) L[0:i] = SL return L Lycée Buffon 3/6 DS 2 (1h15) Question 8 : (2 pt) Expliquer simplement pourquoi, si n est la longueur de la liste, alors la complexité de cet algorithme est en O(n log(n)). On précise que : Ø les méthodes "classiques" des listes comme pop, append... fonctionnent en temps constant. Ø on n'attend pas une démonstration mathématique rigoureuse, mais il faut tout de même justifier votre réponse. En déduire l'avantage et l'inconvénient de cette version du tri par insertion par rapport à l’algorithme « classique » vu en cours. La boucle for effectue n itérations. A chacune d’elle, on appelle la fonction ajouter(L,x) sur une sous-liste de longueur i ; les autres opérations s'effectuent en temps constant. Le coût du tour est donc en O(log(i)) (cf. question 6). Le coût total de la fonction est donc en O(log(1) + log(2) + ... + log(n)) soit en O(n log(n)). Sans entrer dans les détails mathématiques, cela peut se justifier simplement : • soit par une comparaison série/intégrale, une primitive de ln étant la fonction x -­‐> xln(x) -­‐ x, • soit en remarquant que log(1) + ... + log(n) = log(n!), si l'on sait que n! se comporte comme nn (d'après la formule de Stirling par exemple). Avantage : on évite le "pire cas" en O(n2) de l’algorithme classique (liste triée à l'envers). Inconvénient : on évite le "meilleur cas" en O(n) de l'algorithme classique (liste triée). Question 9 : (3 pt)Donner une version récursive, notée Tri_Insertion_REC(L) de la fonction Tri_Insertion(L). def Tri_Insertion_REC(L): # recursive if len(L)==1: return L else: x=L.pop() Tri_Insertion_REC(L) ajouter(L,x) return L Lycée Buffon 4/6 DS 2 (1h15) EXERCICE 3 : TRI PAR SELECTION L'algorithme du tri par sélection est le suivant : Ø on recherche le plus petit élément de la liste à trier, Ø on l'échange avec le premier élément, Ø puis on répète le même procédé sur la "sous-liste" restante. Question 10 : (3 pt)Ecrire une fonction Tri_Selection(L) qui utilise l’algorithme défini. def tri_selection(L): n = len(L) # boucle sur l'elt a placer (sauf le dernier) for i in range(n): # recherche du minimum de la sous-liste Lmin,jmin = L[i],i for j in range(i+1,n): if L[j] < Lmin: Lmin,jmin = L[j],j # echange L[jmin],L[i] = L[i],L[jmin] return L Question 11 : (1,5pt) Estimer la complexité de cette fonction et comparer son efficacité par rapport aux algorithmes de tri évoqués de la question 5. Soit n la taille de la liste. La fonction possède deux boucles imbriquées de longueur n, dont le contenu s'effectue en temps constant. On a donc une complexité en O(n2). Cet algorithme est moins efficace que les algorithmes évoqués dans l’exercice 2 ; au mieux, il est aussi efficace. Si la liste est quasiment triée, même le tri par insertion est plus efficace ! On pourrait donc aussi l’appeler le tri_naze, le tri_mou, ou le tri_qu_est_pas_efficace. Lycée Buffon 5/6 DS 2 (1h15) BROUILLON : Même pas besoin. Lycée Buffon 6/6