Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Posibnyk_C.doc
Скачиваний:
1
Добавлен:
01.03.2025
Размер:
1.61 Mб
Скачать

4 Елементарні алгоритми

4.1 Лiнiйна програма

Лiнiйною називається програма, яка виконується в тiй послiдовностi, в якiй записанi її оператори, вона не має ні розгалужень, ні переходiв, ні повторень. Найпростіша лiнiйна програма може складатися з директив препроцесору, оголошень даних, операторiв-виразiв i функцiй вводу-виводу. В чистому вигляді лінійну програму застосовують порівняно рідко, бо тоді ефективність використання комп’ютера найнижча.

Покажемо програму цього типу на прикладі. Нехай потрібно обчислити висоту зубців ht шарошкового долота типу В-93Т після відпрацювання ним t одиниць часу. Зменшення висоти зубців долота в часі обчислюється за формулою [3], де k = 4,14, а ω = -1,94.

Приступаючи до побудови алгоритму, простежимо хід виконання роботи. Зрозуміло, що всі змінні повинні бути ініціалізовані (задані в програмі) перш, ніж будуть використані для обчислень. Змінним k і ω – присвоїмо значення відповідних заданих констант. Конкретне значення t не задано, тому забезпечимо його ввід з клавіатури під час виконання програми. Після обчислення ht його значення буде занесено у відповідну комірку пам’яті, для його візуального спостереження передбачимо вивід результату на екран. Виведемо й значення змінної t для візуальної перевірки правильності її вводу.

Графічний алгоритм програми показано на рисунку 4.1. Вiн має лiнiйний тип i складається з шести графічних символів (блоків), першим з яких є “Початок”, а останнiм “Кiнець”. Знаки, якi розташовані мiж ними, вiдповiдають послiдовностi рiшення задачi: присвоєння числових значень змiнним k i ω (блок 2), ввід значення змінної t (блок 3), обчислення заданого виразу i присвоєння його значення змiннiй ht (блок 4) та її вивiд на екран (блок 5).

Зауважимо, що лінії рисунка 4.1 не закінчуються стрілками, які б визначали напрямок виконання роботи. Якщо операції виконуються зверху вниз або зліва направо, то тоді стрілки не ставляться, бо це вважається за природний хід роботи. Зате, якщо робота виконується в напрямку зправа наліво або знизу вверх, то тоді стрілки обов’язкові. В цьому алгоритмі всі дiї виконуються послiдовно зверху вниз.

Форма і розміри символів графічного алгоритму стандартизовані, за формою значка можна визначити тип дії, яку він символізує. Наприклад, початок і кінець позначається двома паралельними лініями, заокругленнями з боків, операції вводу-виводу – паралелограмом та ін.

Написи, які визначають вид роботи в кожному символі алгоритму, повинні бути чіткими й лаконічними, проте зрозумілими. Всі символи повинні мати однакові розміри, тому не варто застосовувати надто довгі написи. Якщо напис не поміщається в символі, то його слід скоротити, а в тексті або в підрисунковому написі дати відповідні пояснення, як це, наприклад, зроблено під рисунком 4.1 для f(t).

Перед складанням програми замінимо імена змінних заданої вище формули на ідентифікатори, результати заміни показані в таблиці 4.1. Зауважимо, що мова С не має ні надрядкових, ні підрядкових індексів, тому ідентифікатор ht містить дві букви однакових розмірів. Ім’я змінної ω – грецька буква, яка не належить до алфавіту мови С, тому вона позначена ідентифікатором omega.

Таблиця 4.1 Ідентифікатори змінних лінійної програми

Ім’я змінної

у формулі

Ідентифікатор

у програмі

Найменування

ht

ht

висота зубців долота

t

t

кількість годин роботи долота

k = 4,14

k

постійний коєфіцієнт

ω = -1,94

omega

постійний коєфіцієнт

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

Приклад 4.1.1 – Лінійна програма

#include<math.h>

#include<stdio.h>

int main(void)

{

const float k=4.14, omega=-1.94;

float ht, t;

scanf("%f", &t);

ht=k*exp(omega*pow(t, 2));

printf("%f%f", ht, t);

return(0);

}

Її можна умовно подiлити на такі двi частини: директиви препроцесору i головна функцiя. Тут є двi директиви. В першiй мiститься вказiвка компiлятору пiд’єднати до програми файл math.h, в якому знаходяться описи математичних функцiй, зокрема, pow() – пiднесення до степеня та exp() – експоненти. В другій – вимога під’єднати до програми файл stdio.h з функціями вводу-виводу. В цьому файлі містяться стандартні функції scanf() і printf(), які й використовуються в нашій програмі, та інші.

Головна функцiя main() являє собою блок, взятий у фiгурнi дужки. Вона починається з оголошення іменованих констант k i omega за допомогою модифікатора const. Він не обов’язковий, але його застосування дозволяє програмісту страхуватися від помилок, наприклад, від випадкової спроби змінити значення констант. Оскільки ці константи є дійсними числами, для них відводиться по одній комірці пам’яті типу float. Вони ініціалізовані під час оголошення – константа k числом 4.14, а omega – -1.94.

Змінні ht i t – теж дійсні числа, тому для них теж відводиться по одній комірці пам’яті типу float за допомогою оголошення, яке міститься в наступному рядку. Ці числа порівняно короткі, тому для їх запам’ятовування достатньо типу float.

У наступному рядку програми маємо звернення до функції scanf("%f", &t), призначеної для вводу значення змінної t під час виконання програми. Функція scanf() має два види параметрів, розділені комою. Першим є перелік специфікаторів форматів (у нашому випадку – f, бо змінна t має тип float), яким передують символи %. У прикладі цей перелік обрамлений подвійними лапками. Далі через кому дається перелік адрес змінних, які підлягають введенню, в нашому прикладі це адреса змінної t. Під час виконання програми функція scanf() її зупиняє, на екрані появляється курсор і комп’ютер очікує вводу числа.

Наступним оператором є оператор-вираз, призначений для обчислення i занесення в оперативну пам’ять значення змiнної ht. Він складається зi знака присвоєння, злiва вiд нього маємо iм’я змiнної ht, яка одержує значення виразу, що розмiщений зправа. Права частина цього оператора містить знаки операції множення * (зірочка) і дві функції: експоненти exp() та піднесення до степеня pow(). Як видно з таблиці 2.5, мова С не має операції піднесення до степеня, натомість має відповідну функцію. Функція pow() має два параметри, записані через кому: основу степеня дійсного типу і показник – неіменовану константу цілого типу. Деякі компілятори дозволяють мати показник дісного типу та навіть ідентифікатор змінної або іменованої константи.

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

  • знак % (процент), який означає, що наступним після нього символом є специфiкатор формату (для змінної ht);

  • специфiкатор формату f (типу float). Зауважимо, що специфікатори форматів функцій scanf() i printf() однакові;

  • ще один знак %, наступним після нього символом теж є специфiкатор формату (для змінної t);

  • специфiкатор формату f (типу float).

