web-dev-qa-db-fra.com

Comment trouver efficacement les index des éléments correspondants dans deux listes

Je travaille sur deux grands ensembles de données et ma question est la suivante.

Supposons que j'ai deux listes:

list1 = [A,B,C,D]

list2 = [B,D,A,G]

Comment trouver efficacement l’index correspondant en utilisant Python, autre que O (n2) recherche? Le résultat devrait ressembler à:

matching_index(list1,list2) -> [(0,2),(1,0),(3,1)]

12
Haoran

Sans doublons

Si vos objets sont hashable et que vos listes ne contiennent pas de doublons, vous pouvez créer un index inversé de la première liste, puis parcourir la seconde liste. Ceci ne parcourt chaque liste qu'une seule fois et est donc O(n).

def find_matching_index(list1, list2):

    inverse_index = { element: index for index, element in enumerate(list1) }

    return [(index, inverse_index[element])
        for index, element in enumerate(list2) if element in inverse_index]

find_matching_index([1,2,3], [3,2,1]) # [(0, 2), (1, 1), (2, 0)]

Avec des doublons

Vous pouvez étendre la solution précédente pour prendre en compte les doublons. Vous pouvez suivre plusieurs index avec un set.

def find_matching_index(list1, list2):

    # Create an inverse index which keys are now sets
    inverse_index = {}

    for index, element in enumerate(list1):

        if element not in inverse_index:
            inverse_index[element] = {index}

        else:
            inverse_index[element].add(index)

    # Traverse the second list    
    matching_index = []

    for index, element in enumerate(list2):

        # We have to create one pair by element in the set of the inverse index
        if element in inverse_index:
            matching_index.extend([(x, index) for x in inverse_index[element]])

    return matching_index

find_matching_index([1, 1, 2], [2, 2, 1]) # [(2, 0), (2, 1), (0, 2), (1, 2)]

Malheureusement, ce n'est plus O(n) . Prenons le cas où vous avez entré [1, 1] et [1, 1], la sortie est [(0, 0), (0, 1), (1, 0), (1, 1)]. Ainsi, vu la taille de la sortie, le pire des cas ne peut pas être meilleur que O(n^2).

Bien que cette solution soit toujours O(n) s'il n'y a pas de doublons.

Objets non-lavables

Vient maintenant le cas où vos objets ne sont pas traitables, mais comparables. L'idée ici sera de trier vos listes de manière à préserver l'index d'origine de chaque élément. Ensuite, nous pouvons regrouper des séquences d'éléments égaux pour obtenir des indices correspondants.

Puisque nous utilisons beaucoup groupby et product dans le code suivant, j'ai fait find_matching_index renvoyer un générateur d'efficacité de la mémoire sur de longues listes.

from itertools import groupby, product

def find_matching_index(list1, list2):
    sorted_list1 = sorted((element, index) for index, element in enumerate(list1))
    sorted_list2 = sorted((element, index) for index, element in enumerate(list2))

    list1_groups = groupby(sorted_list1, key=lambda pair: pair[0])
    list2_groups = groupby(sorted_list2, key=lambda pair: pair[0])

    for element1, group1 in list1_groups:
        try:
            element2, group2 = next(list2_groups)
            while element1 > element2:
                (element2, _), group2 = next(list2_groups)

        except StopIteration:
            break

        if element2 > element1:
            continue

        indices_product = product((i for _, i in group1), (i for _, i in group2), repeat=1)

        yield from indices_product

        # In version prior to 3.3, the above line must be
        # for x in indices_product:
        #     yield x

list1 = [[], [1, 2], []]
list2 = [[1, 2], []]

list(find_matching_index(list1, list2)) # [(0, 1), (2, 1), (1, 0)]

Il s'avère que la complexité temporelle ne souffre pas beaucoup. Le tri prend bien sûr O(n log(n)), mais groupby fournit alors des générateurs capables de récupérer tous les éléments en parcourant nos listes deux fois. La conclusion est que notre complexité est principalement liée à la taille de la sortie de product. Donnons donc le meilleur des cas où l’algorithme est O(n log(n)) et le pire des cas, encore une fois O(n^2).

11
Olivier Melançon

Si vos objets ne sont pas haschal, mais peuvent toujours être commandés, vous pouvez envisager d'utiliser sorted pour faire correspondre les deux listes.

En supposant que tous les éléments des deux listes ont une correspondance

Vous pouvez trier les index des listes et jumeler les résultats

indexes1 = sorted(range(len(list1)), key=lambda x: list1[x])
indexes2 = sorted(range(len(list2)), key=lambda x: list2[x])
matches = Zip(indexes1, indexes2)

Si tous les éléments ne correspondent pas, mais qu'il n'y a pas de doublons dans chaque liste

Vous pouvez trier les deux en même temps et conserver les index pendant le tri. Ensuite, si vous attrapez des doublons consécutifs, vous savez qu'ils proviennent de listes différentes

biglist = list(enumerate(list1)) + list(enumerate(list2))
biglist.sort(key=lambda x: x[1])
matches = [(biglist[i][0], biglist[i + 1][0]) for i in range(len(biglist) - 1) if biglist[i][1] == biglist[i + 1][1]]
4
Fred

Une réponse brute-force à ce problème, ne serait-ce que pour valider une solution, est donnée par:

[(xi, xp) for (xi, x) in enumerate(list1) for (xp, y) in enumerate(list2) if x==y]

