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

6 Вкладені алгоритми

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

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

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

Розгалуження в циклі. Продемонструємо хід побудови алгоритму із вкладеними елементарними алгоритмами на прикладі розв’язування рівняння Редліха-Квонга, поданого в задачі розділу 4.3, методом ділення відрізка наполовину. Цей метод вигідний тим, що не потребує дослідження збіжності ітераційного процесу. Він оснований на тому, що на кінцях відрізка [a,b] функція f(z) має різні знаки. Суть методу полягає в тому, що відрізок [a,b] ділять багаторазово на дві частини точкою z, після чого перевіряють умову: якщо f(a)*f(z)<0, то для подальшого ділення вибирають відрізок [a,z], тобто переносять точку b в z шляхом присвоєння b=z, інакше – відрізок [z,b], тобто a=z. Таким чином, після кожного ділення область пошуку кореня скорочується вдвоє. Ітераційний процес припиняється при досягненні умови: b-aε.

Приступаючи до побудови алгоритму для цієї задачі, визначаємо, що він циклічний, адже ділення відрізка [a,b] наполовину виконується багаторазово. Разом з тим, він розгалужений, бо містить умову для визначення на кінцях якого з двох пів-відрізків функція f(z) має різні знаки. В даному випадку зразу можна сказати, що маємо розгалуження в циклі, оскільки умова перевіряється при кожному виконанні циклу. Залишається тільки вибрати тип циклу, правильно побудувати тіло циклу та саму умову. Виберемо цикл типу while, він саме й призначений для побудови програм ітераційного типу, коли кількість виконань циклу заздалегідь невідома.

Алгоритм показаний на рисунку 6.1. На його початку (блоки 2, 3) всім змінним: k=3.2; c=1.4; b=2; ε=10-5; a=0 присвоюються відповідні числа та обчислюється значення функції y=f(a). Це значення y буде використано при першому виконанні циклу в умові, що розгалужує обчислювальний процес. Оскільки буде застосовуватися оператор циклу типу while, умова виходу з циклу (блок 4) встановлена перед його тілом. Тіло циклу починає блок 5, який символізує обчислення місця точки z на відрізку [a,b], z=a+(b-a)/2 та значення в ній функції p=f(z). Далі (блок 7) відбувається розгалуження – перевіряється умова y*p<0, нагадаємо, що y=f(a) – зліва, а p=f(z) – посередині відрізка [a,b]. Незадовільнення цієї умови буде означати, що корінь рівняння знаходиться зправа від точки z, тому повинно не лише відбутися перенесення точки a в точку z за допомогою переприсвоєння a=z, але й змінитися значення y=p (блок 9), яке тоді буде дорівнювати новому f(a).

Цей алгоритм реалізований у програмі прикладу 6.1. Її початок і циклічна частина відповідає графічному алгоритму. З метою перевірки кореня рівняння після закінчення циклу обчислюється p, ця частина програми виконує допоміжну функцію і в графічному алгоритмі не відображена. Закінчується програма виводом результату – значення кореня та змінної p. Аналізуючи їх, показаних зразу за текстом програми, бачимо, що вони співпадають з виданими програмою прикладу 4.3.4.

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

#include<stdio.h> /* Ділення відрізка наполовину */

int main(void)

{

float a=0, b=2, z, p, y, k=3.2, c=1.4, eps=1e-6;

y=a*a*a-a*a+k*a-c;

while(b-a>eps)

{

z=a+(b-a)/2;

p=z*z*z-z*z+k*z-c;

if(p*y<0)b=z;else {a=z; y=p;}

}

p=z*z*z-z*z+k*z-c;

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

getch();

return(0);

}

Korin= 0.474471 perevirka= -0.000001

Вкладені цикли. У вкладених циклах при кожному значенні зовнішнього циклу відбувається перебір усіх значень внутрішнього. Це різко збільшує кількість обчислень. Наведемо такий приклад: нехай необхідно обчислити і з метою візуального дослідження надрукувати всі значення функції сімох аргументів y=f(x1,x2,...,x7), де кожний аргумент приймає по 10 значень у заданому діапазоні з певним кроком.