Специфікаторами форматів функцій printf() та scanf() можуть бути також такі, як d або i – для цілих чисел типу int, c – для однієї літери типу char, s – для рядка (масиву) символів типу char та інші. Завдяки наявності специфікаторів форматів ці функції називаються функціями форматованого вводу-виводу.

До параметрів другого виду належать iмена змiнних ht та t, значення яких будуть виведені.

Кожний оператор програми мовою С закінчується символом ; – крапка з комою. Цей символ обов’язковий, на відміну від інших мов програмування, таких, наприклад, як Pascal, де крапка з комою служить не для закінчення оператора, а для розділювання операторів між собою.

Перед запуском готової програми слід надати їй ім’я, назвемо нашу програму doloto та запишемо її в довготривалу пам’ять.

Запустимо програму на виконання. Програма почне виконуватися, але зупиниться, очікуючи на ввід числа t. Нехай t = 0,8, введемо це число, воно матиме вигляд: 0.8. Нагадаємо, що для розділення цілої і дробової частин дійсного числа мовою С служить крапка.

Результати виконання програми побачимо в такому виглядi:

0.8

1.1961350.800000

Вони правильні, але їх важко читати. По-перше, тут одне число набігає на друге, звичайно, що їх варто якось розділити. Це можна зробити, встановивши потрібну кількість пробілів між специфікаторами форматів функції printf(), наприклад, так: %f %f. Маємо й інший спосіб: застосувати символ \t – горизонтальної табуляції. По-друге, варто вставити якісь слова, текстові константи перед специфікаторами форматів, наприклад, ht= та t=, для пояснення результатів. По-третє, варто обмежити кількість виведених розрядів чисел, для цього можна задавати між символами % та f загальну кількість символів виводу, включаючи десяткову крапку, та число цифр після крапки. Нижче подано варіант програми, де для всього числа t відводиться три цифри, а після крапки – одна; довжина числа ht дорівнює 5, а дробової частини ­ – 3 символи. Задавати довжину чисел або слів особливо вигідно тоді, коли виведені значення необхідно представити як таблицю.

Також хорошим тоном програмування є переведення курсора на новий рядок після кожного виводу за допомогою символа \n, доданого до параметрів функції printf().

З метою перегляду результату рекомендується зупиняти програму під час її виконання в потрібному місці за допомогою якоїсь функції, наприклад, getch(), яка теж знаходиться в файлі stdio.h. Ця функція служить для вводу з клавіатури одного символа, але можна розширити область її застосування. Зупинка програми може бути особливо корисною на стадії її налагодження, пошуку помилки в алгоритмі, коли необхідно проконтролювати хід виконання всіх або частини операторів.

Маємо й інші недоліки. Для того, щоб користувач, який експлуатує програму, знав причину її зупинки під час роботи функції scanf(), варто використати функцію printf("Введіть значення змінної t: "); яка видасть на екран відповідне запрошення. Цей прийом буде особливо корисним, якщо матимемо багато вводів різних змінних у різних місцях програми.

У даному випадку застосування функції pow() є невигідним, тут простіше використати операцію множення (вираз t*t), бо вона виконується швидше за функцію піднесення до степеня. Крім того, це дозволяє скоротити текст програми, нижче подано такий її варіант. Звичайно, що тоді не треба було б під’єднувати й файл math.h, але тут він залишений, бо маємо ще функцію exp().

У нашому прикладі вигідніше використовувати не модифікатор const для констант, а директиву #define, тоді програма виконається швидше і не потребуватиме окремих комірок пам’яті для запам’ятовування констант.

Як правило, кожна програма містить пояснення, коментарі. Їх можна встановлювати в будь-якому місці: між операторами, на початку або наприкінці програми, вони обрамляються символами /* та */. Коментар ігнорується компілятором і служить лише для підвищення “читабельності” тексту програми. Не слід, однак, захаращувати текст зайвими коментарями. У зміненому варіанті програми (прикдад 4.1.2) додано коментар: “ Обчислення висоти зубців долота ”.

Приклад 4.1.2 – Лінійна програма, покращений варіант

#include<math.h> /* Обчислення висоти зубців долота */

#include<stdio.h>

#define k 4.14

#define omega -1.94

int main(void)

{

float ht, t;

printf("Введіть значення змінної t: "); scanf("%f", &t);

ht=k*exp(omega*t*t);

printf("ht = %5.3f\t t = %3.1f\n", ht, t);

getch();

return(0);

}

Результати, видані програмою прикладу 4.1.2, будуть такими:

Введіть значення змінної t: 0.8

ht = 1.196 t = 0.8

За своїм виглядом вони можуть нас задовільнити, в них враховані усі зауваження, висловлені в ході розгляду недоліків попередньої програми. Додамо лише, що одна й та ж програма, складена різними програмістами, видасть по-різному оформлені результати.

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

Запитання для самоперевiрки

  1. Що таке препроцесор?

  2. Що таке директива препроцесору?

  3. Якою мовою пишуться директиви препроцесору?

  4. Яка програма називається лiнiйною?

  5. Задано змінні: int i=010, c=97; Чому після виконання операторів i+=2; c++; printf("і=%i c=%c", i, c); одержимо: i=10 та c=b?

  6. Задано змінну: char i='0'; Чому після виконання операторів: i++; printf("i=%i", i); одержимо: i= 49?

  7. Задано змінні: int i=0, a=0xa; Чому в результаті виклику функції printf("i=%d", i+=a++); одержимо і = 10?

  8. Чим відрiзняються між собою оператори: s=s+1; s+=1; s++;?

  9. Назвiть специфiкатори форматiв функцiї printf().

  10. Назвіть два можливi способи занесення в комiрку x значення 3,27.

  11. Чому у програмi прикладу змiнна t має тип float? Який об’єм пам’ятi вона займає? Хто призначає комiрку пам’ятi для цiєї змiнної i чи можна визначити її фiзичну адресу?

  12. Для чого у програмі прикладу 4.1.1 служить функцiя pow()? Назвiть iншi можливостi мови С.

  13. Яке число матимемо в комiрцi float е=2.71828182836405;?

  14. Що таке іменовані константи і в яких випадках вони вигідніші від неіменованих?

  15. Який клас пам’яті матиме кожна змінна у програмі прикладу?

  16. Який тип буде мати результат виконання виразу, до якого входять змінні різних типів?

  17. Що побачимо на екрані після виклику функції printf("%f", 2+2)?

4.2 Розгалуження

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

В графічному алгоритмі розгалуження позначається за допомогою символа, який має вигляд установленого горизонтально ромба. Всередині ромба записується вираз, а два виходи супроводжуються написами, як правило, це “так” і “ні” або цілі числа: 0 – неправда та 1 – правда.

