Рекурсия (2017)

46
Рекурсия вебинар программы « Предуниверсарий НИУ ВШЭ» к.т.н., доц. Незнанов Алексей Андреевич 2017 - 03 - 09 МНУЛ ИССА, ФКН НИУ ВШЭ, 2017

Upload: alexey-neznanov

Post on 12-Apr-2017

106 views

Category:

Education


0 download

TRANSCRIPT

Рекурсиявебинар программы «Предуниверсарий НИУ ВШЭ»

к.т.н., доц.

Незнанов Алексей Андреевич

2017-03-09

МНУЛ ИССА, ФКН НИУ ВШЭ, 2017

Содержание

Зачем и почему: рекурсия

Математические основы

Общие принципы

Оптимизация рекурсивных подпрограмм

Мифы и их разоблачение

ПРИМЕРЫ!

© 2017, А.А. Незнанов 2

Эпиграфы

© 2017, А.А. Незнанов 3

Программа = алгоритм + данные Niklaus K. Wirth

Умные структуры данных и тупой код работают куда лучше, чем наоборот

Eric S. Raymond

Сначала выучите теорию программирования. Далее выработаете свой программистский стиль. Затем забудьте все и просто программируйте

George Carrette

Лучшие программисты не чуть-чуть лучше хороших. Они на порядок лучше по любым меркам: концептуальное мышление, скорость, изобретательность и способность находить решения

Randall E. Stross

Рекурсивный эпиграф

Как занять программиста (см. ниже).

Как занять программиста (см. выше).

Автор неизвестен

© 2017, А.А. Незнанов 4

Математические основы

✓ Базовые понятия

✓ История

✓ От примитивных рекурсивных функций к частично-рекурсивным и общерекурсивным

© 2017, А.А. Незнанов 5

Рекурсивная функция

Понятие рекурсии пришло из математики и связано со способом задания последовательностей (рядов)

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

Рекурсивная функция – функция, значение которой для данного аргумента вычисляется с использованием значений для предшествующих аргументов

Это подразумевает, что:

аргументы упорядочены

для любого аргумента мы имеем конечное число «меньших» аргументов (термины «больше» и «меньше» в данном контексте выражают любой реальный порядок аргументов –аргументы могут являться объектами произвольной природы)

© 2017, А.А. Незнанов 6

Чуть-чуть истории

Класс рекурсивных функций, как математических объектов, впервые формально был описан К. Гёделем [Kurt Friedrich Gödel] в 1933 г.

Общепринятый вариант формализации предложен С. Клини[Stephen Cole Kleene] в 1936 г. и использует 𝜆-нотацию Kleene S.C. General recursive functions of natural numbers. Mathematische

Annalen 112, 1936. pp. 727–742.

В 1981 г. Клини очень интересно рассказал историю создания теории рекурсивных функций Kleene S.C. Origins of recursive function theory. Annals of the History of

Computing 3, 1981. pp. 52–67.

В 1996 г. вышла статья Соара «Вычислимость и рекурсия» Soare R.I. Computability and recursion / Bulletin of Symbolic Logic, 2, 1996.

