Python: використання рекурсивного алгоритму в якості генератора


99

Нещодавно я написав функцію генерування певних послідовностей з нетривіальними обмеженнями. Проблема виникла з природним рекурсивним рішенням. Зараз буває так, що навіть при порівняно невеликому введенні послідовностей є кілька тисяч, тому я вважаю за краще використовувати свій алгоритм як генератор, а не використовувати його для заповнення списку всіма послідовностями.

Ось приклад. Припустимо, ми хочемо обчислити всі перестановки рядка з рекурсивною функцією. Наступний наївний алгоритм займає додатковий аргумент "сховище" і додає до нього перестановку, коли він знайде його:

def getPermutations(string, storage, prefix=""):
   if len(string) == 1:
      storage.append(prefix + string)   # <-----
   else:
      for i in range(len(string)):
         getPermutations(string[:i]+string[i+1:], storage, prefix+string[i])

storage = []
getPermutations("abcd", storage)
for permutation in storage: print permutation

(Будь ласка, не дбайте про неефективність. Це лише приклад.)

Тепер я хочу перетворити свою функцію в генератор, тобто отримати перестановку замість додавання її до списку пам’яті:

def getPermutations(string, prefix=""):
   if len(string) == 1:
      yield prefix + string             # <-----
   else:
      for i in range(len(string)):
         getPermutations(string[:i]+string[i+1:], prefix+string[i])

for permutation in getPermutations("abcd"):
   print permutation

Цей код не працює (функція поводиться як порожній генератор).

Я щось пропускаю? Чи є спосіб перетворити вищезазначений рекурсивний алгоритм у генератор, не замінивши його на ітеративний ?

Відповіді:


117
def getPermutations(string, prefix=""):
    if len(string) == 1:
        yield prefix + string
    else:
        for i in xrange(len(string)):
            for perm in getPermutations(string[:i] + string[i+1:], prefix+string[i]):
                yield perm

Або без акумулятора:

def getPermutations(string):
    if len(string) == 1:
        yield string
    else:
        for i in xrange(len(string)):
            for perm in getPermutations(string[:i] + string[i+1:]):
                yield string[i] + perm

29
У Python 3.4 ви можете замінити останні два рядки на yield from getPermutations(string[:i] + string[i+1:]), що є більш ефективним у багатьох аспектах!
Мануель Еберт

1
Вам все одно потрібно було б якось будувати результат. Використання yield fromвимагає використання аргументу акумулятора ( prefix).
Маркус Джардеро

Пропозиція: Визначте інший генератор, який повертає string[i],string[:i]+string[i+1:]пари. Тоді це було б:for letter,rest in first_letter_options(string): for perm in getPermuations(rest): yield letter+perm
Томас Ендрюс

29

Це дозволяє уникнути len(string)глибокої рекурсії і взагалі є приємним способом роботи з генераторами всередині генераторів:

from types import GeneratorType

def flatten(*stack):
    stack = list(stack)
    while stack:
        try: x = stack[0].next()
        except StopIteration:
            stack.pop(0)
            continue
        if isinstance(x, GeneratorType): stack.insert(0, x)
        else: yield x

def _getPermutations(string, prefix=""):
    if len(string) == 1: yield prefix + string
    else: yield (_getPermutations(string[:i]+string[i+1:], prefix+string[i])
            for i in range(len(string)))

def getPermutations(string): return flatten(_getPermutations(string))

for permutation in getPermutations("abcd"): print permutation

flattenдозволяє нам продовжувати прогрес в іншому генераторі, просто yieldвводячи його, замість того, щоб повторювати його та yieldобробляти кожен елемент вручну.


Python 3.3 додасть yield fromдо синтаксису, що дозволяє природне делегування до підгенератора:

def getPermutations(string, prefix=""):
    if len(string) == 1:
        yield prefix + string
    else:
        for i in range(len(string)):
            yield from getPermutations(string[:i]+string[i+1:], prefix+string[i])

20

Внутрішній виклик getPermutations - це теж генератор.

def getPermutations(string, prefix=""):
   if len(string) == 1:
      yield prefix + string            
   else:
      for i in range(len(string)):
         getPermutations(string[:i]+string[i+1:], prefix+string[i])  # <-----

Потрібно повторити це за допомогою циклу for (див. Публікацію @MizardX, яка обробляла мене секундами!)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.