Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Матрицы и типовые алгоритмы обработки матриц




 

Многие задачи связаны с обработкой многомерных массивов данных. Наиболее распространены при этом двумерные массивы.

В математике двумерные массивы представляются матрицей:

или, в сокращенной записи,

,

где n -- число строк матрицы, m -- число столбцов, i, j -- индексы (номера) текущих строки и столбца, на пересечении которых находится элемент .

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

var ИмяМатрицы: array

[n1..n2, m1..m2] of Тип;

Здесь n1..n2 -- диапазон значений номера строки, n1 и n2 -- целочисленные константы, m1..m2 -- диапазон значений номера столбца, значения m1 и m2 также целочисленные.

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

Под матрицу из n строк и m столбцов выделяется область памяти размером n*m*k байт, где k -- размер в байтах одного элемента. Для известных нам типов данных этот размер можно узнать из табл. 2.1. В оперативной памяти матрица хранится всегда построчно.

Например, для матрицы A, описанной оператором вида

var A:array [1..5,1..4] of real;

выделяется 20 ячеек памяти по 6 байт, причем в следующей за элементом A1,4 ячейке хранится значение элемента A2,1.

Обращение к отдельным элементам матрицы осуществляется с помощью переменной с двумя индексами, например:

ai,j a[i,j]

a2,1 a[2,1]

a2n,k a[2*n,k]

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

Поскольку адресация памяти в любом случае линейна, следует понимать матрицу как удобный для программиста структурный тип данных. В отдельных случаях использование матрицы может быть заменено использованием вектора с тем же количеством элементов: так, матрице An,m всегда может быть сопоставлен вектор b из n*m элементов, а обращение к элементу A[i,j] при нумерации строк и столбцов с единицы может быть заменено на обращение к элементу b[(i-1)*m+j].

Подобно тому, как любая последовательная обработка вектора выполняется в цикле for, обработка матрицы осуществляется в двойном цикле for:

for i:=1 to n do

for j:=1 to m do

{Обработка элемента A[i,j]}

Согласно правилу выполнения кратных циклов, переменная j меняется в этом двойном цикле быстрее, чем i, таким образом, обработка всех элементов матрицы будет выполнена построчно. Для последовательной обработки всех элементов матрицы по столбцам достаточно поменять местами циклы по i и j:

for j:=1 to m do

for i:=1 to n do

{Обработка элемента A[i,j]}

Теоретически мы могли бы решить эту же задачу и перестановкой индексов в обращении к элементу матрицы (использовать запись A[j,i] вместо A[i,j]), однако, во избежание путаницы, делать этого не рекомендуется.

Приведем примеры использования двойного цикла for для ввода и вывода элементов матрицы. Пусть матрица c размерностью 432 (как мы помним, это означает, что в матрице 4 строки и 2 столбца) описана оператором вида

var c:array [1..4,1..2] of real;

В программе предусмотрено также 2 целочисленных счетчика для строк и столбцов матрицы:

var i,j:integer;

В этом случае типовой ввод матрицы с клавиатуры мог бы выглядеть так:

writeln ('Введите матрицу c ',

'размерностью 4*2');

for i:=1 to 4 do

for j:=1 to 2 do read (c[i,j]);

Иногда удобнее печатать отдельное приглашение к вводу каждого элемента:

writeln ('Введите матрицу c[4,2]');

for i:=1 to 4 do

for j:=1 to 2 do begin

write ('c[',i,',',j,']=');

readln (c[i,j]);

end;

Например, в качестве приглашения к вводу элемента c1,1 на экране будет напечатано:

c[1,1]=

Оператор readln используется, поскольку элементы вводятся по одному.

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

const d:array [1..2,1..3] of integer=(

(1,2,3),

(4,5,6)

);

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

Вывод на экран матрицы небольшой размерности бывает удобно реализовать в виде "одна строка матрицы на одной строке экрана". Для этого элементы матрицы во внутреннем цикле печатаются оператором write, не переводящим строку на экране, а во внешнем цикле выполняется writeln:

