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 est un module de haute performance qui fait appel à bisect pour maintenir des données ordonnées.

  • 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 on or 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)