Для програмування розгалужень мова С має такi засоби:

  • умовний оператор: if(вираз)оператор; Вiн працює так: якщо значення виразу не дорiвнює нулю, то виконується оператор, який знаходиться в складі оператора if, iнакше – наступний оператор програми. Тут і далі під поняттям оператор будемо розуміти один оператор або блок – декілька операторів, обрамлених фігурними дужками;

  • альтернативний оператор: if(вираз)оператор1; else оператор2; Його виконання буде таким: якщо значення виразу не дорiвнює нулю, то далi виконується оператор1, iнакше – оператор2;

  • оператор безумовного переходу: goto мiтка; При його виконаннi вiдбувається безумовний перехiд на оператор з вiдповiдною мiткою. Мiтка являє собою iдентифiкатор, він ставиться перед оператором, на який відбувається перехід, і відділяється від нього двокрапкою. Зауважимо, що широке застосування цього оператора не вважається хорошим тоном програмування. Існують мови програмування, наприклад, FoxPro, які взагалі не мають подібного оператора;

  • оператор припинення: break; При його виконанні відбувається вихід за межi блока;

  • оператор-перемикач: switch(вираз)

{оголошення локальних змінних;

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

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

... ...

default: операторих;

}

Тiло перемикача являє собою блок, він може мати власнi оголошення даних. Його виконання починається з обчислення виразу, який може мати тiльки тип int. (Нагадаємо, що лiтернi константи типу char можуть сприйматися як коди лiтер, тому вони теж мають тип int). Виконання операторiв починається вiд того мiсця, де константа спiвпадає зi значенням виразу, до слова default або, якщо його немає, то до кiнця блока, незважаючи на розташовані нижче оператори з iншими константами-мiтками. Операторих виконуються, якщо значення виразу не спiвпадає нi з однiєю константою.

Продемонструємо застосування оператора типу if на прикладі. Нехай для заданого діаметра долота d (мм) необхідно знайти діаметр обважнених бурильних труб D, використавши таку залежність [3]:

На рисунку 4.2 показана числова вісь, на якій відображені області існування функції D.

З цього рисунка випливає, що обчислювальний процес розгалужується на дві гілки в одному вузлі (при d = 295,3). Звернемо увагу на те, що дві умови, встановлені в постановці задачі, по суті однакові, тому маємо лише один вузол і дві гілки.

Графічний алгоритм для даної задачі подано на рисунку 4.3. Аналізуючи його, можна зразу сказати, що для його програмування слід застосувати умовний альтернативний оператор. Нижче в прикладі 4.2.1 подано варіант програми, яка реалізовує цей алгоритм. У ній позначено: D = d1, d = d.

Приклад 4.2.1 – Розгалужена програма

#include<stdio.h> /* Розгалуження */

int main(void)

{

float d, d1;

printf("Введіть діаметр долота: "); scanf("%f", &d);

if(d>295.3)d1=0.7*d; else d1=0.8*d;

printf("D=%6.1f\td=%6.1f\n", d1, d);

getch();

return(0);

}

Випробуємо цю програму на всіх діапазонах існування функції D, для цього запустимо її на виконання два рази: при d=320 (тобто при d>295,3) і при d=190,5 (d≤295,3). Отримаємо такі результати:

Введіть діаметр долота: 320

D= 224.0 d= 320.0

Введіть діаметр долота: 190.5

D=152.4 d=190.5

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

Програму прикладу 4.2.1 можна спростити, застосувавши тренарну операцію, замість оператора if запишемо вираз: d1=d>295.3?0.7*d:0.8*d. Результати виконання програми будуть такими ж. Можна також внести цю тренарну операцію в параметри функції printf(). Тодi й змінна d1 буде зайвою, а програма набуде такого вигляду (приклад 4.2.2):

Приклад 4.2.2 – Застосування тренарної операції

#include<stdio.h> /* Розгалуження, варіант */

int main(void)

{

float d;

printf("Введіть діаметр долота: "); scanf("%f", &d);

printf("D=%6.1f\td=%6.1f\n", d>295.3?0.7*d:0.8*d, d);

getch();

return(0);

}

Наведемо ще один приклад. Нехай потрібно обчислити коефіцієнт λ гідравлічного опору нафтопровідного колектора нафтопроводу залежно від числа Рейнольдса за однією з таких формул [7]:

при Re≤2320 (формула Стокса);

при 2320 < Re ≤ 10 (формула Блазіуса);

при 10 < Re ≤ 500 (формула Альтшуля);

при Re > 500 (формула Шифринсона),

де Re – число Рейнольдса; Δ – шорсткість труб, Δ = 0,15 мм; d – діаметр колектора, d = 400 мм.

Число Рейнольдса залежить від технічних характеристик колектора та швидкості протікання рідини, воно може бути лише додатнім. Покажемо області існування функції λ на числовій осі Re (рисунок 4.4). З метою спрощення рисунка скоротимо вигляд формул, за якими обчислюється λ, зашифруємо їх першими буквами авторів відповідно С, Б, А і Ш.

Аналізуючи рисунок 4.4 з метою побудови графічного алгоритму, бачимо, що на числовій осі Re знаходяться 4 вузли, в яких мають відбуватися розгалуження. Відповідно до них будемо мати 5 гілок. Зрозуміло, що для забезпечення “читабельності” алгоритму слід обходити вузли осі Re та ставити умови в якомусь порядку. Побудуємо графічний алгоритм для цієї задачі, він показаний на рисунку 4.5, де вузли розглядаються зліва направо. Для скорочення запису умов зашифруємо їх так же, як і на числовій осі, але додамо слово “Умова”.

Для зображення розгалужень в алгоритмі використані спецiальні знаки (блоки 4, 6, 8,10), які мають вигляд поверненого горизонтально ромба. Всерединi кожного знака записана в зашифрованому вигляді умова, в залежностi вiд якої вибирається потрібна гiлка подальшого виконання алгоритму. Гілки підписані словами “Так” або “Ні”, які означають відповідно задовільнення або незадовільнення умов.

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

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

Тут можна запропонувати такий прийом: пристосувати графічний алгоритм до вимог програми. Для цього вишикуємо всі символи алгоритму в один ряд, не змінюючи зв’язків між ними, тобто всі лінії повинні потягнутися за символами. В нашому прикладі всі блоки можна залишити на місці, лише блок за номером 13 помістимо (уявно) між 12-тим і 14-тим. Тоді оператори програми можна буде написати в тій же послідовності, що й відповідні їм символи графічного алгоритму, це спростить процес її побудови.

Вищенаведені формули для обчислення величини λ містять операцію добування кореня 4-го степеня. Їх реалізацію в програмі можна виконати за допомогою раніше вже розгляненої в розділі 4.1 функції pow() з показником степеня 1/4 або дворазового кореня квадратного. Використаємо в програмі функцію sqrt() – корінь квадратний, тому за допомогою директиви препроцесору #include підключимо файл math.h, де вона описана.