writeln ('Матрица c:');

for i:=1 to 4 do begin

writeln;

for j:=1 to 2 do write (c[i,j]:4:1);

end;

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

Базовые алгоритмы обработки матриц -- те же, что мы изучили в гл. 11-15. Отличие состоит в том, что реализацию этих алгоритмов можно условно рассматривать для двух типов задач:

· алгоритмы реализуются при обработке всех элементов матрицы;

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

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

1. Приведем пример реализации алгоритма первого типа.

В матрице A размерностью 4*4 найти сумму ее положительных элементов, произведение элементов, значения которых попадают в интервал [2, 10], а также отношение этих двух величин.

var a:array [1..4,1..4] of real;

i,j:integer;

s,p,z:real;

begin

{Цикл ввода}

writeln ('Ввод матрицы размерностью 4*4');

for i:=1 to 4 do

for j:=1 to 4 do read (a[i,j]);

{Начальные значения и цикл обработки}

s:=0;

p:=1;

for i:=1 to 4 do

for j:=1 to 4 do begin

if a[i,j]>0 then s:=s+a[i,j];

if (a[i,j]>=2) and (a[i,j]<=10)

then p:=p*a[i,j];

end;

{Вывод результатов}

writeln ('Сумма=',s:6:2);

writeln ('Произведение=',p:6:2);

if p<>0 then begin

z:=s/p;

writeln ('Отношение=',z:6:2);

end

else writeln ('p=0, отношение ',

'вычислить нельзя');

reset (input); readln;

end.

Поскольку о типе матрицы в условии задачи ничего не сказано, выбран "более общий" тип real. Способ ввода также выбран "по умолчанию" -- пользователь вводит значения элементов матрицы с клавиатуры. Искомые сумма, произведение и отношение обозначены, соответственно, s, p и z.

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

Задана матрица C размерностью 334. Найти номер строки, сумма элементов которой максимальна.

На этот раз заполним матрицу случайными числами. Обозначим s сумму элементов очередной строки, max -- максимальную из этих сумм. В качестве искомого номера строки введем целочисленную переменную imax, в которую будем заносить номер текущей строки i, если для этой строки выполняется соотношение s>max. Переменную s следует инициализировать на каждом шаге внешнего цикла по i (ведь сумма элементов ищется заново в каждой строке), тогда как переменная max ищется для всей матрицы и инициализируется один раз до двойного цикла:

var c:array [1..3,1..4] of real;

i,j,imax:integer;

s,max:real;

begin

{Формирование матрицы с ее выводом}

writeln;

write ('Матрица C');

for i:=1 to 3 do begin

writeln;

for j:=1 to 4 do begin

c[i,j]:=random*10

{вещественные числа от 0 до 10};

write (c[i,j]:6:2);

end;

end;

{Поиск номера строки и вывод результата}

max:=-1e30;

for i:=1 to 3 do begin

s:=0;

for j:=1 to 4 do s:=s+c[i,j];

if s>max then begin

max:=s;

imax:=i;

end;

end;

writeln;

writeln ('Номер строки=',imax);

end.

Аналогично могут быть реализованы типовые алгоритмы в столбце матрицы -- как описано выше, для этого достаточно поменять местами циклы по i и j.

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

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

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

var s:array [1..20,1..4] of integer;

b:array [1..20] of real;

i,j:integer;

begin

{Матрица s из случайных оценок от 3 до 5}

for i:=1 to 20 do

for j:=1 to 4 do s[i,j]:=3+random(3);

{Обрабатываем матрицу построчно,

формируя вектор B}

for i:=1 to 20 do begin

b[i]:=0;

for j:=1 to 4 do b[i]:=b[i]+s[i,j];

b[i]:=b[i]/4;

end;

{Выводим оценки со средними баллами}

writeln;

write ('Оценки':8,' Баллы');

