Четвертий розділ

5. ІНШІ ВИДИ ЦИКЛІВ

5.1 Доти, поки не...

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

І.Ільф, Є.Петров

Повернемося до обчислення квадратного кореня (приклад 4.4):

X:=(a+1)/2; Y:=0.5*(X+a/X);

while abs(X-Y)>d do

begin

X:=Y; Y:=0.5*(X+a/X);

end;

Цей алгоритм задає послідовність дій такого вигляду:

X:= (a+1)/2;

Y:= 0.5*(X+a/X);

обчислення умови продовження: true;

X:=Y;

Y:= 0.5*(X+a/X);

обчислення умови продовження: true;

X:=Y;

Y:= 0.5*(X+a/X);

обчислення умови продовження: false.

Якщо в цій послідовності замінити найперший оператор на Y:=(a+1)/2; X:=Y, то вона буде циклічною, починаючи з другого оператора, і циклом буде

X:=Y;

Y:= 0.5*(X+a/X);

обчислення умови продовження

Можна було б подумати про оператор

do X:=Y;

Y:=0.5*(X+a/X);

while abs(X-Y)>d;

або в загальному вигляді

do послідовність операторів

while умова.

Такого оператора в мові Паскаль немає, а є схожий за виглядом

repeat

послідовність операторів

until умова

Він називається repeat-оператором, або оператором циклу з пост-умовою ("пост" означає "після"), і дослівно перекладається українською мовою як

повторювати

послідовність операторів

доти, поки не умова.

"Поки не" перетворює умову в умову завершення. Справа в тім, що спочатку виконується послідовність операторів (тіло), потім обчислюється умова, і якщо вона хибна, то знову виконується тіло тощо. Виконання оператора завершується після того, як при обчисленні умови одержано значення true. Таким чином, істинність умови означає завершення, а не продовження виконання всього оператора. Ми б назвали цей оператором циклу з умовою завершення, але такий термін у літературі не зустрічався.

Перепишемо алгоритм із прикладу 4.4 з використанням repeat-оператора. Цикл повинен починатися оператором X:=Y, тому перед циклом треба задати ініціалізацію Y. Умовою завершення повинно стати

not abs(X-Y)>d, або abs(X-Y)<=d,

тобто заперечення умови продовження:

Y:=(a+1)/2;

repeat

X:=Y;

Y:=0.5*(X+a/X);

until abs(X-Y)<=d;

{abs(X-Y)<=d; значення Y – шукане}

Оператору циклу з пост-умовою відповідає блок-схема, зображена на рис.5.1.

Задачі

5.1. Імітувати виконання послідовності операторів (усі змінні типу integer):

а) a:=1; s:=1;

repeat a:=2*a; s:=s+a; until a>10;

writeln(a, s);

б)* a:=1;

repeat a:=a+2; writeln(a); until a=4.

5.2.* Написати процедуру читання цілого числа від 0 до 100, причому якщо читається число поза цією множиною, то виконання процедури повинно повертатися до читання числа – доти, поки не буде прочитано число від 0 до 100.

5.3.* Нехай S позначає довільний оператор, B – булів вираз. Виразити оператор:

а) while B do S за допомогою repeat-оператора;

б) repeat S until B за допомогою while-оператора,

використовуючи, можливо, також умовний оператор.

5.4.* Нехай f – неперервна функція, яка визначена на відрізку [a;b] і має на його кінцях значення різних знаків. Нехай її обчислення задається функцією (у розумінні мови Паскаль) із заголовком

function ff( x : real ) : real;

Одним із найпростіших засобів пошуку наближеного значення хоча б одного кореня рівняння f(x)=0 на відрізку [a; b] є метод половинного ділення. Він полягає в тім, що обчислюється середина m відрізка, і якщо f(m)¹ 0, то пошук продовжується таким самим способом на тому з відрізків [a; m], [m; b], на кінцях якого функція приймає значення різних знаків. З використанням оператора циклу з пост-умовою описати обчислення кореня з можливою помилкою, не більшою, ніж 0.001. Забезпечити, щоб не було повторних викликів функції ff із тим самим значенням аргументу.

5.2. "Для"

Розглянемо алгоритм обчислення n!=1× 2×× n при n>0, 0!=1, припускаючи, що всі змінні в ньому цілі й значення змінної n є невід'ємним:

f:=1;

{! }

k:=1;

while k<=n do

begin f:=f*k; k:=k+1 end

