"bisect" — Algorithme de bissection de listes
*********************************************

**Code source :** Lib/bisect.py

======================================================================

Ce module fournit des outils pour maintenir une liste triée sans avoir
à la trier à chaque insertion. Il pourrait être plus rapide que
l'approche classique pour de longues listes d'éléments dont les
opérations de comparaison sont lourdes. Le module se nomme "bisect"
car il utilise une approche simple par bissection. Si vous voulez un
exemple de cet algorithme, le mieux est d'aller lire le code source de
ce module (les conditions sur les limites y étant justes !).

Les fonctions suivantes sont fournies :

bisect.bisect_left(a, x, lo=0, hi=len(a), *, key=None)

   Trouve le point d'insertion de *x* dans *a* permettant de conserver
   l'ordre. Les paramètres *lo* et *hi* permettent de limiter les
   emplacements à vérifier dans la liste, par défaut toute la liste
   est utilisée. Si *x* est déjà présent dans *a*, le point
   d'insertion proposé sera avant (à gauche) de l'entrée existante. Si
   *a* est déjà triée, la valeur renvoyée peut directement être
   utilisée comme premier paramètre de "list.insert()".

   Le point d'insertion renvoyé, *i*, coupe la liste *a* en deux
   moitiés telles que, pour la moitié de gauche : "all(val < x for val
   in a[lo : i])", et pour la partie de droite : "all(val >= x for val
   in a[i : hi])".

   *key* specifies a *key function* of one argument that is used to
   extract a comparison key from each element in the array.  To
   support searching complex records, the key function is not applied
   to the *x* value.

   If *key* is "None", the elements are compared directly with no
   intervening function call.

   Modifié dans la version 3.10: ajout du paramètre *key*.

bisect.bisect_right(a, x, lo=0, hi=len(a), *, key=None)
bisect.bisect(a, x, lo=0, hi=len(a), *, key=None)

   Semblable à "bisect_left()", mais renvoie un point d'insertion
   après (à droite) d'une potentielle entrée existante valant *x* dans
   *a*.

   Le point d'insertion renvoyé, *i*, coupe la liste *a* en deux
   moitiés telles que, pour la moitié de gauche : "all(val <= x for
   val in a[lo : i])" et pour la moitié de droite : "all(val > x for
   val in a[i : hi])".

   *key* specifies a *key function* of one argument that is used to
   extract a comparison key from each element in the array.  To
   support searching complex records, the key function is not applied
   to the *x* value.

   If *key* is "None", the elements are compared directly with no
   intervening function call.

   Modifié dans la version 3.10: ajout du paramètre *key*.

bisect.insort_left(a, x, lo=0, hi=len(a), *, key=None)

   Insère *x* dans *a* en préservant l'ordre.

   Cette fonction commence par appliquer "bisect_left()" pour
   déterminer la position de l'insertion, puis appelle la méthode
   "insert()" de *a* pour ajouter *x* à l'endroit adéquat.

   To support inserting records in a table, the *key* function (if
   any) is applied to *x* for the search step but not for the
   insertion step.

   Gardez à l'esprit que la complexité logarithmique O(\ln(n)) de la
   recherche est dominée par la lenteur de l'insertion, de complexité
   linéaire O(n).

   Modifié dans la version 3.10: ajout du paramètre *key*.

bisect.insort_right(a, x, lo=0, hi=len(a), *, key=None)
bisect.insort(a, x, lo=0, hi=len(a), *, key=None)

   Similaire à "insort_left()", mais en insérant *x* dans *a* après
   une potentielle entrée existante égale à *x*.

   Le principe est le même que "insort_left()", mais avec
   "bisect_right()".

   To support inserting records in a table, the *key* function (if
   any) is applied to *x* for the search step but not for the
   insertion step.

   Gardez à l'esprit que la complexité logarithmique O(\ln(n)) de la
   recherche est dominée par la lenteur de l'insertion, de complexité
   linéaire O(n).

   Modifié dans la version 3.10: ajout du paramètre *key*.


Notes sur la performance
========================

Pour écrire du code sensible à la performance utilisant "bisect()" et
"insort()", prenez en compte ces quelques considérations :

* La bissection est une bonne idée pour rechercher une plage de
  valeurs. Pour une seule valeur, mieux vaut un dictionnaire.

* Les fonctions d'insertion dans une liste classée ont une complexité
  linéaire car c'est le coût d'une insertion, même si la recherche est
  logarithmique.

