Обновление. После сравнительного анализа двух подходов в этом ответе второй подход значительно лучше, до такой степени, что его следует почти отдавать предпочтение.
Следующий подход одинаково обрабатывает n-измерения:
class NearestDict(dict):
def __init__(self, ndims):
super(NearestDict, self).__init__()
self.ndims = ndims
# Enforce dimensionality
def __setitem__(self, key, val):
if not isinstance(key, tuple): key = (key,)
if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
super(NearestDict, self).__setitem__(key, val)
@staticmethod
def __dist(ka, kb):
assert len(ka) == len(kb)
return sum((ea-eb)**2 for (ea, eb) in zip(ka, kb))
# Helper method and might be of use
def nearest_key(self, key):
if not isinstance(key, tuple): key = (key,)
nk = min((k for k in self), key=lambda k: NearestDict.__dist(key, k))
return nk
def __missing__(self, key):
if not isinstance(key, tuple): key = (key,)
if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
return self[self.nearest_key(key)]
Демо:
a = NearestDict(1)
a[1] = 100
a[55] = 101
a[127] = 102
print a[20] # 100
print a[58] # 100
print a[167] # 102
print a.nearest_key(20) # (1,)
print a.nearest_key(58) # (55,)
print a.nearest_key(127) # (127,)
b = NearestDict(2)
b[90, 1] = 100
b[90, 55] = 101
b[90, 127] = 102
b[70, 1] = 40
b[70, 45] = 41
b[70, 107] = 42
print b[73, 40] # 41
print b.nearest_key((73,40)) # (70, 45)
Обратите внимание, что если ключ существует, поиск выполняется не медленнее, чем стандартный поиск по словарю. Если ключ не существует, вы вычисляете расстояние между каждым существующим ключом. Ничего не кэшируется, хотя, я полагаю, вы могли бы это исправить.
Изменить:
Из подхода, предложенного Kasra answer, следующий подход реализует тот же класс, что и выше, с использованием scipy cKDTree
:
Обратите внимание, что существует дополнительный необязательный аргумент regenOnAdd
, который позволит вам отложить (повторное) построение KDTree до тех пор, пока вы не завершите (большую часть) вставок:
from scipy.spatial import cKDTree
class KDDict(dict):
def __init__(self, ndims, regenOnAdd=False):
super(KDDict, self).__init__()
self.ndims = ndims
self.regenOnAdd = regenOnAdd
self.__keys = []
self.__tree = None
self.__stale = False
# Enforce dimensionality
def __setitem__(self, key, val):
if not isinstance(key, tuple): key = (key,)
if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
self.__keys.append(key)
self.__stale = True
if self.regenOnAdd: self.regenTree()
super(KDDict, self).__setitem__(key, val)
def regenTree(self):
self.__tree = cKDTree(self.__keys)
self.__stale = False
# Helper method and might be of use
def nearest_key(self, key):
if not isinstance(key, tuple): key = (key,)
if self.__stale: self.regenTree()
_, idx = self.__tree.query(key, 1)
return self.__keys[idx]
def __missing__(self, key):
if not isinstance(key, tuple): key = (key,)
if len(key) != self.ndims: raise KeyError("key must be %d dimensions" % self.ndims)
return self[self.nearest_key(key)]
Вывод такой же, как и в описанном выше подходе.
Результаты сравнения
Чтобы понять производительность трех подходов (NearestDict
, KDDict(True)
(регенерация при вставке) и KDDict(False)
(отложенная регенерация)), я провел их краткий бенчмаркинг.
Я провел 3 разных теста. Параметры, которые оставались неизменными во всех тестах:
- Количество тестовых итераций. Я выполнил каждый тест 5 раз и занял минимальное время. (Примечание
timeit.repeat
по умолчанию равно 3).
- Границы точек: я сгенерировал целочисленные ключевые точки в диапазоне 0 ‹= x ‹ 1000
- Количество поисковых запросов. Я замерял время вставок и поисковых запросов отдельно. Три приведенных ниже теста используют 10 000 поисковых запросов.
В первом тесте использовались ключи 4 измерений и 1000 вставок.
{'NDIMS': 4, 'NITER': 5, 'NELEMS': 1000, 'NFINDS': 10000, 'DIM_LB': 0, 'DIM_UB': 1000, 'SCORE_MUL': 100}
insert::NearestDict 0.125
insert::KDDict(regen) 35.957
insert::KDDict(defer) 0.174
search::NearestDict 2636.965
search::KDDict(regen) 49.965
search::KDDict(defer) 51.880
Во втором тесте использовались ключи 4 размеров и 100 вставок. Я хотел варьировать количество вставок, чтобы увидеть, насколько хорошо работают два подхода при различной плотности словаря.
{'NDIMS': 4, 'NITER': 5, 'NELEMS': 100, 'NFINDS': 10000, 'DIM_LB': 0, 'DIM_UB': 1000, 'SCORE_MUL': 100}
insert::NearestDict 0.013
insert::KDDict(regen) 0.629
insert::KDDict(defer) 0.018
search::NearestDict 247.920
search::KDDict(regen) 44.523
search::KDDict(defer) 44.718
В третьем тесте использовалось 100 вставок (как и во втором тесте), но 12 измерений. Я хотел посмотреть, как работают подходы по мере увеличения ключевой размерности.
{'NDIMS': 12, 'NITER': 5, 'NELEMS': 100, 'NFINDS': 10000, 'DIM_LB': 0, 'DIM_UB': 1000, 'SCORE_MUL': 100}
insert::NearestDict 0.013
insert::KDDict(regen) 0.722
insert::KDDict(defer) 0.017
search::NearestDict 405.092
search::KDDict(regen) 49.046
search::KDDict(defer) 50.601
Обсуждение
KDDict
с непрерывной регенерацией (KDDict(True)
) либо немного быстрее (при поиске), либо значительно медленнее (при вставке). По этой причине я не буду обсуждать это и сосредоточусь на NearestDict
и KDDict(False)
, которые теперь называются просто KDDict
.
Результаты неожиданно оказались в пользу KDDict с отложенной регенерацией.
Для вставки во всех случаях KDDict работал немного хуже, чем NearestDict. Этого следовало ожидать из-за дополнительной операции добавления списка.
Для поиска во всех случаях KDDict работал значительно лучше, чем NearestDict.
По мере уменьшения разреженности словаря / увеличения плотности производительность NearestDict снижалась в гораздо большей степени, чем KDDict. При переходе от 100 ключей к 1000 ключам время поиска NearestDict увеличилось в 9,64 раза, а время поиска KDDict увеличилось всего в 0,16 раза.
По мере увеличения размерности словаря производительность NearestDict снижалась в большей степени, чем KDDict. При переходе от 4 к 12 измерениям время поиска NearestDict увеличилось на 0,64x, в то время как время поиска KDDict увеличилось только на 0,13x.
В свете этого и относительно равной сложности двух классов, если у вас есть доступ к инструментарию scipy, настоятельно рекомендуется использовать подход KDDict
.
person
jedwards
schedule
17.03.2015
dict
. И доступ к словарю может осуществляться 100 раз в секунду с помощьюa[i, j]
, с помощью0<i<128
,0<j<128
. Это должно работать наRaspberry Pi
и быть очень легким;) - person Basj   schedule 17.03.2015for
в своем коде, ответ Тимоти подойдет, но цикл все еще существует. Если вам нужна меньшая сложность, вам придется рассмотреть альтернативные контейнеры (см. методы разделения пространства), которые, как правило, более затратны для однократного создания, но их поиск выполняется быстрее. - person Francis Colas   schedule 17.03.2015