Рекурсия и рекуррентные соотношения

Рекурсия, как альтернатива циклу, является одним из полезных средств в арсенале программиста, но содержит в себе «подводные камни». Попробуем разобраться, когда она бывает эффективна.

Простейший пример рекуррентного соотношения: F(0)=1, F(1)=1*F(0), F(2)=2*F(1), … , F(n)=n*F(n-1). Как Вы понимаете, это вычисление факториала. Но можно эти соотношения записать короче: F(n)=n*F(n-1) для n=1,2,…,N, причем F(0)=1.

Рекуррентные соотношения могут быть реализованы как через цикл (от начала к концу), так и через рекурсию (от конца к началу). Часто используют такие структуры данных, для которых целесообразно применять рекурсивные методы при написании программ.

Рекурсивные методы удобны при работе с рекурсивными структурами данных — списками, деревьями.

Примеры применения рекурсивных методов: вычисление факториала, Ханойские башни, закрашивание замкнутой области (см. ниже), подсчет числа слов в тексте, поиск пути в лабиринте, обход деревьев, обработка списков.

Необходимые определения

Метод P называется рекурсивным, если при выполнении тела метода происходит вызов метода P.
Рекурсия может быть прямой, если вызов P происходит непосредственно в теле метода P.
Рекурсия может быть косвенной, если в теле P вызывается метод Q (эта цепочка может быть продолжена), в теле которого вызывается метод P.
Важно! Для того чтобы рекурсия не приводила к зацикливанию, в тело нормального рекурсивного метода всегда встраивается оператор выбора, одна из ветвей которого не содержит рекурсивных вызовов.

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

Приведем традиционный пример рекурсивного определения функции, вычисляющей

Факториал целого числа

// Вычисление факториала в цикле и рекурсивно
// n! = 1*2*3*...*(n-1)*n    0!=1 (ро определению)
// Поясните, почему правильный результат только для n<21 ?
using System;
namespace factorial
{
    class Program
    {
        static void Main(string[] args)
        {
            // для наглядности введем 10 разных n
            for (int i = 0; i < 10; i++)
            {
                Console.Write("Введите n= ");
                int a = Convert.ToInt32(Console.ReadLine());
                Console.WriteLine(fact(a));
                Console.WriteLine(fakt(a));
                Console.ReadKey();
            }
        }
        // Факториал через цикл
        static Int64 fact(int n)
        {
            Int64 f = 1;
            for (int i = 1; i <= n; i++)
                f = f * i;
            return f;
        }
        // Факториал через рекурсию
        static Int64 fakt(int n)
        {
            Int64 f;
            if (n < 1)
                f = 1;
            else
                f = fakt(n - 1) * n;
            return f;
        }
    }
}

Этот пример приводят для пояснения связи между циклом и рекурсией. В рекурсивном методе fakt(n) мы видим две ветви (n<1) и (n>0). Отметим, что можно выразить n! через (n-1)! :  n!=n*(n-1)! . Это выражение совпадает со 2 ветвью рекурсии.

Честно говоря, этого примера недостаточно, чтобы убедить вас использовать рекурсивный метод. Если можете написать циклический алгоритм — то всегда используйте только его. Однако есть забавная задача, которая решается только через рекурсию (мне не попадалось ее решение через цикл для произвольного n) — это

Ханойские башни:

«Требуется за минимальное число операций переложить n алмазных колец разного диаметра пирамидки с 1-й на 3-ю через 2-ю иглу. Запрещено перекладывать более одного кольца за одну операцию и помещать кольцо большего диаметра над меньшим (в целях сохранности). В исходном положении — все кольца на игле 1, в конечном — на игле 3 имеют форму пирамиды» :

using System;
namespace HanoiTowers
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("n=");    // число алмазных колец
            int n = Convert.ToInt32(Console.ReadLine());
            hanoi(n, '1', '2', '3'); // Вызов. Иглы 1,2,3
            Console.ReadKey();
        }
        // Рекурсивная функция
        static void hanoi(int n, char a, char b, char c )
        {
            if (n < 2)
                Console.WriteLine(a.ToString() + "->" + c.ToString());
            else
            {
                hanoi(n - 1, a, c, b);
                Console.WriteLine(a.ToString() + "->" + c.ToString());
                hanoi(n - 1, b, a, c);
            }
        }
    }
}

Минимальное число операций равно 2n-1. Попробуйте придумать более быстрый алгоритм, не изменяя условия задачи. При n=30 мы имеем более 1 миллиарда операций.

А вот практический пример применения рекурсии —

Закрашивание замкнутой области

