Рекурсивный код и стек времени исполнения
Функция — это последовательность инструкций, выполняемых в ответ на ее вызов. Процесс выполнения начинается с того, что вызывающий блок заполняет активизирующую запись (activation record), которая включает список параметров и местоположение следующей инструкции, подлежащей выполнению после возврата из блока.
При вызове функции данные из активизирующей записи заталкиваются в стек, организуемый системой (стек времени исполнения). Данные объединяются с локальными переменными и образуют активизирующий фрейм” доступный функции.
При выходе из функции устанавливается местоположение следующей инструкции (рис. 10.4), а данные в стеке, соответствующие активизирующей записи, уничтожаются. Рекурсивная функция повторно вызывает саму себя, используя всякий раз модифицированный список параметров. При этом последовательность активизирующих записей заталкивается в стек до тех пор, пока не будет достигнуто условие останова. Последовательное выталкивание этих записей и дает нам наше рекурсивное решение. Функция вычисления факториала иллюстрирует использование активизирующих записей.
Стек времени исполнения
С помощью примера вычисления факториала от 4 мы проиллюстрируем использование активизирующих записей и стека, создаваемого во время вы полнения рекурсивной функции. Начальный вызов факториала производите! из главной программы. После выполнения функции управление возвращается в точку RetLockl, где переменной N присваивается значение 24(4!):
void main (void)
{
int n;
// поместить в стек запись с помощью вызова FACTORIAL(4)
// RetLockl - адрес присвоения n = FACTORIAL(4)
n=FACTORIAL(4);
RetLockl *
}
Рекурсивные вызовы в функции FACTORIAL возвращают управление я точку RetLock2, где вычисляется произведение п * (п-1)!. Результат вычисления запоминается в переменной temp, чтобы помочь читателю проследить код и продемонстрировать стек времени исполнения:
long FACTORIAL(long n)
{
int temp;
if(n=0}
return 1;
// вытолкнуть из стека активизирующую запись
else
{
// поместить в стек активизирующую запись с помощью вызова
// FACTORIAL(n-1)
// Retlock2 - адрес вычисления n * FACTORIAL(n-1).
temp=n*FACTORIAL(n-1);
RetLock2
return temp; // вытолкнуть из стека активизирующую запись
}
Для функции FACTORIAL активизирующая запись имеет два поля.
Выполнение FACTORIAL(4) инициирует последовательность из пяти вызовов. На рис. 10.5 показаны активизирующие записи для каждого вызова. Записи входят в стек снизу вверх вместе с вызовом из главной процедуры, занимая нижнюю часть стека.
При обращении к функции FACTORIAL с параметром 0 возникает условие останова, и начинается выполнение последовательности операторов возврата. Когда из стека выталкивается самая верхняя активизирующая запись, управление передается в точку возврата. Очистка стека от активизирующих записей описывается следующими операциями.
Решение задач с помощью рекурсии
Многие вычислительные задачи имеют весьма простую и изящную формулировку, которая непосредственно переводится в рекурсивный код. В разделе 1 рассмотрен пример, про Ханойскую башню. В этом разделе мы расширим диапазон примеров и рассмотрим рекурсивное определение алгоритма бинарного поиска.
Бинарный поиск
При бинарном поиске берется некоторый ключ и просматривается упорядоченный массив из n элементов на предмет совпадения с этим ключом. Функция возвращает индекс совпавшего с ключом элемента или –1 при отсутствии такового. Алгоритм бинарного поиска может быть описан рекурсивно.
Допустим, отсортированный список А характеризуется нижним граничным индексом low и верхним – high. Имея ключ, мы начинаем искать совпадение в середине списка (индекс mid).
mid = (low+high)/2 Сравнить A[mid] с ключом
Если совпадение произошло, мы имеем условие останова, что позволяет нам прекратить поиск и возвратить индекс mid.
Если совпадение не происходит, можно воспользоваться тем фактом, что список упорядочен, и ограничить диапазон поиска "нижним подсписком" (слева от mid) или "верхним подсписком" (справа от mid).
Бели ключ < A[mid], совпадение может произойти только в левой половине списка в диапазоне индексов от low до mid-1.
Бели ключ > A[mid], совпадение может произойти только в правой половине списка в диапазоне индексов от mid+1 до high.
Шаг рекурсии направляет бинарный поиск для продолжения в один из подсписков. Рекурсивный процесс просматривает все меньшие и меньшие списки. В конце концов поиск заканчивается неудачей, если подсписки исчезли. Это происходит тогда, когда верхний предел списка становится меньше чем нижний предел. Условие low>high — второе условие останова. В этом случае алгоритм возвращает -1.
Бинарный поиск (рекурсивная форма). В шаблонной версии бинарного поиска в качестве параметров используется массив элементов типа Т, значение ключа, а также верхний и нижний граничные индексы. Оператор IF обрабатывает два условия останова: 1) совпадение произошло; 2) ключевого значения нет в списке. В блоке ELSE оператора IF выполняется шаг рекурсии, который направляет дальнейший поиск в левый (ключ<А[mid]) или в правый подсписок (ключ>А[mid]). Тот же алгоритм применяется по принципу "разделяй и властвуй" к последовательности все меньших интервалов, пока не произойдет успех (совпадение) или неудача.
// рекурсивная версия бинарного поиска ключевого значения
//в упорядоченном массиве А
template <clasa T>
int BinSearch{t A[], int low, int high, Т key)
{
int mid; J
Т midvalue; ;1
// условие останова:! ключ не найден
if (low > high) return (-1);
// сравнить ключ с элементом в середине списка.
// если совпадения нет, разделить на подсписки.
// применить процедуру бинарного поиска к подходящему подсписку
else
{
mid = (low+high)/2;
midvalue = A[mid]; // условие останова: ключ найден
if (key == midvalue)
return mid; // ключ найден по индексу mid
// просматривать левый подсписок, если key < midvalue;
//в противном случае - правый подсписок
else if (key < midvalue)
// шаг рекурсии
return BinSearch(A, low, mid-1, key);
else
// шаг рекурсии
return BinSearch(A, mid+1, high, key);
}
}
