Lycée Louis-Le-Grand, Paris MPSI 4 Informatique pour tous A. Troesch, J.-P. Becirspahic 2013/2014 Corrigé du TP n o 5 Exercice 1. recherche d un élément dans un tableau non trié 1. Le principe de la recherche d un élément dans un tableau non trié est simple : un parcours séquentiel jusqu à trouver cet élément ou aboutir à la fin de la liste. def est_present(a, lst): Notons toutefois que cette fonction n est guère utile en pratique car si x est un objet python et seq une séquence (c est à dire un objet de type list, tuple, range, str et quelques autres), l expression «x in seq» répond à la question posée. Par exemple : >>> 3 in [1, 2, 3, 4, 5] True >>> 5 in range(2, 10, 2) False >>> 'y' in 'asymptote' True 2. Si on souhaite retourner l indice minimum de la liste correspondant à l élément recherché, se pose la question de savoir quoi faire en cas d absence de cet élément. Une solution simple consiste à afficher à l écran un message signalant le problème ou à retourner la valeur None : def indice(a, lst): print("l'élément recherché n'est pas dans la liste") mais il existe une solution plus élégante consistant à déclencher une exception. Une exception se déclenche et interrompt le script en cours dès lors qu un évènement inattendu se produit : erreur arithmétique, variable non définie, erreur de typage, etc. Par exemple : >>> 1 + 2 / 0 ZeroDivisionError: division by zero >>> 1 + a NameError: name 'a' is not defined >>> 2 + '3' TypeError: unsupported operand type(s) for +: 'int' and 'str' Il est possible de déclencher une exception à l aide de l instruction raise ; c est ce que nous allons faire dans le cas où x ne se trouve pas dans lst : def indice(a, lst): raise ValueError('élément non trouvé') Par Exemple : >>> indice(5, [1,2,3,4,6,7,8,9]) ValueError: élément non trouvé L intérêt de cette démarche est qu il est possible de rattraper une exception à l aide de la syntaxe try... except. Illustrons ceci en revenant un instant sur la première question de cet exercice, et utilisons cette fois une boucle conditionnelle pour parcourir les éléments de la liste : page 1
def est_present(a, lst): while lst[k]!= a: Cette version n est pas satisfaisante : elle renvoie bien True lorsque a est dans la liste, mais déclenche l exception IndexError dans le cas contraire (dès lors que k >= len(lst)). Nous allons la corriger en rattrapant cette exception pour renvoyer False à la place : def est_present(a, lst): try: while lst[k]!= a: except IndexError: Notez enfin qu il est possible de créer ses propres exceptions si celles qui sont prédéfinies ne suffisent pas (consulter l aide en ligne pour plus d information sur les exceptions). L exercice 2 nous donnera une autre occasion d illustrer l intérêt de ce mécanisme de rattrapage d une exception. 3. Calculer le nombre d occurrences d un élément dans une liste ne présente aucune difficulté : def occurrences(a, lst): 4. Nous allons maintenant simuler la recherche d un élément de l intervalle 0, N 1 dans une liste de 10000 éléments du même intervalle, en réalisant 1000 fois l expérience. Pour ce faire, nous allons modifier la fonction de la première question de manière à dénombrer le nombre d éléments testés : def est_present(a, lst): Nous allons utiliser la fonction randint du module numpy.random qui permet de créer un tableau pseudo-aléatoire d une taille donnée : from numpy.random import randint La fonction permettant de réaliser l expérience se définit alors de la façon suivante : def moyenne(n): for k in range(1000): s += est_present(randint(n), randint(n, size = 10000)) print("moyenne des comparaisons effectuées :", s/1000) Quelques essais avec différentes valeurs de N : >>> moyenne(10) moyenne des comparaisons effectuées : 9.861 >>> moyenne(20) moyenne des comparaisons effectuées : 19.788 >>> moyenne(50) moyenne des comparaisons effectuées : 50.183 >>> moyenne(100) moyenne des comparaisons effectuées : 98.527 On constate un coût en moyenne proche de N, ce qui n est pas étonnant. En effet, la probabilité que l élément recherché soit trouvé au bout de k comparaisons est égal à 1 N ( 1 1 N ) k 1 et la probabilité qu il ne se trouve pas page 2
( dans la liste égal à 1 1 ) n donc le coût en moyenne est egal à : N n k=1 ( k 1 1 ) k 1 ( + n 1 1 ) n ( = N 1 ( 1 1 ) n ). N N N N Lorsque n est grand devant N, ( 1 1 N ) n est petit devant 1, et le coût en moyenne proche de N. En revanche, lorsque n et N sont voisins, ( 1 1 N ) n est proche 1 de 1 e donc la complexité en moyenne voisine de N ( 1 1 e ), ce que l on peut constater expérimentalement : >>> moyenne(10000) moyenne des comparaisons effectuées : 6338.666 >>> from math import exp >>> 10000*(1-1/exp(1)) 6321.205588285577 5. Rappelons que l écart-type d un ensemble de valeurs (x 1,...,x n ) se calcule à l aide de la formule : σ = où x désigne la moyenne des valeurs. On calcule moyenne et écart-type en parallèle : from math import sqrt def ecarttype(n): s = s for k in range(1000): x = est_present(randint(n), randint(n, size = 10000)) s += x ss += x*x print("écart-type des comparaisons effectuées :", sqrt(ss/1000-(s/1000)**2)) L expérience montre que lorsque n est grand devant N l écart-type est proche de N : >>> ecarttype(10) écart-type des comparaisons effectuées : 9.011146375461891 >>> ecarttype(20) écart-type des comparaisons effectuées : 19.986668656882266 >>> ecarttype(50) écart-type des comparaisons effectuées : 49.610211841918186 1 n n xi 2 x2 Exercice 2. recherche dans un tableau trié 1. Pour chercher x dans la liste triée par ordre croissant [a 0,...,a n 1 ], nous allons appliquer le principe dichotomique : comparer x et a p avec p = n. 2 si x < a p alors x, s il se trouve dans la liste, ne peut qu être dans la liste [a 0,...,a p 1 ] ; si x = a p, la recherche est terminée ; si x > a p alors x, s il se trouve dans la liste, ne peut qu être dans la liste [a p+1,...,a n 1 ]. Pour mettre ce principe en pratique, nous allons le généraliser à la recherche de l élément x dans la liste extraite [a i,...,a j 1 ] : si i j la liste en question est vide et x ne s y trouve pas ; si i < j l élément médian a pour indice k = i + j et la comparaison entre x et ak ramène la recherche à l un des deux tableaux [a 2 i,...,a k 1 ] ou [a k+1,...,a j 1 ]. i=1 a k a k a k 1. car ( 1 + 1 n def recherche_dicho(a, lst): return None ) n ( ( 1 )) 1 = exp nln 1 = exp( 1 + o(1)) = n e + o(1). i k j 1 page 3
2. Pour pouvoir évaluer expérimentalement le coût de cette fonction, nous allons la modifier en ajoutant un compteur dénombrant le nombre de comparaisons à l élément médian effectués : def recherche_dicho(a, lst): Réalisons alors 1000 expériences à partir d un tableau de longueur n contenant des entiers pris au hasard dans 0,2n : def experience(n): for k in range(1000): s += recherche_dicho(randint(2*n+1), sorted(randint(2*n+1, size = n))) print("moyenne des comparaisons effectuées : ", s/1000) L expérience montre que le nombre de comparaisons est en moyenne de l ordre de log 2 (n) : >>> from math import log >>> experience(10) moyenne des comparaisons effectuées : 3.233 >>> log(10, 2) 3.3219280948873626 >>> experience(100) moyenne des comparaisons effectuées : 6.231 >>> log(100, 2) 6.643856189774725 >>> experience(1000) moyenne des comparaisons effectuées : 9.505 >>> log(1000, 2) 9.965784284662087 3. Si on cherche la première occurrence de a dans le tableau, il faut poursuivre la recherche dans la partie gauche même si on a trouvé a. C est l occasion d utiliser le mécanisme de rattrapage d une exception évoquée dans l exercice précédent, plus particulièrement de l exception NameError qui se déclenche lorsqu on fait appel à un nom de variable non défini : def premiereoccurrence(a, lst): sol = k try: ol except NameError: return None À l issue de la recherche dichotomique, deux situations peuvent se rencontrer : si a est présent dans le tableau, la variable sol contient l indice de la première occurrence et c est ce résultat qui est retourné par la fonction ; si a n est pas présent dans le tableau, la variable sol n a pas été définie et la tentative de retourner son contenu déclenche l exception NameError qui est rattrapée pour renvoyer la valeur None à la place. La recherche de la dernière occurrence se traite de la même façon : page 4
def derniereoccurrence(a, lst): sol = k try: ol except NameError: return None Exercice 3. recherche d un mot dans un texte par la méthode naïve 1. La méthode de recherche naïve d un mot dans un texte consiste à comparer tous les facteurs du texte avec le mot recherché : def recherche(mot, texte): for k in range(len(texte)-len(mot)+1): if texte[k:k+len(mot)] == mot: raise ValueError Si on ne s autorise que des comparaisons entre caractères individuels, il est préférable de commencer par écrire une fonction déterminant si un mot est suffixe d un autre : def suffixe(mot, texte): for k in range(len(mot)): if mot[k]!= texte[k]: (cette fonction suppose que la longueur de mot est inférieure ou égale à celle de texte) avant d écrire la fonction principale : def recherche(mot, texte): for k in range(len(texte)-len(mot)+1): if suffixe(mot, texte[k]): raise ValueError 2. Pour tester cette fonction, commençons par définir une fonction retournant une chaîne de caractère de longueur 1000 arbitraire : def alea(n=1000): chn = 'abcdefghijklmnopqrstuvwxyz' return ''.join([chn[randint(26)] for k in range(n)]) (la méthode S.join renvoie une chaîne de caractères obtenue par concaténation des chaînes passées en paramètres, séparés par les caractères de S ; c est une façon de convertir une liste en un élément de type str). 3. Pour estimer la probabilité qu une chaîne de ce type contienne le mot llg, nous allons modifier la fonction de la première question pour renvoyer un booléen : def recherche(mot, texte): for k in range(len(texte)-len(mot)+1): if texte[k:k+len(mot)] == mot: Réalisons maintenant 10000 fois l expérience consistant à rechercher ce mot dans un texte tiré au hasard : for k in range(10000): if recherche('llg', alea(1000)): print("fréquence d'apparition du mot 'llg' :", s / 10000) Le script ci-dessus donne une probabilité de l ordre de 5,5% page 5
Exercice 4. paradoxe de Walter Penney 1. Nous allons arbitrairement représenter Pile par l entier 0 et Face par l entier 1. Nous pouvons dès lors évaluer le temps d attente de la première apparition de la séquence PPF de la façon suivante : def ppf(): lst = list(randint(2, size = 3)) s = 3 while lst!= [0, 0, 1]: lst.append(randint(2)) del lst[0] Réalisons maintenant 100000 expériences pour évaluer le nombre moyen de tirages nécessaires : for k in range(100000): s += ppf() print('nombre moyen de tirages nécessaires :', s/100000) Le script ci dessus donne un nombre de tirages moyens proche de 8. 2. La même expérience réalisée avec cette fois la séquence FPP donne là encore un nombre de tirages moyens proche de 8. 3. Nous allons maintenant définir une fonction déterminant laquelle des deux séquences apparaît la première : def penney(): lst = list(randint(2, size = 3)) while True: if lst == [0, 0, 1]: return 1 if lst == [1, 0, 0]: return 0 lst.append(randint(2)) del lst[0] 4. Il nous reste à estimer la probabilité que le motif PPF apparaisse en premier en réalisant 100000 expériences : for k in range(100000): s += penney() print('probabilité que PPF apparaisse avant FPP :', s / 100000) Le script ci-dessus conduit à une estimation de l ordre de 25% ; le motif FPP a donc trois fois plus de chances d apparaître en premier. Cet apparent paradoxe s explique aisément en considérant les deux premiers tirages, qui conduisent à l une des quatre configurations équiprobables suivantes : FF, FP, PF et PP. Or les trois premières configurations conduisent nécessairement à l apparition de FPP en premier 2, et seule la troisième fait apparaitre en premier PPF. En réalité, tout est joué après le deuxième tirage! 2. en effet dès lors qu un F est apparu, lorsqu un motif PPF apparaît il est nécessairement précédé d un motif de la forme FPPP PPPF page 6