Пара фундаментальных идей программирования (конспект к занятиям 27/11/2020)

Последнее обновление 20/03/2021

Рекурсия

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

Поскольку мы будем писать алгоритм для поиска факториалов, стоит пересмотреть определение факториала. Факториал натурального числа n — это произведение всех натуральных чисел, меньших или равных n:

n! = n\times(n-1)\times(n-2)\times(n-3)\times \ldots \times 3\times2\times1

Например, если n = 6:

6! = 6 \times 5 \times 4 \times 3 \times 2 \times 1 = 720

Подумаем об этом рекурсивно. Если у нас есть рекурсивная функция f, мы хотели бы использовать f для вычисления 6! следующим образом:
6! = f(6)
f(6)= 6\times f(5)
f(5) = 5\times f(4)\ или\ f(6) = 6\times5\times f(4)
f(4) = 4\times f(3)\ или\ f(6) = 6\times5\times4\times f(3)
f(3) = 3\times f(2)\ или\ f(6) = 6\times5\times4\times3\times f(2)
f(2) = 2\times f(1)\ или\ f(6) = 6\times5\times4\times3\times2\times f(1)
f(1) = 1\ или\ f(6) = 6\times5\times4\times3\times2\times1.

Чтобы реализовать это в python, нам нужно определить функцию, назовём её recursive_factorial, которая на вход принимает аргумент n и возвращает n * recursive_factorial(n-1). Более того, мы хотим, чтобы функция продолжала выполняться до тех пор, пока вход не стал равен единице. В этот момент мы возвращаем 1, и рекурсия прекращается. Делаем следующее:

def recursive_factorial(n):
    if n == 1:
        return 1
    else:
        return n*recursive_factorial(n-1)

Кроме того, необходимо обработать случай, когда n = 0, учитывая, что 0! = 1.

Соответствующим образом изменим оператор if:

def recursive_factorial(n):
    if n <= 1:
        return 1
    else:
        return n*recursive_factorial(n-1)

Еще стоит отметить, что наша функция не обрабатывает отрицательные числа. Этот метод работает только для натуральных чисел и нуля.

Посмотрим, что делает эта функция для 4!:

Нам нужно вычислить recursive_factorial (4).
Функция спрашивает, n = 1? Нет, n = 4, поэтому давайте вернем 4 * recursive_factorial (4-1), что равно 4 * recursive_factorial (3).

Теперь нам нужно вычислить recursive_factorial (3).

N = 1? Нет, n = 3, поэтому давайте вернем 4 * 3 * recursive_factorial (3-1), что равно 4 * 3 * recursive_factorial (2).Теперь нам нужно вычислить recursive_factorial (2).

N = 1? Нет, n = 2, поэтому давайте вернем 4 * 3 * 2recursive_factorial (2-1), что равно 4 * 3 * 2recursive_factorial (1).

Теперь нам нужно вычислить recursive_factorial (1).
N = 1? да, n = 1, поэтому вернем 4 * 3 * 2 * 1.

Случаи, когда n > 1, называются рекурсивными случаями, а когда n <= 1, завершением рекурсии.

Исходя из этой логики, recursive_factorial(4) = 4 * 3 * 2 * 1 = 24. Проверим это:

def recursive_factorial(n):
    if n <= 1:
        return 1
    else:
        return n*recursive_factorial(n-1)

print("4! =", recursive_factorial(4))

4! = 24

Попробуем предыдущий пример, 6!:

print("6! =", recursive_factorial(4))

6! = 720

Я остановлюсь на этом, но не стесняйтесь поиграть с кодом.

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

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

Мемоизация

Мемоизация — это термин, введенный Дональдом Мичи в 1968 году, который происходит от латинского слова «меморандум» (запомнить). Мемоизация — это метод, используемый в информатике для ускорения вычислений за счет сохранения (запоминания) предыдущих вычислений. Если повторные вызовы функций выполняются с одинаковыми параметрами, можно сохранить уже вычисленные значения вместо того, чтобы повторять их ещё раз. В этом уроке мы будем использовать мемоизацию, чтобы найти числа Фибоначчи.