for i:=1 to 20 do begin

writeln;

for j:=1 to 4 do write (s[i,j]:2);

write (b[i]:5:2);

end;

end.

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

for i:=1 to n do begin

{Обработка элемента A[i,i]}

end;

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

for i:=1 to n do

for j:=1 to n do begin

if i>j then begin

{Обработка элемента A[i,j]}

end;

end;

Приведенный код имеет тот недостаток, что в нем выполняются заведомо лишние шаги цикла. Действительно, для квадратной матрицы размерностью n3n мы выполняем n2 шагов двойного цикла, тогда как имеется всего элементов под главной диагональю. Исправить ситуацию может двойной цикл с переменной границей, подобный изученному выше:

for i:=1 to n do

for j:=1 to i-1 do begin

{Обработка элемента A[i,j]}

end;

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

for i:=1 to n do

for j:=i+1 to n do begin

{Обработка элемента A[i,j]}

end;

5. Распространенный класс задач, связанных с умножением и обращением матриц, мы рассмотрим в гл. 18. Сейчас же ограничимся реализацией хорошо известного алгоритма умножения матрицы A размерностью n3m на вектор b размерности m. Напомним, что в результате умножения получается вектор размерности n (обозначим его c), а само умножение состоит в том, что каждая строка матрицы скалярно умножается на вектор по формуле и результат заносится в компоненту ci. Применив известные нам типовые алгоритмы, напишем следующую программу:

const n=3; m=4;

{размерности матрицы и векторов}

var a:array [1..n,1..m] of real;

b:array [1..m] of real;

c:array [1..n] of real;

i,j:integer;

begin

{Формируем матрицу A из случайных чисел}

writeln ('Матрица A');

for i:=1 to n do begin

for j:=1 to m do begin

a[i,j]:=random*10;

write (a[i,j]:6:2);

end;

writeln;

end;

{Аналогично для вектора b}

writeln ('Вектор b');

for j:=1 to m do begin

b[j]:=random*10;

writeln (b[j]:6:2);

end;

{Умножение совмещаем с выводом вектора с,

это возможно, т.к. компоненты вектора

ищутся последовательно}

writeln ('Вектор c=A*b');

for i:=1 to n do begin

c[i]:=0; {c[i] - сумма}

for j:=1 to m do

c[i]:=c[i]+A[i,j]*b[j];

writeln (c[i]:6:2);

end;

end.


 

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

 

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

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

Использование подпрограмм в чем-то похоже на расчеты с использованием математических или физических формул. Так, имея общие формулы решения квадратного уравнения, мы можем подставить вместо коэффициентов a, b и c любые числовые значения и решить любое конкретное уравнение. Аналогично, мы могли бы написать подпрограмму с входными параметрами a, b, c и выходными параметрами x1, x2 (найденные корни уравнения), а затем, применяя нужное число раз эту подпрограмму (однократное использование подпрограммы называется ее вызовом), найти корни любого количества квадратных уравнений.

Итак, использование подпрограмм позволяет решить следующие задачи:

· уменьшение размеров кода и экономия памяти за счет возможности неоднократного вызова одной и той же подпрограммы в рамках одной программы;

· лучшее структурирование программы за счет разбиения задачи на более простые подзадачи;

· эффективное повторное использование однажды написанного кода.

Рассмотрим общую структуру сложной программы, содержащей две подпрограммы:

var раздел_описаний_1;

 

Заголовок Подпрограммы_1;

begin

{Тело подпрограммы_1}

end;

 

Заголовок Подпрограммы_2;

begin

{Тело подпрограммы_2}

end;

 

var раздел_описаний_2;

begin

{Тело главной программы}

end.

