Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

программирование_занятие_8

.doc
Скачиваний:
11
Добавлен:
14.05.2015
Размер:
113.66 Кб
Скачать

изучается:

Подпрограммы:

формальные и фактические параметры,

глобальные и локальные переменные.

Линейная сложность алгоритма.

Степенная сложность алгоритма, вложенные циклы.

Рекурсия, показательная (экспоненциальная) и факториальная сложность алгоритма.

Модульное программирование.

«Программирование сложных алгоритмов».

–Какой алгоритм следует отнести к сложным?

- в котором много шагов...

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

Подпрограммы

Несколько объединенных шагов в машинном языке «ассемблер» называют макрокомандой, в языке «бейсик» - подпрограммой, в языке «паскаль» - процедурой или функцией.

В общем случае, будем называть обособленную группу шагов, имеющую имя, подпрограммой.

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

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

Для вызова процедур в бейсике используется метка строки, являющаяся аргументом команды GOSUB, заканчивается подпрограмма командой возврата RETURN.

30 GOSUB 1000 . . . 100 END . . . 1000 . . . . . . 1200 RETURN

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

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

В языке «паскаль» подпрограмма выглядит как программа, то есть имеет свой раздел описаний и свой раздел команд.

PROCEDURE <имя>; {раздел описаний} BEGIN {раздел команд} END;

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

Наличие собственного раздела описаний позволяет иметь в процедуре переменные, одноименные с переменными в программе. Внутри процедуры такие переменные имеют свои значения, они называются локальными. По окончании процедуры локальные значения теряются, программа «вспоминает» свои значения одноименных переменных.

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

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

PROCEDURE <имя>(<параметры>); {} BEGIN {} END;

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

Чтобы отметить передачу адреса формальной переменной используется служебное слово var.

В приведенном примере процедуры, вычисляющей объем параллелепипеда по его размерам, параметры a,b,c передаются по значению, а параметр v адресом. Внутри процедуры переменная a используется для хранения площади основания, на которую потом умножается высота и получается итог. Параметр v, переданный адресом, сохранит результат, образованная внутри процедуры локальная переменная a значение 12 потеряет. В программе a=3.

procedure obiem(a,b,c:real; var v:real); begin a:=a*b; v:=a*c end; {в процедуре a изменилось} var a,b,c x:real; {переменная основной программы} begin {начало программы} a:=3; b:=4; c:=2; {}

obiem(a,b,c,x); {x получит значение 24, параметры a, b, c имеют исходные значения, «вспоминается» a=3} writeln(x); {вывод результата} end. {конец программы}

Как показано ниже, на место параметров, передаваемых по значению, можно ставить просто значения.

var x:real; {используем сразу значения} begin obiem(3,4,2,x); writeln(x); end.

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

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

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

FUNCTION <имя>(<параметры>):<тип результата>; {раздел описаний} BEGIN {раздел команд} <имя>:=<выделенное значение> END;

function obiem(a,b,c:real):real; begin obiem:=a*b*c end; begin writeln(obiem(3,4,2)); end.

Использование функций заметно упрощает программу.

Сложность алгоритмов

Линейный алгоритм можем считать одной большой командой. Время исполнения такой команды зависит от числа шагов в алгоритме. Считая, что время исполнения каждого шага примерно одинаково, имеем, что при увеличении числа шагов в два раза, время исполнения алгоритма увеличится так же в два раза.

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

Среди операторов тела цикла можно организовать другой цикл. Будем иметь ВНЕШНИЙ и ВНУТРЕННИЙ циклы. Получается, что один цикл ВЛОЖЕН в другой.

Внешний и внутренний циклы называют вложенными.

(н)

(*)-<─────────────────────┐

│ │

/ \ │

/ \ │

нет / \ да │

┌-<условие>───────┐ │

│ \ / │ │

│ \ / (*)-<───┐ │

│ \ / │ │ │

│ │ ┌─┴──┐ │

│ │ │тело│ │

│ / \ └─┬──┘ │

│ / \ │ │

│ нет / \ да│ │

│ ┌-<условие>──┘ │