// Рекурсивное закрашивание
using System;
namespace Закрашивание
{
    class Program
    {
        static void Main()
        {
            // Задание границ звездочками 13х16
       string[] a ={"    *** ***    ",
                    "   *  ***  *   ",
                    "  *         *  ",
                    " *           * ",
                    "*     ***     *",
                    "*    *   *    *",
                    "*    *   *    *",
                    "*    *   *    *",
                    "*    *   *    *",
                    "*    *   *    *",
                    "*     ***     *",
                    "*             *",
                    "***************"};
            char[][] b = new char[a.Length][];  // массив b через конструктор
            // заполнение массива b[][] строками a[]
            for (int y = 0; y < a.Length; y++) b[y] = a[y].ToCharArray();
            print(b);           // вывод незакрашенной картинки
            Console.WriteLine();
            //  стартовая позиция кисти, попробуй (6,8) и другие !!!
            func(b, 3, 8);      
            print(b);           // Вывод после закраски
            Console.ReadKey();
        }
        // Рекурсивное заполнение пробелов символом 'X'
        static void func(char[][] b, int x, int y)
        {
            b[y][x] = 'X';
            if (b[y - 1][x] == ' ') func(b, x, y - 1);
            if (b[y][x + 1] == ' ') func(b, x + 1, y);
            if (b[y + 1][x] == ' ') func(b, x, y + 1);
            if (b[y][x - 1] == ' ') func(b, x - 1, y);
        }
        // покраска в три цвета
        static void print(char[][] b)
        {
            for (int y = 0; y < b.GetLength(0); y++)
            {
                for (int x = 0; x < b[y].Length; x++)
                {
                  //  Console.Write(b[y][x]);
                    if (b[y][x] == ' ') Console.BackgroundColor = ConsoleColor.Green;
                    if (b[y][x] == '*') Console.BackgroundColor = ConsoleColor.Yellow;
                    if (b[y][x] == 'X') Console.BackgroundColor = ConsoleColor.Blue;
                    Console.Write("  ");
                }
                Console.WriteLine();
            }
        }
    }
}

Основная идея рекурсии заложена в func(char[][] b, int x, int y): проверка соседних клеток на предмет возможности закрашивания произвольной замкнутой области.

Пример косвенной рекурсии

Даны рекуррентные соотношения:
Cn = Cn-1*x –Sn-1*y,
Sn= Sn-1*x + Cn-1*y,
где:
n = 2, 3, …, N;
x и y – числа, связанные соотношением x2+y2=1;
C1 = x, S1 = y.
Требуется:

  • Через рекуррентные соотношения, используя цикл, найти CN и SN.
  • Записать решение, используя косвенную рекурсию.
  • Сравнить время выполнения алгоритмов для N = 10, 11, … , 32.

Решение

class Program
    {
        static double x;
        static double y;
        static void Main()
        {
            Console.Write("Введите x ( |x| <= 1 ) : ");
            x = Convert.ToDouble(Console.ReadLine());
            y = Math.Sqrt(1-x*x);
            Console.Write("Введите N: ");
            int n = Convert.ToInt32(Console.ReadLine());

            // через рекурентные соотношения
            DateTime t1 = DateTime.Now;
            double C = x, C0;
            double S = y, S0;
            for (int k = 2; k <= n; k++) { C0 = C; S0 = S; C = C0 * x - S0 * y; S = S0 * x + C0 * y; } Console.WriteLine("Результат через рекуррентные соотношения: C = {0}, S = {1}.", C, S); DateTime t2 = DateTime.Now; int w = t2.Minute * 60 + t2.Second - t1.Minute * 60 - t1.Second; Console.WriteLine("Время в секундах: " + w); // через рекурсию t1 = DateTime.Now; Console.WriteLine("Результат через косвенную рекурсию: C = {0}, S = {1}.", CR(n), SR(n)); t2 = DateTime.Now; w = t2.Minute * 60 + t2.Second - t1.Minute * 60 - t1.Second; Console.WriteLine("Время в секундах: " + w); Console.ReadKey(); } // косвенная рекурсия static double CR(int n) { double c; if (n > 1)
                c = CR(n - 1) * x - SR(n - 1) * y;
            else
                c = x;
            return c;
        }
        static double SR(int n)
        {
            double s;
            if (n > 1)
                s = SR(n - 1) * x + CR(n - 1) * y;
            else
                s = y;
            return s;
        }
    }

Отмечу, что в языке логического программирования  Prolog конструкции типа цикла могут быть реализованы только через рекурсию.

Завершим раздел рассмотрением двух из трех ключевых принципов ООП — наследования и полиморфизма, считаю принцип инкапсуляции уже достаточно ясным.


NEW: Наш Чат, в котором вы можете обсудить любые вопросы, идеи, поделиться опытом или связаться с администраторами.


Помощь проекту:

Понравилась статья? Поделиться с друзьями:
5 1 голос
Рейтинг статьи
Подписаться
Уведомить о
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
()
x