{k=n+1, останнім співмножником у значенні f було n}

Очевидно, що кількість виконань тіла циклу збігається зі значенням змінної n і, якщо n>0, то оператор f:=f*k виконується при k=1, 2, … , n (за n=0 не виконується жодного разу). "Забудемо на хвилинку" про останнє виконання оператора k:=k+1. Тоді дії, задані операторами після коментарю {!}, можна описати словами "для k=1, 2, … , n виконати f:=f*k" або англійською мовою "for k=1, 2, … , n do f:=f*k". А це вже майже оператор мови Паскаль:

for k := 1 to n do f:=f*k

Його назва – for-оператор, або оператор циклу переліком, оскільки в ньому задано перелік значень змінної k, при яких виконується тіло циклу. Ця змінна називається параметром циклу. Загальний вигляд for-оператора:

for ім'я := вираз1 to вираз2 do оператор

Ім'я позначає змінну цілого типу (параметр циклу), а вирази однотипні з нею. У розд. 6 ми дізнаємося, що параметр циклу може бути не лише цілого типу. Частина оператора від слова for до слова do називається заголовком циклу, а оператор – тілом. Виконується for-оператор не так просто, як це може здатися на перший погляд. Опишемо його докладніше.

Спочатку обчислюються значення виразів у заголовку й порівнюються. Ці значення називаються граничними; позначимо їх відповідно lv і hv. Якщо lv>hv, то виконання закінчується. При цьому тіло циклу не виконується жодного разу, а параметр циклу (нехай його ім'я k) не одержує ніякого значення! Якщо ж lv<=hv, то виконується k:=lv, потім тіло циклу. Потім порівнюються k і hv: при k<hv значення k збільшується на 1. Це збільшення називається неявним, тому що явно ніяк не позначено в операторі. Потім знову виконується тіло циклу і збільшується значення параметра тощо.

Коли після чергового збільшення значення k досягає hv, то виконується тіло циклу та після порівняння k і hv усе закінчується, тобто значення параметра циклу більше не збільшується.

Зверніть увагу на такі особливості:

У діалекті Турбо Паскаль остання заборона відсутня. Крім того, при порівнянні k і hv насправді обчислюється значення виразу k<>hv. Разом ці особливості дозволяють написати цикл for, тіло якого виконується аж ніяк не при значеннях параметра циклу, указаних у заголовку. Наприклад, для обчислення функції n!!, означеної за непарного n як 1× 3×× n і як 2× 4×× n за парного, в Турбо Паскаль можна, здавалося б, зробити такий трюк:

ff:=1; if odd(n) then lb:=1 else lb:=2;

{n і lb або обидва парні, або обидва непарні}

for k := lb to n do

begin ff:=ff*k; k:=k+1 end

Але це неправильно! Між двома послідовними виконаннями ff:=ff*k значення k збільшується двічі на 1 (явно і неявно), і, поки k<n, значення ff обчислюється правильно. Проте при k=n відбувається множення на n і явне збільшення k. Його значенням стає n+1. Воно порівнюється з n, n+1<>n, тому k збільшується неявно, і повторення тіла циклу продовжуються! Зрештою при черговому обчисленні ff*k одержується значення, більше maxint. Комп'ютер такого "не розуміє", і програма аварійно завершується.

Отже, краще не ризикувати й не задавати в тілі явні зміни параметра циклу. Нехай він буде чимось на зразок "священної корови" в індусів.

Наведемо "безпечне" розв'язання задачі про n!!. Очевидно, що серед чисел 1, 2, 3, … , n множниками в шуканому добутку повинні стати парні за парного n або, навпаки, непарні за непарного n. Можна перебрати всі ці числа, пропустивши непотрібні, у тому числі й 1:

ff:=1;

for k := 2 to n do

if odd(k)=odd(n) then ff:=ff*k.

Оператор циклу переліком має ще одну форму:

for ім'я := вираз1 downto вираз2 do оператор,

де всі позначення мають той самий зміст, що й у першій формі. Слово downto замість слова to задає не збільшення, а зменшення на 1 значення параметра циклу після виконання його тіла. Крім того, тіло циклу не виконується жодного разу, якщо спочатку значення виразу1 менше значення виразу2. Наприклад, обчислення n!! за допомогою цього оператора задається так:

ff:=n;

for k := n-1 downto 2 do

if odd(k)=odd(n) then ff:=ff*k.

Приклад 5.1. У математиці є чимало знаменитих формул, і одна з них – формула бінома Ньютона: , де коефіцієнти називаються біноміальними. Напишемо функцію C обчислення біноміального коефіцієнта за значеннями n і k. Використовуючи функцію fct обчислення Факторіала, можна написати:

C:=fct(n)/(fct(k)*fct(n-k))

Важко придумати щось гірше. Це все одно, що їхати з Берліна в Париж через джунглі Амазонії, піддаючи себе всіляким небезпекам. По-перше, ми змушуємо комп'ютер обчислювати ті самі значення тричі. По-друге, функція n! зростає дуже швидко за зростання n. Наприклад, 8!=40320, а в деяких системах програмування це вже більше, ніж maxint.

Подивимося уважніше на формулу біноміального коефіцієнта:

.

При використанні будь-якої з рівностей треба обчислити два добутки й знайти їх частку, яка за означенням є цілою. Позначимо їх num і den – скорочення від англійських numerator і denumerator (чисельник і знаменник). За другою з рівностей обчислення очевидні:

num := n; den := 1;

for i := 2 to k do

begin num := num*(n-i+1); den := den*i end;

C := num div den

Все одно погано: наприклад, , але доводиться доходити до 8!=40320 і 7!=5040, щоб потім їх поділити й одержати всього лише 8. Між тим, у самій формулі вже закладено обчислення послідовності значень

d0=1, d1=n/1, d2=n(n-1)/(1× 2), ... , dk=n(n-1)× ... × (n-k+1)/(1× 2× ... × k).

Її члени задаються рекурентним співвідношенням di=di-1× (n+1-i)/i. Оскільки з кожних m послідовних натуральних чисел одне ділиться на m без остачі, то всі члени цієї послідовності цілі. Тому обчислення dk можна задати так:

d:=1;

for i:=1 to k do d:=d*(n+1-i) div i

І нарешті, якщо k>n/2, то останній цикл також задає зайві обчислення. Наприклад, при n=8, k=7 тіло циклу виконується 7 разів, хоча значення 8 утворюється вже після першого виконання. Скористаємося тим, що , і напишемо остаточний варіант:

function C (n, k : integer) : integer;

var d : integer;

begin

if k > n div 2 then k:=n-k;

d:=1;

for i := 1 to k do d:=d*(n+1-i) div i;

C:=d

end;

ç

Задачі

5.5.* Указати, що буде надруковано при виконанні програми:

program forTest;

var i , k : integer;

begin k := 2;

for i := k to k+2 do

begin k := k + 1; writeln(i, ' ', k) end;

writeln('Alles! ')

end.

5.6. Використовуючи for-оператор, написати функцію обчислення М-го числа Фібоначчі за номером М>0.

5.7. Функція визначена на відрізку [a;b]. Нехай її значення в точці відрізка обчислюється при виконанні виклику f(E), де E позначає дійсний вираз. Запрограмувати друкування її значень у точках a, a+h, … , a+m*h, де

а)* h задається, а m таке, що a+m*h£ b і a+(m+1)*h>b;

