Паскаль. Прикладне програмне забезпечення


ПАСКАЛЬ: ЦИКЛ ПОКИ ТА ЙОГО ВИКОРИСТАННЯ

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

З Оксфордського словника англійської мови

1. Поки...

Приклад 4.1. Розглянемо дещо штучну задачу: написати цілочислову функцію з ім'ям pow для обчислення степеня An за довільним натуральним A і N 0. Задача має елементарне розв'язання: An= EnLn A, і в тілі функції достатньо написати pow:=roundexpnlna. Проте невід'ємні степені цілих чисел є цілими, тому спробуємо обійтися без нецілих виразів із функціями exp і ln.

За означенням, An є A a ... A, тобто A0=1, Ai= Ai1 A для I=1, 2,..., N. Це підштовхує до спроби обчислення An шляхом багаторазового множення на A. Спочатку шуканий степінь P=1, і треба N разів умножити його на A. Після першого множення P= A, і треба N1 разів умножити його на A тощо. Перед останнім множенням P= An1. Таким чином,

Спочатку p=1 і треба виконати n множень на a, і поки залишаються невикористані множники a, ми множимо p на a, одержуємо новий степінь p і запам'ятовуємо, що невикористаних множників стало менше на 1.

Остання фраза, власне, і є алгоритмом обчислення An. Перекладемо його на мову Паскаль.

Нам потрібні змінні P і A для збереження степеня і його основи, а також змінні N і K для збереження показника степеня й кількості невикористаних множників. Змінні A і N – параметри нашої функції, тому їх початкові значення тут не важливі. Тепер алгоритм можна уточнити:

P:=1; k:=n;

Поки k0 Виконувати {залишилися невикористані співмножники}

Begin p:=pa; k:=k1 End

Якщо перекласти на англійську мову слова Поки І Виконувати Як While І do, то утвориться:

P:=1; k:=n;

While k0 Do{залишилися невикористані співмножники}

Begin p:=pa; k:=k1 End

Але це вже Паскаль! Справа в тім, що вираз вигляду

While Умова Do Оператор

Називається Whileоператором, або Оператором Циклу З Перед Умовою. Вираз While Умова Do називається Заголовком Циклу, Оператор Тілом. Ми б назвали whileоператор циклом з умовою продовження, але цей термін дотепер у літературі не з'являвся.

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

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

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

Повернемося до задачі. Послідовність операторів для обчислення An при A=2, N=3 задає процес

P:=1; k:=3;

Обчислення умови k0: True;

P:=12; k:=31; {p=2; k=2}

Обчислення умови k0: True;

P:=22; k:=21; {p=4; k=1}

Обчислення умови k0: True;

P:=42; k:=11; {p=8; k=0}

Обчислення умови k0: False,

А при A=5, N=0 – процес

P:=1; k:=0;

Обчислення умови k0: False.

У вигляді коментарів тут указано стани пам'яті наприкінці циклів.

Запишемо, нарешті, функцію pow:

Function powa, n:integer:integer;

Var p, k: integer;

Begin

P:=1; k:=n;

While k0 Do

Begin p:=pa; k:=k1 End;

Pow:=p

End;

Приклад 4.2. Розглянемо задачу: за цілим A0 обчислити мінімальне N таке, що N! A.

За означенням, N!=1 2 N, тобто 1!=1, 2!=1! 2, 3!=2! 3 тощо, N!= N1! N. Обчислимо 1! та порівняємо з A; якщо 1! A, то обчислимо 2!=2 1! і знову порівняємо тощо. Зрештою після чергового збільшення N виявиться, що N! A, і отримане значення N – шукане. Наприклад, при A=5 треба дійти до N=3, при A=120 – до N=5. Нам потрібна змінна N для запам'ятовування чисел 1, 2, 3 тощо, та змінна f – для значень 1!, 2!, 3! тощо. Отже,

Спочатку n:=1, f:=1, і

Поки fA, треба збільшувати n на 1 і f домножати на n.

Перекладемо цей алгоритм на Паскаль. Скористаємося whileоператором із умовою продовження fA і тілом, у якому задано зміни n і f:

N:=1; f:=1;

While fA Do {значення n менше шуканого}

Begin n:=n+1; f:=fn End;

