Как стать автором
Обновить

О рекурсивных процедурах

Время на прочтение7 мин
Количество просмотров1.2K
Приветствую вас, Хабролюди!

Речь пойдёт о прелестях рекурсивных процедур.


Функция, как мы знаем, принимает аргументы и возвращает значение в зависимости от них. Она обычно имеет вид y = f(x1,...xn);
При этом она не должна совершать никаких побочных действий. Потому что в таком случае она будет называтья процедурой.
Процедура может иметь примерно такой вид:

Copy Source | Copy HTML
  1. void someProc (x1,...xn)
  2. {
  3. doDomething1();
  4. ...
  5. doSomethingN();
  6. }



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

Если в теле функции мы организовываем вызов её же самой (рекурсию), то как правило,
используем возвращаемое значение для промежуточных вычислений. К примеру, подсчитать факториал или обработать список.

Набивший оскомину пример факториала, но необходимый для иллюстрации:

Copy Source | Copy HTML
  1. int fac(n)
  2. {
  3. if(n == 0)
  4.     return 1;
  5. return n * fac(n-1);
  6. }



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

А что же нам может дать рекурсивная процедура? У неё есть очень хорошая особенность, а именно: пространство ДО вызова самой себя и ПОСЛЕ.
Причём то, что ПОСЛЕ, выполняется впервые именно в тот момент, когда встретилось условие выхода из рекурсии (в простом случае).

Copy Source | Copy HTML
  1. void recProc(....)
  2. {
  3. // вот тут делаем всё до рекурсивного вызова
  4. recProc(...);
  5. // а здесь - всё, что после выхода из рекурсии
  6. }



То есть рекурсивную процедуру можно сравнить с пружиной, которая сначала закручивается, а потом раскручивается в исходное положение.
Проиллюстрируем на примере. Скажем, стоит задача распечатать числа от 0 до N не используя цикл.

Вот так выглядит процедура:

Copy Source | Copy HTML
  1. void cnt(int i)
  2. {
  3.     if(i) {
  4.         // printf("%d\n",i); // ДО вхождения
  5.         cnt(i-1);
  6.     }
  7.     printf("%d\n",i); // ПОСЛЕ вхождений
  8. }



В ней обозначен выход — если i станет нулём. Каждый раз она вызывается с аргументом, уменьшенным на единицу. Как только она вызвалась с нулём, происходит печать i. И выход. Но выходит-то она пока сама в себя и на предыдущую итерацию.
В результате мы получим печать цифр от 0 до заданного аргумента. Пружина затянулась и распускается. Мы наблюдаем процесс роспуска как раз.

Если раскомментировать строчку ДО вхождения, а ПОСЛЕ закомментировать, то мы получим обратный счёт. Если раскомментировать всё, то получится палиндром. Сначала нисходящая последовательность (закручивание), а потом возрастающая (роспуск).

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

К примеру, число 496 в десятичной системе раскладывается так:
496 % 10 (берём остаток от деления на основание) = 6
49 % 10 (делим то, что осталось от предыдущего деления) = 9
4 % 10 = 4

Способ имеет 2 недостатка:
1. Мы получаем цифры с конца
2. Мы не знаем, какой длины число

Керниган и Ричи предложили вот такое решение:

Copy Source | Copy HTML
  1. void itoa(int n, char s[])
  2. {
  3.     int i, sign;
  4.  
  5.     if ((sign = n) <  0) /* записываем знак */
  6.         n = -n; /* делаем n положительным числом */
  7.     i =  0;
  8.     do { /* генерируем цифры в обратном порядке */
  9.         s[i++] = n % 10 + '0'; /* берем следующую цифру */
  10.     } while ((n /= 10) >  0); /* удаляем */
  11.     if (sign < 0)
  12.         s[i++] = '-';
  13.     s[i] = '\0';
  14.     reverse(s);
  15. }



То есть они выстраивают строчку задом наперёд, а потом, с помощью функции reverse, её переворачивают. Решение, что называется «в лоб» и оставляет вопросы.
К примеру, сколько памяти выделять под строчку. Хитрые отцы-основатели предпочли это обойти и писать данные в уже определённый массив.
Да и переворачивать каждый строчку после преобразования — не комильфо. Потому как подобные вызовы весьма часты и это, несомненно, скажется на производительности.