* Les fonctions de recherche ne maintiennent pas d'état et ne
  conservent pas le résultat de la fonction clé après utilisation. Par
  conséquent, si elles sont appelées dans une boucle, la fonction clé
  peut être appelée de nombreuses fois sur les mêmes entrées. Si cette
  fonction clé est lente, une possibilité est de l'encapsuler dans
  "functools.cache()" pour éviter de répéter les opérations. Une autre
  possibilité est d'effectuer la recherche sur un tableau de clés
  pré-calculées pour trouver le point d'insertion (voir les exemples
  plus bas).

Voir aussi:

  * Sorted Collections is a high performance module that uses *bisect*
    to managed sorted collections of data.

  * SortedCollection recipe utilise le module *bisect* pour construire
    une classe collection exposant des méthodes de recherches
    naturelles et gérant une fonction clef.  Les clefs sont
    pré-calculées pour économiser des appels inutiles à la fonction
    clef durant les recherches.


Chercher dans des listes triées
===============================

Les fonctions "bisect()" ci-dessus sont utiles pour insérer des
éléments, mais peuvent être étranges et peu naturelles à utiliser pour
rechercher des éléments. Les cinq fonctions suivantes montrent comment
les transformer en recherche plus classique pour les listes triées :

   def index(a, x):
       'Locate the leftmost value exactly equal to x'
       i = bisect_left(a, x)
       if i != len(a) and a[i] == x:
           return i
       raise ValueError

   def find_lt(a, x):
       'Find rightmost value less than x'
       i = bisect_left(a, x)
       if i:
           return a[i-1]
       raise ValueError

   def find_le(a, x):
       'Find rightmost value less than or equal to x'
       i = bisect_right(a, x)
       if i:
           return a[i-1]
       raise ValueError

   def find_gt(a, x):
       'Find leftmost value greater than x'
       i = bisect_right(a, x)
       if i != len(a):
           return a[i]
       raise ValueError

   def find_ge(a, x):
       'Find leftmost item greater than or equal to x'
       i = bisect_left(a, x)
       if i != len(a):
           return a[i]
       raise ValueError


Exemples
========

La fonction "bisect()" peut être utile pour des recherches dans des
tableaux de nombres. Cet exemple utilise "bisect()" pour rechercher la
note (sous forme de lettre) correspondant à une note sous forme de
points, en se basant sur une échelle prédéfinie : plus de 90 vaut 'A',
de 80 à 89 vaut 'B', etc. :

   >>> def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
   ...     i = bisect(breakpoints, score)
   ...     return grades[i]
   ...
   >>> [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]
   ['F', 'A', 'C', 'C', 'B', 'A', 'A']

The "bisect()" and "insort()" functions also work with lists of
tuples.  The *key* argument can serve to extract the field used for
ordering records in a table:

   >>> from collections import namedtuple
   >>> from operator import attrgetter
   >>> from bisect import bisect, insort
   >>> from pprint import pprint

   >>> Movie = namedtuple('Movie', ('name', 'released', 'director'))

   >>> movies = [
   ...     Movie('Jaws', 1975, 'Speilberg'),
   ...     Movie('Titanic', 1997, 'Cameron'),
   ...     Movie('The Birds', 1963, 'Hitchcock'),
   ...     Movie('Aliens', 1986, 'Scott')
   ... ]

   >>> # Find the first movie released after 1960
   >>> by_year = attrgetter('released')
   >>> movies.sort(key=by_year)
   >>> movies[bisect(movies, 1960, key=by_year)]
   Movie(name='The Birds', released=1963, director='Hitchcock')

   >>> # Insert a movie while maintaining sort order
   >>> romance = Movie('Love Story', 1970, 'Hiller')
   >>> insort(movies, romance, key=by_year)
   >>> pprint(movies)
   [Movie(name='The Birds', released=1963, director='Hitchcock'),
    Movie(name='Love Story', released=1970, director='Hiller'),
    Movie(name='Jaws', released=1975, director='Speilberg'),
    Movie(name='Aliens', released=1986, director='Scott'),
    Movie(name='Titanic', released=1997, director='Cameron')]

If the key function is expensive, it is possible to avoid repeated
function calls by searching a list of precomputed keys to find the
index of a record:

   >>> data = [('red', 5), ('blue', 1), ('yellow', 8), ('black', 0)]
   >>> data.sort(key=lambda r: r[1])       # Or use operator.itemgetter(1).
   >>> keys = [r[1] for r in data]         # Precompute a list of keys.
   >>> data[bisect_left(keys, 0)]
   ('black', 0)
   >>> data[bisect_left(keys, 1)]
   ('blue', 1)
   >>> data[bisect_left(keys, 5)]
   ('red', 5)
   >>> data[bisect_left(keys, 8)]
   ('yellow', 8)