{f=A, f=n!, тому значення n – шукане}

Коментар після циклу містить умову f=A – по суті, Заперечення умови продовження fA.

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

Оформлення алгоритму у вигляді функції з параметром A залишається як вправа.

Досвідчені програмісти в деяких випадках користуються нескінченним циклом вигляду While True Do Оператор. Засоби, якими задається вихід із тіла циклу незалежно від значення умови продовження, ми розглянемо в підрозділах 5.3, 5.4.

Задачі

1. Проімітувати виконання послідовності операторів:

А i:=1; x:=1; y:=2;

While xy Do

Begin

I:=i+1; x:=xi; y:=y2;

Writelni, ' ', x, ' ', y

End;

Б i:=1; x:=1; y:=2;

While i=10 Do

Begin

I:=i+1; x:=xi; y:=y2;

Writelni, ' ', x, ' ', y

End;

2. За цілим A0 обчислити найбільше N таке, що N! A.

3. Написати функцію обчислення N!, де N 0 0!=1.

2. Рекурентні послідовності та співвідношення

2.1. Деталі конструктора

У прикладі 4.1 змінна p у процесі виконання операторів приймала значення 1, A, A2, A3, , An. У цій послідовності перший член 1, а кожний наступний дорівнює попередньому, помноженому на A. Позначивши члени послідовності через P0, P1, P2,... Pn, маємо рівність: Pi= Pi1 A при i=1,2,, N. Така рівність, що виражає член послідовності через попередні один або кілька, називається Рекурентним Співвідношенням.

Рекурентний означає зворотний. Справді, елемент послідовності тут визначається через попередні, і для його обчислення треба повернутися до них. Усім добре відомі рекурентні співвідношення вигляду An= An1+ D або Bn= Bn1 Q – їм задовольняють члени відповідно арифметичних або геометричних прогресій. Конкретна ж прогресія, тобто послідовність чисел, задається першим членом A1 і різницею D або знаменником Q. Власне, послідовність степенів у прикладі 4.1 P0, P1, P2, – геометрична прогресія: вона визначається першим членом P0=1 і рекурентним співвідношенням PI= Pi1 A при будьякому I0.

У прикладі 4.2 змінна N послідовно приймала значення, що утворюють арифметичну прогресію з першим членом 1 і різницею 1. Послідовність же значень змінної f не була прогресією, але визначалася першим членом F1=1 і співвідношенням Fn= Fn1 N при N1.

Послідовність, члени якої задовольняють деяке рекурентне співвідношення, також називається Рекурентною.

Приклад 4.3. Розглянемо послідовність { F} чисел 1, 1, 2, 3, 5, 8, 13, , у якій F1= F2=1, а наступні члени задаються рекурентним співвідношенням

Fn= Fn2+ Fn1, N2.

Вона називається Послідовністю Чисел Фібоначчі – за прізвиськом Леонардо Пізанського, який першим її описав. За першими двома її членами можна обчислити третій. Для обчислення четвертого перший член уже не потрібний, тому що F4= F2+ F3. Для обчислення п'ятого достатньо пам'ятати лише третій і четвертий тощо. Обчислюючи члени послідовності один за одним, ми дістанемося будьякого, почавши з перших двох. При цьому щоразу ми використовуємо лише два останніх значення і, обчисливши наступне, забуваємо перше з двох використаних.

Нехай дано номер N, N2, і треба обчислити Fn. Опишемо ці обчислення. З попередніх міркувань випливає, що потрібні дві змінні для двох сусідніх членів і третя для наступного назвемо їх Fa, Fb і Fc, а також змінна M для зберігання номера останнього з обчислених членів.

Спочатку fa=1, fb=1, m=2,

Потім обчислимо Fc:= Fa+ Fb і збільшимо M на 1. Якщо значення Fb і Fc зробити відповідно значеннями Fa і Fb Fa:= Fb, Fb:= Fc, то обчислення четвертого члена можна задати таким самим оператором Fc:= Fa+ Fb. Отже,

Поки mn, Виконуємо:

Fc:=fa+fb, m:=m+1, fa:=fb, fb:=fc.