│ │ \ / │

│ │ \ / │

└─────┐ │ \ / │

│ │ │

(к) └──────────────────┘

ГЛУБИНА ВЛОЖЕННОСТИ циклов - сколько циклов находятся один внутри другого. Приведена блок-схема вложенных циклов с глубиной вложенности равной двум.

Если внешний цикл выполняется N раз, а внутренний цикл выполняется M раз, то тело внутреннего цикла будет выполняться N*M раз.

В раде случаев, внешний и внутренний циклы выполняются одинаковое количество N раз, то есть тело внутреннего цикла выполняется N^2 раз. При увеличении счетчика N в 2 раза, время исполнения такого алгоритма увеличится в 4 раза.

Сложность алгоритма с вложенными циклами считается степенной. С увеличением глубины вложенности увеличивается степень.

Рекурсия

Другой вид усложнения программ – рекурсия.

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

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

РАЗЛИЧАЮТ:

1) РЕКУРСИВНОЕ ОПИСАНИЕ

В исполняемой части описываемого алгоритма присутствует обращение к самому описываемому алгоритму.

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

2) РЕКУРСИВНОЕ ОБРАЩЕНИЕ

При вызове алгоритма, выдающего значения, для исполнения, среди среди исходных данных используется вызов этого алгоритма.

Простейший пример - вычисление значения выражения Sin(Sin(x)).

3) КОСВЕННУЮ РЕКУРСИЮ

При выполнении алгоритма А вызывается алгоритм В, который при выполнении снова вызывает алгоритм А.

Косвенная рекурсия - довольно редко используемое явление. Возможна не только при вызове отдельных процедур, но и при использовании отдельных модулей.

Часто косвенная рекурсия является причиной "зацикливания" программ.

Примером одновременного использования рекурсивного описания и рекурсивного обращения является функция Аккермана.

Это функция двух переменных m и n:

┌ n+1 при m=0

Ack(m,n) = ┤ Ack(m-1,n+1) при m>0 и n=0

└ Ack(m-1), Ack(m,n-1) ) при m>0 и n>0

Таблица значений функции Аккермана приведена ниже.

В первой строке (m=0) значения на 1 больше аргумента n.

В начале очередной строки (n=0) находится элемент таблицы с 1 места вышестоящей строки. Остальные элементы берутся из вышестоящей строки, с места, номер которого берется из предыдущей клетки этой же строки таблицы.

m\n

0

1

2

3

4

5

6

7

8

9

10

11

12

0

1

2

3

4

5

6

7

8

9

10

11

12

13

1

2

3

4

5

6

7

8

9

10

11

12

13

2

3

5

7

9

11

13

3

5

13

4

13

Рассмотрим рекурсивное определение факториала:

┌ 1 при n=0

n! = ┤

└ n * (n-1)! при n>0

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

В рекурсивной ветви находится обращение к самой функции

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

if <условие> then ... <выход>

else <обращение к себе при других параметрах>

Рекурсивное программирование – сведение задачи к себе самой.

В некоторых языках рекурсия запрещена (фортран), в других (паскаль) - ее можно широко использовать.

В языке basic рекурсия не запрещена, но и нет аппарата передачи данных (все переменные глобальны).

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

программа на PASCALе (вычисление факториала)

procedure fakt(n:integer; var f:integer);

begin

if x<=0 then f:=1 {базовая (тривиальная) ветвь}

else begin

fakt(n-1,f); {рекурсивная ветвь}

f:=n*f

end

end;

var f,n:integer;

begin readln(n); fakt(n,f); writeln(f) end.

{получим число, вычислим его факториал, выведем результат}

Рассмотрим блок-схему алгоритма вычисления факториала.

(начало)

/─┴───/ Б Л О К - С Х Е М А

/ n / рекурсивной программы

────┬─ вычисления факториала

│0

+------------------┼-----------------------------------+

| FAKT(f,n) /\ ┌┬──────────────────┬┐ |

| / \ да 2││ FAKT(f,n-1) ││ |

| 1 <n>1 >────────┤├0 ││ |

