Проведя некоторое исследование, я обнаружил, что проверка, являются ли два списка непересекающимися, выполняется в O (n + m) , где n и m - это длины списков (см. здесь ). Идея состоит в том, что вставка и поиск элементов выполняются в постоянное время для хэш-карт. Следовательно, для вставки всех элементов из первого списка в хэш-карту требуется O (n) операций, а проверка для каждого элемента во втором списке, находится ли он уже в хэш-карте, занимает O (m). операций. Поэтому решения, основанные на сортировке, которые выполняются в O (n log (n) + m log (m)) , не являются оптимальными асимптотически.
Хотя решения @Divakar очень эффективны во многих случаях, они менее эффективны, если второе измерение велико. Тогда решение на основе хеш-карт лучше подходит. Я реализовал это следующим образом в cython :
import numpy as np
cimport numpy as np
import cython
from libc.math cimport NAN
from libcpp.unordered_map cimport unordered_map
np.import_array()
@cython.boundscheck(False)
@cython.wraparound(False)
def get_common_element2d(np.ndarray[double, ndim=2] arr1,
np.ndarray[double, ndim=2] arr2):
cdef np.ndarray[double, ndim=1] result = np.empty(arr1.shape[0])
cdef int dim1 = arr1.shape[1]
cdef int dim2 = arr2.shape[1]
cdef int i, j
cdef unordered_map[double, int] tmpset = unordered_map[double, int]()
for i in range(arr1.shape[0]):
for j in range(dim1):
# insert arr1[i, j] as key without assigned value
tmpset[arr1[i, j]]
for j in range(dim2):
# check whether arr2[i, j] is in tmpset
if tmpset.count(arr2[i,j]):
result[i] = arr2[i,j]
break
else:
result[i] = NAN
tmpset.clear()
return result
Я создал контрольные примеры следующим образом:
import numpy as np
import timeit
from itertools import starmap
from mycythonmodule import get_common_element2d
m, n = 3000, 3000
a = np.random.rand(m, n)
b = np.random.rand(m, n)
for i, row in enumerate(a):
if np.random.randint(2):
common = np.random.choice(row, 1)
b[i][np.random.choice(np.arange(n), np.random.randint(min(n,20)), False)] = common
# we need to copy the arrays on each test run, otherwise they
# will remain sorted, which would bias the results
%timeit [set(aa).intersection(bb) for aa, bb in zip(a.copy(), b.copy())]
# returns 3.11 s ± 56.8 ms
%timeit list(starmap(np.intersect1d, zip(a.copy(), b.copy)))
# returns 1.83 s ± 55.4
# test sorting method
# divakarsMethod1 is the appraoch #1 in @Divakar's answer
%timeit divakarsMethod1(a.copy(), b.copy())
# returns 1.88 s ± 18 ms
# test hash map method
%timeit get_common_element2d(a.copy(), b.copy())
# returns 1.46 s ± 22.6 ms
Эти результаты указывают на то, что наивный подход на самом деле лучше, чем некоторые векторизованные версии. Однако векторизованные алгоритмы разыгрывают свои сильные стороны, если учитывать много строк с меньшим числом столбцов (другой вариант использования). В этих случаях векторизованные подходы более чем в 5 раз быстрее, чем простой метод, и метод сортировки оказывается наилучшим.
Вывод: Я пойду с версией Cython на основе HashMap, потому что она является одним из наиболее эффективных вариантов в обоих случаях использования. Если бы мне сначала пришлось настроить Cython, я бы использовал метод сортировки.