Очевидно, що з кожним виконанням Fc:= Fa+ Fb , M:= M+1 ми переходимо до наступного члена послідовності і в M запам'ятовуємо його номер. Оскільки значення m щоразу зростає, зрештою виявиться, що M= N, умова M N стане хибною, і змінних Fb і Fc матимуть потрібне нам значення Fn. Залишилося перекласти на Паскаль рядки, відзначені і:

Fa=1; fb=1; m=2;

While mn Do

Begin

Fc:=fa+fb; m:=m+1;

Fa:=fb; fb:=fc

End;

{m=n, значення змінних fc і fb – шукане}

Відзначимо, що присвоювання Fa:= Fb та Fb:= Fc ні в якому разі не можна переставляти – можете проімітувати початок виконання цього алгоритму з переставленими присвоюваннями й переконатися, що значеннями змінної fc будуть аж ніяк не числа Фібоначчі.

У загальному випадку рекурентне співвідношення задає залежність члена рекурентної послідовності Sn від K попередніх у вигляді деякого виразу Sn= F Snk, , Sn1. Число K називається Порядком Рекурентного співвідношення. Якщо відомі Snk,..., Sn1, то вираз F фактично задає обчислення Sn. Назвемо це обчислення Застосуванням Рекурентного Співвідношення.

Припустимо, нам відомо рекурентне співвідношення Sn= F Snk,..., Sn1 і перші K членів рекурентної послідовності. Треба за номером P обчислити Sp. Знаючи перші K членів, можна застосувати до них співвідношення й обчислити Sk+1; аналогічно за S2,..., Sk+1 обчислюється Sk+2 тощо. Щоразу для обчислення чергового члена потрібні тільки K останніх із попередніх.

Отже, для опису цих обчислень потрібні:

    K змінних для K останніх членів нехай їх імена A, B, , X, змінна для нового члена нехай її ім'я Y, змінна M для номера останнього з обчислених членів.

Треба створити Деталі Конструктора, тобто запрограмувати:

    ініціалізацію змінних A, B, , X першими K значеннями послідовності; застосування рекурентного співвідношення, тобто обчислення нового члена й запам'ятовування його в змінній Y; присвоювання значень змінних B, , X, Y відповідно змінним A, B, , X назвемо це Переприсвоюванням.

Тоді розв'язання задачі має вигляд:

Ініціалізація змінних A, B, , X ;

M:=k;

While Умова Продовження Do

Begin

Присвоїти Y результат застосування рекурентного співвідношення до значень змінних A, B, , X ;

M:=M+1;

A:=B;...; X:=Y {переприсвоювання}

End

У нашому випадку умова продовження – це просто вираз M P.

Розв'язанням такого вигляду є алгоритм обчислення числа Фібоначчі за його номером приклад 4.3. Там K=2 і використано імена fa, fb, fc замість A,..., X, Y.

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

Зауважимо, що якщо порядок рекурентного співвідношення K=1, то для обчислення нового члена може виявитися достатнім однієї змінної. Так було в перших задачах, де, наприклад, при виконанні p:=pa спочатку за старим значенням змінної p обчислювалося нове й потім їй же присвоювалося. Проте далі ми наведемо приклади, де послідовність задається співвідношенням порядку 1, але в умові продовження обчислень використовуються два останніх члени. Тому там будуть потрібні дві змінні.

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

    зрозуміти, що розв'язання задачі можна побудувати на використанні рекурентної послідовності; записати відповідне рекурентне співвідношення; визначити перші члени послідовності, що обчислюються без застосування співвідношення; сформулювати умову, за якої треба продовжувати застосування рекурентного співвідношення.

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

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

2.2. Системи рекурентних співвідношень

Є чимало задач, у розв'язанні яких використовуються не одна, а кілька рекурентних послідовностей. Члени послідовності можуть залежати від попередніх членів як своєї, так і інших послідовностей. Ці залежності записуються у вигляді Систем Рекурентних Співвідношень. Насправді, ми вже бачили зв'язані послідовності: члени послідовності 1!, 2!, 3!, залежать від їх номерів і попередніх членів. Але послідовність номерів сама рекурентна, оскільки кожний номер на 1 більше попереднього.

Задачі

4. Написати функцію обчислення кількості десяткових цифр натурального числа.

