Почему некоторые переменные или списки сохраняются при выходе из рекурсивных вызовов, а другие нет? - PullRequest
0 голосов
/ 29 сентября 2019

Я пытаюсь понять, что происходит с вашими переменными в Python при выходе из списка.Я думал все переменные, которые передаются в рекурсивную функцию, помещаются в стек.И когда вы выходите из стека, эти переменные выталкиваются.Однако, я вижу, что это не так.

Я создал простую рекурсивную строковую функцию для проверки этого.Когда я запускаю этот код:

def recursiveArr(n, listB):

    if(n==0):
        return True;
    else:
        listB.append(n);
        listC = listB;
        recursiveArr(n-1, listB[:]);
        print(n)
        print(listB)

, я получаю следующий вывод:

1
[4, 3, 2, 1]
2
[4, 3, 2]
3
[4, 3]
4
[4]
[4]

Это говорит о том, что и стек, и переменная n были сохранены в стеке.Затем я изменил вызов на recursiveArr(n-1, listB)

Я получил следующий вывод:

1
[4, 3, 2, 1]
[4, 3, 2, 1]
2
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
4
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]

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

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

Вот код:

def stringify(theList):
    theString = ''

    return theString.join(theList)
def getPerm(charArray, countArray, result, level, lenOrigString): 
    if(level == lenOrigString):
        print(stringify(result))
        return
    for i in range(len(charArray)):
        if (countArray[i]!=0):
            result.append(charArray[i])
            countArray[i] = countArray[i]-1;
            level = level + 1;
            getPerm(charArray, countArray, result, level, lenOrigString)
            result.pop(len(result)-1)
            countArray[i] = countArray[i]+1
            level = level - 1;



    #print("Congratulations! You made it to the second layer!")
def printPermutations(toPerm):
    toPerm = toPerm.lower()
    lenOrigString = len(toPerm)
    charArray = [ord(iChar)-97 for iChar in toPerm]
    countArray = [];
    for iChar in range(26):
        countArray.append(0)
    for i in range(len(charArray)):
        countArray[charArray[i]] = countArray[charArray[i]] + 1
    uniqueChar = [];
    for iChar in charArray:
        if iChar not in uniqueChar:
            uniqueChar.append(iChar)
    uniqueChar = [chr(iChar+97) for iChar in uniqueChar] #keeps track of the possible characters to use
    countArray = list(filter(lambda x: x>0, countArray))#keep track of how many of each character has not been used
    uniqueChar.sort()
    result = [];
    level = 0;
    for i in range(len(uniqueChar)):
        if (countArray[i]!=0):
            result.append(uniqueChar[i])
            countArray[i] = countArray[i]-1;
            level = level + 1;
            getPerm(uniqueChar, countArray, result, level, lenOrigString)#call next level
            result.pop(len(result)-1)
            countArray[i] = countArray[i]+1
            level = level - 1;


printPermutations("ApPLe")

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

Вот как выглядит этот измененный код:

    for i in range(len(uniqueChar)):
        if (countArray[i]!=0):
            result.append(uniqueChar[i])
            countArray[i] = countArray[i]-1;
            level = level + 1;
            getPerm(uniqueChar, countArray[:], result[:], level, lenOrigString)#call next level
            level = level -1;

Мой вопрос: Почему шаблон, который я нашел в моей базовой строковой функции, не соответствует истине в моей функции строковых перестановок?

У меня готовятся интервью, и я не хочу смущать себя путанием рекурсивных выходных вызовов.

PS Это алгоритм , который я использовалвдохновлять код перестановки строк.

Ответы [ 2 ]

1 голос
/ 29 сентября 2019

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

В общем, при обходе дерева вызовов, такого как этот, с использованием спискаВ качестве параметра для рекурсивной функции существует два подхода.Первый - передать копию списка, который иллюстрирует ваш первый пример (с использованием синтаксиса slice [:], который создает копию всего списка).

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

Вот быстрая перезапись примера функции, которую вы предоставили для соблюдения PEP8 и удаляет бесполезную строку (listC = listB;):

