Ответ 1
import heapq, itertools
def intersect(*its):
for key, values in itertools.groupby(heapq.merge(*its)):
if len(list(values)) == len(its):
yield key
>>> list(intersect(*postings))
[100, 322]
Вот, казалось бы, простая проблема: учитывая список итераторов, которые приводят последовательности целых чисел в порядке возрастания, напишите сжатый генератор, который дает только целые числа, которые появляются в каждой последовательности.
Прочитав несколько статей прошлой ночью, я решил взломать полностью минимальный полнотекстовый индекс в Python, как показано здесь (хотя это версия довольно старая).
Моя проблема связана с функцией search()
, которая должна проходить через каждый список проводки и отображать только идентификаторы документов, которые отображаются в каждом списке. Как видно из приведенной выше ссылки, моя текущая нерекурсивная "рабочая" попытка ужасна.
Пример:
postings = [[1, 100, 142, 322, 12312],
[2, 100, 101, 322, 1221],
[100, 142, 322, 956, 1222]]
Должен выдать:
[100, 322]
Для этого существует хотя бы одно изящное решение для рекурсивных функций, но я хотел бы избежать этого, если это возможно. Тем не менее, решение, включающее вложенные выражения генератора, itertools
злоупотребления или любой другой вид гольф-кода, более чем приветствуется.: -)
Должна быть предусмотрена возможность для функции требовать столько шагов, сколько есть элементов в самом маленьком списке, и не всасывать весь набор целых чисел в память. В будущем эти списки могут считываться с диска и больше, чем доступная оперативная память.
За последние 30 минут у меня появилась идея на кончике моего языка, но я не могу понять ее код. Помните, это просто для удовольствия!
import heapq, itertools
def intersect(*its):
for key, values in itertools.groupby(heapq.merge(*its)):
if len(list(values)) == len(its):
yield key
>>> list(intersect(*postings))
[100, 322]
def postings(posts):
sets = (set(l) for l in posts)
return sorted(reduce(set.intersection, sets))
... вы можете попробовать и воспользоваться тем фактом, что списки упорядочены, но так как сокращение, выражения генератора и набор все реализованы на C, вам, вероятно, будет трудно работать лучше, чем выше, с логикой реализован в python.
Это решение вычислит пересечение ваших итераторов. Он работает, продвигая итераторы один шаг за раз и ищет одинаковое значение во всех них. При обнаружении таких значений даются - это делает функцию intersect
самой генератором.
import operator
def intersect(sequences):
"""Compute intersection of sequences of increasing integers.
>>> list(intersect([[1, 100, 142, 322, 12312],
... [2, 100, 101, 322, 1221],
... [100, 142, 322, 956, 1222]]))
[100, 322]
"""
iterators = [iter(seq) for seq in sequences]
last = [iterator.next() for iterator in iterators]
indices = range(len(iterators) - 1)
while True:
# The while loop stops when StopIteration is raised. The
# exception will also stop the iteration by our caller.
if reduce(operator.and_, [l == last[0] for l in last]):
# All iterators contain last[0]
yield last[0]
last = [iterator.next() for iterator in iterators]
# Now go over the iterators once and advance them as
# necessary. To stop as soon as the smallest iterator is
# exhausted we advance each iterator only once per iteration
# in the while loop.
for i in indices:
if last[i] < last[i+1]:
last[i] = iterators[i].next()
if last[i] > last[i+1]:
last[i+1] = iterators[i+1].next()
Если это действительно длинные (или даже бесконечные) последовательности, и вы не хотите заранее загружать все в набор, вы можете реализовать это с помощью 1-позиционного окна на каждом итераторе.
EndOfIter = object() # Sentinel value
class PeekableIterator(object):
def __init__(self, it):
self.it = it
self._peek = None
self.next() # pump iterator to get first value
def __iter__(self): return self
def next(self):
cur = self._peek
if cur is EndOfIter:
raise StopIteration()
try:
self._peek = self.it.next()
except StopIteration:
self._peek = EndOfIter
return cur
def peek(self):
return self._peek
def contained_in_all(seqs):
if not seqs: return # No items
iterators = [PeekableIterator(iter(seq)) for seq in seqs]
first, rest = iterators[0], iterators[1:]
for item in first:
candidates = list(rest)
while candidates:
if any(c.peek() is EndOfIter for c in candidates): return # Exhausted an iterator
candidates = [c for c in candidates if c.peek() < item]
for c in candidates: c.next()
# Out of loop if first item in remaining iterator are all >= item.
if all(it.peek() == item for it in rest):
yield item
Использование:
>>> print list(contained_in_all(postings))
[100, 322]
Как насчет этого:
import heapq
def inalliters(iterators):
heap=[(iterator.next(),iterator) for iterator in iterators]
heapq.heapify(heap)
maximal = max(heap)[0]
while True:
value,iterator = heapq.heappop(heap)
if maximal==value: yield value
nextvalue=iterator.next()
heapq.heappush(heap,(nextvalue,iterator))
maximal=max(maximal,nextvalue)
postings = [iter([1, 100, 142, 322, 12312]),
iter([2, 100, 101, 322, 1221]),
iter([100, 142, 322, 956, 1222])]
print [x for x in inalliters(postings)]
Я не тестировал его очень тщательно (просто использовал ваш пример), но я считаю, что основная идея звучит.
Я хочу показать, что есть элегантное решение, которое выполняет только итерацию вперед. Извините, я не знаю Python достаточно хорошо, поэтому я использую вымышленные классы. Он читает input
, массив итераторов и записывает на output
на лету, даже не возвращаясь или используя какую-либо функцию массива!
def intersect (input, output)
do:
min = input[0]
bingo = True
for i in input:
if (i.cur < min.cur):
bingo = False
min = i
if bingo:
output.push(min.cur)
while (min.step())
Это выполняется в O(n*m)
, где n
- сумма всех длин итератора, а m
- количество списков. Его можно сделать O(n*logm)
, используя кучу в строке 6.
def intersection(its):
if not its: return
vs = [next(it) for it in its]
m = max(vs)
while True:
v, i = min((v,i) for i,v in enumerate(vs))
if v == m:
yield m
vs[i] = next(its[i])
m = max(m, vs[i])