La manière dont vous devrez optimiser cela dépend en grande partie des volumes de données et de la capacité de mémoire. Il est donc utile de savoir quelle est la taille de ces listes. J'imagine que la méthode dont je discute ci-dessous conviendrait aux listes comportant au moins des millions de valeurs.

Puisque l'accès au dictionnaire est O (1), il semblerait intéressant d'essayer de mapper les éléments de la deuxième liste sur leurs positions. En supposant que le même élément puisse être répété, un collections.defaultdict nous permettra facilement de construire le dict nécessaire.

l2_pos = defaultdict(list)
for (p, k) in enumerate(list2):
    l2_pos[k].append(p)

L'expression l2_pos[k] est maintenant une liste des positions dans list2 auxquelles l'élément k apparaît. Il ne reste plus qu'à jumeler chacun de ceux-ci avec les positions des clés correspondantes dans list1. Le résultat sous forme de liste est

[(p1, p2) for (p1, k) in enumerate(list1) for p2 in l2_pos[k]]

Si ces structures sont grandes, cependant, vous pourriez être mieux servi par une expression génératrice. Pour lier un nom à l'expression dans la compréhension de liste ci-dessus, vous écririez

values = ((p1, p2) for (p1, k) in enumerate(list1) for p2 in l2_pos[k])

Si vous parcourez ensuite values, vous évitez ainsi de créer une liste contenant toutes les valeurs, ce qui réduit la charge de la gestion de la mémoire et du garbage collection de Python, ce qui est quasiment tout le temps système nécessaire pour résoudre votre problème.

Lorsque vous commencez à gérer de gros volumes de données, la compréhension des générateurs peut faire toute la différence entre avoir suffisamment de mémoire pour résoudre votre problème ou non. Dans de nombreux cas, ils ont un net avantage sur la compréhension des listes.

EDIT: Cette technique peut être encore accélérée en utilisant des ensembles plutôt que des listes pour maintenir les positions, à moins que les changements d’ordre ne soient nuisibles. Ce changement est laissé comme un exercice pour le lecteur.

2
holdenweb

L'utilisation d'une dict réduit le temps de recherche et la spécialisation collections.defaultdict peut aider à la comptabilité. L’objectif est une dict dont les valeurs sont les paires d’indexation que vous recherchez. Les valeurs en double remplacent les valeurs précédentes de la liste.

import collections

# make a test list
list1 = list('ABCDEFGHIJKLMNOP')
list2 = list1[len(list1)//2:] + list1[:len(list1)//2]

# Map list items to positions as in: [list1_index, list2_index]
# by creating a defaultdict that fills in items not in list1,
# then adding list1 items and updating with with list2 items. 
list_indexer = collections.defaultdict(lambda: [None, None],
 ((item, [i, None]) for i, item in enumerate(list1)))
for i, val in enumerate(list2):
    list_indexer[val][1] = i

print(list(list_indexer.values()))
0
tdelaney

Voici une approche simple avec un defaultdict.

Donné

import collections as ct


lst1 = list("ABCD")
lst2 = list("BDAG")
lst3 = list("EAB")
str1 = "ABCD"

Code

def find_matching_indices(*iterables, pred=None):
    """Return a list of matched indices across `m` iterables."""
    if pred is None:
        pred = lambda x: x[0]

    # Dict insertion
    dd = ct.defaultdict(list)
    for lst in iterables:                                          # O(m)
        for i, x in enumerate(lst):                                # O(n)
            dd[x].append(i)                                        # O(1)

    # Filter + sort
    vals = (x for x in dd.values() if len(x) > 1)                  # O(n)
    return sorted(vals, key=pred)                                  # O(n log n)

Démo

Trouver des correspondances dans deux listes (par OP):

find_matching_indices(lst1, lst2)
# [[0, 2], [1, 0], [3, 1]]

Trier par un index résultant différent:

find_matching_indices(lst1, lst2, pred=lambda x: x[1])
# [[1, 0], [3, 1], [0, 2]]

Correspondre aux éléments dans plus de deux itérables (de longueur éventuellement variable):

find_matching_indices(lst1, lst2, lst3, str1)
# [[0, 2, 1, 0], [1, 0, 2, 1], [2, 2], [3, 1, 3]]

Détails

Insertion de dictionnaire

Chaque élément est ajouté aux listes de defaultdict. Le résultat ressemble à ceci, qui est ensuite filtré:

defaultdict(list, {'A': [0, 2], 'B': [1, 0], 'C': [2], 'D': [3, 1], 'G': [3]})

À première vue, à partir de la double boucle for, on pourrait être tenté de dire que la complexité temporelle est O (n²). Cependant, la liste des conteneurs dans la boucle externe a une longueur m. La boucle interne traite les éléments de chaque conteneur de longueur n. Je ne suis pas certain de la complexité finale, mais en me basant sur cette réponse , je le soupçonne d’être O (n * m) ou du moins inférieur à O (n²).

Filtrage

Les non-correspondances (listes de longueur 1) sont filtrées et les résultats sont triés (principalement pour les dictés désordonnés dans Python <3.6).

En utilisant l'algorithme timsort via sorted pour trier les valeurs dictées (listes) en fonction d'un index, le pire des cas est O (n log n). Comme l'insertion de clé dict est conservée dans Python 3.6+, les éléments triés au préalable réduisent la complexité O (n).

Globalement, la meilleure complexité temporelle dans un cas est O (n); le pire des cas est O (n log n) si vous utilisez sorted en Python <3.6, sinon c'est O (n * m).

0
pylang