web-dev-qa-db-ja.com

2つのリストで一致する要素のインデックスを効率的に見つける方法

私は2つの大きなデータセットに取り組んでおり、私の質問は次のとおりです。

2つのリストがあるとします。

_list1 = [A,B,C,D]_

_list2 = [B,D,A,G]_

O(n以外のPythonを使用して、一致するインデックスを効率的に見つけるにはどうすればよいですか?2)検索?結果は次のようになります。

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

13
Haoran

重複なし

オブジェクトがハッシュ可能で、リストに重複がない場合は、最初のリストの逆インデックスを作成してから、2番目のリストをトラバースできます。これは各リストを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)のままです。

ハッシュできないオブジェクト

ここで、オブジェクトがハッシュ可能ではなく、比較可能な場合があります。ここでの考え方は、各要素のOriginインデックスを保持する方法でリストを並べ替えることです。次に、一致する要素のシーケンスをグループ化して、一致するインデックスを取得できます。

次のコードでは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は、リストを2回だけトラバースすることですべての要素を回復できるジェネレーターを提供します。結論は、私たちの複雑さは主にproductの出力のサイズによって制限されるということです。したがって、アルゴリズムがO(n log(n))である最良のケースと、再びO(n^2)である最悪のケースが与えられます。

11

オブジェクトがハッシュ可能ではないが順序付け可能な場合は、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]]
4
Fred

解決策を検証する以外に理由がない場合、この問題に対する強引な答えの1つは次のように与えられます。

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

これをどのように最適化する必要があるかは、データボリュームとメモリ容量に大きく依存するため、これらのリストがどれほど大きいかを理解しておくと役立ちます。以下で説明する方法は、少なくとも数百万の値を持つリストに適していると思います。

辞書へのアクセスはO(1)なので、2番目のリストの要素をそれらの位置にマッピングすることは価値があるように思えます。同じ要素を繰り返すことができると仮定すると、collections.defaultdictを使用すると、必要な辞書を簡単に作成できます。

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

l2_pos[k]は、要素kが発生するlist2の位置のリストになりました。これらのそれぞれを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のメモリ管理とガベージコレクションの負荷が軽減されます。これは、問題の解決に関する限り、ほとんどすべてのオーバーヘッドです。

大量のデータを処理し始めるとき、ジェネレーターを理解することは、問題を解決するために十分なメモリを持っているかどうかの違いを意味します。多くの場合、リスト内包表記よりも明らかに有利です。

編集:順序の変更が有害でない限り、位置を保持するためにリストではなくセットを使用することにより、この手法をさらに加速できます。この変更は、読者のための演習として残されています。

2
holdenweb

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

以下は、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)

デモ

2つのリストで一致を検索(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]]

3つ以上の反復可能アイテム(オプションで可変長)のアイテムに一致:

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(n²)であると言いたくなるかもしれません。ただし、外側のループ内のコンテナーのリストの長さはmです。内部ループは、長さnの各コンテナーの要素を処理します。最終的な複雑さが何かはわかりませんが、 この答え に基づいて、O(n * m)または少なくともO(n²)未満であると思います。

フィルタリング

一致しないもの(長さ1のリスト)はフィルターで除外され、結果がソートされます(主にPython <3.6の無秩序な辞書用)。

sortedを介して timsort アルゴリズムを使用して、dict値(リスト)をインデックスでソートすると、最悪のケースはO(n log n)になります。 dictキーの挿入はPython 3.6+)で保持されるため、事前にソートされたアイテムは複雑度O(n)を減らします。

全体として、最良の場合の時間の複雑さはO(n)です。 Python <3.6でsortedを使用する場合、最悪のケースはO(n log n)です。それ以外の場合は、O(n * m)です。

0
pylang