Как видно из листинга, каждая подпрограмма имеет заголовок (по меньшей мере, в этом заголовке должно быть указано ее назначенное программистом имя) и тело, состоящее из операторов. Подобно телу цикла, тело подпрограммы заключено в операторные скобки begin... end;. Обратите внимание, что в листинге два раздела описаний. Первый из них расположен до обеих подпрограмм, второй -- после них перед телом главной программы. Данные, описанные в первом разделе -- глобальные, они доступны всем частям программы, расположенным ниже по ее тексту. Данные второго раздела описаний также глобальны, но доступны лишь главной программе, так как описаны непосредственно перед ней. Общее правило очень простое: подпрограммы "видят" все глобальные переменные, описанные выше их тела. Аналогично, без принятия специальных мер подпрограмма "видит" и может вызвать любую другую подпрограмму, расположенную выше нее по тексту программы. Здесь вторая подпрограмма может вызвать первую, но не наоборот. Главная программа, как правило, расположенная последней, может вызвать все подпрограммы.

На практике не рекомендуется делать подпрограммы зависящими от глобальных данных, так как это снижает их переносимость (возможность повторного использования). Разумеется, любого из разделов описаний в конкретной программе может и не быть. Более того, поскольку подпрограмма -- отдельная и, в идеале, независимая часть программы, она может содержать собственный раздел описания локальных переменных, предназначенных лишь для ее нужд и невидимых из других частей программы. Например, для подпрограммы решения квадратного уравнения такой локальной переменной могла бы быть переменная d, предназначенная для вычисления дискриминанта уравнения. Поясним этот важный момент на примере:

var i:integer;

{глобальная переменная –

описана вне всех подпрограмм}

Заголовок Подпрограммы;

var i:integer;

{локальная переменная –

описана после заголовка подпрограммы}

begin

{Тело подпрограммы}

end;

 

begin

{Тело главной программы}

end.

Описанная перед телом подпрограммы локальная переменная i не имеет никакого отношения к одноименной переменной, описанной выше. На время выполнения подпрограммы локальная переменная i вытесняет глобальную, делая значение последней недоступной. После завершения подпрограммы значение локальной i будет утеряно, а значение глобальной i никак от этого не изменится. Итак, глобальные переменные видимы (доступны) от точки их определения до конца файла. Локальные переменные доступны только в том блоке операторных скобок, в котором они описаны и во вложенных в него блоках.

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

 

Процедуры

 

Общий вид подпрограммы-процедуры следующий:

procedure Имя

(Список формальных параметров);

{выше - заголовок подпрограммы}

var описания локальных переменных;

begin

{Тело процедуры}

end;

Другие подразделы раздела описаний, такие как label, const и изучаемый в п. 18.3 оператор type, также могут присутствовать между заголовком и телом процедуры и действие их также будет локально -- то есть, определено лишь в рамках данной процедуры. Единственная правильная связь процедуры с "внешним миром", то есть, другими подпрограммами и главной программой -- указанный после имени список формальных параметров. В этом списке через запятую указываются входные и выходные параметры процедуры с указанием их типов данных. Входные параметры служат исходными данными для процедуры, а выходные определяют результаты ее вычислений, передаваемые в главную программу или другую подпрограмму. Перед выходными параметрами, измененные значения которых должны сохраняться после завершения процедуры, следует указывать ключевое слово var. Причины именно такого указания мы обсудим ниже.

Само по себе написание процедуры еще не вызывает выполнения никаких действий. Чтобы процедура сработала, ее нужно вызвать, записав в нужной точке программы имя процедуры со списком фактических параметров, которые будут подставлены на место формальных. Все это делается не так сложно, как звучит. Представим, что мы решили написать процедуру решения любого квадратного уравнения с именем Equation. Входными данными этой процедуры будут значения коэффициентов уравнения a, b и c, выходными -- найденные корни x1 и x2 (если они существуют). Кроме того, нам понадобится "временная" (а точней, локальная) переменная d, позволяющая процедуре хранить вычисленное значение дискриминанта:

procedure Equation (a,b,c:real;

var x1,x2:real);

var d:real;

begin

d:=sqr(b)-4*a*c;

if d>=0 then begin

x1:=(-b+sqrt(d))/(2*a);

x2:=(-b-sqrt(d))/(2*a);

end;

end;

Поскольку все параметры этой процедуры -- вещественные, в ее заголовке нам хватило двух списков -- один для входных параметров a, b и c, другой -- для выходных x1 и x2 (обратите внимание на слово var перед этим списком). В случае отрицательного значения дискриминанта, значения x1 и x2 остаются неопределенными (что, вообще говоря, не совсем корректно), в противном случае они вычисляются и должны быть переданы в главную программу. Вызвать эту процедуру мы могли бы, например, так:

var a,b,c,d,x1,x2,x3,x4:real;

...

write ('Введите значения a,b,c:');

read (a,b,c);

Equation (a,b,c,x1,x2);

Здесь мы попытались решить уравнение ax2+bx+c=0, значения a, b, c, ввел пользователь, ответы, если они вычислялись, будут помещены в переменные x1 и x2. При вызове вида

Equation (1,4,-3.5,x3,x4);

мы решили уравнение x2+4x-3.5=0, а ответы поместили в переменные x3 и x4. Еще один вызов

Equation (4,1,-3.5,x3,x4);

позволяет решить уравнение 4x2+x-3.5=0, записав ответы в переменные x1 и x2. Наконец, четвертое обращение к процедуре

Equation (1,b,0,x1,x3);

решает уравнение x2+bx=0, помещая ответы в переменные x1 и x3.

И так далее. Суть в том, что при каждом вызове подпрограммы значения фактических параметров подставляются на место формальных и с ними производятся вычисления, предусмотренные операторами подпрограммы. Указанные требования называют согласованием параметров и описывают следующим образом: формальные и фактические параметры должны быть согласованы между собой по количеству, типу и порядку следования. Это означает, что количество формальных и фактических параметров должно быть одинаковым, при этом, при вызове процедуры каждый фактический параметр должен иметь тот же тип и занимать в списке то же место, что и соответствующий ему формальный параметр. Из сказанного следует, что нашу процедуру Equation можно вызвать только с пятью параметрами (а не тремя, семью или нулём), причем все эти параметры должны быть вещественными. Если формальный параметр является выходным (перед ним в заголовке процедуры указано ключевое слово var), то соответствующий фактический параметр не может быть константой (ведь значение константы нельзя изменить). Для больше наглядности опишем данное соответствие в табл. 18.1.

 

Табл. 18.1. Соответствие формальных и фактических параметров

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

 

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

procedure p1 (x:integer);

{для x создастся локальная копия}

begin

x:=x+1; {значение копии увеличилось на 1}

writeln ('x=',x); {и было выведено}

end;

 

var x:integer;

begin

x:=3;

p1(x);

writeln ('x=',x);

{после вызова с передачей параметра

по значению, x по-прежнему равно 3}

end.

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

procedure p1 (var x:integer);

{получаем адрес переменной,

переданной в качестве x}

begin

x:=x+1; {значение x увеличилось на 1}

writeln ('x=',x); {и было выведено}

end;

 

var x:integer;

begin

x:=3;

p1(x);

writeln ('x=',x);

{после вызова с передачей параметра

по ссылке,x стало равно 4 –

оно было изменено процедурой p1}

end.

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

Перейдем к примерам.

1. Для точки на плоскости с заданными координатами (x,y) найти расстояние l от точки до начала координат, а также длину окружности и площадь круга радиусом l с центром в начале координат.

Обозначим длину окружности как c, а площадь круга -- s. Нетрудно заметить, что помимо выделения в отдельную процедуру расчета величин l, c и s, целесообразно написать также процедуру Get для ввода вещественного значения с предварительным выводом приглашения к вводу (нам придется вводить, по меньшей мере, 2 значения -- x и y), а также процедуру Put для печати вещественного значения с указанной шириной, точностью и строкой комментария (выводиться будут искомые величины l, c и s).