Перш, ніж програмувати цю задачу, визначимо потрібну кількість паперу. Число обчислень функції дорівнюватиме 107. Якщо на кожний рядок виводу затратимо по 1 см, то загальна кількість паперу дорівнюватиме 107 см або 105 м, або 100 км.

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

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

Задача накопичення суми або добутку досить поширена в програмуванні, зокрема, вона вже розв’язувалася в програмах розділу 4.3 для нарощення змінних n та z. Вона може застосуватися також під час статистичної обробки експериментальних даних, їх дослідження методами числового аналізу та ін.

Можливi такi варiанти постановки задачi накопичення суми або добутку:

1) 2) 3) 4)

де Σ – символ суми;

D – символ добутку;

– черговий елемент (член ряду) суми або добутку, і=0,1,2, ...

n – кількість елементів (членів ряду);

s – значення суми або добутку

Накопичення суми або добутку виконують у циклi. Перед циклом змiннiй s присвоюється початкове значення для суми s=0, для добутку s=1, а значення лічильника і в тілі циклу починаються з 0. Можна в обох випадках присвоювати початкове значення s=a0, тоді в циклі значення лічильника і починаються з 1. Обчислення відбувається за формулами для суми: s=s+ai, для добутку: s=s*ai. Якщо ряд має кiнцеве число (варіанти 1, 3) членiв, то в якостi параметра циклу вибирають змiнну i – порядковий номер члена, якщо – безмежне, то член ряду ai.

При накопиченнi суми або добутку безмежного числа членiв, тобто при прямуванні лічильника i до безмежності, ряд повинен бути збіжним, тоді ai прямує до певного числа с=const. Обчислювальний процес припиняється, якщо │ai-c│≤ε, де ε – задана похибка. Вважається, що тоді задовільняється й умова |Δs|≤ε, де Δs – абсолютна похибка суми або добутку.

Отже, в кожному конкретному випадку перед програмуванням задачі необхідно провести дослідження ряду ai на збіжність. Можуть бути й інші недоліки. Навіть у випадку збіжності ряду або при заданій кількості його членів може відбутися некоректне завершення програми через переповнення пам’яті до задовільнення умови виходу із циклу, якщо числа ai великі. Особливо це стосується накопичення добутку, який при |ai|>1 зростає швидше за суму.

Має значення й швидкість збіжності. Якщо ряд збігається занадто повільно, то може статися, що вже при задовільненні умови |ai-c|ε умова |Δs|≤ε ще не задовільняється або далеко не задовільняється, тоді не можна нехтувати рештою членів ряду ai і умова виходу із циклу повинна бути змінена.

Накопичення суми або добутку знакозмінного ряду чисел теж має свої особливості, він часто являє собою значення якоїсь періодичної функції, обчислені з певним кроком, тоді перш за все необхідно правильно вибрати цей крок. Нехай, наприклад, ai=|sin(xi)|, де x0=0, а крок дорівнює числу π. Тоді x1= π, x2=2π і т.д., а всі значення ai та їх сума дорівнюватимуть нулю. Заздалегідь можна сказати, що це невірний результат. При накопиченні добутку теж одержимо нуль, якщо хоч один член ряду дорівнюватиме нулю. Друге, на що слід звернути увагу, є умова завершення циклу. Може статися, що вона вже задовільняється, але не тому, що рештою членами ряду можна знехтувати, а через її випадкову близькість до числа c. Тоді умовою виходу із циклу може бути наближення до c не числа ai, але амплітуди зміни членів ряду, наприклад, max|ai-c|ε на певних проміжках зміни лічильника i.

Можливі й інші неполадки, продемонструємо деякі з них на прикладі. Нехай необхідно обчислити суму s з похибкою ε =10-5 для довільного значення x за формулою:

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

Алгоритм розв’зку даної задачі порівняно не складний, він являє собою два вкладені цикли. У зовнішньому циклі (типу while) відбувається обчислення значень ai та накопичення їх суми, умовою його закінчення є |ai|≤ε. У внутрішньому (типу for) – накопичення добутку k=i!, при зміні параметра j від 1 до i.

У прикладі 6.2 подана програма, яка реалізовує цей алгоритм при x=15, в ній змінна ε позначена ідентифікатором eps.