| \ / ││ 5 ││ |

| нет\/ └┴───────┼──────────┴┘ |

| ┌──┴──┐ ┌───┴───┐ |

| 4│ f=1 │ 3│ f=n*f │ |

| └──┬──┘ └───┬───┘ |

| ├────────────────────┘ |

+-----------------5┼-----------------------------------+

/─┴───/ 0-1-2-3-5 рекурсивная ветвь

/ f / 0-1-4-5 базовая (тривиальная)

/───┬─/ ветвь

(конец)

Прямоугольник с двойной рамкой (блок 2) ПОЛНОСТЬЮ совпадает с большим прямоугольником, о чем говорит одинаковое имя FAKT. У него есть своя точка входа (0) и точка выхода (5).

0-1-2-3-5 рекурсивная ветвь.

0-1-4-5 базовая (тривиальная) ветвь.

В языке basic рекурсия не запрещена, но поскольку все переменные глобальны, для передачи данных требуются специальные средства.

Например, можно передавать данные, организовав стек на базе массива. Элемент массива хранит данные, используемые рекурсивным алгоритмом, индекс соответствует количеству входов в рекурсию.

Вариант программы:

10 z=20 ' организация стека

20 dim n(z),f(z) '

30 z=1 ' нач. значение счетчика рекурсий

40 INPUT N(Z) ' получить аргумент факториала

50 GOSUB 100 ' вход в рекурсию (на точку 0)

60 PRINT F(0) ' вывод результата вычислений

70 END '

80 '

100 IF N(Z)>1 THEN 130 ' точка 0, вход на блок 1 (ветвление)

110 F(Z)=1 ' блок 4, тривиальная ветвь

120 GOTO 170 ' конец тривиальной ветви.

130 Z=Z+1 'рекурсивная ветвь (следующий шаг рекурсии)

140 N(Z)=N(Z-1)-1 'новый параметр при сохранении старого,

150 GOSUB 100 ' рекурсивное обращение : блок 2

160 F(Z)=N(Z)*F(Z+1) ' блок 3, вычисление значений

170 Z=Z-1 ' выход из рекурсии

180 F(Z)=F(Z+1) ' передача значений

190 RETURN ' точка 5.

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

(* вывод 5 раз слова "Привет!" *)

var i:integer;

procedure cikl;

begin for i:=1 to 5 do writeln('Привет! ',i); end;

procedure rek;

begin

if i<=5 then

begin writeln('Привет! ',i); i:=i+1; rek; end;

end;

BEGIN

cikl; writeln('сработал цикл');

i:=1; rek; writeln('сработала рекурсия');

END.

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

(* поиск НОД *) var a,b,a1,b1:integer;

procedure cikl;

begin

while a1<>b1 do if a1<b1 then b1:=b1-a1 else a1:=a1-b1;

end;

procedure rek;

begin

if a<>b then

begin if a<b then b:=b-a else a:=a-b; rek; end;

end;

BEGIN

write('введите два числа '); readln(a,b); a1:=a; b1:=b

cikl; writeln(a1,' сработал цикл'); writeln;

rek; writeln(a,' сработала рекурсия');

END.

Сейчас предлагается программа на языке Pascal, которая показывает, что циклы типа ПОКА (WHILE) так же может быть заменен использованием рекурсии. Проследив соответствие между циклической и рекурсивной организацией программы, учитывая универсальность цикла ПОКА, делаем вывод, что любой цикл всегда может быть заменен рекурсией. Это заявление можно строго доказать.

Справедливо ли обратное утверждение, что рекурсию всегда можно заменить циклами?

Это заявление пока не доказано, но и не опровергнуто.

Замечу только, что способов вычисление функции Аккермана с помощью циклов мне неизвестно.

Существуют алгоритмы, в которых рекурсивных ветвей не одна, а несколько. (к концу отрезка пририсовать, под некоторым углом влево и вправо, два новых отрезка, к которым снова пририсовать отрезки по этому же правилу).

Если глубина рекурсии N, то отрезок будет рисоваться 2^N раз – показательная сложность алгоритма. Увеличение глубины вложенности на 1 приводит к увеличению времени исполнения алгоритма в 2 раза!

