L'art de l'algorithmique¶

In [1]:
from IPython.display import YouTubeVideo
YouTubeVideo('6RegAtoeUuQ')
Out[1]:

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.

In [2]:
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 :

In [ ]:
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).

In [ ]:
%load data/bubblesort.py

On vérifie que le tri donne le bon résultat :

In [ ]:
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) :

In [ ]:
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 :

In [3]:
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)
In [ ]:
%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.

In [4]:
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.

In [ ]: