
- •Полустатические структуры данных
- •4.1. Характерные особенности полустатических структур
- •4.2. Строки
- •4.2.1. Логическая структура строки
- •4.2.2. Операции над строками
- •4.2.3. Представление строк в памяти
- •Динамические структуры данных. Связные списки
- •5.1. Связное представление данных в памяти
- •5.2. Стеки
- •5.2.1. Логическая структура стека
- •5.2.2. Машинное представление стека и реализация операций
- •5.2.3. Стеки в вычислительных системах
- •5.3. Очереди fifo
- •5.3.1. Логическая структура очереди
- •5.3.2. Машинное представление очереди fifo и реализация операций
- •5.3.3. Очереди с приоритетами
- •5.3.4. Очереди в вычислительных системах
- •5.4. Деки
- •5.4.1. Логическая структура дека
- •5.4.2. Деки в вычислительных системах
- •5.5. Связные линейные списки
- •5.5.1. Машинное представление связных линейных списков
- •5.5.2. Реализация операций над связными линейными списками
- •5.5.3. Применение линейных списков
- •5.6 Мультисписки
- •5.7. Нелинейные разветвленные списки
- •5.7.1. Основные понятия
- •5.7.2. Представление списковых структур в памяти
- •5.7.3. Операции обработки списков
- •5.8. Управление динамически выделяемой памятью
- •6. Деревья
- •6.1. Бинарные деревья
- •6.2. “Прошитые” деревья
- •6.3. Графы
- •6.4. Алгоритмы поиска путей в графе
- •6.4.1. Путь с минимальным количеством промежуточных вершин (волновой алгоритм)
- •6.4.2. Путь минимальной суммарной длины во взвешенном графе с неотрицательными весами (алгоритм Дейкстры)
- •6.4.3. Путь минимальной суммарной длины во взвешенном графе с произвольными весами для всех пар вершин (алгоритм Флойда)
- •6.4.4. НахождениеKпутей минимальной суммарной длины во взвешенном графе с неотрицательными весами (алгоритм Йена)
- •7. Классы и объекты
- •8. Рекурсия
- •8.1. Некоторые задачи, где можно применить рекурсию
- •8.2. Использование рекурсии в графике
- •8.2.1. Кривые Гильберта
- •8.2.2. Кривые Серпинского
- •9. Алгоритмы Сжатия информации
- •9.1. Что такое архивирование и зачем оно нужно
- •9.2. Терминология
- •9.3. Методы кодирования
- •9.4. Модели входного потока
- •9.5. Моделирование и энтропия
- •9.6. Адаптированные и неадаптированные модели
- •9.7. Алгоритмы архивации данных
- •9.8. Сжатие способом кодирования серий (rle)
- •9.9. Алгоритм Хаффмана
- •9.10. Арифметическое кодирование
- •9.11. Алгоритм Лемпеля-Зива-Велча (Lempel-Ziv-Welch - lzw)
- •9.11.1. Двухступенчатое кодирование. Алгоритм Лемпеля-Зива
- •Библиографический Список
- •Оглавление
8. Рекурсия
Рекурсивным называется объект, частично состоящий или определяемый с помощью самого себя.
Рекурсивные определения представляют собой мощный аппарат в математике. Например:
Натуральные числа:
а) 1 есть натуральное число,
б) число, следующее за натуральным, есть натуральное число.
Деревья:
а) 0 есть дерево ("пустое дерево"),
б) если А1 и А2 - деревья, то построение, содержащее вершину с двумя ниже расположенными деревьями, опять дерево.
Функция n! "факториал" (для неотрицательных целых чисел):
а) 0!=1,
б) n>0: n!=n·(n-1)!
Мощность рекурсивного определения заключается в том, что оно позволяет с помощью конечного высказывания определить бесконечное множество объектов. Аналогично, с помощью конечной рекурсивной программы можно описать бесконечное вычисление, причем программа не будет содержать явных повторений. В общем виде рекурсивную программу Р можно выразить как некоторую композицию Р из множества операторов С (не содержащих Р) и самой Р: Р=Р[С,Р].
Для выражения рекурсивных программ удобнее пользоваться процедурами или функциями. Если некоторая процедура Р содержит явную ссылку на саму себя, то ее называют прямо рекурсивной, если же Р ссылается на другую процедуру В, содержащую ссылку на Р, то Р называют косвенно рекурсивной.
Как правило, с процедурой связывают множество локальных переменных, которые определены только в этой процедуре. При каждой рекурсивной активации процедуры порождается новое множество локальных, связанных переменных. Хотя они имеют те же самые имена, что и соответствующие элементы локального множества предыдущего "поколения" этой процедуры, их значения отличны от последних, а любые конфликты по именам разрешаются с помощью правил, определяющих область действия идентификаторов: идентификатор всегда относится к самому последнему порожденному множеству переменных.
Рекурсивные процедуры могут приводить к не заканчивающимся вычислениям. Очевидно основное требование, чтобы рекурсивное обращение к Р управлялось некоторым условием Х, которое в какой-то момент становится ложным.
Р : if X then P[C,P]
Основной способ доказательства конечности некоторого повторяющегося процесса:
Определяется функция f(x), такая, что из f(x)
0 следует истинность условия окончания цикла.
Доказывается, что при каждом прохождении цикла f(x) уменьшается.
Аналогично
доказывается и окончание рекурсии -
показывается, что Р
уменьшает f(x),
такую, что из f(x)0
следует истинностьВ.
В практических приложениях важно убедиться, что максимальная глубина рекурсий не только конечна, но и достаточно мала. Дело в том, что каждая рекурсивная активация процедуры Р требует памяти для размещения ее переменных. Кроме этих переменных нужно еще сохранять текущее "состояние вычислений", чтобы можно было вернуться в него по окончании новой активации Р.
8.1. Некоторые задачи, где можно применить рекурсию
Задача 1. Вычисление факториала целого положительного числа n.
Для решения будем использовать равенства 0!=1, n!=n·(n-1)!
long Factorial( int n )
{
return n>1 ? n * Factorial( n-1 ) \ // if n > 1
: 1; // if n <= 1
}
Обратите внимание на некоторую двойственность использования имени factorial внутри описания функции: оно обозначает как переменную, так и вызываемую рекурсивно функцию. К счастью, в нашем случае они различаются по скобкам после имени, но если бы функция была без параметров, то дело было бы плохо. (Стандартная, но трудно находимая ошибка возникает, если автор полагает, что он использует значение переменной, а компилятор в этом месте видит рекурсивный вызов.)
Задача
2. Написать
рекурсивную функцию вычисления xn,
где n0.
Для решения будем использовать равенства xn = 1, при n = 0, и xn =x·xn-1, при n>0.
long Step( int x, int s )
{
return s>0 ? x * Step( x, s-1 ) : 1;
}
Задача 3. Ханойские башни. Когда-то в Ханое стоял храм и рядом с ним три башни (столба). На первую башню надеты 64 диска разного диаметра: самый большой - внизу, а самый маленький - вверху. Монахи этого храма должны были перенести все диски с первого столба на третий, соблюдая следующие правила:
можно перемещать только по одному диску;
больший диск нельзя класть на меньший;
снятый диск нельзя отложить, его необходимо сразу надеть на другой столб.
Предположим, с первого столба А надо перенести на третий С n дисков. Диски пронумерованы в порядке возрастания их диаметров. Предположим, что мы умеем переносить n-1 дисков. В этом случае n дисков перенесем посредством следующих шагов:
верхние n-1 дисков перенесем с первого на второй, пользуясь свободным третьим столбом;
последний диск наденем на третий столб;
n-1 дисков перенесем на третий, пользуясь свободным первым столбом.
Аналогичным образом можно перенести n-1, n-2 и т.д. дисков. Когда n=1, перенос осуществляется непосредственно с первого столба на третий.
void Move( int n, char a, char b, char c )
{
if( n > 0 )
{
Move( n-1, a, c, b );
printf( "%d -> %d ", a, c );
Move( n-1, b, a, c );
}
}
int main()
{
Move( 3, 1, 2, 3 );
return 0;
}
Напишем рекурсивную процедуру перемещения i верхних колец с m-го стержня на n-й (предполагается, что остальные кольца больше по размеру и лежат на стержнях без движения).
void Move( int i, int m, int n )
{
if( i == 1 ) printf("Can move %d -> %d\n", m, n );
else
{
int s = 6 - m - n; // s - третий стержень: сумма номеров равна 6
Move( i-1, m ,s );
printf("Can move %d -> %d\n", m, n );
Move( i-1, s, n );
}
}
(Сначала переносится пирамидка из i-1 колец на третью палочку.
После этого i-е кольцо освобождается, и его можно перенести куда следует. Остается положить на него пирамидку.)
Задача 4. Вывести все сочетания из n по k.
При вводе задается набор символов в виде строки. Длина строки len = n. Затем вводится число k.
#include <iostream>
typedef unsigned char byte;
std::string s, ss;
byte k, n, ii;
int num, count;
void Cmn( std::string q, byte i, byte j )
{
int len;
char ch;
while( count <= n )
{
q.assign( (char*)&s[j], 1 ); // в стек заносятся элементы по одному
count++;
Cmn( q, 0, j+1 );
}
Label:
len = q.size();
if( len < k )
if( j+i <= n ) Cmn( q, i+1, j );
if( len < k && j+i <= n )
if( q[len] != s[j+i] )
{
q.insert( q.size(), (char*)&s[j+i], 1 );
goto Label;
}
if( q.size() == k )
{
printf("%s : %d ", q.c_str(), num );
num++;
}
}
int main()
{
num = 1;
char chh[] = "12345"; // эдементы
k = n = strlen( chh ); // количество элементов
s.assign( chh, n );
count = 1;
Cmn( std::string(), 0, 0 );
return 0;
}
Задача 5. Перечислить все способы расстановки n ферзей на шахматной доске n×n, при которых они не бьют друг друга.
const int N = 8;
int x[N+1]; // индекс - номер ферзя, то есть номер вертикали
bool a[N+1]; // занятость горизонтали, где индекс массива номер горизонтали
bool b[ (N-1)*2 + 1 ]; /* номер диагонали определяется
разностью индексов квадратной матрицы i-j, а диагонали
параллельны главной диагонали матрицы */
bool c[ N*2+1 ]; /* номер диагонали определяется суммой
индексов i+j, а диагонали параллельны вспомогательной */
int Count; // номер варианта расстановки ферзей
void Print()
{
Count++;
for( int k = 0; k<N; k++ ) printf( "%d..",x[k] );
printf( "%d\n", Count );
char ch;
if( Count % 24 == 0 ) { printf(" Press Enter...");gets(&ch); }
}
void Try( int i )
{
for( int j=1; j<=N; j++ )
if( a[j] && b[ i-j + (N-1) ] && c[i+j] ) // если клетка не бьется, то
{
x[i] = j; // то ферзю i устанавливается номер горизонтали j
// соответствующие горизонтали и две вертикали становятся занятыми
a[j] = false;
b[ i-j + (N-1) ] = false;
c[i+j] = false;
/* если это не последний ферзь, то пытаемся поставить следующий ферзь, иначе распечатка варианта*/
if( i<n ) Try( i+1 );
else Print();
/* если нет небитого поля для данного ферзя, то предыдущая занятая позиция освобождается (отход назад) и все повторяется */
a[j] = true;
b[ i-j + (N-1) ] = true;
c[i+j] = true;
}
}
void Init()
{
Count = 0;
/* Все элементы логических массивов a,b,c становятся TRUE (1), то есть клетки доски не находятся под боем */
for( int i=0; i< N+1; i++ ) a[i] = true;
for( int i=0; i< (N-1)*2 + 1; i++ ) b[i] = true;
for( int i=0; i< N*2+1; i++ ) c[i] = true;
// все ферзи вне доски, то есть все x[i]=0
for( int i=0; i< N+1; i++ ) x[i] = 0;
}
int main()
{
Init();
Try( 1 );
return 0;
}
При анализе рекурсивной программы возникает, как обычно, два
вопроса:
(а) почему программа заканчивает работу?
(б) почему она работает правильно, если заканчивает работу?
Для (б) достаточно проверить, что (содержащая рекурсивный вызов) программа работает правильно, предположив, что вызываемая ею одноименная программа работает правильно. В самом деле, в этом случае в цепочке рекурсивно вызываемых программ все программы работают правильно (убеждаемся в этом, идя от конца цепочки к началу).
Чтобы доказать (а), обычно проверяют, что с каждым рекурсивным вызовом значение какого-то параметра уменьшается, и это не может продолжаться бесконечно.