procedure Circle2 (x,y:real;

var l,c,s:real);

const pi=3.1415926;

{для иллюстрации; на самом деле

есть функция pi}

begin

l:=sqrt(sqr(x)+sqr(y));

c:=2*pi*l;

s:=pi*sqr(l);

end;

 

procedure Get (s:string; var x:real);

{s - приглашение,

x - параметр-переменная,

вводимая с клавиатуры величина}

begin

write (s,': ');

readln (x);

end;

 

procedure Put (s:string; x:real);

{s - комментарий к выводу,

x - выводимая величина}

begin

writeln (s,'= ',x:8:3);

end;

 

var x,y,c,s,l:real;

 

begin

writeln;

Get ('Введите координату x',x);

Get ('Введите координату y',y);

Circle2 (x,y,l,c,s);

Put ('Удаление от начала координат',l);

Put ('Длина окружности',c);

Put ('Площадь круга',s);

end.

Несмотря на то, что процедура Circle2 вызвана в этой программе однократно, она может пригодиться при повторном использовании кода. Исходными данными (параметрами-значениями) для этой процедуры являются координаты точки x и y, выходными данными (параметры-переменные) -- найденные значения l, c и s.

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

2. Написать процедуру, печатающую первые N членов ряда Фибоначчи.

Члены ряда Фибоначчи вычисляются по формуле:

F0 = 0, F1 = 1, FN = FN-1 + FN-2, N=2,3,...

Заметим, что кроме простой и красивой формулы, ряд Фибоначчи замечателен еще тем, что отношение двух его соседних членов дает в пределе константу "золотого сечения", широко используемую в самых различных расчетах. Для вычисления и вывода на экран N первых членов ряда Фибоначчи напишем процедуру Fibo с формальным параметром all, определяющим, сколько членов ряда требуется найти. В главной программе организуем цикл ввода значения фактического параметра N.

procedure Fibo (all:integer);

var n,n1,n2:longint;

{n – текущий элемент ряда,

n1 – предыдущий,

n2 – вычисленный 2 шага назад}

i:integer;

begin

n1:=1; n2:=0;

writeln;

write (n2,' ',n1,' ');

for i:=2 to all do begin

n:=n1+n2;

write (n,' ');

n2:=n1; n1:=n;

{переприсвоили для следующего шага}

end;

end;

 

var n:integer;

begin

repeat

writeln ('Введите число членов ряда ',

'или 0 для выхода:');

read (n);

if n=0 then halt(1)

else Fibo(n);

until false;

end.

 

Функции

 

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

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

function Имя

(Список формальных параметров):

ТипРезультата;

{выше - заголовок подпрограммы}

var описания локальных переменных;

begin

{Тело функции}

Имя:=Выражение1;

end;

Здесь Выражение1 должно иметь тот же тип, что и указанный в заголовке ТипРезультата, а оператор Имя:=Выражение1; не обязан быть последним в теле функции. Поскольку результат выполнения функции возвращается в основную программу через ее имя, то обращение к функции записывается аналогично стандартным функциям в виде операнда-выражения, стоящего справа от знака присваивания:

Результат:=Выражение2;

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

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

Фактически, стандартные подпрограммы Паскаля также делятся на функции и процедуры, в зависимости от способа их вызова. Так, writeln относится к стандартным процедурам, а sin -- к функциям. Правда, некоторые из стандартных подпрограмм имеют переменное число параметров, что запрещено пользовательским подпрограммам.

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

Чтобы окончательно уяснить не-синтаксические различия между функциями и процедурами, обобщим их в табл. 18.2.

 

Табл. 18.2. Различия между процедурами и функциями

Процедура Функция
Вызывается отдельным оператором Вызывается из выражения справа от знака присваивания
Использует параметры-значения и переменные Использует параметры-значения и переменные, дополнительно доступен параметр-переменная с именем, совпадающим с именем функции

 

Напишем и вызовем простейшую функцию, находящую максимальный из двух своих вещественных аргументов:

function max(a,b:real):real;

begin

if a>b then max:=a

else max:=b;

end;

Вызвать эту функцию мы могли бы различными способами:

var x,y,z,r,t:real;

...

read (x,y,z);

r:=max(x,y);

С помощью функции вычислили максимальное из значений x, y и записали его в переменную r.

t:=max(max(x,y),z);

Максимальное из значений x, y, z записали в переменную t.

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

procedure max (a,b:real; var c:real);

begin

if a>b then c:=a

else c:=b;

end;

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

Мы уже знакомы с оператором halt;, позволяющим аварийно (или просто из бесконечного цикла) завершить программу. Немедленно завершить текущий блок, в том числе и подпрограмму-функцию или процедуру, позволяет оператор Exit;.

Перейдем к примерам.

1. Используя подпрограмму, вычислить сумму первых k членов ряда 1+1/n.

Сумма ряда -- это скаляр, естественным выглядит использование подпрограммы-функции. Применив известные алгоритмы, составим программу:

function sum (k:integer):real;

var i:integer; s:real;

begin

s:=0;

for i:=1 to k do s:=s+1/i;

sum:=s;

end;

 

var k:integer; s:real;

begin

write ('Введите число шагов:');

readln (k);

s:=sum(k);

writeln ('Сумма=',s:8:3);

end.

Обратите внимание -- несмотря на то, что функция sum вычисляет единственную величину s, мы были бы не вправе написать в ней оператор

for i:=1 to k do sum:=sum+1/i;,

поскольку sum -- это имя функции, и справа от знака присваивания оно было бы воспринято как попытка функции sum вызвать саму себя, причем, без соблюдения правил соответствия параметров. Поэтому нам понадобилась локальная переменная s.

Тем не менее, рекурсивные функции, вызывающие сами себя, существуют и будут кратко рассмотрены далее.

2. Вычислить значение выражения , где z(x)= sin 2x + ln |x|.

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

function z(x:real):real;

begin

z:=sin(2*x)+ln(abs(x));

end;

 

begin

write ('Ответ=', (z(3)+2*sqr(z(5)))/

(z(0.1)+1):6:2);

readln;

end.

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

3. В заключение раздела скажем несколько слов о рекурсии. Рекурсивными называют функции, способные повторно вызывать сами себя. С точки зрения программирования, в этом нет ничего удивительного -- просто при повторном входе в функцию в программном стеке создается ее новая копия и расчет выполняется заново с измененным значением параметра функции. Затем функция проверяет значение параметра, при необходимости изменяет его и вновь повторно вызывает сама себя. Разумеется, во избежание зацикливания, при некотором значении параметра должно быть предусмотрено завершение вычислительного процесса. Использование рекурсии целесообразно везде, где расчет следующего значения некоторой функции зависит от ее предыдущего значения. Так, классический пример на рекурсию -- расчет факториала (факториал целого положительного числа n, обозначаемый n!, равен произведению всех чисел от 1 до N включительно). Используя очевидную формулу n!=n*(n-1)!, напишем следующую рекурсивную функцию:

function Factorial (n:integer):longint;

begin

if n<2 then Factorial:=1

else Factorial:=n*Factorial(n-1);

end;

Тип функции longint использован, так как факториал очень быстро растет и уже 8!=40320, то есть, больше максимального значения типа integer. Эта функция, как нетрудно увидеть, при значении аргумента, меньшем двух, возвращает единицу, а в противном случае возвращает свой аргумент n, умноженный на факториал от n-1. Несложный тест функции мог бы выглядеть так:

begin

writeln ('10!=',Factorial(10));

end.

Рекурсия удобна при составлении различного рода переборных стратегий. Так, игровая задача составления пути по "лабиринту" лучше всего решается рекурсивно, ведь всякий такой путь -- это шаг плюс новый путь, уменьшенный на один шаг.

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

 

Поделиться:





Читайте также:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...