Как эффективно найти индексы совпадающих элементов в двух списках

Я работаю над двумя большими наборами данных, и мой вопрос заключается в следующем.

Предположим, у меня есть два списка:

list1 = [A,B,C,D]

list2 = [B,D,A,G]

как я могу эффективно найти соответствующий индекс, используя Python, кроме O (n2) поиск? Результат должен выглядеть так:

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

5 ответов


без дубликатов

если объекты хэшируются, а списки не имеют дубликатов, можно создать перевернутый индекс первого списка, а затем перейти ко второму списку. Это пересекает каждый список только один раз и, таким образом,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) если нет дубликатов.

не hashable объекты

теперь наступает случай, когда ваши объекты не хэшируются, но сопоставимы. Идея здесь будет сортировать ваши списки таким образом, чтобы сохранить исходный индекс каждый элемент. Затем мы можем сгруппировать последовательности элементов, которые равны, чтобы получить соответствующие индексы.

в виду того что мы делаем тяжелую пользу groupby и product в следующем коде, я сделал 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).


Если ваши объекты не хэшируются, но все еще упорядочиваются, вы можете рассмотреть возможность использования 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]]

один ответ грубой силы на эту проблему, если только для проверки любого решения, дается:

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

как вам придется оптимизировать это во многом зависит от объемов данных и емкости памяти, поэтому может быть полезно некоторое представление о том, насколько велики эти списки. Я бы предположил, что метод, который я обсуждаю ниже, будет хорош для списков с миллионами значений по крайней мере.

поскольку доступ к словарю O (1), казалось бы, стоит попытаться сопоставить элементы во втором списке их позиции. Предполагая, что один и тот же элемент может повторяться, а collections.defaultdict легко позволит нам построить необходимый dict.

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 и сборку мусора, что в значительной степени является накладными расходами в отношении решения вашей проблемы.

когда вы начинаете иметь дело с большими объемами данных, понимание генераторов может означать разница между наличием достаточного объема памяти для решения вашей проблемы или нет. Во многих случаях они имеют явное преимущество перед перечнем.

EDIT: этот метод можно ускорить, используя наборы, а не списки для удержания позиций, если только изменения в заказе не будут вредными. Это изменение оставлено как упражнение для читателя.


С помощью 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()))

вот простой подход с 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. Результат выглядит примерно так, что позже отфильтровано:

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

на первый взгляд, с двойной for петли может возникнуть соблазн сказать, что сложность времени O (n2). Однако список контейнеров во внешнем цикле имеет длину m. Внутренний цикл обрабатывает элементы каждого контейнера длины n. Я не уверен, какова конечная сложность, но на основе ответ, я подозреваю, что это O(n*m) или, по крайней мере, ниже O (n2).

фильтрация

не совпадения (списки длины 1) отфильтровываются, и результаты сортируются (в основном для неупорядоченных диктов в Python

С помощью timsort через sorted для сортировки значений dict (списки) по некоторому индексу худший случай-O (N log n). Поскольку вставка ключа dict сохраняется в Python 3.6+, предварительно отсортированные элементы уменьшают сложность O (n).

в целом, в лучшем случае сложность времени O(n); в худшем случае O (N log n) при использовании sorted в Python