
- •Глава 1. Разработка эффективных алгоритмов
- •Value значение переменной X, для которого рассчитывается Pn(X);
- •Value значение переменной X, для которого рассчитывается Pn(X);
- •Value значение переменной X, для которого рассчитывается Pn(X);
- •Void Insert_Element(char New_Unit, unsigned Free, unsigned Position, char *Name, unsigned Next)
- •Void main()
- •Void Max_Min_Element(int array[], unsigned Size, unsigned max, unsigned min)
- •Алгоритм 4.2 нахождения наибольшего и наименьшего элементов множества
- •Алгоритм 6. Сортировка последовательности чисел слиянием
- •Void Merge(int *out1, int *out2, unsigned size, int *sorted)
- •Алгоритм 7. Динамического программирования для вычисления порядка, минимизирующего сложность умножения цепочки из n матриц m1m2…Mn
- •Void main()
Void main()
{
// Опустошение стека
vertex.top=0;
// В этом месте должна стоять функция ввода бинарного дерева в виде структуры,
// состоящей из двух массивов leftson и rightson
// Процедура прохождения дерева
Left: while (graph.leftson[node] != 0)
{
vertex.name[++vertex.top]=node; // Заталкивание узла в стек
node=graph.leftson[node];
}
Center: number[node]=counter++;
if (graph.rightson[node] != 0)
{
node=graph.rightson[node];
goto Left;
}
if (vertex.top != 0)
{
node=vertex.name[vertex.top--]; // Выталкивание узла из стека
goto Center;
}
} // Конец main
Корректность работы этого алгоритма была проверена на практике, но её можно доказать и теоретически по индукции. Следует обратить внимание на то, что использование языка C позволяет здесь обойтись без создания специальных функций (подпрограмм, процедур) Pull и Pop для вталкивания и выталкивания узла из стека.
Недостаток данного алгоритма заключается в том, что он в двух местах использует оператор goto, что считается “дурным тоном” у современных программистов. Действительно, использование данного оператора затрудняет понимание алгоритма, так как зачастую тяжело уследить за причинами, приведшими к переходу программы на указанную метку. Поэтому современные программисты стараются избегать использования оператора goto. Однако goto часто применяют, когда непредвиденное состояние или ошибка могут возникнуть внутри многократно вложенных циклов. В этой ситуации goto может значительно упростить программу. Похожая ситуация имеет место и в запрограммированном алгоритме, где обойтись без оператора goto достаточно сложно.
При создании алгоритма, который присваивает узлам номера в соответствии с внутренним порядком, постоянно приходится сталкиваться с ситуациями, когда используемая процедура должна повториться с самого начала. Хорошо было бы описать подобную процедуру специальной функцией и обращаться к ней по мере надобности при прохождении бинарного дерева во внутреннем порядке. Здесь на помощь приходит рекурсия, которая позволяет сделать программу прохождения дерева более наглядной и простой, однако коэффициент c в выражении для порядка временной сложности O(cn) у этой программы может быть выше, чем у программы 1.
Программа 3.2 нумерации узлов двоичного дерева в соответствии с внутренним
порядком.
Вход. Тот же, что и у программы 1.
Выход. Тот же, что и у программы 1.
Метод. Алгоритм тоже использует переменные node и counter, смысл которых тот же, что и в программе 1. Функция Inorder применяется рекурсивно.
// Программа прохождения бинарного дерева во внутреннем порядке с использованием
// рекурсии, STACK_SIZE – число узлов в дереве, ROOT – номер корня.
#include <stdio.h>
#include <io.h>
#include <stdlib.h>
#define STACK_SIZE 10 // Размер стека и число узлов графа – двоичного дерева
#define ROOT 1 // Номер корня
typedef struct { // Структура, задающая двоичное дерево, и состоящая из
unsigned leftson[STACK_SIZE]; // двух массивов leftson и rightson
unsigned rightson[STACK_SIZE];
} binary_tree;
binary_tree graph;
void Inorder(unsigned node, binary_tree graph, unsigned number[], unsigned *counter);
unsigned number[STACK_SIZE], counter=1, index;
void main()
{
// В этом месте должна стоять функция ввода бинарного дерева в виде структуры,
// состоящей из двух массивов leftson и rightson
// Прохождение дерева во внутреннем порядке
Inorder(ROOT, graph, number, &counter);
} //Конец main
void Inorder(unsigned node, binary_tree graph, unsigned number[], unsigned *counter)
// Рекурсивная процедура прохождения бинарного дерева во внутреннем порядке
{
unsigned vertex, count;
count=*counter;
if (graph.leftson[node] != 0)
{
vertex=graph.leftson[node];
Inorder(vertex, graph, number, &count);
}
number[node]=count++;
if (graph.rightson[node] != 0)
{
vertex=graph.rightson[node];
Inorder(vertex, graph, number, &count);
}
*counter=count;
} //Конец Inorder
Сама главная программа предельно проста. В функции Inorder переменная count вводится для обеспечения возможности изменять значение глобальной переменной counter на выходе функции Inorder.
В основе реализации процедур с рекурсией лежит стек, где хранятся данные, участвующие во всех вызовах процедуры, при которых она ещё не завершила работу. Иными словами в стеке находятся все неглобальные данные. Стек разбит на фрагменты стека, представляющие собой блоки последовательных ячеек (регистров). Однако, благодаря тому, что язык C допускает применение рекурсивных функций, в данной программе можно не использовать стек для запоминания ненумерованных пропущенных узлов. Компилятор это сделает за нас. С одной стороны, это очень удобно, поскольку значительно упрощает процесс создания программ, но с другой стороны, самостоятельная организация запоминания данных в стеке могла бы улучшить временную сложность программы.
1.7. Разделяй и властвуй
Для решения той или иной задачи её часто разбивают на части, находят их решения и затем их них получают решение всей задачи. Этот приём, особенно если применять его рекурсивно, часто приводит к эффективному решению задачи, подзадачи которой представляют собой ее меньшие версии. Проиллюстрируем данную технику на примерах, сопровождаемых анализом получающихся рекуррентных уравнений.
Рассмотрим задачу нахождения наибольшего и наименьшего элементов множества S, содержащего n элементов. Для простоты будем считать, что n есть степень числа 2. Очевидный путь поиска наибольшего и наименьшего элементов состоит в том, чтобы искать их по отдельности. Например, следующая функция находит наибольший и наименьший элементы множества S, заданного массивом array, произведя 2n-2 сравнений его элементов. Не ограничивая общности, массив выбрали типа int. Однако если требуется другой тип массива, то необходимо будет изменить тип у самой функции Max_Min_Element, у массива array и у указателя *ptr на соответствующий.
// Программа 4.1 поиска наибольшего и наименьшего элементов массива