IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Contribuez Python Discussion :

Recherche rapide par dichotomie dans une liste complexe triée


Sujet :

Contribuez Python

  1. #1
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 474
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 474
    Points : 9 277
    Points
    9 277
    Billets dans le blog
    6
    Par défaut Recherche rapide par dichotomie dans une liste complexe triée
    Rechercher par dichotomie dans une liste triée est très rapide et bien connu. Pour l'expliquer, on prend souvent l'exemple d'une recherche dans un dictionnaire papier:
    - on cherche un mot
    - on ouvre le dictionnaire par son milieu: le mot est "avant" ou "après". Par exemple "avant".
    - on ouvre la partie "avant" par son milieu: le mot est "avant" ou "après"
    - etc...
    - jusqu'à ce qu'on trouve la page qui contient le mot.

    Cette recherche est très efficace, parce qu'il suffit avec 1000 pages de 10 manipulations seulement, puisque 2**10=1024. On peut même trouver plus tôt si on a de la chance.

    On peut vouloir rechercher par dichotomie pour plusieurs raisons. Par exemple:

    - trouver si le mot existe ou pas

    - si le mot existe, trouver à quel indice de la liste il se trouve

    - si le mot n'existe pas, trouver à quel endroit on peut l'insérer dans la liste. Ce qui est intéressant dans cette insertion, c'est que la liste continue à être triée! C'est beaucoup plus efficace que d'ajouter le mot avec append et de re-trier la liste après.

    Le module Python qui fait ça s'appelle "bisect". On utilise par exemple la fonction "bisect_left" pour trouver l'indice de présence ou d'insertion d'un élément dans la liste. Par exemple:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    from bisect import bisect_left
     
    seq = [1,2,3,5,5,6,7,8]
     
    ib = bisect_left(seq, 3)
    # ib==2 puisque 3 existe dans la liste à l'index 2
     
    ib = bisect_left(seq, 4)
    # ib==3 puisque 4 n'existe pas, on a l'indice d'insertion 3
    En cas d'insertion de 4 à l'indice 3 de cette liste, c'est facile:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    seq.insert(3, 4)
    # résultat: seq => [1,2,3,4,5,5,6,7,8]
    Mais, car il y a un "mais", cette fonction bisect_left n'est valable que pour des listes "simples", c'est à dire que la valeur cherchée doit être de même type que les autres éléments de la liste: nombres, chaînes de caractères, (etc...). Dans le cas d'une liste plus complexe, par exemple une liste de listes (une liste de dictionnaires, etc...), on ne peut pas faire ça pour chercher une des valeurs d'une sous-liste.

    Sauf à partir de Python 3.10 qui a ajouté l'argument "key" à ses fonctions de bisect! C'est d'ailleurs le même argument key que pour la fonction de tri (.sort ou sorted), puisqu'il s'agit dans les 2 cas de situer la valeur qui nous intéresse dans l'élément quelconque de la liste: sous-liste, dictionnaire, etc...

    Mais avant Python 3.10, on faisait comment? On se fabriquait une fonction de recherche par dichotomie perso! Ce n'est d'ailleurs pas compliqué à faire en Python. Mais, tant qu'à faire, voilà une fonction qui marche pour tout Python 3 (avant ou après 3.10):

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import sys
    from bisect import bisect_left
     
    def bisect_left2(seq, val, ib=0, ih=None, key=None):
        """Trouve l'indice de présence ou d'insertion de la valeur val dans la
           séquence triée seq. la fonction key permet de retrouver val dans
           l'élément de seq. La séquence seq doit être triée par rapport à val.
           Arguments:
           - seq: séquence (list, tuple) triée selon val
           - val: valeur à rechercher, incluse dans l'élément de seq
           - ib: indice de recherche bas
           - ih: indice de recherche haut
           - key: fonction permettant de retrouver val dans l'élément de seq
           Retour:
           - ib: indice de présence ou d'insertion de l'élément de seq
             contenant val
        """
        # corrige les données si nécessaire
        ib = 0 if ib<0 else ib
        ih = len(seq) if ih is None or ih>len(seq) else ih
        # calcule l'indice de présence ou d'insertion de l'élément contenant val
        if sys.version_info.major==3 and sys.version_info.minor>=10:
            ib = bisect_left(seq, val, lo=ib, hi=ih, key=key)
        else:
            key = (lambda x:x) if key is None else key
            while ib < ih:
                k = (ib + ih) // 2
                if key(seq[k]) < val:
                    ib = k + 1
                else:
                    ih = k
        return ib
    Avec Python >= 3.10, il est en effet intéressant d'utiliser plutôt le module bisect, parce qu'il utilise des fonctions codées en C, donc plus rapides.

    Exemple pour une liste de listes, pour laquelle on recherche le 2ème élément de la sous liste:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    seq = [[9,1], [5,2], [7,3], [3,5], [5,5], [1,5], [12,7], [8,8]]
    key = lambda x: x[1] # même key pour le tri et la recherche
     
    val = 4 # n'existe pas
    ib = bisect_left2(seq, val, key=key)
    ok = False if ib >= len(seq) else key(seq[ib]) == val
    print("valeur:", val, "index:", ib, "Existe déjà" if ok else "N'existe pas")
    # affiche: valeur: 4 index: 3 N'existe pas
     
    val = 5 # existe
    ib = bisect_left2(seq, val, key=key)
    ok = False if ib >= len(seq) else key(seq[ib]) == val
    print("valeur:", val, "index:", ib, "Existe déjà" if ok else "N'existe pas")
    # affiche: valeur: 5 index: 3 Existe déjà
    Pour l'insertion, c'est la même chose que précédemment, à part qu'on insère une sous-liste, et pas seulement la valeur recherchée! Par exemple pour la valeur 4 qui n'existe pas et qu'on insère à l'index 3, on insère en fait une sous-liste comme [12, 4]:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    seq.insert(3, [12, 4])
    # résultat: [[9,1], [5,2], [7,3], [12, 4], [3,5], [5,5], [1,5], [12,7], [8,8]]
    On voit cependant que la valeur booléenne "ok" pour "présent ou pas" nécessite un petit calcul pas si simple que ça. Alors, on peut proposer une fonction qui retourne en plus cette valeur:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    def dichot(seq, val, key=None):
        """Trouve l'indice de présence ou d'insertion de la valeur val dans la
           séquence triée seq. la fonction key permet de retrouver val dans
           l'élément de seq. La séquence seq doit être triée par rapport à val.
           Fonction à utiliser avec Python version >= 10
           Arguments:
           - seq: séquence (list, tuple) triée selon val
           - val: valeur à rechercher, incluse dans l'élément de seq
           - key: fonction permettant de retrouver val dans l'élément de seq
           Retour:
           - si ok==True: ib est l'indice de l'élément de seq contenant val
           - si ok==False: ib est l'indice d'insertion de l'élément de seq
             contenant val.
        """
        # calcule key pour une séquence simple (retourne l'argument donné)
        key = (lambda x:x) if key is None else key
     
        # calcule l'indice de présence ou d'insertion de l'élément contenant val
        ib, ih = 0, len(seq)
        ib = bisect_left2(seq, val, ib=ib, ih=ih, key=key)
     
        # calcule ok qui représente l'existence ou non de val dans seq
        ok = False if ib >= len(seq) else key(seq[ib]) == val
     
        return ok, ib
    Le même exemple que ci-dessus, sera donc plus simple:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    seq = [[9,1], [5,2], [7,3], [3,5], [5,5], [1,5], [12,7], [8,8]]
    key = lambda x: x[1]
     
    val = 4 # n'existe pas
    ok, ib = dichot(seq, val, key=key)
    print("valeur:", val, "index:", ib, "Existe déjà" if ok else "N'existe pas")
    # affiche: valeur: 4 index: 3 N'existe pas
     
    val = 5 # existe déjà
    ok, ib = dichot(seq, val, key=key)
    print("valeur:", val, "index:", ib, "Existe déjà" if ok else "N'existe pas")
    # affiche: valeur: 5 index: 3 Existe déjà
    On peut aller plus loin, dans le cas où une même valeur peut être rencontrée plusieurs fois. On obtiendra alors l'indice de la 1ère valeur et celui de la dernière:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    def dichots(seq, val, key=None):
        """Trouve l'indice de présence ou d'insertion de la valeur val contenue
           dans les éléments de la séquence triée seq. la fonction key permet
           de retrouver val dans les éléments de seq. La séquence seq doit être
           triée par rapport à val.
           Fonction à utiliser avec Python version < 10
           Arguments:
           - seq: séquence (list, tuple) triée selon val
           - val: valeur à rechercher, incluse dans l'élément de seq
           - key: fonction permettant de retrouver val dans l'élément de seq
           Retour:
           - si ok==True: ib est l'indice du 1er élément de seq contenant val, et
             ih celui du dernier. Si ib==ih, val est unique dans seq.
           - si ok==False: ib est l'indice d'insertion de l'élément de seq
             contenant val. Dans ce cas, ih (==ib) n'a aucune signication.
        """
        # calcule key pour une séquence simple (retourne l'argument donné)
        key = (lambda x:x) if key is None else key
     
        # calcule l'indice de présence ou d'insertion
        ib, ih = 0, len(seq)
        ib = bisect_left2(seq, val, ib=ib, ih=ih, key=key)
     
        # calcule ok qui représente l'existence ou non de val dans seq
        ok = False if ib >= len(seq) else key(seq[ib]) == val
     
        # calcule en cas de doublon: ih est l'indice du dernier val
        ih = ib
        if ok:
            for ih in range(ib+1, len(seq)):
                if key(seq[ih]) != val:
                    ih -= 1
                    break
     
        return ok, ib, ih
    Même exemple que plus haut:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    seq = [[9,1], [5,2], [7,3], [3,5], [5,5], [1,5], [12,7], [8,8]]
    key = lambda x: x[1]
     
    val = 4 # n'existe pas
    ok, ib, ih = dichots(seq, val, key=key)
    print("valeur:", val, "ib:", ib, "ih", ih, "Existe déjà" if ok else "N'existe pas")
    # affiche: valeur: 4 ib: 3 ih 3 N'existe pas
     
    val = 5 # existe en plusieurs exemplaires
    ok, ib, ih = dichots(seq, val, key=key)
    print("valeur:", val, "ib:", ib, "ih", ih, "Existe déjà" if ok else "N'existe pas")
    # affiche: valeur: 5 ib: 3 ih 5 Existe déjà
    On voit que dans le dernier cas, la valeur 5 est rencontrée 3 fois, à l'indice 3, 4 et 5.


    Bonnes recherches!

  2. #2
    Membre expert
    Homme Profil pro
    Inscrit en
    Octobre 2011
    Messages
    2 886
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Octobre 2011
    Messages : 2 886
    Points : 3 725
    Points
    3 725
    Par défaut
    Merci pour cet article.

  3. #3
    Rédacteur/Modérateur

    Avatar de User
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    8 344
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : France, Ain (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur informatique

    Informations forums :
    Inscription : Août 2004
    Messages : 8 344
    Points : 19 590
    Points
    19 590
    Billets dans le blog
    65
    Par défaut
    Merci à vous

Discussions similaires

  1. Recherche rapide de chaine dans une liste
    Par Esil2008 dans le forum Langage
    Réponses: 3
    Dernier message: 04/11/2008, 14h03
  2. recherche par attribut dans une liste d'objet
    Par Jacobian dans le forum Débuter avec Java
    Réponses: 1
    Dernier message: 28/05/2008, 21h11
  3. Recherche sur 2 elements dans une liste box.
    Par molarisapa dans le forum Access
    Réponses: 2
    Dernier message: 29/05/2006, 18h43
  4. Recherche d'un élément dans une liste triée (vitesse)
    Par Rodrigue dans le forum Algorithmes et structures de données
    Réponses: 9
    Dernier message: 18/05/2006, 09h23
  5. [LG]Tri par insertion dans une liste chainée
    Par mister_dsg dans le forum Langage
    Réponses: 4
    Dernier message: 18/12/2003, 22h34

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo