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)]
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)]
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.
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)
.
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.
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)
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]]
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.
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()))
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).