Приклад 6.2 – Накопичення суми й добутку

#include<stdio.h> /* Suma */

#include<math.h>

int main(void)

{

float a=1, s=a, x=15, eps=1e-5, k;

int i=0,j;

while(fabs(a)>eps)

{

i++;

k=1.0;

for(j=1;j<=i;j++)k*=j;

a=pow(-1,i)* pow(x,i)/k;

s+=a;

}

printf("s=%f\n", s);

getch();

return(0);

}

Ця програма була запущена на виконання в середовищі системи, з типом float довжиною 4 байти. Вона закінчилася некоректно і видала повідомлення про переповнення пам’яті:

Floating point error: Overflow.

Вона може видати й правильний результат, але лише при малих значеннях x, коли чисельник формули ai менший за знаменник на всьому діапазоні зміни лічильника i (наприклад, при х=1.2 одержимо s=0.301).

При більших значеннях (наприклад при x=15 як у програмі) в ході виконання зовнішнього циклу та нарощення змінної i чисельник спочатку є більшим за знаменник (степінь xi зростає швидше за факторіал i!), тому великою стає й кількість виконань цього циклу та збільшується кінцеве значення числа i. Зрозуміло, що це спричиняє різке зростання і чисельника, і знаменника настільки, що їхні значення (хоча б одне з них) не поміщаються в пам’ті. Це й сталося в нашому випадку. Не залагодить тут справу й використання базового типу double, хіба що максимально можливе число x тоді може бути трохи більшим.

Дослідити візуально хід циклічного процесу цієї програми можна, помістивши в тіло циклу таку конструкцію:

printf("i=%d k=%f a=%f\n", i, k, a);

if(!(i % 15))getch();

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

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

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

a0=1, ai+1= -aix/i.

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

Приклад 6.3 – Програма із застосування рекурентної формули

#include<stdio.h> /* Застосування рекурентної формули */

#include<math.h>

int main(void)

{

float a=1, s=a, x=15, p, eps=1e-5;

int i=0;

while(fabs(a)>eps)

{

i++;

a*=-x/i;

s+=a;

}

printf("s=%f\n", s);

getch();

return(0);

}

s=0.009355

У цій програмі можна використати й оператор циклу типу for, для цього замість заголовка while і тіла циклу, взятого в фігурні дужки, слід написати таку конструкцію:

for(a=1, s=a; fabs(a)>eps; a*=-x/i, s+=a)i++;

У зв’язку з цим може виникнути запитання про те, цикл якого типу: for чи while кращий. Як бачимо, і в цьому прикладі, і в прикладах розділу 4.3 цикли обох типів виглядають взаємозамінними. Тут немає однозначної відповіді, в кожній конкретній задачі це питання вирішується по-різному. При цьому можна висловити два зауваження. По-перше, як правило, в тих випадках, коли кількість виконання циклу фіксована, вибирають цикл типу for, інакше – типу while. Це підвищує “читабельність” програми, бо за типом циклу стає відомим тип задачі, яку він реалізовує, і навпаки. По-друге, як уже було вище сказано в розділі 4.3, компілятор поміщує змінні, що знаходяться в заголовку циклу типу for, в пам’ять класу register – швидкого доступу. За рахунок цього можна скоротити час виконання програми.

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

  1. Що таке вкладені алгоритми?

  2. Поясніть вкладення розгалуження в цикл та типу цикл у циклі.

  3. Наведіть приклад задачі, де могло б знайти застосування вкладення циклу в розгалуження. Скільки разів у цьому випадку буде виконуватися цикл та перевірятися умова розгалуження?

  4. За якими формулами вiдбувається накопичення суми та добутку?

  5. Що таке рекурентна формула? Наведiть приклад.

  6. Що служить в якостi параметра циклу в програмах з накопиченням суми або добутку кінцевого та безмежного числа членів ряду?

  7. Якi типи елементарних алгоритмiв використовуються при накопиченні суми або добутку?

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

  9. Складіть програму для наближеного обчислення площі пів-круга радіусом r, розбивши його на n=20 елементарних частин. Перевірте результат за формулою, відомою з курсу геометрії.

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

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