Виконаємо ідентифікацію змінних, діаметр колектора позначимо буквою латини d, а число Рейнольдса – re. Оскільки мова С не має букв грецького алфавіту, назвемо змінні λ і Δ латиною відповідно lambda i delta. Іменованим константам d i delta надамо відповідні значення за допомогою директив #define. Значення константи d запишемо як дійсне число, тобто 400.0, це попередить спробу компілятора приводити вирази дійсного типу, в яких вона використовується, до цілого типу. Змінним lambda і re надамо тип float, у даній задачі він задовільний.

Ввід змiнної re забезпечимо за допомогою функцiї scanf(), а вивід результату – printf(), тому за допомогою директиви препроцесору #include підключимо файл stdio.h.

Ці розмірковування реалізовані в програмі прикладу 4.2.3.

Приклад 4.2.3 – Програма з декількома розгалуженнями

#include<stdio.h> /* Варіант goto */

#include<math.h>

#define d 400.0

#define delta 0.15

int main(void)

{

float lambda, re;

printf("Введіть число Рейнольдса: ");scanf("%f", &re);

if(re>0 && re<=2320){lambda=64/re; goto a;}

if(re>2320 && re<=10*d/delta){lambda=0.3164/sqrt(sqrt(re)); goto a;}

if(re>10*d/delta && re<=500*d/delta)

{lambda=0.11*sqrt(sqrt(delta/d+68/re)); goto a;}

if(re>500*d/delta){lambda=0.11*sqrt(sqrt(delta/d)); goto a;}

printf("Функція не існує, Re= %7.0f\n", re); goto b;

a:printf(" Re=%7.0f Lambda= %5.3f\n", re, lambda);

b:getch();

return(0);

}

Цю програму слід перевірити на правильність не лише обчислення виразів, але й логіки переходів. Випробуємо її на всіх діапазонах, які показані на рисунку 4.4, результати будуть такими:

Введіть число Рейнольдса: 0

Функція не існує, Re= 0

Введіть число Рейнольдса: 1001

Re= 1001 Lambda=0.064

Введіть число Рейнольдса: 2500

Re= 2500 Lambda=0.045

Введіть число Рейнольдса: 1e5

Re= 100000 Lambda=0.020

Введіть число Рейнольдса: 1.5e6

Re=1500000 Lambda=0.015

Виконавши паралельні обчислення вручну, переконаємося в тому, що вони правильні.

Попри працездатність програма прикладу 4.2.3 має суттєві недоліки. По-перше, дублюються логічні вирази, наприклад, у виразі першого оператора if (при обчислені lambda = С) ставиться умова re<=2320, а в другому – re>2320, що по суті одне й те ж. Подібні повторення виразу мають також решта операторів if. Це сталося внаслідок того, що вирази операторів if не по порядку розглядають області існування функції, показані на рисунку 4.4. Першою областю, яка повинна бути розглянена і відібрана (виключена з розгляду в наступних операторах if), повинна бути крайня: ліва або права, в нашому випадку – ліва, де re<=0.

По-друге, вона має багато операторів goto. З них перші чотири забезпечують перехід на мітку a:, встановлену перед функцією виводу re i lambda. П’ятий – здійснює перехід на мітку b:, тобто на кінець програми. (З метою підвищення “читабельності” програми в графічний алгоритм варто вносити всі мітки, що й показано на рисунку 4.5).

Перенасиченість програми мітками та операторами goto, як було вже сказано вище, не є хорошим тоном програмування. Одним із способів зменшення їх числа є застосування альтернативного умовного оператора і безперечно, що він буде використаний, але спочатку розглянемо сам графічний алгоритм. Аналізуючи його, бачимо, що він має два символи, де збігаються гілки розгалужень: 13-й (мітка a:) і 14-й (мітка b:). Це є ознакою того, що алгоритм складається з двох розгалужень, вкладених одне в друге, тобто маємо зовнішнє розгалуження або першого ступеня вкладеності і внутрішнє – другого. Зовнішнє розгалуження має лише дві гілки, з яких ліва служить для виводу повідомлення про те, що функція не існує, а права містить всі решта розгалужень (другого ступеня вкладеності) і вивід значень λ та Re. Нижче у прикладі 4.2.4 подано змінений варіант програми, де ці зауваження враховані.

Приклад 4.2.4 – Вкладені розгалуження

#include<stdio.h> /* Варіант else */

#include<math.h>

#define d 400.0

#define delta 0.15

int main(void)

{

float lambda, re;

printf("Введіть число Рейнольдса: "); scanf("%f", &re);

if(re<=0)printf("Функція не існує, Re= %7.0f\n", re);

else{if(re<=2320) lambda=64/re;

else if(re<=10*d/delta) lambda=0.3164/sqrt(sqrt(re));

else if(re<=500*d/delta)lambda=0.11*sqrt(sqrt(delta/d+68/re));

else lambda=0.11*sqrt(sqrt(delta/d));

printf("Re= %7.0f Lambda= %5.3f\n", re, lambda);

}

getch();

return(0);

}

Цей приклад показує чому у програмі не варто використовувати оператор goto. Зрозуміло, що графічний алгоритм обчислювального процесу мусить правильно відображати його хід. Його не можна ніяк змінити, хіба що інакше розкласти символи (не змінюючи суть алгоритму). Проте, якщо на стадії програмування застосовується спрощений підхід, як у прикладі 4.2.3, або, хоча й використовується альтернативний оператор if-else подібно до програми прикладу 4.2.4, але не враховується вкладеність розгалужень, то матимемо небажані безумовні переходи.

Покажемо варiант розгалуженого алгоритму з перемикачем switch на розгляненому вже вище прикладі обчислення коефіцієнта λ.

default:

1:

default:

6:

5:

3:

8

9

Кінець

10

Рисунок 4.6 – Розгалужений алгоритм з перемикачем, тут

N = Re<=0;

H = (Re ≤ 2320) + 2(Re ≤ 10 ) + 3(Re ≤ 500 ).

Нагадаємо, що константами-мiтками перемикача можуть бути лише цiлi числа або букви, тому як вираз не можна використовувати багато умов, хіба що одну з виходами 0 або 1. Для багатьох умов можна застосувати такий прийом: перетворити їх так, щоб результатом обчислення були різні цілі числа. Скористаємося тим, що при задовільненні умови значення логічного виразу дорівнює одиниці, а при незадовільненні – нулю. Якщо помножимо різні умови на різні цілі числа, то при задовільненні умов отримаємо не одиниці, а ті числа. При незадовільненні умов отримаємо нулі. Для різних областей існування функції λ сума цих чисел буде різною.