pp. 284–321. (http://www.people.cs.uchicago.edu/~soare/History/compute.pdf)

© 2017, А.А. Незнанов 7

Примитивно-рекурсивные функции (1)

Базовые ПРФ: Нулевая функция 𝑶 — функция без аргументов, всегда возвращающая 0

Функция следования 𝑺 одного переменного, сопоставляющая любому натуральному числу 𝑥 непосредственно следующее за ним натуральное число 𝑥 + 1

Функции 𝑰𝒏𝒎, где 0 < 𝑚 ≤ 𝑛, от 𝑛 переменных, сопоставляющие любому

упорядоченному набору 𝑥1, … , 𝑥𝑛 натуральных чисел число 𝑥𝑚 из этого набора

Операторы подстановки и примитивной рекурсии: Оператор суперпозиции. Пусть 𝑓 — функция от 𝑚 переменных, а 𝑔1, … , 𝑔𝑚 —

упорядоченный набор функций от 𝑛 переменных каждая. Тогда результатом суперпозиции функций 𝑔𝑘 в функцию 𝑓 называется функция ℎ от 𝑛 переменных, сопоставляющая любому упорядоченному набору 𝑥1, … , 𝑥𝑛 натуральных чисел числоℎ 𝑥1, … , 𝑥𝑛 = 𝑓(𝑔1 𝑥1, … , 𝑥𝑛 , … , 𝑔𝑚(𝑥1, … , 𝑥𝑛))

Оператор примитивной рекурсии. Пусть 𝑓 — функция от 𝑛 переменных, а 𝑔 —функция от 𝑛 + 2 переменных. Тогда результатом применения оператора примитивной рекурсии к паре функций 𝑓 и 𝑔 называется функция ℎ от 𝑛 + 1переменной видаℎ 𝑥1, … , 𝑥𝑛, 0 = 𝑓(𝑥1, … , 𝑥𝑛); ℎ 𝑥1, … , 𝑥𝑛, 𝑦 + 1 = 𝑔(𝑥1, … , 𝑥𝑛, 𝑦, ℎ(𝑥1, … , 𝑥𝑛, 𝑦))

© 2017, А.А. Незнанов 8

Примитивно-рекурсивные функции (2)

Множество примитивно рекурсивных функций [primitive recursive function] – это минимальное множество, содержащее все базовые функции и замкнутое относительно операторов подстановки и примитивной рекурсии

В терминах императивного программирования – примитивно рекурсивные функции соответствуют программным блокам, в которых используется только арифметические операции, условный оператор и оператор арифметического цикла(оператор цикла, в котором число итераций известно на момент начала цикла) Если же программист начинает использовать оператор цикла while, в

котором число итераций заранее неизвестно и, в принципе, может быть бесконечным, то он переходит в класс частично рекурсивных функций

© 2017, А.А. Незнанов 9

Частично-рекурсивные функции

Дополнительный минимизации аргумента.

Оператор минимизации аргумента. Пусть 𝑓 — функция от 𝑛 натуральных переменных. Тогда результатом применения оператора минимума аргумента к функции 𝑓 называется функция ℎ от 𝑛 − 1 переменной, задаваемая следующим определением: ℎ 𝑥1, … , 𝑥𝑛−1 = min 𝑦, при условии 𝑓(𝑥1, … , 𝑥𝑛−1, 𝑦) = 0

Множество частично-рекурсивных функций [partial recursive function] – это минимальное множество, содержащее все базовые функции и замкнутое относительно операторов подстановки, примитивной рекурсии и минимизации аргумента

Тезис А. Чёрча: класс частично-рекурсивных функцийтождественен классу всюду определённых вычислимых функций

© 2017, А.А. Незнанов 10

Общерекурсивные функции

Общерекурсивная функция [total recursive function] — частично рекурсивная функция, определённая для всех значений аргументов

Задача определения того, является ли частично рекурсивная функция с данным описанием общерекурсивной или нет, алгоритмически неразрешима!

© 2017, А.А. Незнанов 11

Рекурсия и математическая индукция

Рекурсивные функции напрямую связаны с принципом математической индукции: Основание индукции (база): доказательство истинности утверждения

для некоторого целочисленного параметра, равного 𝐴

Шаг индукции: предположим, что утверждение истинно при всех положительных значениях этого целочисленного параметра, меньших 𝑁, и докажем истинность утверждения для значения параметра, равного 𝑁

Как метод доказательства, индукция оформилась намного раньше программирования...

© 2017, А.А. Незнанов 12

Рекурсия в императивном программировании

✓ Общее описание

✓ Реализация в Pascal

✓ Примеры

✓ Оптимизация

✓ Некоторые мифы

© 2017, А.А. Незнанов 13

Три уровня рассмотрения систем

© 2017, А.А. Незнанов 14

Уровень

рассмотренияОбъект рассмотрения Связываемые понятия

КонцептуальныйРешаемая задача Понятия предметной

области

ЛогическийАлгоритм Абстрактные типы данных

(формализация понятий)

ФизическийПрограмма

(исходный код на языке

программирования)

Конкретные типы данных(реализация понятий)

Язык примеров и среды разработки

Современный Pascal Полноценный объектно-ориентированный язык со строгой типизацией,

имеющий несколько популярных реализаций

Delphi и совместимые реализации: [Free] Lazarus is a Delphi compatible cross-platform IDE for Rapid Application

Development (http://www.lazarus-ide.org)

[Free] Embarcadero Delphi 10.1 Berlin Starter Edition (https://www.embarcadero.com/ru/products/delphi/starter/promotional-download)

© 2017, А.А. Незнанов 15

Некоторые ссылки

☺ Рекурсия в Абсурдопедии (http://absurdopedia.net/wiki/Рекурсия) На русский язык была хорошо переведена книга:

Баррон Д. Рекурсивные методы в программировании. – М: Мир, 1974.

Много примеров на классическом Паскале приведено в книге: Rohl J.S. Recursion via Pascal. – Cambridge University Press, 1984. – 204 p.

Другие учебники и монографии на русском языке: Кормен Т., Лейзерсон Ч., Ривест Р., Штайн К. Алгоритмы: построение и анализ, 3-е изд. –

М.: Вильямс, 2013. – 1328 с. Левитин А.В. Алгоритмы: введение в разработку и анализ. – М. : Вильямс, 2006. – 576 с. Вирт Н. Алгоритмы и структуры данных. – СПб. : Невский Диалект, 2001. – 352 с. Ахо А., Хопкрофт Дж., Ульман Дж. Структуры данных и алгоритмы. – М. : Вильямс, 2000. –

384 с. Седжвик Р. Алгоритмы на C++. Фундаментальные алгоритмы и структуры данных. – М.:

Вильямс, 2013. – 1056 с. Макконнелл Дж. Основы современных алгоритмов. – 2006. – 368 c. Кнут Д. Искусство программирования, том 1. Основные алгоритмы, 3-е изд. – М.: Вильямс,

2000. – 720 с. И следующие тома...

Липский В. Комбинаторика для программистов. – М.: Мир, 1988. – 213 с. Зубов В.С., Шевченко И.В. Структуры и методы обработки данных. Практикум в среде

Delphi. – М. : ФИЛИНЪ, 2004. – 304 с.

© 2017, А.А. Незнанов 16

Базовые определения (1)

Рекурсия [recursion] – метод построения алгоритмов, обладающих свойством самоподобия, при реализации которого алгоритм может выполнить сам себя в качестве «элементарного действия» В классическом императивном программировании давно победила

парадигма структурного программирования

В структурном программировании элементом инкапсуляции процессов являются подпрограммы

Рекурсивная подпрограмма [recursive subroutine] –подпрограмма, при выполнении которой происходит вызов её самой

© 2017, А.А. Незнанов 17

Базовые определения (2)

Явно рекурсивная подпрограмма [recursive subroutine] –подпрограмма, в теле которой содержится по крайней мере один явный вызов её самой

Косвенно рекурсивная подпрограмма [indirect recursivesubroutine] – подпрограмма 𝑃1, которая содержит вызов подпрограммы 𝑃2, которая содержит явный или косвенный вызов 𝑃1

Уровень рекурсии [recursion level] при выполнении тела рекурсивной подпрограммы – число произведённых подряд рекурсивных вызовов данной подпрограммы к настоящему моменту Переход на следующий уровень рекурсии называется рекурсивным

спуском, а переход на предыдущий – рекурсивным возвратом

© 2017, А.А. Незнанов 18

Рекурсия и стек

Стек – механизм реализации рекурсии! Напомним, что стек также является механизмом обеспечения локальных

переменных подпрограмм

Правильная работа со Стеком – это альфа и омега рекурсивного программирования

Так как стек – это формально описанный абстрактный тип данных (АТД), то:

любая рекурсивная реализация может быть заменена функционально-эквивалентной реализацией,

не использующей рекурсию

© 2017, А.А. Незнанов 19

О косвенной рекурсии в языке Pascal

1. // Предварительное (пустое) объявление подпрограммы:2. procedure Proc2; forward;3.

procedure Proc1;4. begin5. //...6. Proc2; // Эта подпрограмма уже предварительно объявлена!7. //...8. end;

9. procedure Proc2;10. begin11. //...12. Proc1;13. //...14. end;

© 2017, А.А. Незнанов 20

Правила проектирования рекурсивных подпрограмм

1. В алгоритме обязано присутствовать условие завершения рекурсии – тщательно его проверьте

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

3. Так как при рекурсивном вызове создаётся копия контекста выполнения, то возникает задача минимизации кадра стека

1. У рекурсивной подпрограммы должно быть как можно меньшепараметров и локальных переменных

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

4. Поведение формальных параметров и локальных переменных рекурсивных процедур кардинально отличается: проверьте, правильно ли они используются

1. Особое внимание обратите на параметры, передаваемые по ссылке

© 2017, А.А. Незнанов 21

К вопросу минимизации кадра стека

Пример очень плохого варианта в общем случае:

1. procedure Proc(параметры);

2. var

3. LocalArr : array [1..1000000] of integer;

4. begin

5. if (условие завершения) then

6. //...

7. else Proc;

8. end;

Сколько уровней рекурсии сможет выдержать компьютер?

© 2017, А.А. Незнанов 22

Основные формы рекурсии

Концевая [tail] рекурсия Рекурсивный вызов – последнее действие в теле подпрограммы

Выполнение остальных действий происходит на рекурсивном спуске

Начальная [head] рекурсия Рекурсивный вызов – первое (после проверки условия завершения

рекурсии) действие в теле подпрограммы

Выполнение остальных действий – на рекурсивном возврате

Произвольная рекурсия Рекурсивный вызов или несколько вызовов находятся в любом месте

подпрограммы

Остальные действия выполняются и на рекурсивном спуске, и на рекурсивном возврате

Концевая рекурсия тривиально заменяется итерацией без использования стека, из за чего имеет особое значение!

© 2017, А.А. Незнанов 23

Пример 1. Сравнение концевой и начальной рекурсии1. //Концевая рекурсия:2. procedure TestRecurs1(Level: integer);3. begin4. Output(IntToStr(Level)+',');5. if Level<9 then TestRecurs1(Level+1);6. end;7. //Начальная рекурсия:8. procedure TestRecurs2(Level: integer);9. begin10. if Level<9 then TestRecurs2(Level+1);11. Output(IntToStr(Level)+',');12. end;13. begin14. TestRecurs1(0); OutputNL;15. TestRecurs2(0); OutputNL;16. end;

Результат:1. 0,1,2,3,4,5,6,7,8,9,2. 9,8,7,6,5,4,3,2,1,0,

© 2017, А.А. Незнанов 24

Числа Фибоначчи

Числа Фибоначчи – популярная рекуррентная последовательность для демонстрации рекурсивных алгоритмов

Рекуррентное соотношение для рекурсивной функции 𝐹𝑖𝑏(𝑛), возвращающей число Фибоначчи с номером 𝑛:

𝐹𝑖𝑏 1 = 1𝐹𝑖𝑏 2 = 1

𝐹𝑖𝑏 𝑛 = 𝐹𝑖𝑏 𝑛 − 1 + 𝐹𝑖𝑏 𝑛 − 2 , 𝑛 ≥ 3

Напомним, что есть прямая формула вычисления чисел Фибоначчи:

𝐹𝑖𝑏 𝑛 =

1+ 5

2

𝑛

−1− 5

2

𝑛

5

© 2017, А.А. Незнанов 25

Пример 2. Наивное вычисление чисел Фибоначчи1. // Наивное рекурсивное вычисление n-го числа Фибоначчи

2. function Fib1(n: integer): integer;

3. begin

4. // Проверка входных данных

5. if n<1 then

6. raise Exception.CreateFmt('Параметр "%d" меньше 1!', [n]);

7. // Условия завершения рекурсии

8. if n=1 then result := 1 else

9. if n=2 then result := 1 else

10. // Рекурсивный вызов

11. result := Fib1(n-1) + Fib1(n-2);

12. end;

© 2017, А.А. Незнанов 26

Дерево рекурсии для чисел Фибоначчи

Пусть 𝑛 = 5. Тогда дерево рекурсивных вызовов будет выглядеть так:

© 2017, А.А. Незнанов 27

5

4

3

2 1

2

3

2 1

4

3

2

1

Уровни рекурсии

Оптимизация рекурсивных подпрограмм

Удаление повторяющихся действий, не зависящих от уровня рекурсии

«Чистка стека», то есть удаление параметров подпрограммы, не зависящих от уровня рекурсии

Оптимизация проверки условия завершения рекурсии

Устранение последнего рекурсивного вызова

Оптимизируем функцию Fib1 из предыдущего раздела. Можно увидеть, что при каждом рекурсивном вызове проверяется

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

Для этого используем стандартную технику разделения рекурсивной подпрограммы на две части – рекурсивную и нерекурсивную (оболочку)

При этом очень полезной окажется поддержка языком Pascal/Delphiлокальных подпрограмм

© 2017, А.А. Незнанов 28

Пример 3.Начало оптимизации рекурсивного вызова1. // Вариант с нерекурсивной оболочкой

2. function Fib15(n: integer): integer;

3. // Локальная рекурсивная функция

4. function InnerFib(n: integer): integer;

5. begin

6. // Оптимизированные условия завершения рекурсии:

7. if n<3 then result := 1 else

8. // Рекурсивный вызов:

9. result := InnerFib (n-1) + InnerFib (n-2);

10. end;

11. begin

12. // Проверка входных данных:

13. if n<1 then

14. raise Exception.CreateFmt('Параметр "%d" меньше 1!', [n]);

15. result := Fib1(n);

16. end;

© 2017, А.А. Незнанов 29

Во что выливаются два рекурсивных вызова? Количество рекурсивных вызовов на нижних десяти

уровнях рекурсии (для 𝑛 от 10 до 40 с шагом 5):

© 2017, А.А. Незнанов 30

10 15 20 25 30 35 40

1 21 233 2584 28657 317811 3524578 39088169

2 34 377 4181 46368 514229 5702887 63245986

3 21 233 2584 28657 317811 3524578 39088169

4 13 144 1597 17711 196418 2178309 24157817

5 8 89 987 10946 121393 1346269 14930352

6 5 55 610 6765 75025 832040 9227465

7 3 34 377 4181 46368 514229 5702887

8 2 21 233 2584 28657 317811 3524578

9 1 13 144 1597 17711 196418 2178309

10 1 8 89 987 10946 121393 1346269

Пример 4. Нерекурсивный вариант вычисления чисел Фибоначчи1. function Fib2(n: integer): integer;2. var3. Buffer: array of integer; // Буфер для результатов решения подзадач4. i: integer; // Вспомогательная переменная5. begin6. // Проверка входных данных7. if n<1 then8. raise Exception.CreateFmt('Параметр "%d" меньше 1!', [n]);9. // Проверка тривиальных решений:10. if n<3 then result := 1 else begin11. SetLength(Buffer, n); // Выделение памяти под буфер12. // Решение тривиальных подзадач:13. Buffer[0] := 1; Buffer[1] := 1;14. // Накопление результатов более сложных задач:15. for i := 2 to n-1 do16. Buffer[i] := Buffer[i-1] + Buffer[i-2];17. result := Buffer[n-1]; // Получение итогового результата18. end;19. end;

© 2017, А.А. Незнанов 31

Обсуждение

За счёт использования дополнительного пространства памяти (массива Buffer) нам удалось организовать решение каждой подзадачи по одному разу

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

Время работы функции Fib2 пропорционально 𝒏, так как она содержит один цикл, перебирающий i от 2 до (n – 1)

© 2017, А.А. Незнанов 32

Какие алгоритмы естественным образом формулируются как рекурсивные? Бинарный поиск

Быстрая сортировка (Ч. Хоара)

Обход дерева

Обход графа в глубину

Построение фракталов

Обработка списков Язык Lisp (Джон Маккарти [John McCarthy], 1958 г.) и его наследники

(Scheme и др.)

© 2017, А.А. Незнанов 33

Наибольший общий делитель

Задача нахождения наибольшего общего делителя (НОД) двух чисел 𝑎 и 𝑏 есть задача нахождения наибольшего числа, на которое одновременно делится как 𝑎, так и 𝑏 Обозначим НОД 𝑎 и 𝑏 через 𝑮𝑪𝑫(𝑎, 𝑏)

Алгоритм Евклида Леонард Эйлер [Leonhard Euler] в восемнадцатом веке, обобщая

результат Евклида, приведённый в Началах, сформулировал: в случае, если остаток от деления 𝑎 на 𝑏 равен 0, то 𝐺𝐶𝐷(𝑎, 𝑏) = 𝑏, в противном случае 𝐺𝐶𝐷(𝑎, 𝑏) = 𝐺𝐶𝐷(𝑏, 𝑎 𝑚𝑜𝑑 𝑏)

mod – операция взятия остатка Отсюда автоматически следует рекурсивный алгоритм вычисления НОД

© 2017, А.А. Незнанов 34

Пример 5.Рекурсивный вариант вычисления НОД

1. function GCDRec(a, b: integer): integer;

2. function CalcGCDRec(a, b: integer): integer;

3. begin

4. if a mod b = 0 then Result := b

5. else Result := CalcGCDRec(b, a mod b);

6. // Ни в коем случае не CalcGCDRec(a mod b, b).

7. // Параметры должны поменяться местами!

8. // Почему это требование является обязательным?

9. end;

10. begin

11. if (a > 0) and (b > 0) then

12. Result := CalcGCDRec(a,b)

13. else Result := -1;

14. end;

© 2017, А.А. Незнанов 35

CalcGCDRec: Ассемблер x64 (1)

formmain.pas:90 begin

000000010002CC90 55 push %rbp

000000010002CC91 4889e5 mov %rsp,%rbp

000000010002CC94 488d6424c0 lea -0x40(%rsp),%rsp

000000010002CC99 48894de8 mov %rcx,-0x18(%rbp)

000000010002CC9D 8955f8 mov %edx,-0x8(%rbp)

000000010002CCA0 448945f0 mov %r8d,-0x10(%rbp)

formmain.pas:91 if a mod b = 0 then Result := b

000000010002CCA4 486345f8 movslq -0x8(%rbp),%rax

000000010002CCA8 48634df0 movslq -0x10(%rbp),%rcx

000000010002CCAC 4899 cqto

000000010002CCAE 48f7f9 idiv %rcx

000000010002CCB1 4885d2 test %rdx,%rdx

000000010002CCB4 7508 jne 0x10002ccbe <CALCGCDREC+46>

000000010002CCB6 8b45f0 mov -0x10(%rbp),%eax

000000010002CCB9 8945e0 mov %eax,-0x20(%rbp)

000000010002CCBC eb1f jmp 0x10002ccdd <CALCGCDREC+77>

© 2017, А.А. Незнанов 36

CalcGCDRec: Ассемблер x64 (2)

formmain.pas:92 else Result := CalcGCDRec(b, a mod b);

000000010002CCBE 486345f8 movslq -0x8(%rbp),%rax

000000010002CCC2 48634df0 movslq -0x10(%rbp),%rcx

000000010002CCC6 4899 cqto

000000010002CCC8 48f7f9 idiv %rcx

000000010002CCCB 4989d0 mov %rdx,%r8

000000010002CCCE 8b55f0 mov -0x10(%rbp),%edx

000000010002CCD1 488b4de8 mov -0x18(%rbp),%rcx

000000010002CCD5 e8b6ffffff callq 0x10002cc90 <CALCGCDREC>

000000010002CCDA 8945e0 mov %eax,-0x20(%rbp)

formmain.pas:96 end;

000000010002CCDD 8b45e0 mov -0x20(%rbp),%eax

000000010002CCE0 90 nop

000000010002CCE1 488d6500 lea 0x0(%rbp),%rsp

000000010002CCE5 5d pop %rbp

000000010002CCE6 c3000000000000000000 retq

© 2017, А.А. Незнанов 37

CalcGCDRec: Ассемблер x86 (1)

formmain.pas.28: begin

005C9CCC 55 push ebp

005C9CCD 8BEC mov ebp,esp

005C9CCF 83C4F4 add esp,-$0c

005C9CD2 8955F8 mov [ebp-$08],edx

005C9CD5 8945FC mov [ebp-$04],eax

formmain.pas.29: if a mod b = 0 then Result := b

005C9CD8 8B45FC mov eax,[ebp-$04]

005C9CDB 99 cdq

005C9CDC F77DF8 idiv dword ptr [ebp-$08]

005C9CDF 85D2 test edx,edx

005C9CE1 7508 jnz $005c9ceb

005C9CE3 8B45F8 mov eax,[ebp-$08]

005C9CE6 8945F4 mov [ebp-$0c],eax

005C9CE9 EB17 jmp $005c9d02

© 2017, А.А. Незнанов 38

CalcGCDRec: Ассемблер x86 (2)

formmain.pas.30: else Result := CalcGCDRec(b, a mod b);

005C9CEB 8B4508 mov eax,[ebp+$08]

005C9CEE 50 push eax

005C9CEF 8B45FC mov eax,[ebp-$04]

005C9CF2 99 cdq

005C9CF3 F77DF8 idiv dword ptr [ebp-$08]

005C9CF6 8B45F8 mov eax,[ebp-$08]

005C9CF9 E8CEFFFFFF call CalcGCDRec

005C9CFE 59 pop ecx

005C9CFF 8945F4 mov [ebp-$0c],eax

formmain.pas.34: end;

005C9D02 8B45F4 mov eax,[ebp-$0c]

005C9D05 8BE5 mov esp,ebp

005C9D07 5D pop ebp

005C9D08 C3 ret

© 2017, А.А. Незнанов 39

Пример 6.Нерекурсивный вариант вычисления НОД

1. function GCDNoRec(a, b: integer): integer;

2. var

3. Tmp: integer;

4. begin

5. if (a > 0) and (b > 0) then begin

6. Tmp := a mod b;

7. while (Tmp <> 0) do begin // Основной цикл

8. a := b;

9. b := Tmp;

10. Tmp := a mod b;

11. end;

12. Result := b;

13. end else Result := -1;

14. end;

© 2017, А.А. Незнанов 40

Пример 7(1). Процедураслучайного заполнения булева массива1. type TVBoolArr = array of Boolean;2. procedure CTRandTVBoolArr(Arr: TVBoolArr; ALen, ATrueCount: integer);3. var N: Integer;4. Inv: Boolean;5. begin6. if ATrueCount >= ALen then begin7. CTFillChar(Arr,ALen,#1); // Заполнение «истиной»8. end else if ATrueCount <= 0 then begin9. CTZeroMem(Arr, ALen); // Заполнение «ложью»10. end else begin11. N := ALen;12. if ATrueCount > N shr 1 then begin13. ATrueCount := N - ATrueCount;14. Inv := True;15. end else Inv := False;16. CTZeroMem(Arr, ALen);17. FillRange(0, N - 1, ATrueCount);18. if Inv then // Если нужно инвертировать значения19. for N := 0 to ALen-1 do20. if Arr[N] then Arr[N] := false else Arr[N] := true;21. end;22. end;

© 2017, А.А. Незнанов 41

Пример 7(2). Локальная рекурсивная процедура заполнения булева массива1. procedure FillRange(L, R, ATrueCount: Integer);2. var RangeLength, Offset, Avg, LeftNumTrue: Integer;3. begin4. if (L <= R) and (ATrueCount > 0) then begin5. RangeLength := R - L + 1; // Диапазон6. Offset := CTRandom(RangeLength); // Случайный разрез диапазона7. Avg := L + Offset; // Получаем индекс разреза8. Arr[Avg] := True; // Ставим отметку9. Dec(ATrueCount); // Уменьшаем оставшееся количество отметок10. LeftNumTrue := Round(Offset / RangeLength * ATrueCount);11. if (Avg-L) < (R-Avg) then begin12. FillRange(L, Avg - 1, LeftNumTrue);13. FillRange(Avg + 1, R, ATrueCount - LeftNumTrue);14. end else begin15. FillRange(Avg + 1, R, ATrueCount - LeftNumTrue);16. FillRange(L, Avg - 1, LeftNumTrue);17. end;18. end;19. end;

© 2017, А.А. Незнанов 42

Миф №1

Рекурсия медленнее итерации?

Это давно не так!

Современные процессоры:

Эффективно выполняют подпрограммы

Поддерживают аппаратный стек, который работает намного эффективнее любой собственной реализации программистом

Вывод:

Борьба с рекурсией обычно бессмысленна, если глубина рекурсии мала (априори ограничена). Но! Нерекурсивные варианты могут обладать другими хорошими свойствами...

У Мартина Фаулера есть даже рефакторинг «замена цикла рекурсией» (http://www.refactoring.com/catalog/replaceIterationWithRecursion.html)

... On many c/c++ compilers (most, if you enable optimisation), the recursive version will compile to code that is more efficient!

© 2017, А.А. Незнанов 43

Миф №2

При рекурсии невозможно контролировать стек?

А кто мешает? Традиция...

Контролировать глубину рекурсии и управлять рекурсивными вызовами ничуть не сложнее, чем контролировать расход памяти кучи и управлять другими структурами данных

Как будто свой стек кончиться не может... Существует довольно много гибридных алгоритмов, которые сочетают

рекурсивные и нерекурсивные техники в рамках решения одной задачи

Простейший пример – быстрая сортировка, дополненная сортировкой выбором на последних уровнях

Однако этот миф часто путается с другим – мифом о плохой оптимизируемости рекурсивных алгоритмов

© 2017, А.А. Незнанов 44

Не миф...

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

абстракции

Мичурин А. О пользе рекурсии (http://www.michurin.net/computer-

science/recursion.html)

Stack Size Estimation (http://stackoverflow.com/questions/1756285/stack-size-

estimation)

© 2017, А.А. Незнанов 45

© 2017, А.А. Незнанов 46

Но это ещё не конец?!

Вопросы? Замечания? Предложения?

Контакты: к.т.н., доц. Незнанов Алексей Андреевич

Доцент департамента анализа данных и искусственного интеллекта ФКН НИУ ВШЭ, старший научный сотрудник международной лаборатории интеллектуальных систем и структурного анализа НИУ ВШЭ (School of Data Analysis and Artificial Intelligence, Faculty of Computer Science, NRU HSE, Moscow, Russia)

E-mail: [email protected]

Web-site: http://hse.ru/staff/aneznanov

Blog: http://siberianshamanssongs.blogspot.ru (RU)