Нахождение ближайшего совпадения в коллекции строк, представляющих числа - PullRequest
2 голосов
/ 17 сентября 2009

У меня есть отсортированный список дат и времени в текстовом формате. Формат каждой записи: «2009-09-10T12: 00: 00».

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

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

Есть ли лучший способ, чем этот:

def mid(res, target): 
#res is a list of entries, sorted by dt (dateTtime) 
#each entry is a dict with a dt and some other info
    n = len(res)
    low = 0
    high = n-1

    # find the first res greater than target
    while low < high:
        mid = (low + high)/2
        t = res[int(mid)]['dt']
        if t < target:
            low = mid + 1
        else:
            high = mid

    # check if the prior value is closer
    i = max(0, int(low)-1)
    a = dttosecs(res[i]['dt'])
    b = dttosecs(res[int(low)]['dt'])
    t = dttosecs(target)
    if abs(a-t) < abs(b-t):
        return int(low-1)
    else:
        return int(low)

import time
def dttosecs(dt):
    # string to seconds since the beginning
    date,tim = dt.split('T')
    y,m,d = date.split('-')
    h,mn,s = tim.split(':')
    y = int(y)
    m = int(m)
    d = int(d)
    h = int(h)
    mn = int(mn)
    s = min(59,int(float(s)+0.5)) # round to neatest second
    s = int(s)
    secs = time.mktime((y,m,d,h,mn,s,0,0,-1))
    return secs

Ответы [ 4 ]

4 голосов
/ 17 сентября 2009

«Копирование и вставка кодирования» (получение исходных кодов bisect в ваш код) не рекомендуется, так как оно несет все виды затрат в будущем (много дополнительного исходного кода для тестирования и сопровождения, трудности, связанные с обновления в исходном коде, который вы скопировали и т. д. и т. п.); лучший способ повторно использовать стандартные библиотечные модули - просто импортировать их и использовать их.

Однако, чтобы сделать один проход, преобразовать словари в значимо сравнимые записи - это O (N), что (хотя каждый шаг прохода прост) в конечном итоге затмевает время O (log N) собственно поиска. Поскольку bisect не может поддерживать key= средство извлечения ключей, как sort, то как решить эту дилемму - как вы можете повторно использовать bisect при импорте и вызове без предварительного шага O (N). .

Как указано здесь , решение заключается в известной поговорке Дэвида Уилера: «Все проблемы в информатике могут быть решены с помощью другого уровня косвенности». Рассмотрим, например, ....:

import bisect

listofdicts = [
  {'dt': '2009-%2.2d-%2.2dT12:00:00' % (m,d) }
  for m in range(4,9) for d in range(1,30)
  ]

class Indexer(object):
  def __init__(self, lod, key):
    self.lod = lod
    self.key = key
  def __len__(self):
    return len(self.lod)
  def __getitem__(self, idx):
    return self.lod[idx][self.key]


lookfor = listofdicts[len(listofdicts)//2]['dt']

def mid(res=listofdicts, target=lookfor):
    keys = [r['dt'] for r in res]
    return res[bisect.bisect_left(keys, target)]

def midi(res=listofdicts, target=lookfor):
    wrap = Indexer(res, 'dt')
    return res[bisect.bisect_left(wrap, target)]

if __name__ == '__main__':
  print '%d dicts on the list' % len(listofdicts)
  print 'Looking for', lookfor
  print mid(), midi()
assert mid() == midi()

Вывод (просто запустив этот indexer.py в качестве проверки, затем с timeit, двумя способами):

$ python indexer.py 
145 dicts on the list
Looking for 2009-06-15T12:00:00
{'dt': '2009-06-15T12:00:00'} {'dt': '2009-06-15T12:00:00'}
$ python -mtimeit -s'import indexer' 'indexer.mid()'
10000 loops, best of 3: 27.2 usec per loop
$ python -mtimeit -s'import indexer' 'indexer.midi()'
100000 loops, best of 3: 9.43 usec per loop

Как видите, даже в скромной задаче с 145 записями в списке подход с косвенной адресацией может иметь производительность, которая в три раза лучше, чем метод "прохода извлечения ключа". Поскольку мы сравниваем O (N) с O (log N), преимущество подхода косвенности растет без границ с ростом N. (Для очень малого N более высокие мультипликативные константы из-за косвенности делают подход извлечения ключей более быстрым, но вскоре он превосходит разницу больших-O). Следует признать, что класс Indexer - это дополнительный код, однако его можно повторно использовать во ВСЕХ задачах двоичного поиска в списке диктов, отсортированных по одной записи в каждом из них, поэтому наличие его в «контейнерах утилит-контейнеров» дает хороший результат. инвестиции.

Так много для основного цикла поиска. Для вторичной задачи преобразования двух записей (одна чуть ниже, а другая чуть выше цели) и цели в количество секунд, снова рассмотрим подход с более высоким повторным использованием, а именно:

import time

adt = '2009-09-10T12:00:00'

def dttosecs(dt=adt):
    # string to seconds since the beginning
    date,tim = dt.split('T')
    y,m,d = date.split('-')
    h,mn,s = tim.split(':')
    y = int(y)
    m = int(m)
    d = int(d)
    h = int(h)
    mn = int(mn)
    s = min(59,int(float(s)+0.5)) # round to neatest second
    s = int(s)
    secs = time.mktime((y,m,d,h,mn,s,0,0,-1))
    return secs

def simpler(dt=adt):
  return time.mktime(time.strptime(dt, '%Y-%m-%dT%H:%M:%S'))

if __name__ == '__main__':
  print adt, dttosecs(), simpler()
assert dttosecs() == simpler()

Здесь нет никакого преимущества в производительности для подхода повторного использования (действительно, и наоборот, dttosecs быстрее) - но тогда вам нужно всего лишь выполнить три преобразования за поиск, независимо от того, сколько записей включено. ваш список диктов, так что не ясно, является ли эта проблема производительности уместной. Между тем, с simpler вам нужно только написать, протестировать и поддерживать одну простую строку кода, тогда как dttosecs - это дюжина строк; учитывая это соотношение, в большинстве ситуаций (т.е. без учета абсолютных узких мест) я бы предпочел simpler. Важно знать оба подхода и компромиссы между ними, чтобы гарантировать, что выбор сделан мудро.

4 голосов
/ 17 сентября 2009

Требуется модуль bisect из стандартной библиотеки.Он выполнит бинарный поиск и сообщит вам правильную точку вставки нового значения в уже отсортированный список.Вот пример, который напечатает место в списке, куда будет вставлен target:

from bisect import bisect
dates = ['2009-09-10T12:00:00', '2009-09-11T12:32:00', '2009-09-11T12:43:00']
target = '2009-09-11T12:40:00'
print bisect(dates, target)

Оттуда вы можете просто сравнить с вещью до и после точки вставки, которая в этом случаеdates[i-1] и dates[i], чтобы увидеть, какой из них ближе всего к вашему target.

2 голосов
/ 17 сентября 2009
import bisect

def mid(res, target):
    keys = [r['dt'] for r in res]
    return res[bisect.bisect_left(keys, target)]
1 голос
/ 17 сентября 2009

Сначала перейдите на это.

import datetime
def parse_dt(dt):
    return datetime.strptime( dt, "%Y-%m-%dT%H:%M:%S" )

Это устраняет большую часть «усилий».

Считайте это поиском.

def mid( res, target ):
    """res is a list of entries, sorted by dt (dateTtime) 
       each entry is a dict with a dt and some other info
    """
    times = [ parse_dt(r['dt']) for r in res ]
    index= bisect( times, parse_dt(target) )
    return times[index]

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...