На рисунку 4.6 показано графічний розгалужений алгоритм з перемикачем. Оскільки, як уже було вище сказано, маємо два вкладені розгалуження, то в алгоритмі застосовуються два вкладені один у другий відповідні їм перемикачі. Умова-вираз перемикача записується в кружок, від якого відходять гілки, які визначають напрямок подальшого ходу обчислювального процесу відповідно до умови. З метою скорочення виразів вони позначені для зовнішнього розгалуження за умовою Re<=0 буквою N, а для внутрішнього, яке має умову (Re ≤ 2320) + 2(Re ≤ 10 ) + 3(Re ≤ 500 ), – буквою H.

Нижче поданий приклад 4.2.5 програми для цього алгоритму.

Приклад 4.2.5 – Застосування перемикача

#include<stdio.h> /* Варіант switch */

#include<math.h>

#define d 400.0

#define delta 0.15

int main(void)

{

float lambda,re;

int h;

printf("Введіть число Рейнольдса: ");scanf("%f", &re);

switch(re<=0)

{case 1: printf("Функція не існує, Re= %7.0f\n",re); break;

default: h=(re<=2320) + 2*(re<=(10*d/delta)) + 3*(re<=(500*d/delta));

switch(h)

{case 6: lambda=64/re; break;

case 5: lambda=0.3164/sqrt(sqrt(re)); break;

case 3: lambda=0.11*sqrt(sqrt(delta/d+68/re)); break;

default: lambda=0.11*sqrt(sqrt(delta/d));

}

printf("Re=%7.0f Lambda= %5.3f\n", re, lambda);

}

getch();

return(0);

}

Основою програми прикладу 4.2.5 є два вкладені перемикачі. Вираз зовнішнього перемикача re<=0 може приймати два значення: 0 або 1. Відповідно до цього його блок має мітки: константу 1 та default. При одиниці виводиться повідомлення про те, що функція не існує (ліва гілка рисунка 4.6). Якщо значення виразу дорівнює 0 (мітка default:), то відбувається виконання трьох таких операторів: обчислення допоміжної змінної h, виконання внутрішнього оператора switch та вивід результатів обчислень re і lambda (права гілка рисунка 4.6). Обидві гілки зовнішнього перемикача виводять обчислювальний процес за межі його блока в одне місце – на кінець програми.

Перед блоком внутрішнього перемикача обчислюється значення h типу int. Ця змінна служить для скорочення довжини логічного виразу H, їй присвоюється значення цього виразу. Вираз h внутрішнього перемикача побудований так, що в залежності від числа Рейнольдса він прийматиме значення цілого числа 6 (6=1+2+3), якщо будуть задовільнятися всі три логічні вирази, 5 – будуть задовільнятися два останні вирази або 3 – буде задовільнятися лише один останній вираз. Тоді h=6 лише в тому випадку, коли число Re задовільняє умову С. При h=5 задовільняється лише умова Б, а при h=3 – лише умова А. Умову Ш не було необхідності включати в вираз, зрозуміло, що вона буде задовільнятися в усіх інших випадках.

Вираз h можна було побудувати й так, щоб результатами його обчислення були більш зрозумілі числа 0, 1, 2, 3. Тоді він мав би такий вигляд:

h=(re<=2320) + 2*(re>2320 && re<=(10*d/delta)) + 3*(re>(10*d/delta) &&

re<=(500*d/delta));

А як мітки-константи внутрішнього блока перемикача служили б числа 1:, 2:, 3: і default:. Зразу видно, що він більш складний і що в ньому повторюються умовні вирази, тому він невигідний.

З метою запобiгання переходу обчислювального процесу на нижнi оператори внутрішнього блока перемикача, якi мають iншi мiтки, застосовуються оператори break, вони забезпечують своєчасний вихід за межi внутрішнього блока.

Результати виконання програми прикладу 4.2.5 такi ж, як i в розглянених вище прикладах 4.2.3 та 4.2.4.

Оператор switch вигідно застосовувати в тому випадку, коли результатами обчислення його виразу (умови) є цілі числа, букви або коди клавіш, зокрема при ручному управлінні ходом виконання програми (рухомі фігури на екрані в іграх, управління курсором та ін.). Тоді як вираз оператора switch варто використовувати функцію bioskey(0), яка описана в файлі bios.h. Вона служить для вводу кодів клавіш клавіатури і їх комбінацій з клавішами Ctrl, Alt та Shift. Коди клавіш можуть служити як мітки операторів блока перемикача.

Переглянути коди клавіш можна за допомого функції:

printf("Код=%d\n", bioskey(0));

Запитання для самоперевiрки

  1. Дайте визначення розгалуженого алгоритму.

  2. Відомо, що десяткове число 5 у двійковій системі числення дорівнює 101. Чому n=0 після виконання операцій: int n; n=5 & 101;? Чому n=1, але не 5, якщо n=5 && 101;?

  3. Перелiчiть усi можливi види виразу оператора if, наведiть приклади, пояснiть виконання.

  4. Порівняйте програми 4.2.4 та 4.2.5. Яка з них складніша? В яких випадках доцільно застосовувати перемикач?

  5. Чому дорiвнюватиме k пiсля виконання операторів: if(i=j) k=1; else k=2; при а) int k, i=2, j=2; б) int k, i, j=0; в) int k, i=2, j=0;?

  6. Чи можлива ситуацiя, коли розгалужена програма закiнчена, проте знайдуться її оператори, якi нi разу не виконувались?

  7. Задано зміну: int mis; (порядковий № місяця). Що одержимо в результаті обчислення виразу: mis<4?1:mis<7?2:mis<10?3:4?

  8. Для чого служить оператор break у програмі з перемикачем? Чи можна його застосувати в блоці альтернативного оператора типу if-else?

  9. Чи можна назвати застосування альтернативного оператора типу if-else універсальним способом уникнення оператора goto?

  10. Чому в результаті виконання операторів: float x=-3.2; printf("%d", x+3.2==0); одержимо нуль, а не одиницю, як можна було б очікувати?

  11. Що одержимо в результаті виконання операторів: float x=1; printf("%d", x==0);? printf("%d", x);? printf("%d", x=0);?

  12. Як визначити код клавіші F1? Як можна застосувати цей код у виразі перемикача?

4.3 Цикли

Циклом називається обчислювальний процес, який багаторазово повторюється. Основною його характеристикою є аргумент або параметр циклу. Це змiнна, яка перед циклом одержує початкове значення, а в циклi нарощується i перевiряється на досягнення кiнцевого значення, перевищення якого є умовою завершення циклу. Залежно від характеру зміни параметра розрiзняють простий цикл з рiвномiрним приростом аргумента, який ще називають циклом типу арифметичної прогресiї, та iтерацiйний, коли кількість виконань циклу заздалегідь невідома.

Для програмування циклів служать такі спеціальні оператори:

  • for(вираз1; вираз2; вираз3)оператор;

