• Название:

    Pascal


  • Размер: 0.07 Мб
  • Формат: ODT
  • или
  • Сообщить о нарушении / Abuse

Установите безопасный браузер



1. Алгоритм и программа

 

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

 

1.1. Алгоритм

 

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

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

 

1.2. Свойства алгоритма

 

Перечислим выделенные в п. 1.1 свойства алгоритма.

·       Дискретность -- алгоритм состоит из отдельных инструкций (шагов).

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

·       Массовость -- алгоритм работает при меняющихся в некоторых пределах входных данных.

·       Результативность -- за конечное число шагов достигается некоторый результат.

 

1.3. Формы записи алгоритма

 

Принято выделять 2 основных формы записи алгоритма.

Графическая форма записи (блок-схема) характерна тем, что отдельные шаги алгоритма изображаются геометрическими фигурами, а последовательность выполнения шагов -- связями между этими фигурами. На рис. 1.1 указаны основные элементы блок-схем.

 

Рис. 1.1. Основные элементы блок-схем

 

Указанные на рис. 1.1 геометрические фигуры интерпретируются так:

Прямоугольник -- любая последовательность действий; внутри прямоугольника записываются формулы или словесное описание выполняемых действий;

Ромб -- блок проверки условия; так как любое условие может быть только истинно или ложно, у блока 1 вход и 2 выхода, соответствующие действиям, выполняемым в случаях, когда условие истинно и когда оно ложно. Выходы подписывают символами "+" и "-", "да" и "нет", "1" и "0" и т. п.

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

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

Закругленные прямоугольники -- необязательные блоки начала и конца программы, внутри блоков обычно указываются ключевые слова "нач" и "кон" соответственно;

Последняя фигура служит для изображения циклов, как правило, у нее 2 входа (первый и повторный вход в цикл) и 1 выход, соответствующий завершению циклического процесса.

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

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

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

·       нач      -- начало программы;

·       кон      -- конец программы;

·       если ... то ...иначе  -- проверка условия;

·       ввод  -- ввод данных;

·       вывод  -- вывод данных;

·       для ... от .. до ... нц ... кц     -- цикл со счетчиком (нц -- начало цикла, кц -- конец);

·       пока ... нц ...кц         -- цикл с предусловием;

·       нц ... кц ... пока        -- цикл с постусловием.

 

Рис. 1.2. Блок-схема решения квадратного уравнения

 

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

 

2.1. Константы

 

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

Числовые константы служат для записи чисел. Различают следующие их виды:

Целочисленные (целые) константы: записываются со знаком + или -, или без знака, по обычным арифметическим правилам:

-10          +5        5

Вещественные числа могут записываться в одной из двух форм:

обычная запись: 2.5        -3.14     2.       -- обратите внимание, что целая часть отделяется от дробной символом точки;

экспоненциальная ("научная") форма: в этой записи вещественное число представляется в виде m*10p, где m -- мантисса или основание числа, 0.1≤|m|≤1, p -- порядок числа, заданный целочисленной константой. Действительно, любое вещественное число можно представить в экспоненциальной форме:

-153.5       -0.1535*103

99.005       0.99005*102

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

-153.5    -0.1535*103    -0.1535E3 или -1.535E02

99.005    0.99005*102    0.99005E+2 или 9.9005e+01

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

1030         1e30

-1020        -1E20

10-30         1E-30

Поскольку размер памяти, отводимой под мантиссу и порядок, ограничен, то вещественные числа всегда представляются в памяти компьютера с некоторой погрешностью. Например, простейшая вещественная дробь 2/3 дает в десятичном представлении 0,666666... и, независимо от размера памяти, выделяемой для хранения числа, невозможно хранить все его знаки в дробной части. Одной из типичных проблем программирования является учет возможных погрешностей при работе с вещественными числами.

Кроме числовых констант существуют и другие их виды:

логические константы служат для проверки истинности или ложности некоторых условий в программе и могут принимать только одно из двух значений: служебное слово true обозначает истину, а false -- ложь;

символьные константы могут принимать значение любого печатаемого символа и записываются как символ, заключенный в апострофы ('одинарные кавычки'):

'Y'   'я'      ' '

