在表示数字的字符串集合中查找最接近的匹配项

时间:2009-09-17 13:33:42

标签: python search

我有一个按文本格式排序的日期时间列表。每个条目的格式为'2009-09-10T12:00:00'。

我想找到最接近目标的条目。我需要做的搜索次数比搜索次数多得多。

我可以将每个条目更改为数字,然后以数字方式搜索(例如these方法),但这似乎是多余的努力。

有没有比这更好的方法:

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 个答案:

答案 0 :(得分:4)

您需要标准库中的bisect module。它将执行二进制搜索,并告诉您将新值插入已排序列表的正确插入点。这是一个示例,它将在列表中打印将插入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

答案 1 :(得分:4)

不推荐“复制和粘贴编码”(将bisect的源代码添加到您的代码中),因为它会带来各种各样的成本(许多额外的源代码供您测试和维护,困难)处理你复制的上游代码中的升级等等;);重用标准库模块的最佳方法是导入它们并使用它们。

但是,要做一次将字典转换为有意义的可比较条目,就是O(N),即(尽管传递的每一步都很简单)最终会淹没搜索的O(log N)时间。由于bisect无法支持像key=那样的sort密钥提取程序,这种困境的解决方案是什么 - 如何通过导入和调用重用bisect,而无需初步O(N)步骤......?

引用here,解决方案是David Wheeler的名言,“计算机科学中的所有问题都可以通过另一层次的间接解决”。考虑例如....:

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类是额外的代码 - 但是,它可以重复使用二进制搜索的所有任务,每个dict中的一个条目排序的dicts列表,所以在你的“容器 - 实用程序后面的技巧”中提供它可以提供良好的回报投资。

主搜索循环非常多。对于将两个条目(下面的一个和目标上方的一个)和目标转换为秒数的次要任务,再次考虑更高重用的方法,即:

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更快) - 但是,无论有多少条目,您每次搜索只需执行三次转换在你的dicts列表中,所以不清楚这个性能问题是否密切相关。同时,使用simpler,您只需编写,测试和维护一行简单的代码,而dttosecs是十几行;鉴于这个比例,在大多数情况下(即排除绝对瓶颈),我更愿意simpler。重要的是要注意两种方法和它们之间的权衡,以确保明智地做出选择。

答案 2 :(得分:2)

import bisect

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

答案 3 :(得分:1)

首先,改为此。

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]

这似乎不太“努力”。这也不取决于您的时间戳格式正确。您可以更改为任何时间戳格式,并确保它始终有效。