Тут: вираз1 – присвоєння параметру циклу початкового

значення;

вираз2 – умова, перевiрка на досягнення кiнцевого

значення;

вираз3 – нарощення параметра циклу на заданий крок;

оператор – тiло циклу, один оператор або блок;

  • while(вираз)оператор; Це оператор циклу з передумовою, цикл виконується, поки значення виразу не дорівнює нулю;

  • do оператор; while(вираз); Оператор циклу з постумовою, від попереднього відрізняється тим, що спочатку цикл виконується, а потім превіряється умова виходу з циклу, тому незалежно від задовільнення умови він буде виконаний хоча б один раз;

  • continue; Продовження, його дiя полягає в переходi на початок циклу. Тобто, відбувається нарощення параметра i перевiряється умова завершення циклу а, якщо ця умова ще не задовільняється, то цикл продовжує виконуватися спочатку з новим значенням параметра;

  • break; Виконується подібно до вже розгляненого в розділі 4.2, він служить для передчасного виходу з циклу (за межі блоку).

Слід зауважити, що подібні програмні засоби мають практично всі мови програмування високого рівня. Проте, мова С має свої особливості, які полягають, зокрема, в тому, що вирази оператора циклу можуть мати будь-який вигляд. В заголовку оператора for деякі вирази можуть бути відсутніми, допустима й така конструкція: for(x=0; y<5; z++), тобто вигляд виразів строго не регламентується.

Цикл типу арифметичної прогресії. Цикл з рiвномiрним приростом аргумента є найбiльш вживаним. Число його виконань n можна обчислити за формулою:

де xп, xк, x – вiдповiдно початкове i кiнцеве значення та прирiст

(крок нарощення) параметра циклу.

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

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

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

y=f(z)=3z2-2z+k

при k=3,2 на відрізку [0, 2] не змінює свого знака, тобто приймає лише додатні або лише від’ємні значення. Ця функція являє собою параболу, отже змінюється монотонно, тому достатньо дослідити її в декількох точках. Обчислимо всі її значення на заданому відрізку з кроком 0,2 та представимо результати на екрані у вигляді таблиці.

Графічний алгоритм для даної задачі показаний на рисунку 4.7. Він складається з 6 блоків, з яких перший і останній служать для визначення відповідно початку і кінця його виконання. У другому блоці змінній k присвоюється значення 3,2. Зрозуміло, що перш, ніж виконувати обчислення виразу, необхідно задати значення всіх його змінних. Третій блок містить заголовок циклу, тут сказано, що параметром циклу є змінна z, яка змінюється від 0 до 2 з кроком 0,2. Четвертий і п’ятий блоки складають тіло циклу, де відбувається відповідно обчислення значень функції y=f(z) та вивід результату.

В прикладі 4.3.1 подана програма мовою С яка виконує цей алгоритм. На її початку оголошені змінні z, y, k, всі вони мають тип float, який достатній для нашої задачі. Змінна k ініціалізована під час оголошення константою 3.2. Далі йде заголовок циклу, звернемо увагу на те, що наприкінці його немає крапки з комою, тут цикл лише починається. Тіло циклу взято в фігурні дужки, бо воно являє собою блок, який має два оператори: обчислення значення y і виведення його та аргумента z.

Приклад 4.3.1 – Простий цикл

#include<stdio.h> /* Цикл типу for */

int main(void)

{

float z, y, k=3.2;

for(z=0; z<=2; z+=0.2)

{

y=3*z*z-2*z+k;

printf("y=%5.2f z=%4.1f\n", y, z);

}

getch();

return(0);

}

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

y= 3.20 z = 0.0

y= 2.92 z = 0.2

y= 2.88 z = 0.4

y= 3.08 z = 0.6

y= 3.52 z = 0.8

y= 4.20 z = 1.0

y= 5.12 z = 1.2

y= 6.28 z = 1.4

y= 7.68 z = 1.6

y= 9.32 z = 1.8

Аналізуючи їх, бачимо, що вони нас не задовільняють, бо програма не видала значення y при z=2, хоча в заголовку циклу задана чітка і однозначна умова виконання циклу: z<=2.

Причина цього недоліка полягає в неточному представленні дійсних чисел у пам’яті, через що накопичується похибка обчислень. З метою його усунення в даному випадку можна врахувати похибку та змінити умову, збільшити число 2 на якусь величину, не більшу за один крок, наприклад, на пів-кроку. Тоді умова виконання циклу прийме вигляд: z<=2.1, а до вже показаних результатів додасться ще й такий:

y= 11.20 z = 2.0

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

Тоді напрошується висновок про те, що як параметр циклу варто застосовувати тільки змінну цілого типу. Навіть маємо мови програмування, наприклад, Паскаль, де реалізовано саме такий цикл. Треба, однак, сказати, що існує певний клас задач, де застосування змінної дійсного типу як параметра циклу є оправданим, тому оператор циклу типу for мови С розширює її можливості.

Нижче подано приклад 4.3.2 варіанта даної програми з параметром циклу цілого типу. В її текст внесено дві зміни, а саме: додано змінну n типу int та змінено заголовок циклу. В цьому заголовку початкове значення та нарощення приймають дві змінні – це n i z, але на досягнення кінця циклу перевіряється лише змінна n. Саме вона є параметром, який змінюється від 0 до 10 з кроком 1, тобто приймає 11 значень, оскільки відповідно до наведеної на початку цього розділу формули кількість виконань нашого циклу дорівнює (2-0)/0,2+1=11. Змінена програма видасть усі потрібні результати.

У заголовку циклу програми прикладу 4.3.2 двічі використано операцію , (кома) – послідовне виконання: при набутті початкових значень та при нарощенні змінних n i z. Відомо, що параметр циклу оператора for (в даному випадку це змінна n) компілятор розміщує в регістровій пам’яті (клас register), яка відзначається високою швидкодією. Це вигідно, бо економиться час виконання програми, адже параметр циклу багаторазово змінюється. За рахунок операції послідовного виконання на цей привілей має шанс і змінна z, тобто не відбулося ніякого програшу від того, що вона перестала бути параметром циклу.

Приклад 4.3.2 – Цикл з параметром цілого типу

#include<stdio.h> /* Цикл типу for */

int main(void)

{

float z, y, k=3.2;

int n;

for(n=0, z=0; n<=10; n++, z+=0.2)

{

y=3*z*z-2*z+k;

printf("y=%5.2f z=%4.1f\n", y, z);

}

getch();

return(0);

}

Цикл типу арифметичної прогресії можна побудувати й за допомогою оператора типу while. Нижче подано приклад 4.3.3 програми з його застосуванням для розв’язування поставленої вище задачі.

Приклад 4.3.3 – Застосування оператора while

#include<stdio.h> /* Цикл типу while */

int main(void)