Во-первых, давайте определим рекурсивную функцию, которую мы можем использовать для отображения первых n чисел в последовательности Фибоначчи. Если вы не знакомы с рекурсией, посмотрите урок: Рекурсия в Python.

Напоминаем, что последовательность Фибоначчи определяется так, что каждое число является суммой двух предыдущих чисел. Например, первые 6 членов последовательности Фибоначчи — это

1, 1, 2, 3, 5, 8.

Рекурсивную функцию мы можем определить следующим образом.

def fibonacci(input_value):
    if input_value == 1:
        return 1
    elif input_value == 2:
        return 1
    elif input_value > 2:
        return fibonacci(input_value -1) + fibonacci(input_value -2)

Здесь мы описываем начало последовательности — если входное значение равно 1 или 2, то функция возвращает 1. Если входное значение больше 2, возвращаются результаты рекурсивных вызовов функции, суммирующие предыдущие два числа Фибоначчи.

Теперь напечатаем первые 10 чисел:

for i in range(1, 11):
     print("fib({}) = ".format(i), fibonacci(i))
====================== RESTART: D:\PHYTON\fibonatc\1.py ======================
fib(1) =  1
fib(2) =  1
fib(3) =  2
fib(4) =  3
fib(5) =  5
fib(6) =  8
fib(7) =  13
fib(8) =  21
fib(9) =  34
fib(10) =  55

Кажется, все работает нормально. Теперь давайте попробуем отобразить первые 200 чисел последовательности:

for i in range(1, 201):
     print("fib({}) = ".format(i), fibonacci(i))
====================== RESTART: D:\PHYTON\fibonatc\1.py ======================

...

fib(20) =  6765
fib(21) =  10946
fib(22) =  17711
fib(23) =  28657
fib(24) =  46368
fib(25) =  75025
fib(26) =  121393
fib(27) =  196418
fib(28) =  317811
fib(29) =  514229
fib(30) =  832040
fib(31) =  1346269
fib(32) =  2178309
fib(33) =  3524578
fib(34) =  5702887
fib(35) =  9227465
fib(36) =  14930352
fib(37) =  24157817
fib(38) =  39088169

Заметили? — После fib(20) последующие вычисления занимают значительно больше времени, чем предыдущие. Это потому, что с каждым последующим вычислением мы повторяем одну и ту же работу.

Рассмотрим, как рекурсивная функция вычисляет каждое число:

fib(1) = 1

fib(2) = 1

fib(3) = fib(1)+fib(2) = 2

fib(4) = fib(3)+fib(2) = fib(5) = fib(4)+fib(3) = 5

Обратите внимание, что для fib(5) мы повторяем вычисление fib(4) и fib(3). Если бы у нас был способ запоминать/сохранять эти значения после их вычисления, то мы бы избежали повторений. Это создаёт мотивацию для метода мемоизации.

Теперь давайте пройдемся по этапам реализации метода мемоизации. Для продолжения давайте инициализируем словарь:

fibonacci_cache = {}

Далее определим нашу функцию мемоизации. Сначала проверяем, существует ли в словаре номер числа в последовательности, который будет ключом словаря. Если ключ присутствует, то возвращаем число, соответствующее номеру/ключу:

def fibonacci_memo(input_value):
        if input_value in fibonacci_cache:
            return fibonacci_cache[input_value]

Затем определяем два начальных числа последовательности. Если входное значение (фактический аргумент функции input_value) равно 1 или 2, то устанавливаем значение 1:

def fibonacci_memo(input_value):
    ...
    if input_value == 1:
        value = 1
    elif input_value == 2:
        value = 1

Далее рассмотрим рекурсивные случаи. Если аргумент больше 2, мы устанавливаем значение, равное сумме двух предыдущих чисел:

def fibonacci_memo(input_value):
    ...
    elif input_value > 2:           
        value =  fibonacci_memo(input_value -1) + fibonacci_memo(input_value -2)

В конце сохраняем значение в нашем словаре и возвращаем значение:

def fibonacci_memo(input_value):
    ...
    fibonacci_cache[input_value] = value
    return value

Полностью функция выглядит так:

def fibonacci_memo(input_value):
    if input_value in fibonacci_cache:
        return fibonacci_cache[input_value]
    if input_value == 1:
            value = 1
    elif input_value == 2:
            value = 1
    elif input_value > 2:           
            value =  fibonacci_memo(input_value -1) + fibonacci_memo(input_value -2)
    fibonacci_cache[input_value] = value
    return value

Теперь давайте попробуем получить первые 200 чисел Фибоначчи с помощью нашей новой функции:

for i in range(1, 201):
     print("fib({}) = ".format(i), fibonacci_memo(i))
Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 24 2018, 00:16:47) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license()" for more information.
>>> 
====================== RESTART: D:\PHYTON\fibonatc\2.py ======================
...

fib(184) =  127127879743834334146972278486287885163
fib(185) =  205697230343233228174223751303346572685
fib(186) =  332825110087067562321196029789634457848
fib(187) =  538522340430300790495419781092981030533
fib(188) =  871347450517368352816615810882615488381
fib(189) =  1409869790947669143312035591975596518914
fib(190) =  2281217241465037496128651402858212007295
fib(191) =  3691087032412706639440686994833808526209
fib(192) =  5972304273877744135569338397692020533504
fib(193) =  9663391306290450775010025392525829059713
fib(194) =  15635695580168194910579363790217849593217
fib(195) =  25299086886458645685589389182743678652930
fib(196) =  40934782466626840596168752972961528246147
fib(197) =  66233869353085486281758142155705206899077
fib(198) =  107168651819712326877926895128666735145224
fib(199) =  173402521172797813159685037284371942044301
fib(200) =  280571172992510140037611932413038677189525
>>> 

Запустили наш скрипт, заметили? — довольно быстро мы пришли к 200‑му числу последовательности.

Есть более простой способ реализовать мемоизацию и сократить код. Рассмотрим нашу исходную рекурсивную функцию:

def fibonacci(input_value):
    if input_value == 1:
        return 1
    elif input_value == 2:
        return 1
    elif input_value > 2:
        return fibonacci(input_value -1) + fibonacci(input_value -2)

Из модуля functools можно импортировать декоратор, называемый lru_cache, который позволяет кэшировать наши значения. Название означает «кэш последнего использования». Используя декоратор можно достичь той же производительности, что и наш метод fibonacci_memo:

from functools import lru_cache
@lru_cache(maxsize = 1000)
def fibonacci(input_value):
    if input_value == 1:
        return 1
    elif input_value == 2:
        return 1
    elif input_value > 2:
        return fibonacci(input_value -1) + fibonacci(input_value -2)
for i in range(1, 201):
     print("fib({}) = ".format(i), fibonacci(i))
Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 24 2018, 00:16:47) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license()" for more information.
>>> 
====================== RESTART: D:\PHYTON\fibonatc\3.py ======================

...

fib(184) =  127127879743834334146972278486287885163
fib(185) =  205697230343233228174223751303346572685
fib(186) =  332825110087067562321196029789634457848
fib(187) =  538522340430300790495419781092981030533
fib(188) =  871347450517368352816615810882615488381
fib(189) =  1409869790947669143312035591975596518914
fib(190) =  2281217241465037496128651402858212007295
fib(191) =  3691087032412706639440686994833808526209
fib(192) =  5972304273877744135569338397692020533504
fib(193) =  9663391306290450775010025392525829059713
fib(194) =  15635695580168194910579363790217849593217
fib(195) =  25299086886458645685589389182743678652930
fib(196) =  40934782466626840596168752972961528246147
fib(197) =  66233869353085486281758142155705206899077
fib(198) =  107168651819712326877926895128666735145224
fib(199) =  173402521172797813159685037284371942044301
fib(200) =  280571172992510140037611932413038677189525
>>> 

Заметили? — показатели аналогичны. И на этом я остановлюсь, однако, призываю вас поиграть с кодом самостоятельно.

Заключение

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

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

Далее мы обсудили декоратор lru_cache, который показал производительность, аналогичную нашему методу fibonacci_memo, но с меньшим количеством операторов кода.

Надеюсь, вы нашли этот урок полезным и интересным. Код этого урока доступен на GitHub. Спасибо за внимание!

Print Friendly, PDF & Email

Ваш комментарий будет первым

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Как так? *