б) m задається, а h таке, що a+m*h=b.

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

5.8.* Написати програму малювання в центрі екрана "новорічної ялинки":

*

***

*****

***

*****

*******

|

5.9.* Трикутник Паскаля має такий початок:

1

1 1

1 2 1

1 3 3 1

. . .

Кожне число в трикутнику, крім перших трьох, є сумою чисел, розташованих над ним: 2=1+1, 3=1+2 тощо. Число в i-му рядку (i=0, 1, 2, …) на j-му місці (j=0, 1, … , i), задається формулою , тобто ці числа є біноміальними коефіцієнтами.

Написати програму друкування на екрані перших 13 рядків трикутника Паскаля (з номерами від 0 до 12). Зображення повинно бути симетричним відносно центральної вертикалі екрана. Числа повинні відокремлюватися не менше ніж одним пропуском.

5.10. Значення функцій ax та logax виводяться на екран у два стовпці за значень x=h, 2h, 3h, … таким чином. Спочатку друкуються перші m значень і виводиться запитання, чи продовжувати друкування. Після відповіді користувача "так" друкуються наступні m значень та запитання тощо. Робота завершується після відповіді "ні". Написати програму читання значень a, h, m та описаного друкування значень "сторінками".

5.11. Значення функції f(x)=1-x+1/(x+1) обчислюються в точках x=0, h, 2h, 3h, … , mh, де 0<m<40, 0<h<0.2, та виводяться у вигляді таблиці. За m<20 таблиця має 2 стовпці зі значеннями x і f(x), за m³ 20 таблиця має 4 стовпці: у перших двох значення kh і f(kh) за k=0, 1, 2, ... , 19, в інших двохзначення kh і f(kh) за решти значень k=20, 21, … , m (за m<39 друга пара стовпців коротша за першу). Написати програму читання значень m і h, обчислення функції та формування таблиці.