{

float z=0, y, k=3.2;

int n=0;

while(n<=10)

{

y=3*z*z-2*z+k;

printf("num=%2d y=%5.2f z=%4.1f\n", n, y, z);

z+=0.2;

n++;

}

return(0);

}

Результати виконання програми прикладу 4.3.3 будуть такими:

n= 0 y= 3.20 z = 0.0

n= 1 y= 2.92 z = 0.2

n= 2 y= 2.88 z = 0.4

n= 3 y= 3.08 z = 0.6

n= 4 y= 3.52 z = 0.8

n= 5 y= 4.20 z = 1.0

n= 6 y= 5.12 z = 1.2

n= 7 y= 6.28 z = 1.4

n= 8 y= 7.68 z = 1.6

n= 9 y= 9.32 z = 1.8

n=10 y=11.20 z = 2.0

За змістом вони не відрізняються від виданих попередньою програмою, але з метою підвищення їх “читабельності” тут додано ще й порядковий номер виведеного рядка.

У програмі прикладу 4.3.3 параметром циклу є змінна n, яка змінюється від 0 до 10, тому набуває 11 значень. В тілі циклу, обрамленому фігурними дужками, відбувається обчислення зачення y, вивід результатів і нарощення двох змінних: z – на величину 0,2 та n – на 1.

Останній варіант програми можна вдосконалити – скоротити і текст, і час її виконання. Нижче подано фрагмент програми, де ці скорочення відображені.

n=11; z=0;

while(n--)

{

y=3*z*z-2*z+k;

printf("num=%2d y=%5.2f z=%4.1f\n", 10-n, y, z);

z+=0.2;

}

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

Важливо знати, що після закінчення виконання циклу, тобто після виходу з циклу, значення його параметра залишається нарощеним на один крок. Наприклад, у розгляненому вище фрагменті, де параметром циклу є змінна n, після його завершення n=0. Після досягнення значення n=1 відбудеться його перевірка на нуль, а оскільки воно не дорівнює нулю, то цикл буде підготовлений до виконаня. Потім завдяки операції декременту (n--) n стане дорівнювати нулю, але це вже не вплине на хід роботи оператора циклу, він виконається ще один останній раз при n=0. Воно ж і залишиться в пам’яті, бо в результаті наступної перевірки на нуль відбудеться завершення циклу без чергового нарощення змінної n.

Ітераційний цикл. Циклiчний iтерацiйний алгоритм характерний тим, що нi кiлькiсть його виконань, нi кінцеве значення параметра циклу заздалегiдь невiдомi. Його суть полягає в тому, що цикл виконується за формулою qi+1 = φ(qi) (i=0, 1, 2, ...), починаючи з деякого наперед заданого числа q0, поки |qi+1-qi| > ε, де ε – задана похибка, а формула φ(q) відома з постановки задачі. Як правило, останнє значення змінної qi є результатом виконання циклу.

Цей цикл порівняно простий, що й зумовило його поширення в програмуванні. Однією із областей його застосування є розв’язуваня алгебраїчного нелінійного рівняння методом простої ітерації, звідки й походить його назва. Треба сказати, що існує певний клас задач, зокрема вищезгаданий метод простої ітерації, де використання циклу цього виду є найбільш вигідним.

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

Вважають, що, якщо вiдстань |qi+1 - qi| стане меншою за деяку, наперед задану малу величину (похибку ε), то вона також менша за похибку обчислення, тобто |qi+1 - q*| < ε, тодi iтерацiйний процес припиняється.

Зауважимо, що існують також задачі, для яких розбіжність або зацикленість вважається нормою. Тоді, як правило, кількість виконань циклу обмежують певним числом, достатнім для коректного завершення програми. Їх можна вважати спеціалізованими, тому тут вони не розглядаються.

Дослідимо циклічний ітераційний процес та встановимо умову його збіжності на прикладі розв’язування алгебраїчного нелінійного рівняння методом простої ітерації. Нехай задано рівняння f(x)=0. Для забезпечення збіжності помножимо це рівняння на постійний коефіцієнт k, тобто представимо його у вигляді: kf(x)=0. Перетворимо це рівняння до вигляду, вигідного для ітерації, якимось із способів, наприклад, додамо в його ліву і праву частину число x, з курсу математики відомо, що подібні маніпуляції не змінюють кореня рівняння. Тоді одержимо ітераційну формулу (змінене рівняння):

x=x+k f(x). (4.1)

Замінимо її на систему функцій:

та прослідкуємо їхню поведінку в ході ітераційного процесу. Функція y=φ(x) може мати один із чотирьох можливих варіантів вигляду, показаних на рисунку 4.9, а функція y=x являє собою пряму лінію, проведену під кутом 450 через початок координат.

Аналізуючи рисунок 4.9, знаходимо, що ітераційний процес буде збіжним у випадках а і б, де кут нахилу дотичної (уявної, на рисунку вона не показана) до кривої y=φ(x) менший за |450| на всьому інтервалі зміни x, причому швидкість збігання тим більша, чим цей кут менший, тобто ближчий до 0, а найкраще, якщо він дорівнюватиме нулю. Для того, щоб цю умову можна було використати в математичних виразах, замінимо її на тотожну іншу: тангенс кута нахилу вищевказаної дотичної повинен бути меншим за одиницю або перша похідна функції y=φ(x) – менша за одиницю, тобто

|φ'(x)|<1. (4.3)

Оскільки найкраща збіжність забезпечується при рівності нулю кута нахилу дотичної до кривої y=φ(x), як умову збіжності приймемо рівність нулю її першої похідної, тобто φ'(x)=0 (в цьому випадку модульні дужки стають зайвими).

Підставимо в цю умову збіжності значення φ(x), відоме з виразу 4.2, тоді φ'(x)=[x+k f(x)]'=1+k f'(x)=0. Звідси k = -1/f'(x), підставивши його в формулу 4.1, одержимо: x=x-f(x)/f'(x) або

Це відома формула Ньютона, її математичне обгрунтування можна знайти в підручниках з обчислювальної математики. Вона забезпечує збіжність ітерації на певному відрізку [a, b], названому інтервалом ізоляції кореня, де задовільняються вимоги: sign[f(x)]=const та sign[f(a)]sign[f(b)], тобто зберігається незмінність знака першої похідної функції f(x) (вона монотонно або зростає, або спадає), а також на кінцях відрізка ця функція має різні знаки. Тоді x0 вибирають з діапазону [a, b]. Цю формулу можна поширити й на інші подібні процеси.

Покажемо цикл ітераційного типу на прикладі розв’язування рівняння

f(z)=z3-z2+kz-c=0,

де z – корінь рівняння, k=3,2, c=1,4.

Це відоме в нафтогазовій справі рівняння Редліха-Квонга [4], яке застосовують для обчислення коефіцієнта надстислості газу z при різних значеннях температури і тиску. З його фізичного змісту відомо, що змінна z має значення в межах [a, b], де a=0, b=2.

