L'art de l'algorithmique¶
from IPython.display import YouTubeVideo
YouTubeVideo('6RegAtoeUuQ')
L'algorithmique consiste à trouver les algorithmes les plus performants. En général il s'agit des plus rapides, mais cela peut aussi être les moins gourmand en mémoire ou ceux qui se parallélisent le mieux. On peut aussi se focaliser sur le comportement moyen de l'algorithme ou son comportement dans le pire des cas.
Quoi qu'il en soit, pour écrire un algorithme performant, il faut savoir l'analyser.
La première étape consiste à calculer sa complexité à savoir le nombre d'opérations qu'il nécessite en fonction de la taille des données.
Exemple :
l1 = range(N)
for i in l1:
res += i
est un programme qui exécute la ligne 'res += i
' N fois donc en autant opérations qu'il y a de données. Il est dit linéaire.
for i in l1:
for j in l1:
res += i*j
est un programme en N² opérations car il a 2 boucles de longeur N imbriquées (avec N = len(l1)
). Il est dit quadratique.
Ce cours n'étant pas un cours d'algorithmique, je n'irais pas plus loin dans la théorie. Regardons un exemple pratique pour bien voir l'importance du sujet.
Comment trier efficacement ?¶
Le but est de trier un grand tableau de nombres aléatoires de différentes façons et de calculer laquelle est la plus rapide. Avant de regarder les différentes façon de faire, on va utiliser la méthode de Python sorted
qui peut servir de référence pour valider nos algorithmes.
donnees = [2, 4, 1, 6, 0, 11, 3, 2]
print(donnees)
print(sorted(donnees))
[2, 4, 1, 6, 0, 11, 3, 2] [0, 1, 2, 2, 3, 4, 6, 11]
Exercice : Tri à bulle¶
Le tri à bulle parcourt tous les éléments dans l'ordre et si un élément est plus grand que son suivant, alors on les échange. Remarquez que si on ne parcourt qu'une seule fois la liste, cela ne suffit pas.
Écrire la fonction du tri à bulle :
def bubblesort(data):
# votre code ici
values = [2, 4, 1, 6, 0, 11, 3, 2]
print(values)
bubblesort(values)
Lancez la commande suivante pour voir la solution (%load
est une commande magique de iPython).
%load data/bubblesort.py
On vérifie que le tri donne le bon résultat :
values = [2, 4, 1, 6, 0, 11, 3, 2]
print(values)
print(bubblesort(values))
max([abs(a-b) for a,b in zip(sorted(values),bubblesort(values))]) # we check that our result is fine
Calculez la complexité du tri à bulle dans le cas où les données sont [N, N-1, N-2, ..., 3,2,1].
Cela revient à compter le nombre de fois où la ligne d'échange d'une paire de valeur sera appelée (donc le nombre de fois où la ligne print sera affichée si vous l'avez décommentée).
Exercice : Tri rapide (Quicksort in English)¶
Le tri rapide consiste à choisir un pivot et à mettre tous les éléments plus petit que le pivot dans la première partie du tableau et tous les plus grands dans la seconde partie.
[5, 4, 6, 4, 8, 4, 9, 9, 2, 1]
Choisissons comme pivot la valeur au milieu à savoir la 5ème c.à.d. 8. Je parcours mes données et je place les éléments en fonction de leur valeur. Cela donne :
[5, 4, 6, 4, 4, 2, 1] 8 [9, 9]
maintenant il ne me reste plus qu'à trier le deux tableaux (par récursivité bien sûr).
Écrire la fonction de tri rapide (c'est compliqué et donc n'hésitez pas à sauter cet exercice dans un premier temps) :
def quicksort(data):
values = [2, 4, 1, 6, 0, 11, 3, 2]
print(values)
quicksort(values)
Utilisons la commande magique %timeit
d'iPython pour calculer le temps d'exécution de notre méthode :
import numpy as np
def quicksort(data):
# mes operations
# ...
np.sort(data) # sorted is not very efficient, np.sort is by default Quicksort
values = np.random.randint(9999,size=10000) # j'utilise le générateur aléatoire de Numpy pour avoir un grand tableau
%timeit quicksort(values)
467 µs ± 6.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit bubblesort(values) # to be run to see its duration
La différence de vitesse est significative.
Que fait la bibliothèque de calcul Numpy ?¶
Numpy dispose de la fonction np.sort
dont la documentation est intéressante à lire maintenant que l'on a compris qu'il y a différentes facons de trier.
help(np.sort)
Help on function sort in module numpy: sort(a, axis=-1, kind=None, order=None) Return a sorted copy of an array. Parameters ---------- a : array_like Array to be sorted. axis : int or None, optional Axis along which to sort. If None, the array is flattened before sorting. The default is -1, which sorts along the last axis. kind : {'quicksort', 'mergesort', 'heapsort', 'stable'}, optional Sorting algorithm. The default is 'quicksort'. Note that both 'stable' and 'mergesort' use timsort or radix sort under the covers and, in general, the actual implementation will vary with data type. The 'mergesort' option is retained for backwards compatibility. .. versionchanged:: 1.15.0. The 'stable' option was added. order : str or list of str, optional When `a` is an array with fields defined, this argument specifies which fields to compare first, second, etc. A single field can be specified as a string, and not all fields need be specified, but unspecified fields will still be used, in the order in which they come up in the dtype, to break ties. Returns ------- sorted_array : ndarray Array of the same type and shape as `a`. See Also -------- ndarray.sort : Method to sort an array in-place. argsort : Indirect sort. lexsort : Indirect stable sort on multiple keys. searchsorted : Find elements in a sorted array. partition : Partial sort. Notes ----- The various sorting algorithms are characterized by their average speed, worst case performance, work space size, and whether they are stable. A stable sort keeps items with the same key in the same relative order. The four algorithms implemented in NumPy have the following properties: =========== ======= ============= ============ ======== kind speed worst case work space stable =========== ======= ============= ============ ======== 'quicksort' 1 O(n^2) 0 no 'heapsort' 3 O(n*log(n)) 0 no 'mergesort' 2 O(n*log(n)) ~n/2 yes 'timsort' 2 O(n*log(n)) ~n/2 yes =========== ======= ============= ============ ======== .. note:: The datatype determines which of 'mergesort' or 'timsort' is actually used, even if 'mergesort' is specified. User selection at a finer scale is not currently available. All the sort algorithms make temporary copies of the data when sorting along any but the last axis. Consequently, sorting along the last axis is faster and uses less space than sorting along any other axis. The sort order for complex numbers is lexicographic. If both the real and imaginary parts are non-nan then the order is determined by the real parts except when they are equal, in which case the order is determined by the imaginary parts. Previous to numpy 1.4.0 sorting real and complex arrays containing nan values led to undefined behaviour. In numpy versions >= 1.4.0 nan values are sorted to the end. The extended sort order is: * Real: [R, nan] * Complex: [R + Rj, R + nanj, nan + Rj, nan + nanj] where R is a non-nan real value. Complex values with the same nan placements are sorted according to the non-nan part if it exists. Non-nan values are sorted as before. .. versionadded:: 1.12.0 quicksort has been changed to an introsort which will switch heapsort when it does not make enough progress. This makes its worst case O(n*log(n)). 'stable' automatically choses the best stable sorting algorithm for the data type being sorted. It, along with 'mergesort' is currently mapped to timsort or radix sort depending on the data type. API forward compatibility currently limits the ability to select the implementation and it is hardwired for the different data types. .. versionadded:: 1.17.0 Timsort is added for better performance on already or nearly sorted data. On random data timsort is almost identical to mergesort. It is now used for stable sort while quicksort is still the default sort if none is chosen. For details of timsort, refer to `CPython listsort.txt <https://github.com/python/cpython/blob/3.7/Objects/listsort.txt>`_. 'mergesort' and 'stable' are mapped to radix sort for integer data types. Radix sort is an O(n) sort instead of O(n log n). Examples -------- >>> a = np.array([[1,4],[3,1]]) >>> np.sort(a) # sort along the last axis array([[1, 4], [1, 3]]) >>> np.sort(a, axis=None) # sort the flattened array array([1, 1, 3, 4]) >>> np.sort(a, axis=0) # sort along the first axis array([[1, 1], [3, 4]]) Use the `order` keyword to specify a field to use when sorting a structured array: >>> dtype = [('name', 'S10'), ('height', float), ('age', int)] >>> values = [('Arthur', 1.8, 41), ('Lancelot', 1.9, 38), ... ('Galahad', 1.7, 38)] >>> a = np.array(values, dtype=dtype) # create a structured array >>> np.sort(a, order='height') # doctest: +SKIP array([('Galahad', 1.7, 38), ('Arthur', 1.8, 41), ('Lancelot', 1.8999999999999999, 38)], dtype=[('name', '|S10'), ('height', '<f8'), ('age', '<i4')]) Sort by age, then height if ages are equal: >>> np.sort(a, order=['age', 'height']) # doctest: +SKIP array([('Galahad', 1.7, 38), ('Lancelot', 1.8999999999999999, 38), ('Arthur', 1.8, 41)], dtype=[('name', '|S10'), ('height', '<f8'), ('age', '<i4')])
{{ PreviousNext("06 - Premiers programmes.ipynb", "../lesson2 Deeper in Python/01 - Variables en mémoire.ipynb") }}
Le cours suivant entre dans certains détails du fonctionnement de Python. Il est possible de sauter cette partie dans un premier temps pour aller directement regarder les bibliothèques de calcul scientifique et continuer avec Numpy.