В последнем случае значение символьной константы равно символу пробела. Если требуется записать сам символ апострофа как символьную константу, внутри внешних апострофов он удваивается: ''''.

К символьным также относятся константы вида #X, где X -- числовое значение от 0 до 255 включительно, представляющее собой десятичный ASCII-код символа. Таблицы ASCII-кодов, используемых операционными системами DOS и Windows, приведены в Приложении 1. Например, значение #65 будет соответствовать коду буквы 'A' латинской. Обработка нажатий клавиш и расширенные коды клавиатуры описаны в гл. 24 и Приложении 5.

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

'Введите значение X:'

'Ответ='

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

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

const Имя1=Значение1;

 Имя2=Значение2;

 ...

 ИмяN=ЗначениеN;

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

const e=2.7182818285;

  lang='Turbo Pascal 7.1';

Здесь описана числовая константа e со значением основания натурального логарифма и строковая константа с именем lang, содержащая строку 'Turbo Pascal 7.1'.

Каждое даваемое программистом имя должно быть уникальным в пределах одной программы. Если мы включим этот раздел в свою программу, мы уже не сможем создать в ней других объектов с именами e и lang.

 

2.2. Переменные и типы переменных

 

Переменными называют величины, значения которых могут изменяться в процессе выполнения программы. Каждая переменная задается своим уникальным именем, построенным по правилам, указанным в начале главы. Максимально возможная длина имени зависит от реализации Паскаля, теоретически можно давать переменным имена вплоть до 63 символов длиной, что едва ли актуально -- обычно имена не длиннее 5-10 символов.

Поскольку любые данные в памяти компьютера хранятся в числовой форме и двоичной системе счисления, кроме имени, переменной обязательно следует присвоить и тип, определяющийдиапазон значений, принимаемых переменной, и способ ее обработки машиной. Поясним сказанное на примере. Как видно из Приложения 1, большая латинская буква 'A' имеет десятичный код 65, или 01000001 в двоичном представлении. Без дополнительной информации о типе данных, хранящихся в некоторой ячейке памяти, компьютеру было бы невозможно решить, что именно представляют из себя эти данные -- число 65, код символа 'A' или что-то еще. В любом языке программирования, в том числе и в Паскале, существует стандартный набор типов, к которым может быть отнесена та или иная совокупность ячеек памяти. Информацию о типах данных Паскаля удобно свести в таблицу. Строки этой таблицы будут упорядочены по старшинству типов, от самого "младшего", требующего наименьшее число байт для представления, и, соответственно, представляющего наименьший диапазон возможных значений, до самого "старшего", представляющего наибольший диапазон значений. В табл. 2.1 представлены не все возможные, а лишь основные типы данных Паскаля.

 Табл. 2.1. Основные типы данных Паскаля

Ключевое слово Паскаля

boolean

char

integer

word

longint

real

double

string

Название и описание типа

Объем памяти, байт

Диапазон возможных значений

Логический: хранит одну логическую переменную

1

true и false

Символьный: хранит код одного  символа из набораASCII-кодов

1

от 0 до 255 включительно (28=256)

Целочисленный

2

+215

Целочисленный без знака

2

+216 - диапазон вдвое больше, так как 16-й бит не занят под знак числа

Длинное целое: для представления больших целочисленных значений

4

+231

Вещественное число с точностью представления до 11-12 знака в дробной части

6

~ 2.9*10-39 - 1.7*1038

Вещественное число с точностью представления до 15-16 знака в дробной части

8

~ 5*10-324 - 1.7*10308

Последовательность символов типа charдлиной от 1 до 255

2-256 (данные строки + 1 байт для хранения ее длины)

Любые строки текста, состоящие из печатаемых символов

 

Теоретически для записи переменной типа boolean было бы достаточно 1 бита, но минимальная адресуемая единица памяти -- 1 байт (см. Приложение 1). В этом же Приложении уточните, как именно объем памяти в байтах, выделяемой под переменную, влияет на диапазон представляемых ей значений.

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

Задача правильного выбора типов данных целиком ложится на программиста. Например, если некоторый счетчик в вашей программе может принимать целочисленные значения от 1 до 100 000, неправильно было бы описывать его как переменную типа integer - ведь 215=32768 и при достижении счетчиком этой величины произойдет сброс его значения, которое станет равно -32768. Разумным в данном случае было бы описание счетчика как переменной типа longint.

Переменные описываются в программе оператором следующего вида:

var    Список1:Тип1;

   Список2:Тип2;

   ...

   СписокN:ТипN;

Здесь список -- набор имен переменных, разделенных запятыми (или одна переменная), а тип -- любой из рассмотренных выше типов данных. Например, конструкция

var t,r:real;

 i:integer;

описывает 2 вещественных переменных с именами t и r, а также целочисленную переменную с именем i. Ключевое слово var можно продублировать, но обычно такой необходимости нет. Сокращение var образовано от английского "variable" (переменная).

Приоритет

1

2

Знак операции

Описание операции

*

умножение

/

деление

div

деление 2 целых значений с отбрасыванием остатка

mod

взятие остатка от деления 2 целых значений

+

сложение

-

вычитание

 

Математическая запись

|x|

x2

sin x

cos x

arctg x

ex

ln x

p

 

 

 

Запись на Паскале

Пояснение

Тип

аргумента и результата

abs(x)

Модуль аргумента x

Integer (I) или Real (R)

sqr(x)

Квадрат аргумента x

аргумент - I или R, результат - r

sin(x)

cos(x)

arctan(x)

Остальные тригонометрические функции выражаются через эти

аргумент - I или R, результат - R

exp(x)

ln(x)

Экспонента и натуральный логарифм

аргумент - I или R, результат - R

sqrt(x)

Квадратный корень от аргумента x

аргумент - I или R, результат - R

pi

Функция без аргументов, вернет число p

R

trunc(x)

Функция отбрасывает дробную часть аргумента, аргумент не округляется

аргумент R, результат I

frac(x)

Функция выделяет

дробную часть своего

аргумента

R

round(x)

Округление вещественного числа до ближайшего целого

аргумент R, результат I

 

4.1. Оператор присваивания

 

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

переменная := выражение;

Знак := читается как "присвоить".

Оператор присваивания работает следующим образом: сначала вычисляется выражение, стоящее справа от знака :=, затем результат записывается в переменную, стоящую слева от знака. Например, после выполнения оператора

k:=k+2;

текущее значение переменной k увеличится на 2.

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

Приведем примеры.

1. Записать оператор присваивания, который позволяет вычислить расстояние между двумя точками на плоскости с координатами (x1, y1) и (x2, y2).

Оператор будет иметь вид

d:=sqrt(sqr(x1-x2)+sqr(y1-y2));

2. Записать последовательность операторов присваивания, обеспечивающих обмен значениями переменных x и y в памяти компьютера.

c:=x; x:=y; y:=c;

Здесь с -- дополнительная переменная того же типа, что x и y, через которую осуществляется обмен. Грубой ошибкой было бы, например, попытаться выполнить обмен операторами x:=y; y:=x;-- ведь уже после первого из них мы имеем два значения y, а исходное значение x потеряно.

 

4.2. Оператор ввода

 

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

read(список_переменных);

readln(список_переменных);

Имена переменных в списке перечисляются через запятую. Здесь и далее список данных, передаваемых любому оператору (а позднее и написанным нами подпрограммам), мы будем называтьпараметрами. Таким образом, параметрами оператора (точней, стандартной процедуры) read являются имена переменных, описанных ранее в разделе var.

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

Оператор readln отличается от read только тем, что все переменные должны быть введены в одну строку экрана, клавиша Enter нажимается один раз по окончании ввода. Форма записи readlnиспользуется, в основном, для ввода строк текста, для ввода числовых значений лучше использовать read, т. к. в этом случае пользователь может вводить данные более свободно (и в одну, и в несколько строк экрана).

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

В качестве примера организуем ввод исходных данных для решения квадратного уравнения:

var a,b,c:real;

...

read (a,b,c);

Для задания значений a=1, b=4, c=2.5 на экране вводится:

1_4_2.5¬

Здесь и далее "_" означает пробел, а "¬" -- нажатие Enter. Другой вариант ввода с клавиатуры:

2.5¬

Третий вариант:

4_2.5¬

Во всех вариантах пробелов может быть и несколько, лишние будут проигнорированы оператором.

Как правило, перед оператором ввода ставится оператор вывода, служащий приглашением к вводу и поясняющий пользователю, что именно следует сделать (см. п. 4.3).

 

4.3. Оператор вывода

 

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

write(список);

writeln(список);

Элементы списка перечисляются через запятую.

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

Оператор writeln отличается от write тем, что после вывода значения последнего элемента списка выполняется перевод курсора на следующую строку экрана.

Приведем примеры.

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

var a,a2:integer;

...

writeln ('Введите целое число:');

{это приглашение к вводу}

read (a);

a2:=sqr(a);

writeln ('Квадрат числа=',a2);

Если ввести значение a=2, на экране будет напечатано

Квадрат числа=4

|

Символ |здесь и далее обозначает курсор. Видно, что оператор writeln перевел курсор на следующую строку.

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

readln;

который будет ждать нажатия клавиши Enter.

2. Требуется вывести на экран результаты решения квадратного уравнения: значения x1=1.5 и x2=2.5:

write ('x1=',x1,'_x2=',x2);

Пробел в строкой константе '_x2=' нужен, чтобы значение x1 не слилось со строкой 'x2='. На экране будет напечатано:

x1= 1.5000000000E+00 x2= 2.5000000000E+00|

Курсор остался в конце строки, т.к. использована форма оператора write.

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

 

4.4. Управление выводом данных

 

В операторе write или writeln вещественное значение (а также целое или строковое) зачастую удобнее записывать в виде:

переменная:ширина:точность

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

Точность -- целое положительное число, определяющее, сколько цифр из ширины отводится на вывод дробной части числа. Значение точности определено только для вещественных чисел. Оно не учитывает позицию десятичной точки. Разумные значения точности -- от 0 до ширина-2 включительно. Недопустимые значения ширины и точности не будут учтены при выводе.

В качестве примера выведем на экран значения нескольких переменных:

var x1,p:real;

i:integer;

...

x1:=2.5; p:=-3.175; i:=2;

writeln ('x1=',x1:8:2,'_p=',p:9:4);

write ('I=','_':5,i:2);

На экране будет напечатано:

x1=____2.50_p=__-3.1750

I=______2

 

4.5. Вывод на печать

 

Иногда требуется, чтобы программа вывела результаты своей работы на принтер. Для этого достаточно выполнения двух условий. Первым оператором раздела описаний программы следует указать оператор uses printer;, подключающий стандартную библиотеку для работы с принтером, а первым параметром оператора write или writeln указать символическое имя принтера lst, описанное в библиотеке printer:

write ('Hello');   

- строка 'Hello' выведена на экран,

write (lst,'Hello');

- строка выведена на принтер.

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

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


program p1; var

a,b,c:real;  begin

writeln ('Введите значения A и B:');read(a,b);

 c:=a+b; writeln ('A+B=',c); c:=a-b;

writeln ('A-B=',c); end.

Текст этой программы структурирован явно неудачно, гораздо лучше он воспринимается так:

program p1;

var a,b,c:real;

begin

  writeln ('Введите значения A и B:');

  read (a,b);

  c:=a+b;

  writeln ('A+B=',c);

  c:=a-b;

  writeln ('A-B=',c);

end.

7.1. Логические выражения

 

Логические выражения (ЛВ) строятся из АВ, операций отношения, логических операций и круглых скобок.

Результатом вычисления ЛВ является одно из двух значений: true или false.

 

7.2. Операции отношения

 

Операции отношения (сравнения) имеют следующий общий вид:

АВ1 ОО АВ2

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

< <=   >    >=   =    <>

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

В любое логическое выражение должна входить хотя бы одна операция отношения.

Приведем примеры ЛВ, включающих одну ОО:

d<0            -- выбор ветви вычислений зависит от значения d;

sqr(x)+sqr(y)<=sqr(r) -- результат будет равен true для точек с координатами (x, y), лежащих внутри круга радиуса R с центром в начале координат;

cos(x)>1 -- результат этого ЛВ всегда равен false.

К вещественным значениям в общем случае неприменима операция = ("равно") из-за неточного представления этих значений в памяти компьютера. Поэтому для вещественных переменных отношение вида a=b часто заменяется на abs(a-b)<eps, где eps -- малая величина, определяющая допустимую погрешность.

 

7.3. Логические операции

 

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

 

Табл. 7.1. Логические операции языка Паскаль

Математическая запись

Запись на Паскале

Название

not

Отрицание

and

Операция "И" (логическое умножение)

or

Операция "ИЛИ" (логическое сложение)

xor

Операция "исключающее ИЛИ"

 

Операция NOT применима к одному логическому выражению (является унарной). Ее результат равен true, если выражение ложно и наоборот.

Например, выражение NOT (sin(x)>1) всегда даст значение true.

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

В качестве примера распишем выражение . Т. к. операции принадлежности в Паскале нет, используем операцию AND и операции отношения: (x>=a) and (x<=b).

Математическое выражение a,b,c>0 (одновременно) будет иметь вид (a>0) and (b>0) and (c>0).

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

Распишем выражение . На Паскале оно будет иметь вид(x<a) or (x>b). Другой способ связан с применением операции NOT: not ((x>=a) and (x<=b)).

Условие "хотя бы одно из значений a,b,c положительно" может быть записано в виде (a>0) or (b>0) or (c>0) .

Условие "только одно из значений a,b,c положительно" потребует объединения возможностей операций AND и OR:

(a>0) and (b<=0) and (c<=0) or

(a<=0) and (b>0) and (c<=0) or

(a<=0) and (b<=0) and (c>0).

Операция XOR, в отличие от OR, возвращает значение "ложь" (false) и в том случае, когда все связанные ей логические выражения истинны. Чтобы лучше уяснить это отличие, составим так называемую таблицу истинности двух логических операций (табл. 7.2). Для краткости значение false обозначим нулем, а true -- единицей. Для двух логических аргументов возможно всего 4 комбинации значений 0 и 1.

 

Табл. 7.2. Таблица истинности операций OR и XOR

Аргумент A

0

0

1

1

Аргумент B

A or B

A xor B

0

0

0

1

1

1

0

1

1

1

1

0

 

В качестве примера использования операции XOR запишем условие "только одно из значений a,b положительно":

(a>0) xor (b>0).

К сожалению, записать условие "только одно из значений a,b,c положительно" в напрашивающемся виде (a>0) xor (b>0) xor (c>0) нельзя -- результат этого выражения будет равен true и в случае, когда все три значения положительны. Связано это с тем, что при последовательном расчете логических выражений слева направо (1 xor 1) xor 1 будет равно 0 xor 1 = 1.

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

x := x xor true; writeln ('x=', x);

x := x xor true; writeln ('x=', x);

Независимо от начального значения логической переменной x, второе выведенное на экран значение будет логическим отрицанием первого. В реальной практике конструкции подобные x := xxor true; не дублируются в коде многократно, а применяются внутри цикла (см. гл. 9).

Приоритет логических операций следующий:  самая старшая операция -- not, затем and, следующие по приоритету -- or и xor (равноправны между собой), самый низкий приоритет имеют операции отношения. Последнее служит причиной того, что в составных условиях отдельные отношения необходимо заключать в круглые скобки, как и сделано во всех примерах раздела.

 

7.4. Короткий условный оператор

 

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

if логическое_выражение then оператор1;

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

Рис. 7.1. Блок-схема короткого условного оператора

 

Если по условию требуется выполнить несколько операторов, их необходимо заключить в операторные скобки begin...end;, образуя единый составной оператор:

if d>0 then begin

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

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

     writeln (x1:8:3,x2:8:3);

end;

Здесь по условию d>0 выполняется 3 оператора, первые два из которых вычисляют корни x1 и x2 квадратного уравнения, а последний выводит на экран найденные значения корней.

Следующий пример иллюстрирует поиск значения y=max(a,b,c). Поскольку стандартной функции для нахождения максимума в Паскале нет, применим 2 коротких условных оператора:

y:=a;

if b>y then y:=b;

if c>y then y:=c;

Вообще, для условной обработки N значений требуется N-1 короткий условный оператор.

 

7.5. Полный условный оператор

 

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

if логическое_выражение then оператор1

else оператор2;

Оператор работает следующим образом: если логическое выражение имеет значение true, то выполняется оператор1, иначе выполняется оператор2. Всегда выполняется только один из двух операторов. Перед ключевым словом else ("иначе") точка с запятой не ставится, т.к. if-then-else -- единый оператор.

Вычислим значение m=min(x,y) с помощью полного условного оператора:

if x<y then m:=x else m:=y;

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

if a*b>0 then c:=a*b

else begin

  a:=0; b:=0;

end;

Из примера видно, что к ветви алгоритма после ключевого слова else, состоящей более чем из одного оператора, также применяются операторные скобки.

 

7.6. Составной условный оператор

 

Эта форма условного оператора применяется, когда есть более 2 вариантов расчета. Общий вид составного оператора может включать произвольное число условий и ветвей расчета:

if логическое_выражение1 then оператор1

else if логическое_выражение2 then оператор2

...

else if логическое_выражениеN then операторN

else оператор0;

При использовании оператора последовательно проверяются логические выражения 1, 2, ... ,N, если некоторое выражение истинно, то выполняется соответствующий оператор и управление передается на оператор, следующий за условным. Если все условия ложны, выполняется оператор0 (если он задан). Число ветвей N неограниченно, ветви else оператор0; может и не быть.

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

var x:real;

begin

 write ('Введите x:'); readln (x);

 if x<0 then writeln ('Отрицательный')

 else if x=0 then writeln ('Ноль')

 else if abs(x)<1 then

  writeln ('По модулю меньше 1')

 else writeln ('Больше 1');

end.

Условие x<0 сработает, например, для значения x=-0.5, что не позволит программе проверить условие abs(x)<1.

Еще одну распространенную ошибку работы с составным условным оператором показывает произведенный ниже расчет знака n переменной a:

if a<0 then n:=-1;

if a=0 then n:=0

else n:=1;

Применение одного короткого и одного полного условных операторов является здесь грубой ошибкой -- ведь после завершения короткого условного оператора для всех ненулевых значений aбудет выполнено присваивание n:=1. Правильных вариантов этого расчета, по меньше мере, два:

if a<0 then n:=-1;

if a=0 then n:=0;

if a>0 then n:=1;

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

if a<0 then n:=-1;

else if a<0 then n:=1;

else n:=0;

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

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

В качестве еще одного примера рассчитаем значение функции, заданной графически (рис. 7.2).

Рис. 7.2. Функция, заданная графически

 

Перепишем функцию в аналитическом виде:

Одним из вариантов запрограммировать вычисление y(x) мог бы быть следующий:

if abs(x)>1 then y:=0

else if x<0 then y:=x+1

else y:=1-x;

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

if x<-1 then y:=0

else if x<0 then y:=x+1

else if x<1 then y:=1-x

else y:=0;

7.7. Вложенные условные операторы

 

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

В качестве примера рассмотрим программу для определения номера координатной четверти p, в которой находится точка с координатами (x,y). Для простоты примем, что точка не лежит на осях координат. Без использования вложений основная часть программы может иметь следующий вид:

if (x>0) and (y>0) then p:=1

else if (x<0) and (y>0) then p:=2

else if (x<0) and (y<0) then p:=3

else p:=4;

Однако использование такого количества условий представляется явно избыточным. Перепишем программу, используя тот факт, что по каждое из условий x>0, x<0 оставляет в качестве значения p только по 2 возможных четверти из 4:

if x>0 then begin

 if y>0 then p:=1

 else p:=4;

end

else begin

 if y>0 then p:=2

 else p:=3;

end;

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

Рассмотренный в п. 7.6 пример с определением знака числа может быть переписан и с использованием вложения:

if a>0 then n:=1

else begin

 if a<0 then n:=-1

 else n:=0;

end;

Однако, как эти операторы, так и составной условный оператор из п. 7.6 проверяют не более 2 условий, так что способы примерно равноценны.

 

7.8. Оператор выбора

 

Для случаев, когда требуется выбор одного значения из конечного набора вариантов, оператор if удобнее заменять оператором выбора (переключателем) case:

case выражение of

 список1: оператор1;

 список2: оператор2;

 . . .

 списокN: операторN;

 else оператор0;

end;

Оператор выполняется так же, как составной условный оператор.

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

Мин.значение .. Макс.значение

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

В качестве примера по номеру месяца m определим число дней d в нем:

case m of

 1,3,5,7..8,10,12: d:=31;

 2: d:=28;

 4,6,9,11: d:=30;

end;

Следующий оператор по заданному символу c определяет, к какой группе символов он относится:

case c of

 'A'..'Z','a'..'z':

   writeln ('Латинская буква');

 'А'..'Я','а'..'п','р'..'я':

   writeln ('Русская буква');

 '0'..'9':

   writeln ('Цифра');

 else writeln ('Другой символ');

end;

Здесь отдельные диапазоны для русских букв от "а" до "п" и от "р" до "я" связаны с тем, что между "п" и "р" в кодовой таблице DOS находится ряд не-буквенных символов (см. Приложение 1).

Если по ветви оператора case нужно выполнить несколько операторов, действует то же правило, что для оператора if, т. е. ветвь алгоритма заключается в операторные скобки begin ... end;.

7.9. Примеры программ с условным оператором

 

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

1. Проверить, может ли быть построен прямоугольный треугольник по длинам сторон a, b, c.

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

var a,b,c, {Длины сторон}

 s:real;{Буферная переменная для обмена}

begin

{ Секция ввода данных }

 writeln;

 write ('Введите длину 1 стороны:');

 readln (a);

 write ('Введите длину 2 стороны:');

 readln (b);

 write ('Введите длину 3 стороны:');

 readln (c);

{ Сортируем стороны по неубыванию }

 if (a>b) then begin

    s:=a; a:=b; b:=s;

 end;

 if (a>c) then begin

    s:=a; a:=c; c:=s;

 end;

 if (b>c) then begin

    s:=b; b:=c; c:=s;

 end;

{ Проверка и вывод }

 if abs(a*a+b*b-c*c)<1e-8 then writeln

  ('Прямоугольный треугольник ',

    'может быть построен!')

 else writeln('Прямоугольный треугольник ',

    'не может быть построен!')

end.

 

2. Определить, попадает ли точка плоскости, заданная координатами (a, b) в прямоугольник, заданный координатами двух углов (x1, y1) и (x2, y2).

Как и в предыдущей задаче, было бы не совсем корректно требовать от пользователя вводить данные в определенном порядке -- гораздо лучше при необходимости поменять x- и y-координаты прямоугольника так, чтобы пара переменных  (x1, y1) содержала координаты левого нижнего угла прямоугольника, а (x2, y2) -- правого верхнего.

var x1,y1,x2,y2,a,b:real;

begin

 writeln ('Введите координаты 1 угла:');

 read (x1,y1);

 writeln ('Введите координаты 2 угла:');

 read (x2,y2);

 if x1>x2 then begin

  a:=x1; x1:=x2; x2:=a;

 end;

 if y1>y2 then begin

  a:=y1; y1:=y2; y2:=a;

 end;

 writeln ('Введите координаты точки:');

 read (a,b);

 if (x1<=a) and (a<=x2)

    and (y1<=b) and (b<=y2) then writeln

   ('Точка попадает в прямоугольник')

 else writeln

   ('Точка не попадает в прямоугольник');

end.

 

3. Вводится денежная сумма в рублях и копейках. Программа печатает введенную сумму с правильной формой слов "рубли" и "копейки", например, "123 рубля 15 копеек".

Окончание, используемое для слов "рубли" и "копейки", зависит от последней цифры суммы, которую можно получить, взяв остаток от деления на 10 (1058 рублей, 38 рублей и т.д.). Исключения -- суммы с последними двумя цифрами от 11 до 19 включительно, которые всегда произносятся "рублей" и "копеек" (511 рублей, но 51 рубль). Используя эту информацию, составим программу.

var r,k,o10,o100:integer;

begin

 writeln;

 write ('Введите количество рублей, ',

     'затем пробел и количество копеек:');

 read (r,k);

 writeln;

 o10:=r mod 10;   {Взяли последнюю цифру}

 o100:=r mod 100; {...и 2 последних цифры}

 write ('Правильно сказать: ',r,' ');

 {Печатаем число рублей, затем пробел}

 if (o100>10) and (o100<20)

     or (o10>4) or (o10=0) then

  write ('рублей')

 else if (o10>1) and (o10<5) then

  write ('рубля')

 else

  write ('рубль');

 {аналогично для копеек:}

 o10:=k mod 10;

 o100:=k mod 100;

 write (' ',k,' ');

 {печатаем число копеек с пробелами}

 if (o100>10) and (o100<20) or

     (o10>4) or (o10=0) then

  write ('копеек')

 else if (o10>1) and (o10<5) then

  write ('копейки')

 else write ('копейка');

end.

8. Директивы компилятора и обработка ошибок ввода

 

Компилятор Паскаля -- сложное приложение, имеющее множество настроек. При написании учебных программ большинство этих настроек не имеют значения, но некоторые из них окажутся нам полезны. Для управления компилятором существует 2 основных возможности: настройка режимов работы с помощью верхнего меню Options оболочки Turbo Pascal и настройка конкретной программы с помощью директив компилятора, которую мы кратко рассмотрим. В общем виде директива компилятора представляет собой конструкцию вида {$X+} или {$X-}, где X -- латинская буква. Вариант со знаком "+" включает некоторый режим работы компилятора (например, строгий контроль программой соответствия типов данных, вывод системных диагностических сообщений и т. д.), а вариант со знаком "-" выключает его. Расположение директив, в общем, произвольно, однако, директивы, влияющие на всю программу, принято располагать в самом начале файла с исходным текстом. Фигурные скобки комментария { ... } необходимы как часть синтаксиса директивы.

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

Наиболее   полезной   для   нас   выглядит   директива  {$I-}/{$I+}, соответственно, выключающая и включающая автоматический контроль программой результатов операций ввода/вывода (в/в). К операциям в/в относятся, в числе прочего, ввод данных пользователем, вывод строки на принтер, открытие файла для получения или вывода данных и т. п. Понятно, что даже несложная учебная программа выглядит лучше, если она умеет реагировать на неправильные действия пользователя или возникающие ошибки не просто выводом маловразумительного системного сообщения на английском языке, а доступным неискушенному пользователю текстом. По умолчанию контроль в/в включен и системные сообщения об ошибках генерируются автоматически. Все они кратко приведены в Приложении 3. Для замены системной диагностики своей собственной следует, во-первых, отключить директиву контроля оператором {$I-}, а во-вторых, сразу же после оператора, который мог породить ошибку, проверить значение, возвращаемое системной функцией IoResult. Эта функция возвращает ноль, если последняя операция в/в прошла успешно, в противном случае возвращается ненулевое значение. После завершения "критического" оператора директиву следует включить снова, чтобы не создавать потенциально опасных ситуаций в коде, который будет писаться далее. Приведем пример, написав "расширенную" программу решения квадратного уравнения, корректно реагирующую на возникающие ошибки:

uses printer;

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

begin

  writeln;

  writeln ('Введите коэффициенты a,b,c:');

  {$I-} read (a,b,c); {$I+}

  if IoResult<>0 then begin

   {Возникла ошибка!}

   writeln ('Вы не ввели 3 числа, ',

    'это что-то другое!');

    reset (input); {очищаем стандартный

 поток ввода перед ожиданием нажатия Enter}

    readln;

    halt; {а этим оператором можно

      аварийно завершить программу}

  end;

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

  if d<0 then begin

    writeln ('Ошибка - дискриминант<0');

    reset (input); readln; halt;

  end;

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

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

  {$I-}

  writeln (lst,'x1=',x1:8:2,' x2=',x2:8:2);

  {$I+}

  if IoResult<>0 then

   writeln ('Не удалось напечатать')

  else writeln ('Результаты напечатаны');

  reset (input); readln; halt;

end.

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

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

Для работы с вещественными числами с двойной точностью (тип double) может также понадобиться указать перед программой директиву {$N+}, позволяющую сгенерировать код для аппаратной обработки таких чисел.

9. Оператор цикла. Циклы с предусловием и постусловием

 

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

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

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

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

 

Рис. 9.1. Блок-схемы циклов с предусловием и постусловием

 

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

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

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

while логическое_выражение do begin

 {операторы тела цикла}

end;

Работу цикла можно описать словами: "пока логическое выражение истинно, повторяется тело цикла".

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

Общая запись цикла с постусловием следующая:

repeat

 {операторы тела цикла}

until логическое_выражение;

Работает цикл с постусловием следующим образом: "тело цикла повторяется до тех пор, пока логическое выражение не станет истинным". Обратите внимание, что, в отличие от while, циклrepeat в Паскале работает, пока условие ложно. Это отличие подчеркивается использованием ключевого слова until ("до тех пор, пока не") вместо while ("до тех пор, пока"). Кроме того, в виде исключения, тело цикла repeat, даже если оно состоит из нескольких операторов, можно не заключать в операторные скобки.

Довольно часто циклы взаимозаменяемы. Представим, например, что для каждого из значений переменной x=1, 2, ... ,20, нужно выполнить некоторый расчет (математически этот закон изменения x можно записать как  или ). Это можно сделать как в цикле while:

x:=1;

while x<=20 do begin

 {операторы расчета}

 x:=x+1;

end;

так и с помощью repeat:

x:=1;

repeat

 {операторы расчета}

 x:=x+1;

until x>20;

Как видно из листинга, управляющей переменной x в обоих случаях присвоено начальное значение 1, оба цикла изменяют значение x и, соответственно, условие цикла, оператором x:=x+1;,  но для цикла repeat условие "перевернуто" ("пока x не станет больше 20"), а тело не заключено в операторные скобки.

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


10. Цикл со счетчиком и досрочное завершение циклов

 

Прежде, чем перейти к примерам, обсудим еще ряд проблем, связанных с циклами. Как для while, так и для repeat, во-первых, нигде в явном виде не задается число шагов цикла (хотя его обычно можно вычислить), во-вторых, при использовании обоих циклов программист должен заботиться об изменении управляющей переменной. Между тем, весьма распространены задачи, где объем последовательно обрабатываемых данных известен заранее (а значит, известно и требуемое число шагов цикла), а управляющая переменная меняется с шагом, равным единице. Рассмотренный выше пример с двадцатью значениями x относится именно к таким задачам. Поэтому для обработки заранее известного объема данных с шагом по управляющей переменной, равным единице, вместо цикла while используется цикл со счетчиком (цикл for). Его общий вид следующий:

for счетчик := НЗ to КЗ do begin

 {операторы тела цикла}

end;

Здесь счетчик -- целочисленная переменная, НЗ (начальное) и КЗ (конечное) значения -- целочисленные выражения или константы. Тело цикла образовано не менее чем одним оператором, если этот оператор единственный, операторные скобки можно не писать. Работает цикл for следующим образом: счетчик автоматически меняется от начального значения до конечного включительно, для каждого значения счетчика повторяется тело цикла. После каждого шага цикла значение счетчика автоматически увеличивается на единицу. Если требуется, чтобы значение счетчика уменьшалось, а не увеличивалось, вместо ключевого слова to используется downto.

Подобно while, цикл for может не выполниться и ни разу -- если начальное значение управляющей переменной сразу же больше конечного (при использовании to) или меньше (при использовании downto).

Запишем рассмотренный выше цикл по переменной x с помощью оператора for:

for x:=1 to 20 do begin

 {операторы тела цикла}

end;

Удобства очевидны -- границы изменения x заданы сразу же при входе в цикл, а выполнять шаг по x отдельным оператором не требуется. Понятны и ограничения -- x должен быть описан с типом данных integer, а в случае изменения значения x с шагом, не равным единице, использовать for вместо while не удалось бы.

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

x:=1;

while x<10 do begin

 y:=ln(x);

 if y>2 then x:=10;

  {При y>2 цикл нужно завершить}

 x:=x+0.5;

end;

Однако, во избежание трудноуловимых ошибок, управляющую переменную не принято менять иначе, чем для выполнения шага цикла. Например, после оператора if y>2 then x:=10; в нашем листинге выполнение текущего шага продолжится, что чревато лишними или неправильными вычислениями. Кроме того, текст такой программы воспринимается нелегко.

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

x:=1;

while x<10 do begin

 y:=ln(x);

 if y>2 then break;

 x:=x+0.5;

end;

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

Для немедленного продолжения цикла со следующего шага используется оператор continue (от англ. "to continue" -- продолжить):

var n:integer;

begin

 repeat

  writeln ('Введите положительное число:');

  read (n);

  if n<1 then continue;    

  {Если введено n<1, снова запросить число}

  {Операторы обработки числа}

  break; {Выход из цикла обработки}

 until false;

end.

В этом примере оператор continue использован для повторного перехода к вводу n, если введено n<1. Так как цикл обработки здесь -- бесконечный, для выхода из него необходим break;. Кроме того, пример показывает одну из возможностей контроля правильности ввода данных. Указав директиву {$I-}, изученную в гл. 8, мы могли бы защитить программу и от ввода пользователем нечисловых значений в качестве n.

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


11.1. Алгоритм табулирования

 

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

В общем виде алгоритм можно описать так:

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

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

3.     в конце шага цикла значение управляющей переменной (обозначим ее x) изменяется оператором вида x:=x+d;, где d -- заданный шаг по управляющей переменной.

В качестве примера составим таблицу синусов в пределах от 0 до π с шагом по аргументу 0.25. Обозначим аргумент как x, значение синуса от x обозначим как y. В простейшем случае программа табулирования может выглядеть так:

var x,y:real;

begin

 writeln('x':10,'sin(x)':10);

  {печать заголовка таблицы до цикла}

 x:=0; {начальное значение аргумента}

 while x<=pi+1e-6 do begin

   y:=sin(x); {вычисление функции}

   writeln (x:10:2, y:10:2);

    {печать строки таблицы}

   x:=x+0.25; {шаг по x}

 end;

end.

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

,  значения a, b вводятся пользователем.

Напишем текст программы, сопроводив его соответствующими комментариями.

var x,f,a,b,dx:real;

 n:integer; {счетчик выведенных строк}

begin

 repeat {Цикл ввода с контролем

  правильности значений: a,dx,b должны быть

  числами, dx>0, a+dx должно быть меньше b}

  writeln ('Введите a,dx,b:');

  {$I-}read (a,dx,b);{$I+}

  if IoResult <> 0 then begin

   writeln ('Вы не ввели 3 числовых ',

    'значения, попробуем еще раз');

   continue;

  end;

  if (dx<=0) or (a+dx>=b) then begin

   writeln ('Вы не ввели допустимые ',

    'данные, попробуем еще раз');

   continue;

  end

  else break;

 until false;

 {Печать заголовка таблицы}

 writeln;

 writeln ('x':10,'f(x)':10);

 x:=a;

 n:=2; {2 строки уже использованы}

 while x<=b+1e-6 do begin

   {в условии цикла учитываем возможную

    погрешность работы с real!}

  if x<=0 then f:=sqr(x)*x

  else f:=exp(1/3*ln(abs(x)));

   {корень 3 степени взяли через exp и ln}

  writeln (x:10:2,f:10:2);

  n:=n+1;

  if n=24 then begin

  {На экране консоли по умолчанию 25 строк}

   write ('Нажмите Enter...');

   reset (input); readln;

   n:=1;

  end;

  x:=x+dx;

 end;

 writeln ('Таблица выведена');

 reset (input); readln;

end.

Как видно из примера, основной порядок действий -- такой же, как в предыдущей задаче. Так как экран консоли по умолчанию содержит всего 25 строк, с помощью переменной n мы дополнительно контролируем число уже выведенных строк и делаем по заполнении экрана паузу до нажатия пользователем клавиши Enter.

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

Известна стоимость единицы товара. Составить таблицу стоимости 1, 2, ..., K единиц товара, значение K вводится.

Так как число единиц товара -- заведомо целое, при программировании задачи будет удобен цикл for:

 var t:real;

    i,k:integer;

begin

 writeln;

 writeln ('Стоимость единицы товара:');

 read (t);

 writeln ('Количество единиц товара:');

 read (k);

 writeln ('Единиц':10,'Стоимость':10);

 for i:=1 to k do

  writeln (i:10,(i*t):10:2);

end.

Здесь для простоты мы исключили сделанные в предыдущем примере проверки. Стоимость единицы товара обозначена t, переменная i необходима для перебора возможных значений единиц товара в цикле for. Поскольку счетчик цикла for автоматически меняется с шагом 1, а оператором writeln можно выводить не только значения переменных, но и выражения, основной цикл программы состоит из одного оператора и не нуждается в операторных скобках.

 

11.2. Алгоритм организации счетчика

 

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

1.     в разделе var описать переменную целочисленного типа, с помощью которой будет вестись подсчет;

2.     до цикла присвоить ей начальное значение 0;

3.     в теле цикла, если очередной элемент данных отвечает условию подсчета, увеличить эту переменную на 1 оператором вида k:=k+1;.

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

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

Inc(X,N); -- увеличивает значение переменной.

Здесь параметр X -- переменная порядкового типа, а N -- переменная или выражение целочисленного типа. Значение X увеличивается на 1, если параметр N не определен, или на N, если параметрN определен, то есть Inc(X); соответствует X:=X+1;, а Inc(X,N); соответствует X:=X+N;.

Dec(X,N);  -- уменьшает значение переменной.

Параметр X -- также переменная порядкового типа, N -- целочисленное значение или выражение. Значение X уменьшается на 1, если параметр N не определен, или на N, если параметр Nопределен, то есть Dec(X); соответствует X:=X-1;, а Dec(X,N); соответствует X:=X-N;.

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

В качестве примера реализации алгоритма рассмотрим следующую задачу.

 Последовательность z(i) задана соотношениями , i=1,2,...,100. Найти количество элементов последовательности, больших значения 0.5.

Обозначив искомое количество за k, составим программу:

var z:real;

 i,k:integer;

begin

 k:=0;

 for i:=1 to 100 do begin

  if i mod 2 = 0 then z:=sqr(i)*cos(i)

  else z:=sin(i/2);

  if z>0.5 then inc(k);

 end;

 writeln ('Количество=',k);

end.

Так как шаг по переменной i равен 1, в программе использован цикл for, для проверки того, является ли значение i четным, использована операция mod.

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

Известны оценки за экзамен по информатике для группы из n студентов, 2≤n≤25. Оценить количественную и качественную успеваемость группы по формулам:

, , где k1 -- количество "троек", "четверок" и "пятерок", k2 -- количество только "четверок" и "пятерок".

Для ввода текущей оценки используем целочисленную переменную a, в качестве счетчика цикла for введем переменную i ("номер студента"), остальные величины описаны в условии задачи. При вводе значения n и очередного значения a для простоты не будем контролировать корректность вводимых данных.

var a,i,n,k1,k2:integer;

    ykol,ykach:real;

begin

 writeln;

 writeln ('Введите количество студентов:');

 read (n);

 k1:=0;

 k2:=0;

 for i:=1 to n do begin

  write ('Введите оценку ',i,' студента:');

  read (a);

  if a>2 then begin

   inc(k1);

   if a>3 then inc(k2);

  end;

 end;

 ykol:=k1/n*100;

 ykach:=k2/n*100;

 writeln

('Количественная успеваемость=',ykol:6:2);

 writeln

('Качественная успеваемость  =',ykach:6:2);

 reset (input); readln;

end.

 

11.3. Алгоритмы накопления суммы и произведения

 

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

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

2.     до цикла переменной-сумме присвоить начальное значение 0, а произведению -- значение 1;

3.     в теле цикла, если очередной элемент данных t отвечает условию суммирования или перемножения, сумма накапливается оператором вида s:=s+t;, а произведение -- оператором видаp:=p*t;

Очевидно, почему начальное значение произведения -- 1, а не 0. После оператора p:=0; оператор p:=p*t;, расположенный в теле цикла, будет возвращать только нули.

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

Для поиска арифметического среднего необходимо сначала найти сумму s и количество k положительных значений функции. Составим следующую программу:

var x,f,s,p:real;

    k:integer;

begin

 s:=0; k:=0; p:=1;

 x:=-5;

 while x<=5+1e-6 do begin

  if x<0 then f:=sqr(ln(abs(x)))

  else if x>0 then f:=sin(sqr(x))

  else f:=0;

  if f>0 then begin

   s:=s+f;

   k:=k+1;

  end;

  if f<>0 then p:=p*f;

  x:=x+0.5;

 end;

 s:=s/k; {теперь в s - искомое среднее}

 writeln

 ('Среднее положительных =',s:10:6);

 writeln

 ('Произведение ненулевых=',p:10:6);

 reset (input); readln;

end.

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

Требуется написать программу, имитирующую работу кассового аппарата: пользователь в цикле вводит цену очередного товара или 0 для завершения ввода, программа суммирует цены. По завершении цикла ввода программа начисляет скидку с общей стоимости товара по правилам: скидки нет, если общая стоимость покупки -- менее 10000 руб.; скидка равна 5%, если общая стоимость -- от 10000 до 20000 руб.; скидка равна 7%, если общая стоимость -- свыше 20000 руб. После начисления скидки выводится окончательная стоимость покупки.

Обозначив общую стоимость покупки s, а цену очередного товара -- t, напишем следующую программу:

var s,t:real;

begin

 writeln;

 s:=0; {начальное значение суммы!}

 repeat

  writeln ('Введите стоимость товара или '

   '0 для завершения ввода:');

  {$I-}read(t);{$I+}

  if (IoResult<>0) or (t<0) then begin

   writeln ('Ошибка! Повторите ввод');

   continue;

  end;

  if t=0 then break;

  {Округляем t до 2 знаков после запятой -

   на случай, если есть копейки}

  t:=round (t*100) / 100;

  s:=s+t; {накопление суммы}

 until false;

 {Начисление скидки и вывод ответа}

 writeln ('Стоимость без скидки:',s:8:2);

 if s>20000 then s:=s-s*0.07

 else if s>10000 then s:=s-s*0.05;

 writeln ('Стоимость со скидкой:',s:8:2);

 writeln ('Спасибо за покупку!');

 reset (input); readln;

end.

Тип данных real выбран для s и t не случайно -- выбор integer ограничил бы диапазон обрабатываемых значений и не позволил в удобном виде ввести копейки. Проверки корректности ввода, делаемые программой, знакомы по предыдущим примерам и поэтому не закомментированы.

12. Типовые алгоритмы поиска максимума и минимума

 

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

Рассмотрим алгоритм в общем виде:

1.     описать для каждого максимума и минимума по одной переменной того же типа, что анализируемые данные;

2.     до цикла максимуму присваивается либо заведомо малое для анализируемых данных значение, либо первый элемент данных; минимуму присваивается либо заведомо большое для анализируемых данных значение, либо первый элемент данных;

3.     в теле цикла каждый подходящий для поиска элемент данных t обрабатывается операторами вида:
if t>max then max:=t; -- для максимума;
if t<min then min:=t; -- для минимума,
где max и min -- переменные, введенные для величин максимума и минимума соответственно.

Шаг 2 этого алгоритма требует комментариев, которые мы сделаем на примере поиска максимума. Очевидно, что сам алгоритм несложен -- каждый элемент данных t последовательно сравнивается с ячейкой памяти max и, если обнаружено значение t, большее текущего значения max, оно заносится в max оператором max:=t;. Как мы помним, после описания на шаге 1 переменнойmax, ее значение еще не определено, и может оказаться любым, откуда следует необходимость задания начального значения. Представим, что после выбора начального значения max, равного нулю, при анализе встретились только отрицательные значения элементов t. В этом случае условие t>max не выполнится ни разу и ответом будет max, равное нулю, что неправильно. Выбор заведомо малого начального значения max (например, значение -1E30, т. е., -1030, вряд ли встретится в любых реальных данных) гарантирует, что условие t>max выполнится хотя бы раз и максимум будет найден. Альтернативный способ -- присвоить переменной max значение отдельно вычисленного первого элемента последовательности данных. В этом случае ответ либо уже найден, если первый элемент и есть максимальный, либо будет найден в цикле.

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

Перейдем к примерам. Для функции y(x)=sin2(x),  найти минимальное среди положительных и максимальное значения.

Обозначив искомые значения min и max соответственно, напишем следующую программу:

var x,y,max,min:real;

begin

 x:=-pi/3;

 max:=-2;

 min:=2; {эти начальные значения

  - заведомо малое и большое для синуса}

 while x<=pi/3+1e-6 do begin

  y:=sqr(sin(x));

  if y>0 then

   {ищем min только среди положительных!}

   if y<min then min:=y;

  if y>max then max:=y;

  x:=x+pi/24;

 end;

 writeln ('Минимум =',min:8:2);

 writeln ('Максимум=',max:8:2);

 reset (input); readln;

end.

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

Последовательность T(k) задана соотношениями T(k)=max(sin k, cos k), k=1, 2, ... ,31. Найти номера максимального и минимального элементов последовательности.

Поиск номеров не избавит нас от необходимости поиска значений. Поэтому, кроме переменных min и max, нам понадобятся две целочисленные переменные для хранения номеров минимального и максимального значений, обозначим их kmin и kmax соответственно. Обратите также внимание, что на каждом шаге цикла дополнительно потребуется находить максимальное из значений sin(k) иcos(k), для занесения его в переменную t.

var t,max,min:real;

    k,kmin,kmax:integer;

begin

 min:=1e30;

 max:=-1e30;

 {задаем "надежные" значения,

  близкие к плюс и минус бесконечности}

 for k:=1 to 31 do begin

  if sin(k)>cos(k) then t:=sin(k)

  else t:=cos(k);

  if t<min then begin

   {по условию нужны 2 оператора -

    сохранение нового мин. значения

    и сохранение номера элемента,

    отсюда операторные скобки!}

   min:=t; kmin:=k;

  end;

  if t>max then begin

   max:=t; kmax:=k;

  end;

 end;

 writeln ('Номер мин. элемента =',kmin);

 writeln ('Номер макс. элемента=',kmax);

 reset (input); readln;

end.

13. Решение учебных задач на циклы

 

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

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

Задана функция и ее разложение в ряд:

 n=0, 1, 2, ... (здесь n! обозначает факториал числа n, равный 1*2*3*...*n; при этом 0!=1). Найти число элементов ряда k, требуемое для достижения заданной точности ε.

Перед нами -- типичная задача на ряды. С ростом величины n слагаемые становятся все меньше, равно как и модуль разности между двумя соседними слагаемыми. Поэтому под достижением заданной точности будем понимать выполнение условия  где sn, sn-1  -- суммы ряда, вычисленные на текущем и предыдущем шагах цикла, а значение ε задается малым числом, от 10-6 и ниже. Для вычисления x2n введем переменную xn, которую на каждом шаге цикла будем домножать на x2, аналогично, для вычисления текущего значения факториала используем переменную nf. Иной подход потребовал бы большого количества повторных вычислений на каждом шаге цикла, плюс был бы чреват большими потерями точности. Для вычисления (-1)n было бы странно использовать формулу ax=exln a -- вполне достаточно завести целочисленную переменную znak, равную 1, которая на каждом шаге цикла будет менять свое значение на противоположное оператором znak:=-znak;. Кроме того, так как требуемое число шагов заранее неизвестно, используем для всех основных переменных более точный тип double вместо real.

Реализуем все сказанное в следующей программе:

{$N+} {Совместимость с процессором 80287 -

 для использования double}

var x,sn,sn1,xn,nf,eps:double;

    k,n:longint;

    znak:integer;

begin

 writeln ('Введите значение x:');

 read (x);

 writeln ('Введите требуемую точность:');

 read (eps);

 sn1:=0;

 sn:=0;

 k:=0;

 n:=0;

 xn:=1;

 nf:=1;

 znak:=1;

 repeat

  sn1:=sn; {Текущая сумма стала

            предыдущей для следующего шага}

  sn:=sn+znak*xn/nf; {Выполняем шаг цикла}

  {Меняем переменные для следующего шага:}

  znak:=-znak;

  xn:=xn*sqr(x);

  nf:=nf*(n+1)*(n+2);

  n:=n+2;

  k:=k+1;

 until abs(sn1-sn)<eps;

 writeln ('Значение =',cos(x):20:16);

 writeln ('Предыдущая сумма=',sn1:20:16);

 writeln ('Текущая сумма=',sn:20:16);

 writeln ('Число шагов=',k);

 reset (input); readln;

end.

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

Сформировать на экране изображение функции

f(x)= cos(x)+ln(x)

на интервале [1, 5] с шагом 0.2.

Программа с комментариями приводится далее.

var x,y,{ Значение аргумента и функции }

    a,b,{ Интервал }

    ymin,ymax:real; { Мин. и макс. y }

    cy, { Позиция столбца на экране }

    i:integer;

begin

 a:=1;

 b:=5;

 { Первый цикл нужен, чтобы определить

   минимальное и максимальное значения

   функции на заданном интервале }

 x:=a;

 ymin:=1e20; ymax:=1e-20;

 while x<=b do begin

  y:=cos(x)+ln(x);

  if y<ymin then ymin:=y

  else if y>ymax then ymax:=y;

  x:=x+0.2;

 end;

 { Во втором цикле обработки можно выводить

   на экран значения функции }

 x:=a;

 while x<=b do begin

  y:=cos(x)+ln(x);

  cy:=round(8+(y-ymin)*(70/(ymax-ymin)));

  { Чтобы пересчитать y из границ

    [ymin,ymax] в границы [8,78] (столбцы

    экрана, в которые выводим график),

    используем формулу cy=  8 + (y-ymin)*

     (78-8)/(ymax - ymin) }

  writeln;

  write (x:7:3,' ');

   { Выводим очередной x и пробел }

  for i:=8 to cy do write ('*');

   { Рисуем звездочками значение y }

  x:=x+0.2; { Переходим к следующему x  }

 end;

end.

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

Простое число делится без остатка только на единицу и само на себя. В цикле последовательно проверим остатки от деления N на числа 2, 3, ... ,N/2. Если найден хотя бы один остаток, равный нулю, число не является простым и дальнейшие проверки бессмысленны. Ясно также, что не имеет смысла проверять, есть ли целые делители, которые больше половины исходного числа, поэтому верхняя граница цикла = N/2 (для деления используем операцию div). Обратите внимание на использование логической переменной-флага s в этой программе.

var n,i:longint;

    s:boolean;

begin

 write ('N=');

 readln (n);

 s:=true;

 for i:=2 to n div 2 do

  if n mod i = 0 then begin

   s:=false;

   break;

  end;

 if s=true then writeln ('простое')

 else writeln ('НЕ простое');

end.

14. Одномерные массивы. Описание, ввод, вывод и обработка массивов на Паскале

 

Массивом называют упорядоченный набор однотипных переменных (элементов). Каждый элемент имеет целочисленный порядковый номер, называемый индексом. Число элементов в массиве называют его размерностью. Массивы используются там, где нужно обработать сразу несколько переменных одного типа -- например, оценки всех 20 студентов группы или координаты 10 точек на плоскости. Строку текста можно рассматривать как массив символов, а текст на странице -- как массив строк.

Массив описывается в разделе var оператором следующего вида:

var ИмяМассива: array [НИ .. ВИ] of Тип;

Здесь

НИ (нижний индекс) -- целочисленный номер 1-го элемента массива;

.. -- оператор диапазона Паскаля (см. п. 7.8);

ВИ (верхний индекс) -- целочисленный номер последнего элемента;

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

Опишем несколько массивов разного назначения.

var a: array [1..20] of integer;

Здесь мы описали массив с именем A, состоящий из 20 целочисленных элементов;

var x,y : array [1..10] of real;

Описаны 2 массива с именами x и y, содержащие по 10 вещественных элементов;

var t : array [0..9] of string;

Массив t состоит из 10 строк, которые занумерованы с нуля.

Легко увидеть, что размерность (число элементов) массива вычисляется как ВИ - НИ + 1.

Для обращения к отдельному элементу массива используется оператор вида ИмяМассива [Индекс].

Здесь Индекс -- целочисленный номер элемента (может быть целочисленным выражением или константой). Индекс не должен быть меньше значения нижнего или больше верхнего индекса массива, иначе возникнет ошибка "Constant out of range". Отдельный элемент массива можно использовать так же, как переменную соответствующего типа, например:

A[1]:=1;

x[1]:=1.5; y[1]:=x[1]+1;

t[0]:='Hello';

В этой главе мы изучаем одномерные массивы, в которых каждый элемент имеет один номер (индекс), характеризующий его положение в массиве. В математике понятию одномерного массива изn элементов соответствует понятие вектора из n компонент: A = {Ai}, i=1, 2 ,..., n.

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

Простейший способ ввода -- ввод массива с клавиатуры:

const n = 10;

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

 i:integer;

begin

writeln ('Введите элементы массива');

for i:=1 to n do read (A[i]);

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

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

const a:array [1..5] of real=(

 3.5, 2, -5, 4, 11.7

);

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

Формирование массива из случайных значений уместно, если при решении задачи массив служит лишь для иллюстрации того или иного алгоритма, а конкретные значения элементов несущественны. Для того чтобы получить очередное случайное значение, используется стандартная функция random(N), где параметром N передается значение порядкового типа. Она вернет случайное число того же типа, что тип аргумента и лежащее в диапазоне от 0 до N-1 включительно. Например, оператор вида a[1]:=random(100); запишет в a[1] случайное число из диапазона[0,99].

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

var a:array [1..20] of integer;

 i:integer;

begin

 randomize;

 for i:=1 to 20 do begin

  a[i]:=random(21)-10;

  write (a[i]:4);

 end;

end.

Еще более удобный путь -- чтение элементов массива из текстового или двоичного файла. Об этом рассказывается в гл. 21 и 22.

К массивам применимы все типовые алгоритмы, изученные в теме "Циклы". Приведем один пример, в котором вычисляется сумма s положительных элементов массива.

var b:array [1..5] of real;

s:real; i:integer;

begin

writeln ('Введите 5 элементов массива');

for i:=1 to 5 do read (b[i]);

s:=0;

for i:=1 to 5 do if b[i]>0 then s:=s+b[i];

Вывод массива на экран также делается с помощью цикла for.

for i:=1 to 5 do write (b[i]:6:2);

Здесь 5 элементов массива b напечатаны в одну строку. Для вывода одного элемента на одной строке можно было бы использовать оператор writeln вместо write.

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

Найти арифметическое среднее элементов вещественного массива t размерностью 6 и значение его минимального элемента.

var b:array [1..6] of real;

 s, min:real;

 i:integer;

begin

s:=0; min:=1e30;

writeln ('Ввод B[6]');

for i:=1 to 6 do begin

 read (b[i]);

 s:=s+b[i];

 if b[i]<min then min := b[i];

end;

writeln ('min=',min,' s=', s/6);

end.

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

Задана последовательность Ti = max {sin i, cos i}, i= -5, -4, ..., 5. Найти элемент последовательности, имеющий минимальное отклонение от арифметического среднего положительных элементов.

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

var t : array [-5..5] of real;

 i,k:integer;

 s, ot : real;

begin

 s:=0; k:=0;

 for i:=-5 to 5 do begin

  t[i]:=sin(i);

  if t[i]<cos(i) then t[i]:=cos(i);

  if t[i]>0 then begin

   k:=k+1; s:=s+t[i];

  end;

 end;

 s:=s/k;

 ot:=1e30;

 for i:=-5 to 5 do begin

  if abs(t[i]-s)<ot then ot:= abs(t[i]-s);

 end;

 writeln ('Ot=', ot:8:2);

end.

Распространена обработка в одной задаче сразу нескольких массивов. Приведем пример.

Координаты 10 точек на плоскости заданы массивами x={xi}, y={yi}, i=1, 2, ..., 10. Найти длину ломаной, проходящей через точки (x1, y1), (x2, y2), ..., (x10, y10), а также номер точки, лежащей дальше всего от начала координат.

При решении задачи используем формулу для нахождения расстояния между 2 точками на плоскости, заданными координатами (x1,y1) и (x2,y2): .

Обозначим через r расстояние между текущей точкой и следующей, Len искомую длину ломаной, Dist -- расстояние от текущей точки до начала координат, max -- максимальное из этих расстояний, Num -- искомый номер точки.

var x,y : array [1..10] of real;

I, num:integer;

r, Len, Dist, max : real;

begin

 {найдем max при вводе данных}

 max:=0; {т.к. расстояние не м.б. <0 }

 num:=1; {на случай, если все точки (0,0)}

 writeln ('Введите координаты 10 точек');

 for i:=1 to 10 do begin   

  read (x[i], y[i]);

  Dist:=sqrt (sqr(x[i]) + sqr (y[i]));

  if dist > max then begin

    max:=dist; {запомнили новое расстояние}

    Num:=i;    {и номер точки}

  end;

 end;

 writeln ('Номер точки=',num,

  ' расстояние=',dist:8:2);

 Len:=0;{длина ломаной - сумма длин сторон}

 for i:=1 to 9 do begin

         {у 10-й точки нет следующей!}

   r:= sqrt (sqr(x[i]-x[i+1])+

             sqr(y[i]-y[i+1]));

   Len:=Len+r;

 end;

 writeln ('Длина ломаной=',len:8:2);

end.

Приведем пример задачи формирования массива по правилу.

Задан массив x из 8 элементов. Сформировать массив y по правилу

и найти количество его положительных элементов.

var x,y: array [1..8] of real;

 i,k:integer;

begin

 writeln ('Введите массив x из 8 эл.');

for i:=1 to 8 do begin

  read (x[i]);

  if i mod 2 =0 then y[i]:=4*x[i]

  else y[i]:=cos(2*x[i]);

end;

K:=0;

writeln ('Массив y');

 for i:=1 to 8 do begin

    if y[i]>0 then k:=k+1;

   write (y[i]:8:2);

 end;

 writeln;

 writeln ('K=',k);

end.

15. Решение типовых задач на массивы

 

В гл. 14 мы подчеркивали, что типовые алгоритмы, изученные в теме "Циклы", в полной мере применимы и к массивам. Занимая определенный объем оперативной памяти, однажды полученные элементы массива остаются доступными весь сеанс работы программы и не требуют повторного вычисления или чтения из файла. Это порождает круг приложений, связанных с типовой обработкой наборов данных -- вычисление математических и статистических характеристик векторов, объединение массивов или поиск нужных значений в них и т. д. Другие интересные примеры, такие как подсчет частоты встречаемости элементов в массиве, задача сортировки (упорядочения) данных, будут рассмотрены в гл. 16.

1. Найти сумму, скалярное произведение и длину двух векторов произвольно заданной размерности. Размерность не может превышать значения 100.

Предельную размерность массивов зададим константой size=100. Размерность реальных векторов a и b, с которыми работает программа, не обязательно столь велика, обозначим ее n и позаботимся о том, чтоб при вводе значения n соблюдалось соотношение 2≤n≤size. Введем вектор a с клавиатуры, а вектор b сгенерируем из случайных вещественных чисел, принадлежащих диапазону от 0 до 10. Для этого достаточно умножить на 10 значение, возвращаемое стандартной функцией random, если она вызвана без параметров: b[i]:=random*10;.

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

Как известно из математики, сумма векторов a и b ищется поэлементно по правилу ci=ai+bi, скалярное произведение может быть вычислено по формуле , а длина вектора a из nэлементов ищется по формуле .

const size=100;

var a,b,c:array [1..size] of real;

    n,i:integer;

    la,lb,s:real;

begin

 repeat

  writeln;

  write ('Введите размерность A и B, ',

   'значение от 2 до ',size,':');

  {$I-}readln (n);{$I+}

  if (IoResult<>0) or (n<2) or (n>size)

   then

   writeln ('Неверный ввод, повторите')

  else break;

 until false;

 writeln ('Введите вектор A из ',

  n,' элементов:');

 for i:=1 to n do begin

  repeat

   write ('A[',i,']=');

   {$I-}readln (a[i]);{$I+}

   if IoResult=0 then break;

  until false;

 end;

 writeln ('Генерируется вектор B из ',

  n,' элементов:');

 randomize;

 for i:=1 to n do begin

  b[i]:=random*10;

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

 end;

 la:=0;

 lb:=0;

 s:=0;

 writeln;

 writeln ('Вектор c=A+B:');

 for i:=1 to n do begin

  c[i]:=a[i]+b[i];

  write (c[i]:8:2);

  la:=la+sqr(a[i]);

  lb:=lb+sqr(b[i]);

  s:=s+a[i]*b[i];

 end;

 writeln;

 writeln ('Длина вектора A:       ',

  sqrt(la):8:2);

 writeln ('Длина вектора B:       ',

  sqrt(lb):8:2);

 writeln ('Скалярное произведение:',s:8:2);

 reset (input); readln;

end.

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

2. Задана выборка из n случайных чисел. Для n=100 определить математическое ожидание mx и дисперсию dx выборки по формулам: , .

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

const n=100;

var x:array [1..100] of real;

    s,d:real;

    i:integer;

 

begin

 writeln;

 writeln ('Массив x[100]:');

 randomize;

 s:=0; d:=0;

 for i:=1 to n do begin

  x[i]:=random*2-1;

  write (x[i]:8:3);

  s:=s+x[i];

  d:=d+sqr(x[i]);

 end;

 s:=s/n; {теперь в s - мат. ожидание,}

 d:=d/(n-1)-sqr(s)/(n*(n-1));

         {а в d - дисперсия}

 writeln;

 writeln ('s=',s:8:4);

 writeln ('D=',d:8:4);

 reset (input); readln;

end.

3. Объединить 2 упорядоченных по возрастанию массива A и B в массив C.

Для решения этой задачи тоже достаточно одного прохода по массивам. Действительно, заведя для каждого из массивов a, b и c по собственному счетчику (обозначим их ia, ib и iсоответственно), мы можем, в зависимости от истинности или ложности соотношения aia≤bib переписывать в элемент ci значение aia или bib. Остается проконтролировать, чтобы ни один из счетчиков не вышел за границу размерности своего массива. Увидеть детали вам поможет внимательный анализ листинга.

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

program m_concat;

const size=10;

      step=5;

var a,b:array [1..size] of integer;

    c:array [1..2*size] of integer;

    i,n1,n2,ia,ib,ic:integer;

begin

 writeln;

 repeat

  write('Размерность 1 массива ',

        '(от 2 до ',size,'):');

  read (n1);

 until (n1>1) and (n1<=size);

 randomize;

 a[1]:=random(step);

 write ('A= ',a[1],' ');

 for i:=2 to n1 do begin

  a[i]:=a[i-1]+random(step);

  write (a[i],' ');

 end;

 writeln;

 repeat

  write('Размерность 2 массива ',

        '(от 2 до ',size,'):');

  read (n2);

 until (n2>1) and (n2<=size);

 b[1]:=random(step);

 write ('B= ',b[1],' ');

 for i:=2 to n2 do begin

  b[i]:=b[i-1]+random(step);

  write (b[i],' ');

 end;

 writeln;

 ia:=1; ib:=1;

 write ('c= ');

 for i:=1 to n1+n2 do begin

  if a[ia]<=b[ib] then begin

   c[i]:=a[ia];

   if ia<n1 then Inc(ia)

   else begin

    a[n1]:=b[ib];

    if ib<n2 then Inc (ib);

   end;

  end

  else begin

   c[i]:=b[ib];

   if ib<n2 then Inc(ib)

   else begin

    b[n2]:=a[ia];

    if ia<n1 then Inc(ia);

   end;

  end;

  write (c[i],' ');

 end;

 writeln;

end.