Спробуємо його розв’язати методом простої ітерації. Для цього перетворимо рівняння до вигляду вигідного для ітерації, додамо до лівої і правої його частин z. Зауважимо, що від цього його корінь не зміниться. Тоді воно прийме такий вигляд: z=z3-z2+(k+1)z-c. За початкове наближення кореня візьмемо значення 1 – середину відрізка [0, 2]. Тоді z0=1, а ітераційна формула матиме такий вигляд:

zi+1=zi3-zi2+(k+1)zi-c.

Нам немає необхідності зберігати в пам’яті всі значення z, лише два сусідні наближення: zi+1 та zi, які потрібні для перевірки умови завершення циклу так, як про це сказано на початку цього розділу. Позначимо їх відповідно p=zi+1, а z=zi. Нехай похибка ε дорівнює 10-5. Тоді ітераційну формулу одержимо в такому вигляді: p=z3-z2+(k+1)z-c при початковому наближенні z=1, а умову виконання (продовження) циклу – в такому: |z-p|>ε при ε=10-5.

Графічний алгоритм для цієї задачі показаний на рисунку 4.10. В його 2-му блоці початкове значення присвоєно змінній p=1, а не z, як сказано вище, це зроблено для того, щоб цикл почав працювати. В блоці 3 це та наступні значення p будуть переприсвоєні змінній z, а далі – використовуватися для обчислення за ітераційною формулою.

Нижче (приклад 4.3.4) показано текст програми, яка реалізовує цей алгоритм за допомогою оператора циклу типу do–while. Її ідентифікатор eps означає ε, а решта мають таку ж назву, як і в графічному алгоритмі. Операцію взяття за модулем у виразі оператора while реалізовано за допомогою функції fabs(), яка призначена для обробки чисел типу float. (Зауважимо, що інший її варіант abs() використовується для типу int). Наприкінці програми передбачено вивід результату p. Зауважимо, що замість нього можна було б вивести й z, обидві ці змінні відрізняються між собою не більше, ніж на похибку eps, але значення p – останнє наближення, тому воно точніше.

Приклад 4.3.4 – Циклічна програма ітераційного типу

#include<stdio.h> /* Рівняння Редліха–Квонга */

#include<math.h> /* Файл math.h приєднаний для функції fabs() */

int main(void)

{

float z, p=1, k=3.2, c=1.4, eps=1e-5;

do

{

z=p;

p=z*z*z-z*z+(k+1)*z-c;

}

while(fabs(z-p)>eps);

printf("Korin= %f \n", p);

getch();

return(0);

}

Запустивши цю програму на виконання, ми, однак, не одержимо потрібного результату, натомість вона завершиться некоректно і видасть таке повідомлення:

Floating point error: Overflow.

Воно означає, що відбулося переповнення пам’яті. Спробуємо знайти причини неполадків і усунути їх. Спочатку перевіримо чи задовільняються вищеперелічені вимоги до інтервалу ізоляції кореня [0, 2]. Нагадаємо, що першою з них була незмінність на ньому знака першої похідної функції f(z), яка матиме такий вигляд:

f(z)=3z2-2z+k.

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

Друга умова вимагає різних знаків функції f(z)=z3-z2+kz-c на кінцях відрізка [0, 2]. Виконавши необхідні обчислення, знайдемо, що f(0)=-1,4 – від’ємне число, а f(2)=8-4+3,2*2-1,4=9 – додатнє, тобто, що й ця умова теж задовільняється.

Тоді залишається перевірити, чи задовільняється умова збіжності (4.3). Аналізуючи результати, видані програмою прикладу 4.3.3, бачимо, що всі вони більші за одиницю, отже, причина неполадків полягає в тому, що ця умова не задовільняється. В цьому можна також переконатися шляхом виводу проміжних результатів ітерації, для цього функцію їх виводу слід вставити в тіло циклу програми прикладу 4.3.4.

Забезпечимо збіжність, застосуємо вищенаведену формулу Ньютона (4.4), що й зроблено в програмі прикладу 4.3.5.

Приклад 4.3.5 – Забезпечення збіжності, застосування формули

Ньютона

#include<stdio.h> /* Метод Ньютона */

#include<math.h>

int main(void)

{

float z, p=1, k=3.2, c=1.4, eps=1e-5;

do

{

z=p;

p=z - (z*z*z-z*z+k*z-c)/(3*z*z-2*z+k);

}

while(fabs(z-p)>eps);

printf("Корінь рівняння= %f \n", p);

getch();

return(0);

}

Вона видасть такий результат:

Корінь рівняння= 0.474471

Підставивши це значення кореня в наше рівняння, переконуємося в тому, що воно правильне, f(0,474471)=-0,0000001, тобто одержане значення дорівнює нулю з незначною похибкою.

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

Ітераційний цикл можна побудувати й за допомогою оператора типу for, нижче показано цей варіант програми у прикладі 4.3.6.

Приклад 4.3.6 – Ітераційний цикл з оператором for

#include<stdio.h> /* Ітераційний цикл з оператором for */

#include<math.h>

int main(void)

{

float z, p, k=3.2, c=1.4, eps=1e-5;

for(z=1, p=10; fabs(z-p)>eps; p=z-(z*z*z-z*z+k*z-c)/(3*z*z-2*z+k))z=p;

printf("Корінь рівняння= %f \n", p);

getch();

return(0);

}

Вона видасть той же правильний результат. У списку заголовка оператора for змінній p присвоєно число 10 – формальне значення (будь-яке), яке перевищує змінну z на число, більше за похибку. Це зроблено для забезпечення виконання циклу за першим разом (для початку ітераційного процесу).

Запитання для самоперевiрки

  1. Пояснiть термiни: цикл, параметр циклу, тiло циклу, кiлькiсть виконань циклу.

  2. Що видаcть фрагмент: m=0; for(k=1; m++;)printf("%d\n", k);?

  3. Якi значення будуть мати змiннi z та n прикладу 4.3.2 пiсля останнього їх виводу та пiсля виходу з циклу за межі блока?

  4. Як можна припинити виконання частини операторів тіла циклу і продовжити його виконання з новим нарощеним параметром?

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

  6. Чи можна використовувати значення параметра циклу у виразах блока циклу?

  7. Поясніть поняття: збіжний, розбіжний, зациклений процеси. Чи можна розбіжний ітераційний процес зробити збіжним?

  8. Чому в програмі прикладу 4.3.5 як результат передбачено вивід значення змiнної p? Чи не краще виводити z? Яка різниця між цими змінними?

  9. Складiть графiчний алгоритм i програму для уточнення квадратного кореня x з числа a за формулою: x=(x+a/x)/2.

  10. Знайдiть iтерацiйну формулу для добування кореня будь-якого степеня з довiльного числа a.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]