Если рекурсивных ветвей M, сложность алгоритма M^N.

Показательную сложность иногда называют экспоненциальной, (экспонентой в математике называют функцию e^x, е – число =2.7).

Встречается факториальная сложность алгоритма (факториалом называют произведение подряд идущих натуральных чисел от 0 до некоторого N).

Одно из заданий ЕГ 2004 года имело вид:

Следующий фрагмент программы записывает в переменную Max максимальный элемент в двумерном массиве Dist размера NxN, заполненном целыми неотрицательными числами:

Max:=0;

for i:=1 to N do

for j:=1 to N do

if Dist [i,j]>Max then Max:=Dist [i,j];

На очень медленном компьютере эта программа при N=1000 работала 5 секунд. Оцените время работы этой программы на том же компьютере при N=2000:

В приведенном тексте программы наблюдаем два вложенных цикла, каждый выполняется n раз. Сложность алгоритма – степенная, при увеличении n в 2 раза время исполнения алгоритма увеличится в 4 раза. Вместо 5 секунд программа будет работать 20 секунд, что и составит правильный ответ.

В ЕГ 2005 года так же было похожее задание:

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

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

Модульное строение языка.

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

Модуль - автономно компилируемая программная единица. Включает компоненты раздела описаний и, возможно, исполняемые операторы инициирующей части программы.

Разбивка на модули позволяет проводить раздельную компиляцию программы.

Код отдельного модуля размещается в отдельном сегменте памяти (64 килобайта).

Для подключения модулей в тексте основной программы используется команда

USES <список модулей через запятую>;

Модули можно использовать и внутри других модулей. Однако, рекурсия в любом виде запрещена.

Приведен пример модуля паскаль-программы.

UNIT <правильное имя> > начало модуля

INTERFACE \

<интерфейсная часть> / части модуля

IMPLEMENATION \ части модуля

<исполняемая часть> /

BEGIN \ части модуля

<инициирующая часть> /

END. > конец модуля

Структура модуля:

UNIT modul; <правильное имя>

Имя модуля должно совпадать с именем файла на диске, содержащим текст этого модуля. Расширение такого файла - *.PAS. Отдельно откомпилированный модуль размещается в одноименном файле с расширением *.TPU. (Turbo Pascal Unit)

INTERFACE

Интерфейсная часть содержит объявление всех глобальных объектов модуля, которые должны стать доступными основной программе и/или другим модулям. Указывается только заголовки. Нельзя использовать опережающее описание.

IMPLEMENTATION

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

BEGIN

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

END. – сигнал окончания текста модуля.

Наличие обязательно всех частей в модуле не обязательно.

При работе с модулями можно использовать варианты компиляции:

COMPILE - все используемые модули должны быть заранее откомпилированы.

MAKE - если модуль *.TPU не найден или в соответствующий *.PAS - файл вносились изменения, то происходит компиляция и использование содержимого *.PAS - файла.

BUILD - существующие *.TPU - файлы игнорируются, все модули заново компилируются.

В системе Turbo-Pascal имеются стандартные модули

SYSTEM \ - не требует подключения - содержит стандарт паскаля

DOS \ - содержит средства работы с диском

CRT > - хранятся в - средства работы с экраном

PRINTER / TURBO.TPL - средства доступа к матричному принтеру

OVERLAY / - средства разработки оверлейных программ

тексты этих модулей объединены в файле TURBO.TPL

GRAPH \ - средства работы с графическим экраном

TURBO3 \ - хранятся в отдельных \

GRAPH3 / файлах / для совместимости с TURBO PASCAL 3

Завершение

По окончании нашего урока, вы должны знать ответы на вопросы:

Что называют подпрограммой?

Чем отличаются формальные параметры от фактических?

Что называют глобальной, а что и локальной переменной?

Что такое линейная сложность алгоритма?

В каких случаях алгоритм имеет степенную сложность?

В каких случаях алгоритм имеет показательную (экспоненциальную) сложность?

Что такое модульное программирование?