def foo(n, lst):
    if n:
        lst.append(n)
        foo(n - 1, lst[:])
        print(f"n: {n}, lst: {lst}, id: {id(lst)}")

if __name__ == "__main__":
    foo(4, [])

Вывод:

n: 1, lst: [4, 3, 2, 1], id: 140439070012752
n: 2, lst: [4, 3, 2], id: 140439070012832
n: 3, lst: [4, 3], id: 140439139990576
n: 4, lst: [4], id: 140439072829760

Обратите внимание, что распечатки происходят после разрешения рекурсивных вызовов, поэтому мы видим полностью заполненныйсписок в окончательном вызове печати в первую очередь.Переставьте функцию для печати в первую очередь, если это сбивает с толку - местоположение не имеет значения.Важной частью является то, что в каждом списке имеется совершенно отдельный идентификатор, поэтому мы создали в общей сложности 4 списка.

Теперь вот эквивалентная версия (с точки зрения вывода), которая использует только один список за все времяи использует описанную выше технику «восстановления состояния»:

def bar(n, lst):
    if n:
        lst.append(n)
        bar(n - 1, lst)
        print(f"n: {n}, lst: {lst}, id: {id(lst)}")
        lst.pop() # undo the append to restore state in this frame

if __name__ == "__main__":
    bar(4, [])

Вывод:

n: 1, lst: [4, 3, 2, 1], id: 139793013186800
n: 2, lst: [4, 3, 2], id: 139793013186800
n: 3, lst: [4, 3], id: 139793013186800
n: 4, lst: [4], id: 139793013186800

Обратите внимание на несколько вещей.Во-первых, логический вывод тот же.Убедитесь сами, что каждый append сопровождается pop, который полностью восстанавливает состояние для родительского вызова, отменяя все мутации, выполненные в текущем кадре стека.Во-вторых, обратите внимание, что у нас есть один список за все время с идентификатором 139793013186800.

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


Теперь, когда мы увидели теорию, давайте рассмотрим две версии метода перестановок:

def print_permute_copy(lst, i=0):
    if i == len(lst):
        print("".join(lst))
    else:
        for j in range(i, len(lst)):            
            cpy = lst[:]
            cpy[i], cpy[j] = cpy[j], cpy[i]
            print_permute_copy(cpy, i + 1)

def print_permute_restore(lst, i=0):
    if i == len(lst):
        print("".join(lst))
    else:
        for j in range(i, len(lst)):            
            lst[i], lst[j] = lst[j], lst[i]
            print_permute_restore(lst, i + 1)
            lst[i], lst[j] = lst[j], lst[i]

if __name__ == "__main__":
    print_permute_copy(list("abc"))
    print()
    print_permute_restore(list("abc"))

Вывод:

abc
acb
bac
bca
cba
cab

abc
acb
bac
bca
cba
cab

Мы видим, что оба верны и дают эквивалентный вывод.Функция top создает новые копии списка для передачи детям.При этом ему не нужно беспокоиться о восстановлении своего состояния после того, как список будет возвращен дочерними элементами.Недостатком этого подхода является необходимость создания нового списка для каждого вызова, что неэффективно.

С другой стороны, версия для восстановления просто передает один список и выполняет все операции с ним.После выполнения свопинга он передает мутировавший список своим дочерним элементам, которые также выполняют свопы, но каждый отменяет любые свопы, которые он выполняет, когда его дочерние элементы работают со списком (и в конечном итоге отменяют свои свопы).

Код, который вы показали, имеет гораздо более сложное состояние, но давайте рассмотрим этот фрагмент:

for i in range(len(uniqueChar)):
    if (countArray[i]!=0):
        result.append(uniqueChar[i])
        countArray[i] = countArray[i]-1;
        level = level + 1;
        getPerm(uniqueChar, countArray[:], result[:], level, lenOrigString)
        level = level -1;

Попытка использования нарезки не удалась, потому что countArray[i] = countArray[i]-1 (более ясно, как countArray[i] -= 1) не выполняется для копии, но в родительском списке.То же самое для result.append(uniqueChar[i]) - это должно быть выполнено для копии, которую мы готовим передать ребенку, а не для списка родителей.

Это работает:

for i in range(len(uniqueChar)):
    if countArray[i] != 0:
        cpy = countArray[:]
        cpy[i] = cpy[i] - 1;
        level = level + 1;
        getPerm(uniqueChar, cpy, result + [uniqueChar[i]], level, lenOrigString)
        level = level -1;

Обратите внимание, что это менее эффективно, чем подход "восстановление состояния" в оригинале, поэтому я показываю его исключительно в демонстрационных целях.Также обратите внимание, что result + [uniqueChar[i]] использует конкатенацию списков и создает новый список с добавленным новым элементом.Количество копий всех этих списков увеличивается по мере увеличения длины строки (временная сложность O (N!)).


Для полноты картины обратите внимание, что эту функцию можно записать просто как list(itertools.permutations(iterable)).Даже если вы решите написать это от руки для образовательных целей, таких как этот, есть намного более простые алгоритмы, чем предоставленный, который использует много дополнительного состояния и трудно рассуждать из-за повторяющегося кода getPerm is практически дословно повторяется внутри printPermutations.Частично проблема заключается в том, что Java является гораздо более многословным языком, чем Python, с очень разными операциями и структурами, поэтому буквальный перевод кода Java почти гарантированно не является Pythonic.

Некоторые предложения по присоединению к Pythonстиль более точно:

  • snake_case для переменных и функций.
  • if(foo==bar): должен быть записан как if foo == bar: (использует пробелы вокруг операторов, пропускает ненужные скобки).
  • foo=foo+1 яснее как foo += 1 (оператор приращения, пробел вокруг операторов).
  • result.pop(len(result)-1) яснее как result.pop().
  • Нет необходимости использоватьвведите имена переменных.В Python нет массивов, поэтому countArray может быть counts, charArray может быть characters и т. Д. (Множественное число - хороший способ обозначать списки).
  • Точки с запятой не нужны.
  • Используйте вертикальные пробелы вокруг операторов блока:

    countArray = [];
    for iChar in range(26):
    

    должно быть

    counts = []
    
    for i in range(26):
    
  • Избегать функций, которые создают побочные эффекты и предпочитают возвращать генераторы , следующие itertools:

    def permute(lst, i=0):
        if i == len(lst):
            yield lst[:]
        else:
            for j in range(i, len(lst)):            
                lst[i], lst[j] = lst[j], lst[i]
                yield from permute(lst, i + 1)
                lst[i], lst[j] = lst[j], lst[i]
    
    if __name__ == "__main__":
        print(list(permute(list("abc"))))
    
0 голосов
/ 29 сентября 2019

Когда вы сделаете listB[:], он создаст копию этого списка.Следовательно, в вашей функции он не будет изменять исходный listB из одного стека выше.Когда вы передаете просто listB в качестве аргумента, вы передаете ссылку на исходный список, и, следовательно, это один и тот же список в каждом изменяемом стеке.

Для listB[:] ваша рекурсивная функция выглядит следующим образом:

recursiveArr(4, [])
|
| listB.append(4) --> listB is now [4]
|
+---> recursiveArr(3, [4])  <-- this list is a copy of original listB, let's denote it listB_2
      |
      | listB_2.append(3)  --> listB_2 is now [4, 3], listB is still [4]
      |
      +---> recursiveArr(2, [4, 3])  <-- this is a copy of listB_2, say listB_3
            |
            | listB_3.append(2)  --> list_3 is now [4, 3, 2], listB_2 is [4, 3] and listB is [4]
            |
            +---> and so on

Если вы передадите listB в качестве аргумента с другой стороны, каждый вызов recursiveArr получит ссылку на тот же объект списка и, таким образом, добавится в тот же список.Таким образом, на каждом уровне стека вы просматриваете один и тот же список с одинаковыми значениями.

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

def getPerm(charArray, countArray, level, lenOrigString): 
    if(level == lenOrigString):
        yield ''
    for i in range(len(charArray)):
        if countArray[i] != 0:
            countArray[i] = countArray[i]-1;
            yield from (charArray[i] + x for x in getPerm(charArray, countArray, level + 1, lenOrigString))
            countArray[i] = countArray[i]+1
...