5.3. Ланцюгові дроби та обчислення трансцендентних функцій

Обчислення математичних функцій sin, cos, exp, ln і arctan у бібліотеках стандартних підпрограм реалізується за допомогою ланцюгових, або неперервних, дробів. Нехай a1, a2, … і b0, b1, b2, … – числові послідовності, нескінченні або скінченні з останніми членами an і bn. Послідовність дробів

визначає неперервний (ланцюговий) дріб, нескінченний або скінченний із останнім знаменником bn. Він позначається у вигляді [ ], а також скорочено або .

У скінченного дробу з останнім знаменником zn=bn передостанній знаменник zn-1=bn-1+an/zn, перед-передостанній zn-2=bn-2+an-1/zn-1 тощо, z1=b1+a2/z2. Звідси очевидно, що послідовність zn, zn-1, zn-2, … , z1 разом із z0=b0+a1/z1 задається рекурентним співвідношенням zi=bi+ai+1/zi+1 , i=n-1, n-2, … , 0 і першим членом zn=bn. Припустимо, що відомі обчислення значень a1, a2, … , an і b0, b1, b2, … , bn. Тоді обчислення значення дробу s0 із застосуванням співвідношення можна задати у вигляді

обчислення значення bn; z:= bn;

for i:=n-1 downto 0 do

begin

обчислення значення bi;

обчислення значення ai+1;

z:= bi +ai+1/z

end

{значення z – шукане}

Приклад. Якщо ai=1 при i=1, … , n, bi=i при i=0, 1, … , n, то відповідний дріб

обчислюється так (значення n вважається заданим):

z:=n;

for i:= n-1 downto 0 do z:=i+1/zç

Математики установили, що деякі математичні функції дійсної змінної x можна подати у вигляді нескінченного ланцюгового дробу, елементи якого a1, a2, … і b0, b1, b2, … залежать від x, причому ця залежність, як буде очевидно з прикладів, дуже проста. Для наближеного обчислення значення нескінченного або скінченного ланцюгового дробу використовується поняття підхідного дробу. Нехай F=[] – ланцюговий дріб, скінченний або нескінченний. Дріб , де k£ n, називається k-м підхідним дробом для F. Послідовності P(k) і Q(k) задовольняють системі рекурентних співвідношень:

(**)

у якій P(-1)=1, Q(-1)=0, P(0)=b0, Q(0)=1. Доведення цього факту лишаємо вправою із застосування методу математичної індукції.

Нескінченний ланцюговий дріб називається збіжним, якщо існує скінченна межа послідовності підхідних дробів. Для основних математичних функцій було знайдено подання у вигляді збіжного ланцюгового дробу. За цим дробом із застосуванням рекурентних співвідношень (**) обчислюються значення підхідних дробів, поки, наприклад, модуль різниці між двома послідовними значеннями не стане менше деякого значення (10–6 або біля того). Останнє з них і приймається за наближене значення збіжного ланцюгового дробу.

Відзначимо, що для основних математичних функцій ця послідовність збігається дуже швидко. Як правило, за будь-яких допустимих значень x достатньо не більше десяти членів послідовності, щоб одержати значення функції з помилкою, не більшою 10–6.

Ось деякі функції та збіжні ланцюгові дроби для них (ми вважаємо, що читачу відомі допустимі значення x):

expx=[0; ];

expx=[ ];

lnx=[0; ];

tgx=[0; ];

arctgx=[0; ].

Задача

5.12. З використанням наведених тут ланцюгових дробів запрограмувати обчислення трансцендентних функцій (при допустимих значеннях аргументу). Порівняти одержані значення з тими значеннями, що повертають бібліотечні функції. Підрахувати кількість обчислених членів збіжної послідовності.

5.4. Мітки та переходи

Тихше, дівчино! Жінку прикрашає скромність. До чого ці стрибки?

І.Ільф, Є.Петров

