Добавил:
СПбГУТ * ИКСС * Программная инженерия Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
51
Добавлен:
28.04.2021
Размер:
184.51 Кб
Скачать

Программа вычисления факториала на языке Си:

Функции языка Си, строго говоря, не являются функциями в математическом смысле, так как:

Их значения может зависеть не только от аргументов.

Их выполнение может привести к различным побочным результатам (например, изменению значений глобальных переменных).

Два вызова одной и той же функции с одними и теми же аргументами могут привести к различным результатам.

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

Функциональный код гораздо более понятно и компактно передает вычислительное содержимое воспроизводимой функции. Программа на языке Haskell:

factorial n = if n == 0 then 1 else n * factorial (n - 1)

Это рекурсивная структура, в которой в качестве начального условия выступает факт 0! = 1.

На языке Turbo Prolog программа выглядит следующим образом:

fact ( 0,1 ). fact ( N, M ) : -

N1 = N - 1, fact ( N1, M1 ), M = N * M1.

Первая строка выражает тот факт, что 0! = 1. То есть второй параметр функции (предиката) fact – это результат, а первый параметр – это число, факториал которого надо определить.

Пример выполнения: запрос f(3, F). В качестве F должен быть факториал числа 3.

Порядок выполнения: N1=2 (предыдущее)

Рекурсивный вызов f(2, F1) N=2

Рекурсивный вызов f(1, F2) Рекурсивный вызов f(0, F3).

В результате вычисление будет выполнено следующим образом:

F3=1

F2=1*N2=1

F1=F2*N1=2

F=F1*N=6

1

§. Компиляция.

Процесс компиляции состоит из следующих этапов:

1. Лексический анализ (сканирование).

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

Используется таблица пограничных символов языка, которыми являются пробелы, разделители ( . , : ; ), знаки операций (+, -, :, *, :=) и ключевые слова языка. Создается таблица символических имен (идентификаторов), в которую сканер вносит каждое встретившееся ему имя, которое построено по правилам и не относится к ключевым словам. Для каждой константы запоминаются значение, тип, основание системы счисления, размер.

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

Код типа лексемы

Номер в таблице

I

5

(I – признак того, что лексема – идентификатор; 5 – позиция в таблице идентификаторов)

Каждой лексеме соответствует своя запись в таблице кодов лексем, причем первой записи соответствует первая лексема.

2. Синтаксический анализ (parsing).

На основании таблицы кодом лексем компилятор выделяет конструкции в соответствии с грамматикой языка. К конструкциям относятся операторы, блоки операторов, описания процедур и функций. Состав таких конструкций зависит от языка и реализации компилятора.

Все выделенные конструкции проходят полную синтаксическую проверку. В результате последовательность лексем преобразуется в синтаксическое дерево разбора (parse tree).

3. Семантический анализ.

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

Пример синтаксически правильной конструкции, неправильной с точки зрения семантики

Объявленной константе (const) присваивается значение переменной.

Результатом анализа 1-3 является внутреннее представление программы, понятное компилятору, в одной из следующих форм:

oсвязочные списочные структуры, представляющие синтаксические деревья;

oмногоадресный код с явно именуемым результатом (тетрады);

oмногоадресный код с неявно именуемым результатом (триады);

oобратная (постфиксная) польская запись операций;

oассемблерный код или машинные команды.

2

Отличие одной формы от другой заключается в способе соединения операторов и операндов.

4.Распределение памяти.

5.Генерация кода.

Внутреннее представление программы преобразуется в машинный (объектный) код. Как правило, это преобразование выполняется поэтапно по мере формирования законченных синтаксических конструкций.

6. Оптимизация кода.

Оптимизация программы – это исключение лишних операций, перестановка и изменение операций в компилируемой программе с целью повышения эффективности конечного объектного кода. В качестве составляющих критерия эффективности обычно берутся объем памяти, необходимый для выполнения программы, и скорость выполнения (быстродействие) программы.

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

Оптимизация может выполняться для следующих типовых синтаксических конструкций:

oпоследовательностей операций с одним входом и одним выходом: исключение лишних операций и перестановка операций;

oлогических выражений;

oциклов;

oвызовов процедур и функций: оптимизация передачи параметров.

Кроме того, выполняется машинно-зависимая оптимизация: распределение регистров процессора, оптимизация кода для процессоров, допускающих распараллеливание вычислений.

§. Типы алгоритмов.

жадные алгоритмы. Идеальным случаем можно считать алгоритм, способный «выбрать из нескольких зол» единственно правильное. В основе его так же лежит принцип разделения, но в каждой точке он имеет основание выбрать одну из подзадач. Обычно это делается на основании особенностей организации обрабатываемых данных или их избыточности. Типичный пример: двоичный поиск в упорядоченных данных (4.6). Основой жадных алгоритмов является всегда довольно спорное утверждение: движение «по линии наименьшего сопротивления» в каждой точке приведет к желаемому результату.

полный перебор (исчерпывающий, комбинаторный перебор). Перечисленные выше подходы основаны на всевозможных «ухищрениях», основанных на особенностях предметной области алгоритма. Если же ничего не помогает, то остается полный перебор всех возможных вариантов решения задачи (8.4).

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

Основная идея динамического программирования – запоминание результатов решения повторяющихся подзадач, возникающих при поиске методом полного рекурсивного перебора с получением задач меньшей размерности. В сущности, при динамическом программировании вычисляется решение для всех подзадач. Вычисление идет от малых подзадач к большим, и ответы запоминаются в таблице. Преимущество этого метода состоит в том, что раз уж задача решена, ее ответ где-то хранится и никогда не вычисляется заново.

Рассмотрим эту технику на примере вычисления произведения n матриц М=М1 x М2 x...x Мk , где Mi-матрица с ri-1 строками и ri столбцами. Порядок, в котором эти матрицы перемножаются, может существенно сказаться на общем числе операций, требуемых для вычисления М, независимо от алгоритма, применяемого для умножения матриц.

Пример 4.1.

Будем считать, что умножение (р x q)-матрицы на (q x r)-матрицу требует pqr операций, и рассмотрим произведение (в квадратных скобках указаны размерности матриц).

3

М= М1 x М2 x М3 x М4 [10 x 20] [20 x 50] [50 x 1] [1 x 100]

Если вычислять М в порядке М1x (М2x(М34)), то потребуется 125000 операций, тогда как вычисления в порядке (М1x(М23))xМ4 занимает лишь 2200 операций.

рекурсия.

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

4

Соседние файлы в папке АОПИ. Лекции