Python: utilizzo di un algoritmo ricorsivo come generatore


99

Recentemente ho scritto una funzione per generare determinate sequenze con vincoli non banali. Il problema è arrivato con una soluzione ricorsiva naturale. Ora succede che, anche per input relativamente piccoli, le sequenze sono diverse migliaia, quindi preferirei usare il mio algoritmo come generatore invece di usarlo per riempire un elenco con tutte le sequenze.

Ecco un esempio. Supponiamo di voler calcolare tutte le permutazioni di una stringa con una funzione ricorsiva. Il seguente algoritmo ingenuo accetta un argomento aggiuntivo "archiviazione" e gli aggiunge una permutazione ogni volta che ne trova uno:

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

(Per favore non preoccuparti dell'inefficienza, questo è solo un esempio.)

Ora voglio trasformare la mia funzione in un generatore, ovvero produrre una permutazione invece di aggiungerla all'elenco di archiviazione:

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

Questo codice non funziona (la funzione si comporta come un generatore vuoto).

Mi sto perdendo qualcosa? C'è un modo per trasformare l'algoritmo ricorsivo di cui sopra in un generatore senza sostituirlo con uno iterativo ?

Risposte:


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

O senza accumulatore:

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
In Python 3.4, puoi sostituire le ultime due righe con yield from getPermutations(string[:i] + string[i+1:]), che è più efficiente in molti modi!
Manuel Ebert

1
Avresti comunque bisogno di costruire il risultato in qualche modo. L'utilizzo yield fromrichiederebbe l'utilizzo dell'argomento accumulator ( prefix).
Markus Jarderot

Suggerimento: definire un altro generatore che restituisca le string[i],string[:i]+string[i+1:]coppie. Quindi sarebbe:for letter,rest in first_letter_options(string): for perm in getPermuations(rest): yield letter+perm
Thomas Andrews

29

Questo evita la len(string)ricorsione -deep ed è in generale un bel modo per gestire generators-inside-generators:

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

flattenci consente di continuare a progredire in un altro generatore semplicemente yieldinserendolo, invece di iterare attraverso di esso e yieldinserire manualmente ogni elemento.


Python 3.3 aggiungerà yield fromalla sintassi, che consente la delega naturale a un sub-generatore:

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

La chiamata interiore a getPermutations - è anche un generatore.

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])  # <-----

Devi iterare attraverso questo con un ciclo for (vedi il post di @MizardX, che mi ha battuto di secondi!)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.