Informatique – TD7 : Complexité et structures de données

Informatique – TD7 : Complexité et structures de données
CPP
septembre - novembre 2016
1 Complexité
1.1 Introduction (5mn)
Un algorithme est dit de complexité polynomiale si sa complexité est une fonction polynomiale de la taille
du problème n, par exemple f(n) = 3n3n2+ 10. Dans le cas d’une complexité polynomiale, les coefficients
des monômes n’importent pas puisqu’on s’intéresse à la complexité asymptotique (quand la taille des données
devient grande), et seul le degré du polynôme compte (n3dans l’exemple ci-dessus).
Donc, en général, quand on « compte » les opération pour évaluer la complexité, on compte 1 pour chaque
opération (affectation, comparaison, arithmétique sur des entiers ou des flottants), même si la réalité (le temps
mis par chaque opération à s’exécuter) est différente.
Attention quand même attention aux « opérations » qui coûtent O(n)(comme les comparaisons ou copies
de liste). L’amalgame « 1 ligne de code = O(1) » est délicat avec un langage de haut niveau comme Python.
Cette observation s’applique à tous les calculs de complexité asymptotique, et même au cas des complexités
non-polynomiales : on s’intéresse uniquement au terme dominant.
1.2 Multiplication de deux matrices (10mn)
Soient A= (aij )une matrice n×met B= (bij )une matrice m×p.
Le produit de Aet Best une matrice n×p,C= (cij ), avec cij =Pm
k=1 aikbkj
Quelle est la complexité du calcul d’un élément de la matrice C?
O(m)
Combien y a-t-il d’éléments dans la matrice C?
O(n×p)
Écrire la fonction python matmult(A,B), qui renvoie le produit de deux tableaux numpy Aet B, sans
utiliser numpy.dot (rappel : A.shape[0] est le nombre de lignes et A.shape[1] le nombre de colonnes
de A) – attention aux indices !
def m at mu lt (A , B ):
n = A. shape [0]
m = A. shape [1]
p = B. shape [1]
C = np . zer os ([ n , p ])
for iin ran g e (n ):
for jin rang e ( p ):
for kin rang e ( m ):
C[i , j ] += A [ i , k] * B[ k , j ]
r et u rn C
Calculer la complexité dans le cas meilleur, le cas pire, en moyenne, en fonction de la taille des données,
ici les dimensions (n, m, p)des matrices Aet B.
1
Meilleur cas = pire cas = O(mnp).
Note : En théorie, on peut faire plus efficace. Trouver un algorithme optimal de multiplication de matrices
est un problème de recherche ouvert à l’heure actuelle.
A PASSER DANS LE CM ?
1.3 Recherche dans un tableau (5mn)
On vous demande de concevoir une fonction qui recherche par dichotomie un élément dans un tableau de
nentiers triés, et qui renvoie soit son index, soit -1 si l’élément n’est pas présent. Quelle est la complexité
asymptotique dans le pire cas en temps d’exécution de cette fonction ? (On peut montrer qu’elle correspond
aussi à la complexité moyenne)
Codage en Python :
def r ec h er c he ( T , e ):
i = 0
j = len (T)
while j > i:
# In var ia nt : si l é l é m ent est dan s T , son ind ice est
# dans [ i , j[.
pivot = (i + j ) // 2
if T [ pivot ] == e :
r e tu r n pivot
elif T [ piv ot ] > e :
j = pivot
else:
i = pivot + 1
r et u rn None
# Test :
tabl eau = [0 , 1, 5 , 10 , 42]
print( re ch er ch e (tableau , 0))
print( re ch er ch e (tableau , 2))
print( re ch er ch e (tableau , 42))
print( re ch er ch e (tableau , 43))
Complexité : on l’a vu en cours, il suffit d’appliquer le "Master Theorem" avec k= 1, b = 2, d =0:O(log n)
(d= logbk= 0).
Rappel : Si f(n) = kf(bn/bc) + O(nd), avec k > 0, b > 1, d 0alors
si d > logbk,f(n) = O(nd)
si d= logbk,f(n) = O(ndlog n)
si d < logbk,f(n) = O(nlogbk)
2 Structures de données
Une structure de données est un assemblage de types simples (dans notre cas listes, booléens, entiers,
flottants, chaînes) permettant de représenter un objet plus complexe (exemples : un tuple d’une relation, un
arbre généalogique, un graphe de villes avec leurs distances)
Nous allons créer une structure de données pour représenter un vecteur creux, c’est-à-dire un vecteur conte-
nant beaucoup de fois la valeur 0.
Un vecteur creux se comporte de la même manière qu’un vecteur classique (np.array à 1 dimension), mais
est optimisé pour le cas où le vecteur contient une majorité de valeurs nulles 1.
2.1 Préliminaire : opérations sur les listes et complexité
Dans la suite, nous allons utiliser des listes Python pour représenter une structure de données. Voici un
rappel des opérations et de leur complexité.
1. Par exemple, l’instruction np.zeros(10**10) (créer un vecteur de taille 1010) provoque une MemoryError sur la machine de
l’auteur de ce document, alors que créer un vecteur creux (en utilisant coo_matrix de la bibliothèque scipy ou la version faite
maison) de taille 101000 ne pose aucun problème.
2
Soient xune liste de longueur net iun entier. eest une valeur quelconque.
x[i] : accès à l’élément i,Θ(1)
x.append(e) : insertion en fin de liste, Θ(1)
x=x+[e]: construction d’une nouvelle liste contenant les éléments de xsuivis de e,θ(n)
x.insert(i, e) : insertion de een position i,Θ(nombre d’éléménts déplacés) = Θ(ni), ou Θ(1) si
aucun élément n’est déplacé.
e = x.pop(i) : suppression de l’élément à la position i,Θ(nombre d’éléments déplacés) = Θ(ni), ou
Θ(1) si aucun élément n’est déplacé.
Hypothèse simplificatrice : on suppose que l’espace mémoire alloué par Python pour stocker les éléments de
la liste est suffisant pour ajouter un élément en fin. L’insertion en fin est donc en Θ(1) et l’insertion en milieu
de liste en Θ(nombre d’éléments déplacés)2.
2.2 Structure de données = implantation + constructeurs + testeurs + sélecteurs
+ modifieurs (10mn)
L’utilisateur de notre structure de données ne devrait pas avoir à se soucier de la représentation interne de
notre vecteur creux (on parle de structure de données "opaque" ou "abstraite").
Lorsqu’on doit choisir une structure de données, avant de se poser la question « Comment représenter un
vecteur creux en Python ? » (c’est-à-dire « Quelle structure de données utiliser ? »), il faut se poser la question
«Pourquoi représenter un vecteur creux ? », c’est à dire « Quel jeu de fonctions proposer à l’utilisateur ? ».
Nous allons fournir à l’utilisateur des fonctions permettant de modifier ou de lire cette structure de données.
Pour notre vecteur creux, ces opérations sont les mêmes que pour np.array, mais leur implémentation et
leur complexité sera différente.
Ces fonctions sont de plusieurs types :
les constructeurs permettent de créer une structure de données vide ou de créer une structure de données
à partir d’un fichier ou d’une autre structure de données (exemple : np.array(...)) ;
les testeurs et sélecteurs permettent d’interroger la structure de données sans la modifier (est-ce qu’un
élément est présent dans l’ensemble ? combien d’éléments contient l’ensemble ?) (exemples : x.shape(),
x[...]) ;
les modifieurs permettent de modifier la structure de données (ajouter un élément, en supprimer un,
extraire un élément quelconque) (exemple : x[...] = ...) ;
des opérateurs qui prennent en entrée plusieurs instance de la structure et renvoient une valeur fonction
de ces entrées ;
éventuellement, les destructeurs permettent de détruire la structure de données (en Python, dans beau-
coup de cas ils ne sont pas nécessaires).
Elles constituent l’interface de la structure de données.
2.3 Spécification de l’interface (10mn)
Spécifier l’interface, c’est donner la réponse à la question « Pourquoi ? » ci-dessus, sous la forme de liste de
fonctions avec leurs paramètres et leurs valeurs de retour, sans en donner le contenu.
Note : la bibliothèque NumPy utilise les opérateurs (+,[], ...) pour l’interface de programmation des vecteurs,
mais vous n’avez pas les notions de Python nécessaires pour le faire : toute l’interface utilisera donc des fonctions
(def ...).
Donner une liste des fonctions de l’interface d’une structure de données « ensemble », par catégorie, avec
pour chaque fonction ce qu’elle fait et la liste des paramètres.
Par exemple :
creer_vecteur(n) renvoie un vecteur vide de taille n
creuser(v_dense) construit un vecteur creux représentant la même valeur mathématique que la liste
Python v_dense.
remplir(v_creux) construit une liste Python représentant la même valeur mathématique que le vecteur
creux v_creux
taille_vecteur(v) renvoie la taille du vecteur
2. En réalité, la complexité de ces opérations fait appel à la notion de complexité amortie qui n’est pas au programme, cf.
https://wiki.python.org/moin/TimeComplexity
3
changer_valeur(vect, i, val) Modifie la valeur à l’emplacement idu vecteur vect. Équivalent de
vect[i] = val avec les vecteurs.
somme_vecteurs(v1, v2) ajoute deux vecteurs (renvoie un vecteur).
comparer_vecteur(v1, v2) compare les deux vecteurs v1 et v2. Renvoie True s’ils sont égaux, et False
sinon.
somme_elements_vecteur(v) renvoie la somme des éléments du vecteur v.
2.4 Représentation et implantation (30mn)
Implanter une fonction ou un algorithme, c’est la réponse à la question « Comment? » ci-dessus.
Il faut choisir quel(s) type(s) Python utiliser, mais aussi comment les valeurs y sont stockées.
Nous proposons d’étudier la représentation d’un vecteur creux par une liste de doublets [indice, valeur]
(valeur est une valeur numérique quelconque. Pour simplifier on travaille avec des entiers, mais le code marchera
aussi avec des flottants par exemple). Pour mémoriser la taille du vecteur, on force la dernière valeur à être
représentée par un doublet même si elle est nulle. Les autres valeurs nulles ne sont pas représentées (on interdit
la présence d’un doublet [indice, 0] si indice n’est pas le dernier élément du vecteur).
Par exemple, le vecteur [1, 0, 0, 0, 12, 5, 0, 42, 0, 0] est représenté par : [[0, 1], [4, 12], [5,
5], [7, 42], [9, 0]], qui se lit « la valeur 1 à l’indice 0 puis la valeur 12 à l’indice 4, puis . . ., et toutes les
autres valeurs sont nulles ».
On donne la fonction afficher_vecteur(v) qui affiche le vecteur vmathématique (c’est à dire, valeurs
nulles comprises) :
def afficher_vecteur(v):
print( [ ’ , end = )
f irs t_z ero = 0
for pair in v:
pos = pair [0]
val = pair [1]
# Not e : on peut a us si é cri re
# pos , val = pair
# On af fic he les é ven tue ls z é ros d epu is la dern i è re v ale ur :
for iin r a ng e ( pos - fi rst_ zer o ):
print(0 , end = )
# Puis la va leu r non - nul le elle - m ê me :
print( val , e nd = )
fi rst _ze ro = pos + 1
print( ’] )
Expliquer le code et le faire tourner au tableau sur un exemple.
On note Sla taille du vecteur et Nle nombre d’éléments non-nuls. Quelle est la complexité pire cas et
meilleur cas de afficher_vecteur(v) en fonction de Set N?
Donner l’implantation en Python des fonctions suivantes :
creer_vecteur(n) (indice : 1 ligne suffit)
taille_vecteur(v) (indice : 1 ligne suffit)
Voir fichier vecteur_creux_sentinelle.py séparé.
Évaluer la complexité de ces deux fonctions.
...
Quelle serait la complexité de ces deux fonctions avec des vecteurs classiques (non creux) ?
...
Donnez l’implémentation de la fonction changer_valeur(vect, i, val) (indice : c’est un peu plus
compliqué qu’on aurait pu le croire, cette fois-ci il faudra une dizaine de ligne).
Quelle est la complexité (pire et meilleur cas) de cette fonction ?
4
Donnez l’implémentation de la fonction comparer_vecteur(v1, v2). Quelle est sa complexité ?
Quelle serait la complexité de cette fonction si on n’avait pas interdit les doublets [indice, 0] ?
2.4.1 Note sur l’examen
Un des exercices de l’examen portera sur les vecteurs creux. Les opérations rappelées section 2.1 seront
rappelées dans le sujet d’examen.
5
1 / 5 100%
La catégorie de ce document est-elle correcte?
Merci pour votre participation!

Faire une suggestion

Avez-vous trouvé des erreurs dans linterface ou les textes ? Ou savez-vous comment améliorer linterface utilisateur de StudyLib ? Nhésitez pas à envoyer vos suggestions. Cest très important pour nous !