如何有效地找到两个列表中匹配元素的索引

Posted

技术标签:

【中文标题】如何有效地找到两个列表中匹配元素的索引【英文标题】:How to efficiently find the indices of matching elements in two lists 【发布时间】:2018-08-21 04:44:48 【问题描述】:

我正在处理两个大型数据集,我的问题如下。

假设我有两个列表:

list1 = [A,B,C,D]

list2 = [B,D,A,G]

除了 O(n2) 搜索之外,如何使用 Python 有效地找到匹配索引?结果应如下所示:

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

【问题讨论】:

这些元素是否是可散列项,例如可以用作dict 中的键的字符串? 是的,这些元素是可散列的。现在我拥有的是:[i for i, item in enumerate(list(df1)) if item in set(list(df2))]。这给出了 df1 的匹配索引列表,但我想知道精确匹配的位置在哪里。非常感谢! 我这里没有具体的答案,但是 numpy 擅长这种事情。 【参考方案1】:

没有重复

如果您的对象是可散列的并且您的列表没有重复项,您可以创建第一个列表的倒排索引,然后遍历第二个列表。这只会遍历每个列表一次,因此是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)]

有重复

您可以扩展之前的解决方案以解决重复问题。您可以使用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)]

不幸的是,这不再是 O(n)。考虑输入[1, 1][1, 1] 的情况,输出为[(0, 0), (0, 1), (1, 0), (1, 1)]。因此,根据输出的大小,最坏的情况不会比O(n^2) 好。

虽然,如果没有重复,这个解决方案仍然是O(n)

不可散列的对象

现在出现了您的对象不可散列但可比较的情况。这里的想法是以保留每个元素的原始索引的方式对列表进行排序。然后我们可以对相等的元素序列进行分组以获得匹配的索引。

由于我们在以下代码中大量使用了groupbyproduct,因此我让find_matching_index 返回了一个生成器,以提高长列表的内存效率。

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

事实证明,时间复杂度并没有受到太大影响。排序当然需要O(n log(n)),但是groupby 提供的生成器可以通过只遍历我们的列表两次来恢复所有元素。结论是我们的复杂性主要受product 的输出大小的限制。因此给出了算法为O(n log(n)) 的最佳情况和再次为O(n^2) 的最坏情况。

【讨论】:

非常感谢您的帮助。是的,这正是我正在努力解决的问题。 有没有办法解释重复值?例如:list1 = [A,B,C,D,E] list2 = [B,A,D,A,G] ->[(0,1),(0,3),(1,0), (3,2)]?【参考方案2】:

如果您的对象不可散列,但仍可排序,您可能需要考虑使用 sorted 来匹配两个列表

假设两个列表中的所有元素都匹配

您可以对列表索引进行排序并配对结果

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

如果不是所有元素都匹配,但每个列表中没有重复项

您可以同时对两者进行排序,并在排序时保留索引。然后,如果您发现任何连续的重复项,您就知道它们来自不同的列表

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]]

【讨论】:

嗯,这是 O(n log(n)) 虽然 好吧,我看到每个人都在使用 dicts,所以想给桌子带来一些不同的东西 :) 当然,毕竟如果对象不可散列,这会很有用!你应该提到这一点。 这实际上是一个很好的理由,我没有考虑过大声笑 非常感谢。实际上,这对于不可散列的对象非常有用。谢谢你的想法!【参考方案3】:

这个问题的一个蛮力答案,如果没有其他原因,只是为了验证任何解决方案,由以下给出:

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

如何优化这在很大程度上取决于数据量和内存容量,因此了解这些列表的大小可能会有所帮助。我想我在下面讨论的方法至少适用于具有数百万个值的列表。

由于字典访问是 O(1),因此似乎值得尝试将第二个列表中的元素映射到它们的位置。假设可以重复相同的元素,collections.defaultdict 将很容易让我们构造必要的字典。

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

表达式l2_pos[k] 现在是list2 中出现元素k 的位置列表。只需将它们中的每一个与list1 中相应键的位置配对即可。列表形式的结果是

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

但是,如果这些结构很大,则生成器表达式可能会更好地为您服务。要将名称绑定到上面列表推导中的表达式,您可以编写

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

如果您随后遍历values,您可以避免创建包含所有值的列表的开销,从而减少 Python 的内存管理和垃圾收集的负载,就解决您的问题而言,这几乎是所有开销。

当您开始处理大量数据时,了解生成器可能意味着是否有足够的内存来解决您的问题。在许多情况下,它们比列表推导具有明显的优势。

编辑: 这种技术可以通过使用集合而不是列表来保持位置来进一步加速,除非排序的变化会有害。此更改留作读者练习。

【讨论】:

【参考方案4】:

使用dict 可减少查找时间,而collections.defaultdict 专业化有助于记账。目标是 dict,其值是您所追求的索引对。重复值会覆盖列表中较早的值。

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

【讨论】:

【参考方案5】:

这是一个简单的方法,使用defaultdict

给定

import collections as ct


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

代码

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)

演示

在两个列表中查找匹配项(每个 OP):

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

按不同的结果索引排序:

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

匹配两个以上可迭代项(长度可选):

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

详情

字典插入

每个项目都附加到默认字典的列表中。结果看起来像这样,稍后过滤:

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

乍一看,从双 for 循环中,人们可能会说时间复杂度为 O(n²)。但是,外循环中的容器列表的长度为m。内部循环处理每个长度为n 的容器的元素。我不确定最终的复杂度是多少,但基于this answer,我怀疑它是 O(n*m) 或至少低于 O(n²)。

过滤

过滤掉不匹配项(长度为 1 的列表),并对结果进行排序(主要针对 Python

通过sorted 使用timsort 算法按某个索引对dict 值(列表)进行排序,最坏的情况是O(n log n)。由于在 Python 3.6+ 中保留了 dict 键插入,因此预排序项降低了复杂度 O(n)。

总体来说,最佳情况时间复杂度是O(n);如果在 Python sorted,最坏的情况是 O(n log n),否则是 O(n*m)。

【讨论】:

以上是关于如何有效地找到两个列表中匹配元素的索引的主要内容,如果未能解决你的问题,请参考以下文章

如何使用javascript使两个选定的索引在pdf下拉列表中匹配

如何通过索引从一个非常大的列表中有效地删除元素?

在两个容器 C++ 中搜索匹配项

如何通过拖放同步两个列表框元素?

如何找到已排序容器的匹配元素的索引?

分治算法在子列表中找到两个匹配的元素