5. Один із варіантів Алгоритму Евкліда обчислення Найбільшого Спільного Дільника чисел A і B НСД A, B грунтується на обчисленні рекурентної послідовності { P}, де P1=max{ A, B}, P2=min{ A, B}, Pn= Pn2 Mod Pn1 при N2. Шуканим є останнє ненульове значення послідовності. Уточнити алгоритм Евкліда у вигляді функції.

6. Послідовність { Xn}, задана співвідношеннями

X1= A+ M1/2,

Xi= M1 Xi1 + A/ X/ M при I 1,

Сходиться до A1/ M. Запрограмувати обчислення A1/ M при довільному додатному дійсному A з точністю e, тобто за потрібне число приймається перше Xn таке, що | Xnxn1 |e.

4.7. Послідовність сум { Sn}, де Sn=1+ X+ X2/2!++ Xn/ N!, за умови 0 X1 достатньо швидко сходиться до Ex. Запрограмувати обчислення Ex при X [0;1 із точністю e, тобто за потрібне число приймається перше Sn таке, що | Snsn1 |e.

Запрограмувати обчислення Ex при довільному X, застосовуючи формули зведення – рівності Ex= E[ X] E{ X}, Ex=1/ Ex, де [ X] і { X} позначають цілу й дробову частини X. Обчислити E[ X] шляхом множення сталої E=2.7182818 на себе [ X] разів.

4.8. Послідовність сум { Sn}, де Sn=1x2/2!++1 Nx2 N/2 N!, за умови |x| p /4 достатньо швидко сходиться до cos X. Запрограмувати обчислення cos X при X [p /4; p /4] з точністю e, тобто за потрібне число приймається перше Sn таке, що | Snsn1 |e.

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

3. Числа прості й не тільки

Приклад 6. Напишемо функцію визначення, чи є натуральне значення її параметра, більше 1, простим числом.

Як відомо, число N Є простим, якщо його додатними дільниками є лише 1 і N. Число 2 просте, а N2 просте, якщо не ділиться без остачі на жодне з чисел 2, 3, , N1. Якщо ж воно ділиться хоча б на одне з них, то є не простим, а складеним. Отже, N – просте тоді й тільки тоді, коли n=2 або n2 і n не ділиться на жодне з чисел 2, 3, , n1. Очевидно також, що N – складене, якщо N2 і ділиться хоча б на одне з чисел 2, 3, , N1.

Таким чином, При n2 треба перевірити подільність n на кожне з чисел k=2, 3, , n1.

Результат перевірок можна запам'ятати у вигляді кількості D дільників серед цих чисел. До початку перевірок D=0, тобто жодний дільник ще не знайдений, і з кожним знайденим дільником D збільшується на 1. Тоді умова D=0 є ознакою того, що число просте. Оформимо сказане у вигляді алгоритму:

If n2 Then

Begin

D:=0;

Для всіх k=2, 3, , n1 перевірити, чи ділиться N на K :

Якщо ділиться, то D:=d+1

If d0 Then n – складене

Else n – просте

End

Else n – Просте

Уточнимо цей алгоритм. Нехай issimple, тобто є_простим – ім'я функції, яку ми пишемо. Природно означити тип значень, що повертаються з неї, як булів. Тоді N – просте і N – складене уточнюються як issimple:= True і issimple:= False. На початку обчислень вважаємо, що число просте, тому присвоюємо issimple:= True. Тоді достатньо оператора розгалуження з умовою N2 без Elseгілки. Так само достатньо і скороченого оператора розгалуження з умовою D0.

Фраза для всіх K=2, 3, , N1 перевірити, чи ділиться N на K Задає повторення тих самих дій: перевірка подільності n на k та підготування до наступної перевірки, тобто збільшення k на 1. Спочатку K=2. Умовою продовження перевірок, очевидно, є умова K N.

Issimple:= True;

If n2 Then

Begin

D:=0; k:=2;

While kn Do

Begin

If n Mod k = 0 Then d:=d+1;

K:=k+1

End;

If d0 Then issimple:= False

End

Цей алгоритм можна істотно поліпшити. Почнемо з дрібниць. За оператором If d0 Then змінній issimple фактично присвоюється значення виразу Not d0. Простіше написати: issimple:= Notd0. Насправді змінна d взагалі не потрібна. Дійсно, значенням issimple буде False, якщо хоча б один раз виконується оператор d:=d+1. Замість нього напишемо issimple:= False. Крім того, якщо n=2, то умова продовження kn хибна при початковому значенні k, тому перевірку умови n2 можна взагалі вилучити:

Issimple:= True; k:= 2;

While kn Do {при n=2 тіло циклу не виконується жодного разу й значенням issimple залишається True}

Begin

If n Mod k=0 Then issimple:= False;

K:=k+1

End

Друге. Після того, як у циклі змінна issimple одержала значення False, подальші перевірки не потрібні, тому що вже ясно, що N не просте. Тому до умови продовження слід додати, що issimple має значення істина. А перевірка цієї умови задається таким виразом: issimple.

Було б природно записати вираз ktsn And issimple як умову продовження. Проте Використання імені функції у виразі означає її виклик. Виклик функції тут зовсім ні до чого, тому Цей вираз помилковий. Замість issimple скористаємося допоміжною змінною з ім'ям, наприклад, simp, додавши наприкінці issimple:=simp:

Simp:= True; tsn:= truncsqrtn+1; k:= 2;

While ktsn And simp Do

Begin

If n Mod k=0 Then simp:= False;

K:=k+1

End;

Issimple:=simp

Оформлення функції з заголовком

Function issimplen:integer:boolean

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

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

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

Приклад 4.7. Прості числа 2, 3, 5, 7, 11, 13, розташовані в натуральному ряду дуже загадковим чином. Нехай треба обчислити просте число за його номером у цій послідовності. Є формула, що задає просте число за його номером K, але обчислення за нею не простіші, ніж лобові:

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

Коли цей рахунок доходить до k, ми одержуємо те, що нам треба.

Це і є алгоритм пошуку Kго простого числа. Уточнимо його.

Нехай K0. Означимо для зберігання натуральних чисел, що перебираються, змінну M. З алгоритму випливає, що нам потрібна ще змінна для збереження кількості простих чисел, які вже зустрілися. Нехай cs – ім'я цього лічильника простих чисел. Спочатку cs=1, m=2 у значенні лічильника враховано перше просте число 2. Далі починається цикл:

Якщо умова продовження csk істинна, то треба перейти до ще неперевіреного значення m, перевірити, чи є воно простим, і якщо так, то збільшити значення лічильника cs. Коли значення cs досягає значення k, останнє перевірене число і є шуканим.

Ось переклад останніх фраз на Паскаль у вигляді програми з функцією issimple із попереднього прикладу:

Program Simpiinput, output;

Var K, m, cs: integer;

Function issimplen:integer:boolean;

...

End; {issimple}

Begin

Writeln'задайте номер0:';

Readlnk;

Cs:=1; m:=2;

While csk Do

Begin

M:=m+1;

If issimplem Then cs:=cs+1

End;

{cs=k, значення m – шукане}

Writeln k, 'е просте: ', m

End.

Приклад 4.8. Як відомо, кожне натуральне число, більше 1, однозначно розкладається в добуток простих співмножників, наприклад, 13=13, 105=3 5 7, 72=2 2 2 3 3 тощо. Щоб побудувати розклад довільного числа, або його Факторизацію, знайдемо його найменший дільник очевидно, що він простій, запишемо його і поділимо на нього число. Подальші співмножники розкладу утворюються так само доти, поки в результаті ділень не утвориться 1. Наприклад, 36=2 18 виписали 2, 18=2 9 2, 9=3 3 3, 3=3 1 3.

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

Алгоритм друкування розкладу N оформимо у вигляді процедури simpdivisors із параметром n divisor означає дільник. Можливі дільники будуть значеннями змінної k.

Спочатку k=2. Потім, поки n1, перевіряється подільність n на k. Якщо ділиться, то виписується значення k і виконується ділення, інакше k збільшується, тому що менших дільників уже бути не може.

Наведене описання обчислень уточнюється в такому вигляді:

Procedure simpdivisorsn:integer;

Var k:integer;

Begin

K:=2;

While n1 Do

Begin

If n Mod k = 0 Then

Begin writelnk; n:=n Div k End

Else k:=k+1

End

End