У мові Паскаль будь-який оператор і кінець складеного оператора (слово end) можна відмітити, тобто ідентифікувати, додати йому індивідуальне ім'я. Це ім'я називається міткою. У авторській версії мови мітками могли бути цілі сталі від 1 до 9999, у мові Турбо Паскаль до них додано ідентифікатори. Мітка записується перед оператором або словом end через двокрапку, наприклад,

1 : money := 21;

mmm : money:=0;

finita : end.

Мітки, використовувані в тілі програми або підпрограми, повинні бути означені в її ж блоці. Означення міток має вигляд:

label список-міток-через-кому ;

наприклад, label 1, mmm, finita;.

Мітка, означена в блоці, повинна відмічати рівно один оператор у тілі цього блоку.

Мітки використовуються в операторах переходу, що мають вигляд

goto мітка

наприклад,

goto 1; if x>1000 then goto mmm.

Результатом виконання оператора переходу є те, що слідом за ним виконується оператор, відзначений цією міткою.

У випадку, коли відзначений кінець складеного оператора, можливі варіанти.

Якщо це не кінець тіла циклу і не кінець програми, то буде виконуватися наступний оператор.

Якщо відзначений кінець програми, то її виконання завершується.

Якщо відзначений кінець тіла циклу, то виконуються дії, які слідують за виконанням тіла (перевірка умови продовження while-циклу або неявна зміна параметра for-циклу).

Оператор переходу й відповідний відмічений оператор повинні бути записаними в тілі блоку (програми або підпрограми), де цю мітку означено. Іншими словами,

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

Ми не схильні популяризувати використання операторів переходу. Більше того, у свій час було доведено, що без них узагалі можна обійтися (достатньо умовних операторів і while-циклів). Проте є три випадки, коли указання переходу доречно й зручно:

  • на кінець підпрограми;
  • зсередини циклу на його кінець;
  • зсередини циклу на наступний за циклом оператор.

У Турбо Паскаль для указання таких переходів є спеціальні оператори, відповідно, exit, continue і break, тобто "вийти" (з підпрограми), "продовжувати" і "перервати" (виконання циклу). Розумно використовуючи ці оператори, можна обійтися без міток і goto. Насправді ці три оператори – виклики процедур. Їх імена не є службовими словами, але ми будемо виділяти їхнім жирним шрифтом. Приклади їх використання – у наступному підрозділі.

5.5. Читання послідовностей

"Почни спочатку й продовжуй, – сказав Король, – поки не дійдеш до кінця; тоді закінчуй"

Л.Керрол

Існує чимало задач, у процесі розв'язання яких читаються та обробляються послідовності значень невідомої заздалегідь довжини. До них віноситься практично все, що пов'язано з обробкою файлів – від завантаження машинної програми до друкування списків. У цьому параграфі ми розглянемо задачі, у яких для обробки вхідної послідовності незалежно від її довжини достатньо кількох змінних. На прикладі цих задач ми опишемо три способи завдання кінця послідовності і, відповідно, три види циклів читання й обробки даних.

1. Спочатку читається кількість значень n, n£ maxint, потім самі значення в кількості, яка визначається за n. Для опису читання зручно скористатися for-оператором.

Приклад 5.2. Многочлен, він же поліном pnxn+pn-1xn-1+ … +p1x+p0 задається послідовністю з n+1 коефіцієнтів pn,pn-1, … , p1, p0. Треба прочитати значення x, степінь і коефіцієнти полінома та обчислити його значення в точці x. Оскільки

pnxn+pn-1xn-1+ … +p1x+p0=(... (pnx+pn-1)x+ … +p1)x+p0,

то значення v полінома можна подати як значення останнього члена послідовності: v0=0, v1=v0× x+pn, v2=v1× x+pn-1, … , vn+1=vn× x+p0. Неважко переконатися, що вона задається рекурентним співвідношенням

vi=vi-1× x+pn+1-i, i=1, 2, … n+1,

причому перед його першим застосуванням треба знати лише перший коефіцієнт, перед другим – другий тощо. Тому цикл має складатися з читання чергового коефіцієнта й застосування співвідношення. У будь-який момент виконання програми треба знати лише один, останній прочитаний коефіцієнт, тому для збереження коефіцієнтів достатньо однієї змінної.

Отже, спочатку прочитаємо значення x, потім степінь полінома n, потім n+1 коефіцієнт, застосовуючи "по дорозі" рекурентне співвідношення:

V:=0;

writeln('задайте точку дійсної прямої :'); readln(x);

writeln('задайте цілий невід'ємний степінь полінома :'); readln(n);

for i:=1 to n+1 do

begin

writeln( 'задайте ', n+1-i, '-й коеф-т :'); readln(p);

V:=V*x+p

end;

{прочитано n+1 коефіцієнт; значення V – шукане}

Оформлення алгоритму у вигляді підпрограми залишаємо вправою. ç

2. Відомо особливе значення, поява якого в послідовності вхідних значень позначає її кінець і як вхідне не розглядається. Для опису читання зручно скористатися repeat-оператором, тому що треба прчитати не менше, ніж одне значення перед тим, як з'явиться ознака кінця.

Приклад 5.3. На контрольно-пропускному пункті митниці працює одна бригада інспекторів. Автомобілі прибувають, стають у чергу (якщо вона є) і проходять контроль у порядку прибуття. Для кожного автомобіля відома тривалість його контролю t: автомобіль покидає митницю через t одиниць часу після початку його контролю. Прибуття першого автомобіля задано відносно початкового моменту часу, а прибуття кожного наступного – відносно моменту прибуття попереднього. Отже, вхідними даними є пари цілих чисел x1, t1, x2, t2, … , причому xi³ 0, ti>0, крім останнього: t=0 позначає кінець послідовності вхідних даних. За вхідною послідовністю треба визначити послідовність моментів виїздів автомобілів із контрольно-пропускного пункту.

Перше наближення до розв'язання очевидно:

t0:=0; {особливе значення}

repeat

readln(x, t);

if t>t0 then обробити x, t і обчислити момент від'їзду y

until t=t0.

Припустимо, що в контролі автомобілів немає пауз: контроль наступного автомобіля, якщо він уже прибув, починається відразу після від'їзду попереднього. За x1 і t1 можна обчислити момент від'їзду y1: y1=x1+t1. Введемо поняття "момент початку контролю автомобіля" і позначимо його bi: bi=max{yi-1,xi}. Тоді yi=bi+ti. Звідси очевидним є уточнення фрази "обробити x, t і обчислити момент від'їзду y":

if y<x then b:=x {x – момент приїзду чергового автомобіля}

else b:=y; {y – момент від'їзду попереднього автомобіля}

y:=b+t; {тепер y – момент від'їзду чергового автомобіля}

Для першого автомобіля момент від'їзду попереднього відсутній, тому для нього повинно бути b:=x. Щоб не розглядати окремо випадки, перший або не перший автомобіль, будемо вважати початковий момент часом від'їзду "нульового" автомобіля. Тоді алгоритм набуває вигляду:

t0:=0; y:=0;

repeat

writeln('момент прибуття й тривалість – два невід'ємних цілих :');

readln(x,t);

if t>t0 then {обробити x, t і обчислити момент від'їзду y}

begin

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

y:=b+t;

writeln('час від'їзду : ', y)

end

until t=t0.

Цикл читання можна записати за допомогою одного старого програмістського трюку. Він полягає у використанні "нескінченного циклу" у сполученні з переходом за кінець циклу. Скористаємося оператором break мови Турбо Паскаль:

t0:=0; y:=0;

while true do

begin

readln(x, t);

{!!! } if t=t0 then break; {ознака кінця: вихід із циклу }

{замість break можливо exit – вихід із (під)програми}

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

y:=b+t;

writeln('час від'їзду : ', y)

endç

Більш загальним випадком завдання кінця послідовності є повторення найпершого значення послідовності. Схема розв'язання залишається тією самою, тільки спочатку "особливе значення" запам'ятовується в результаті читання, а не присвоювання.

3. Кінець послідовності значень при читанні з клавіатури задається не їх кількістю і не особливими значеннями – замість набирания чергової сталої натискаються спеціальні клавіші. В усіх системах програмування мовою Паскаль є функція з ім'ям eof. Для читання послідовності значень із клавіатури її виклик (без аргументу або з аргументом input) записується, як правило, в умові продовження while-циклу такого вигляду:

writeln('задайте значення :');

while not eof do

begin

readln(v); використання та обробка значення v;

writeln('задайте значення:');

end

Після друкування запрошення "задайте значення" виконується виклик функції eof, під час чого комп'ютер очікує натискання на клавіші. Якщо натиснути "особливу клавішу Ctrl" і, тримаючи її, натиснути клавішу "Z", то з виконання виклику функції eof повертається true. У цьому випадку умова продовження noteof хибна, і виконання циклу завершується. Якщо ж натиснути будь-яку іншу клавішу, наприклад, почати набирати сталу, то з виклику eof повертається значення false, і починається виконання тіла циклу. При виконанні виклику readln змінній v "присвоюється з зовнішнього світу", тобто від клавіатури, відповідне значення. Далі за програмою воно обробляється, потім з'являється запрошення, потім при обчисленні умови продовження викликається eof тощо.

Якщо замість набирання сталої натиснути на Ctrl-Z, то значення змінної v не читається, і вона зберігає своє старе значення. Якщо задати кінець послідовності відразу, то змінна v залишиться з невизначеним значенням. Тому радимо ініціалізувати змінні, значення яких читаються в циклі. Втім, варто ініціалізувати всі змінні, значення яких мають бути прочитані.

Відзначимо, що з повторних викликів функції eof, що виконуються вже після натискання на Ctrl-Z, буде повертатися значення true.

Приклад 5.4. Напишемо програму, за якою комп'ютер працює "майже як найпростіший калькулятор".

"Найпростіший калькулятор" працює так. Вхідні дані для нього утворюють послідовність вигляду

стала знак стала знак … стала,

де сталі задають дійсні числа, а знаки – операції +, -, *, /. Результат застосування чергової операції виводиться на екран і стає першим операндом наступної операції (якщо вона буде задана). Наприклад, результатом читання послідовності 1+2*4 буде 12, а не звичні 9 тому, що обчислюється 1+2=3 і потім 3*4=12.

Алгоритм роботи "найпростішого калькулятора" дуже простий:

Спочатку читається перше число і стає поточним результатом. Далі циклічно читаються знак операції і нове число і результат застосування операції до поточного результату і нового числа стає новим поточним результатом.

Ми поки що не знаємо, як прочитати й подати символи "+", "-", "*", "/". Замість знаків "+", "-", "*", "/" будемо вживати цілі сталі 1, 2, 3, 4 відповідно. Нехай кожна стала набирається з нового рядка на клавіатурі й ознакою закінчення є натискання на Ctrl-Z замість уведення чергового знака операції.

Означимо змінні first і second для зберігання першого й другого операндів чергової операції. Позначення операції читається в змінну signop і після читання другого операнда застосовується операція. Результат операції записується на місце її першого операнда.

Найперше число не виводиться, тому що воно з'являється на екрані при набирании першої сталої. Одержуємо програму simplecalculator ("простий калькулятор"):

program simplecalculator (input, output);

var first, second : real;

signop : integer;

begin

writeln('Операції +, -, *, / позначаються числами 1, 2, 3, 4');

writeln('Задайте дійсне число : '); readln(first);

writeln('Задайте знак операції (1, 2, 3, 4) : ');

while not eof do

begin

readln(signop);

{Увага 1! }

writeln('Задайте дійсне число : '); readln(second);

if signop=1 then first:=first+second else

if signop=2 then first:=first-second else

if signop=3 then first:=first*second else

first:=first/second; {Увага 2! }

writeln('результат : ', first);

writeln('Задайте знак операції (1, 2, 3, 4) : ');

end

end.

Недоліком цієї програми є те, що в ній не вказано обробку можливих помилкових дій того, хто нею користується (користувача). Наприклад, якщо задати сталу 5, 6 тощо як знак операції, то буде виконуватися ділення. Навряд чи таке рішення має сенс. Крім того, якщо після знака ділення задати другий операнд 0, то при виконанні first/second відбувається ділення на 0, результат якого комп'ютеру "невідомий", і програма аварійно завершиться.

Доповнимо програму, щоб її осмислене виконання продовжувалося й після описаних помилок користувача. Такі доповнення називаються "захистом від дурня", а програма з ними – "дурнестійкою".

По-перше, якщо вказано недопустимий знак операції, повернемо користувача до задання нового знака операції, не примушуючи задавати другий операнд. Для цього скористаємося оператором continue (продовжувати виконання циклу з обчислення умови продовження).

По-друге, перед діленням варто перевірити, чи не рівний дільник нулю.

Така перевірка не завадить у будь-який програмі та в будь-якому її місці, де вказано ділення. У даному випадку ділення на 0 можна запобігти, також повернувши користувача до повторного задання знака операції і нового операнда (зрозуміло, можливі й інші рішення).

Отже, замість рядка з коментарем {Увага 1! } напишемо:

if (signop>4) or (signop<1) then

begin

writeln('-----Недопустимий знак операції-----');

writeln('Задайте знак операції (1, 2, 3, 4) : ');

continue

end;

Замість рядка з коментарем {Увага 2!}, що задає ділення, помістимо оператор:

if second<>0 then first:=first/second

else begin

writeln('-----Спроба ділення на 0: ігнорована-----');

writeln('Задайте знак операції (1, 2, 3, 4) : ');

continue

end;

Проте навіть із доповненнями програма "не захищена" від набирания символів, що не утворюють числову сталу. Для такого захисту потрібні засоби, які ми почнемо розглядати в розд. 14.ç

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

Приклад 5.5. Відрізок [a; b] прямої задається координатами його кінців, тобто парою чисел a, b, де a£ b. Перетином двох відрізків є або відрізок, або порожня множина точок, наприклад, [1;3]Ç [2;4]=[2;3], [1;2]Ç [3;4]=Æ , [1;2]Ç [2;3]=[2;2].

Треба прочитати послідовність пар чисел, що задають відрізки, і знайти їх перетин.

Припустимо, кінець послідовності відрізків задається за допомогою "Ctrl-Z". Проте немає сенсу продовжувати читання відрізків після того, як перетин уже прочитаних став порожнім. У цьому випадку треба відразу видати відповідь і закінчити виконання програми. Припустимо, що можливо "неправильне" задання відрізків у вигляді пари чисел a, b, де a>b. У цьому випадку b і a міняються місцями.

Для збереження поточного перетину означимо змінні lb і hb (скорочення від "low bound" і "high bound" – нижня й верхня межа). Cпочатку відрізків немає, і перетин порожній – виразимо це ініціалізацією lb=1, hb=0, тобто відрізком з неможливими межами. Перший відрізок має стати значенням [lb;hb]. Потім у циклі вводяться інші відрізки та обчислюється перетин:

lb=1; hb=0;

writeln('задайте дійсні кінці відрізка:'); readln(a, b);

if a>b then

begin lb := b; hb := a end

else begin lb:=a; hb:=b end

{прочитано перший відрізок}

{далі читаються інші та обчислюється їх перетин}

while not eof do

begin

writeln('задайте дійсні кінці відрізка:'); readln(a, b);

if a>b then

begin t:=a; a:=b; b:=t end;

if a>lb then lb:=a;

if b<hb then hb:=b;

if lb>hb then break

end;

{введення закінчено або перетин порожній}

if lb>hb then

writeln('перетин порожній')

else writeln('перетин: [', lb, ';', hb, ']')

Оформлення програми залишаємо вправою.ç

Задачі

5.13.* Змінити програму з прикладу 5.3 так, щоб при спробі ділення на 0 видавалося відповідне повідомлення і виконання програми завершувалося (але не аварійно!).

5.14.* Написати програму розв'язання задачі з прикладу 5.4 без використання операторів break, continue, exit і goto.

5.15. Переписати програму з прикладу 5.5 так, щоб за нею була можливою обробка порожньої вхідної послідовності відрізків.

5.16. Плавильна піч має прямокутне вікно. Конвеєр подає в нього зливки, що мають форму прямого прямокутного паралелепіпеда. Зливок може пройти у вікно, якщо сторони однієї з його граней менше відповідних сторін вікна печі. Перед вікном стоїть вимірювальний пристрій; від нього розміри зливків надходять у пристрій обробки, що або повертає зливки так, щоб вони пройшли у вікно, або скидає їх, якщо це неможливо. Повороти зливка можливі на 90° навколо будь-якої зі сторін його нижньої грані. Робота всієї цієї системи закінчується, якщо закінчилися зливки або сумарний обсяг тих зливків, що вже в печі, разом із новим перевищує заданий обсяг печі.

Написати програму моделювання описаної системи за умови, що розміри зливків задаються з клавіатури. У процесі моделювання для кожного зливка вказується або необхідна послідовність поворотів, або скидання з конвеєра.

Описати циклічне читання у спосіб 2 або 3. При використанні способу 2 можна вважати, що кінець послідовності зливків задається трійкою нульових розмірів. Якщо з трьох чисел, що задають черговий зливок, одне або два нулі, то можна розглядати це як помилку виміру й ігнорувати дані.

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

Шостий розділ