Тут нам и приходит на помощь рекурсивная процедура. Она способна преобразовать
10-тичные, 16-ричные, 2-ичные и 8-ричные числа в строковый вид.

Вот как она выглядит:

Copy Source | Copy HTML
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <assert.h>
  4.  
  5. #define DEC  0
  6. #define OCT 1
  7. #define HEX 2
  8. #define BIN 3
  9.  
  10. char *uitoa(int a,int r);
  11.  
  12. int main()
  13. {
  14.     int num = 7654894;
  15.     int e_num = 0x4E35FF;
  16.     int b_num = 0b1110011;
  17.     int o_num = 077432;
  18.  
  19.  
  20.     printf("%s\n",uitoa(e_num,HEX));
  21.     printf("%s\n",uitoa(b_num,BIN));
  22.     printf("%s\n",uitoa(o_num,OCT));
  23. }
  24.  
  25.  
  26.  
  27.  
  28. char *uitoa(int a,int r)
  29. {
  30. /* Процедура принимает число и систему счисления <br/>*<br/>* инициализируются изначальна статические переменные<br/>* которые не будут сбрасываться на различных этапах рекурсии <br/>*/
  31.     static int szmem =  0; // Размер памяти под строчку
  32.     static char *mas =  0; // Указатель на начало памяти под строчку 
  33.     static char *bgmas =  0; // То же самое
  34.  
  35.     char numb [] = "0123456789ABCDEF"; // Знаки 0 - это '0', 15 - 'F'
  36.     int dig[] ={10,8,16,2}; // Основания 
  37.  
  38.     if(a == 0) {
  39.         if(szmem == 0) {
  40.  
  41. // Это если аргументом был самый обыкновенный 0
  42. // Выделяем 2 sizeof(char), кладём знак 0 и конец строки (просто 0)
  43.  
  44.             mas = malloc( 2 * sizeof(char) );
  45.             assert(mas);
  46.             *mas = '0';*(mas + 1) =  0;
  47.  
  48.             return mas; // Больше никаких действий
  49.         }
  50.  
  51. /* Здесь мы оказываемся, когда szmem равен количеству байт<br/>* под строчку и при параметре a равным 0 (условие выхода)<br/>* Выделяем память szmem + sizeof(char) и ставим в конце 0<br/>* как завершение строки и выходим из рекурсии<br/>*/
  52.  
  53.     bgmas = mas = malloc( szmem * sizeof(char) + sizeof(char) );
  54.     assert(mas);
  55.  
  56.     *(mas+szmem) = (char) 0;
  57.  
  58.     }else {
  59.  
  60. /* То самое место ДО входа. Пока ещё a!=0 Тупо увеличиваем <br/>* на единицу необходимое количество памяти и<br/>* делаем рекурсивный вызов с уменьшенным <br/>* параметром a<br/>*/
  61.  
  62.         szmem++;
  63.         uitoa( a / dig[r],r );
  64.  
  65. /* То самое место ПОСЛЕ выхода из рекурсии<br/>* То есть мы на каждом этапе выхода, заполняем<br/>* с самого НАЧАЛА нашу строчку знаками,<br/>* начиная со СТАРШЕГО разряда. <br/>*<br/>* Помним: в это место мы приходим после того,<br/>* как "пружина" начала роспуск. А так как мы <br/>* на этапе "закручивания" входили с уменьшающимся<br/>* параметром, то тут он будет расти. <br/>*<br/>* Указатель<br/>* mas изменяется только здесь, потому что <br/>* объявлен как статический.<br/>*/
  66.  
  67.         *mas++ = numb [a % dig[r]];
  68.     }
  69.  
  70.     szmem =  0;
  71.     return bgmas;
  72. }



Процедура выделяет память и возвращает на неё указатель. Так что после получения строчки и по окончании работы с ней, следует выполнить освобождение памяти, воизбежание memory leak.

Надеюсь, кому-та статья будет полезной в деле понимания рекурсии.
Теги:
Хабы:
Всего голосов 30: ↑14 и ↓16-2
Комментарии36

Публикации