Issuu on Google+

Á³áë³îòåêà «Øê³ëüíîãî ñâ³òó» Çàñíîâàíà ó 2003 ð.

Андрій Ставровський Ірина Скляр ПРОГРАМУЄМО ПРАВИЛЬНО Посібник У двох частинах

Частина 1 ²íôîðìàòèêà. Á³áë³îòåêà

Êè¿â «Øê³ëüíèé ñâ³ò» 2007


ББК 32.973-018я721 С76

Редакційна рада: Н. Вовковінська, М. Мосієнко — канд. філол. наук, Г. Кузьменко, О. Шатохіна

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

С76

Ставровський, Андрій. Програмуємо правильно: Посіб. : У 2 ч. / А. Ставровський, І. Скляр. — К. : Шк. світ, 2007. — Ч. 1. — 128 с. — (Б-ка «Шк. світу»). ISBN 978-966-451-000-1. ISBN 978-966-451-075-9. По сібник містить початковий курс програмування в рамках шкільної дисципліни «Інформатика». У посібнику представлено основи алгоритмізації та програмування мовою Паскаль, міститься необхідний теоретичний матеріал, багато практичних прикладів та докладних розв’язань типових задач. Призначено для учнів, які вивчають інформатику за програмою фізико-математичних шкіл, а також для вчителів інформатики, студентів педагогічних ВНЗ і всіх, хто бажає навчитися програмувати. ББК 32.973-018я721

ISBN 978-966-451-000-1(б-ка ISBN 978-966-451-075-9

«Шк. світу») © А. Ставровський, І. Скляр, 2007 © ТОВ Видавництво «Шкільний світ», дополіграфічна підготовка, 2007


ЗМІСТ

Розділ 1. Основні поняття алгоритмізації 1. Програми, дані, інформаційні моделі ................................................ 5 2. Алгоритми та їхні властивості ............................................................. 6 3. Модель комп’ютера .......................................................................... 10 4. Мови програмування високого рівня .............................................. 11 5. Етапи створення програми ............................................................... 12 6. Кроки роботи з програмою .............................................................. 13 Розділ 2. Представлення чисел 1. Позиційні системи числення ............................................................ 17 2. Перетворення чисел з недесяткової системи в десяткову ............... 19 3. Перетворення з десяткової системи в недесяткову ......................... 20 4. Зв’язок двійкових, вісімкових та шістнадцяткових записів ............... 23 5. Додавання та множення у позиційних системах числення ............ 24 6. Внутрішнє представлення числових даних ...................................... 28 Розділ 3. Програмування лінійних алгоритмів мовою Turbo Pascal 7.0 1. Алфавіт і словник мови ..................................................................... 33 2. Тип — значення та операції .............................................................. 35 3. Типи цілих чисел ............................................................................... 36 4. Булів тип ............................................................................................. 38 5. Тип символів ..................................................................................... 38 6. Поняття перелічуваного типу ........................................................... 39 7. Типи дійсних чисел ........................................................................... 40 8. Змінні величини ................................................................................ 42 9. Вирази ................................................................................................ 43 10. Присвоювання значення змінній .................................................... 45 11. Структура та приклади програм ..................................................... 46 12. Виведення значень виразів на екран .............................................. 49 13. Уведення значень у змінні ............................................................... 51 14. Іменування виразів з константами та ініціалізація змінних ........... 52 15. Сумісність типів ............................................................................... 54 16. Побітові операції з цілими числами ............................................... 55


Îñíîâí³ ïîíÿòòÿ àëãîðèòì³çàö³¿

Розділ 4. Розгалуження, вибір варіантів та складені оператори 1. Умовні вирази .................................................................................... 59 2. Оператори розгалуження ................................................................. 61 3. Складений оператор .......................................................................... 64 4. Оператор вибору варіантів ............................................................... 66 Розділ 5. Циклічні алгоритми 1. Оператор циклу з параметром ......................................................... 72 2. Приклади організації циклів з параметром ...................................... 73 3. Оператор циклу з передумовою ...................................................... 80 4. Приклади організації циклів з передумовою ................................... 81 5. Оператор циклу з післяумовою ....................................................... 87 6. Приклади організації циклів з післяумовою .................................... 88 7. Поняття структурного програмування ............................................ 92 Розділ 6. Допоміжні алгоритми, або підпрограми 1. Підпрограма — опис розв’язання підзадачі .................................... 97 2. Процедура — підпрограма, яка реалізує окремий оператор......... 98 3. Два способи підстановки аргументів на місце параметрів .......... 103 4. Функція — підпрограма обчислення значення, потрібного у виразі ................................................................................................ 104 5. Область дії оголошень імен ............................................................ 107 6. Виконання виклику підпрограми ................................................... 108 7. Приклади використання підпрограм .............................................. 110 8. Глибина рекурсії та загальна кількість рекурсивних викликів ...... 122

4


ðîçä³ë

1 ÎÑÍÎÂͲ

ÏÎÍßÒÒß ÀËÃÎÐÈÒ̲ÇÀÖ²¯

1. ПРОГРАМИ, ДАНІ, ІНФОРМАЦІЙНІ МОДЕЛІ Сучасна людина використовує комп’ютер для розв’язання різноманітних задач — від виконання простих арифметичних дій до моделювання атмосферних явищ та керування польотами в космос. Насправді, все, що «вміє» комп’ютер, — це виконання програм. Щоб комп’ютер міг розв’язувати потрібну задачу, необхідно спочатку створити відповідну програму, а потім запустити її на виконання. Програма — це опис тих дій, які має виконати комп’ютер, щоб розв’язати деяку задачу. Програмування — це діяльність, яка полягає у створенні програм. Мета програмування — забезпечити, щоб замість людини ту чи іншу роботу виконував комп’ютер. Усі дії комп’ютера пов’язано з обробкою даних — числових, символьних, текстових тощо. Дані представляють (позначають) деякий зміст, або інформацію. Поняття змісту та інформації не мають чіткого означення або тлумачення. Будемо розуміти їх як знання, відомості про щось. Приклади. Слово НЕБО записано чотирма буквами. Зміст, який позначено ними, не є чітким, але для кожної людини це щось велике над головою, блакитне вдень та чорне вночі. Уточнювати далі не будемо. Запис 1001 представляє число — уявне поняття, яке виражає кількість. Ми сприймаємо ці дані як «тисяча й один» (предмет, рік тощо) або «тисяча й одна» (ніч, дрібниця тощо). Коли батьки реєструють народжену дитину, вони отримують свідоцтво про народження. У ньому вказано ім’я та прізвище нової людини, дату й місце народження, імена батьків. Ці дані про людину представляють факт і деякі з обставин її появи на світ. ‹* * Закінчення прикладу позначатимемо знаком ‹. 5


Îñíîâí³ ïîíÿòòÿ àëãîðèòì³çàö³¿

Отже, представлення об’єктів реального світу за допомогою даних є основою будь-якої взаємодії, зокрема й комп’ютера із «зовнішнім» світом. Комп’ютер має розв’язувати задачі для людини. У цих задачах фігурують різноманітні об’єкти реального світу — картинки, фізичні явища, особи, технологічні процеси тощо. Щоб запрограмувати обробку даних, пов’язаних із цими об’єктами, треба спочатку представити об’єкти у вигляді даних. Представлення об’єкта або явища за допомогою даних про нього називають його інформаційною моделлю. Інформаційна модель, як правило, містить не лише дані, а й деякий опис їх обробки, яка моделює реальні процеси, пов’язані з об’єктом. Узагалі, модель — це спрощене представлення якогось об’єкта або явища, яке відображає його необхідні суттєві властивості. Існує безліч різновидів інформаційних моделей. Один і той самий об’єкт або явище може мати кілька різних моделей, що виражають «свої» властивості, потрібні з різних точок зору на об’єкт або явище. Приклади. Свідоцтво про народження або паспорт є дуже простою інформаційною моделлю людини. Складнішою є, наприклад, медична картка, яка містить дані про фізичні та фізіологічні властивості людини, зміну їх у часі тощо. Зовсім інші дані про людину присутні в табелі навчання або атестаті. Щоб розв’язати квадратне рівняння вигляду ax 2 + bx + c = 0 , потрібні лише його коефіцієнти — числа a, b, c, тобто трійка цих чисел (дані) представляє рівняння. Формули коренів, по суті, описують дії з розв’язання рівняння (обчислити дискримінант тощо). Кожне сучасне підприємство має програмні системи, які автоматизують облік кадрів, заробітної платні, фінансової та виробничої діяльності. Кожна з цих систем має «свої» дані, пов’язані з підприємством, які використовуються та обробляються тільки в ній. Ці дані складають частину відповідної (кадрової, фінансової тощо) моделі підприємства. Інша частина моделі описує, як присутні в моделі дані використовуються та обробляються. ‹

2. АЛГОРИТМИ ТА ЇХНІ ВЛАСТИВОСТІ Алгоритм — це опис послідовності дій, які треба виконати, щоб розв’язати деяку задачу. Позначення дій в алгоритмі називаються командами або інструкціями. 6


2. Àëãîðèòìè òà ¿õí³ âëàñòèâîñò³

Як правило, в алгоритмі вказано деякі вхідні, результатні (вихідні) та проміжні дані, що не є ні вхідними, ні вихідними. Послідовність дій, яку виконують за алгоритмом, називається процесом. Алгоритм, як правило, визначає не один процес, а деяку їх множину. Приклад. Розглянемо задачу «обчислити дійсні корені квадратного рівняння ax 2 + bx + c = 0 , заданого коефіцієнтами a, b, c (за умови a ≤ 0)». Алгоритм розв’язання цієї задачі, тобто опис визначення коренів, може мати такий вигляд: Прочитати коефіцієнти a, b, c. Обчислити дискримінант d = b*b – 4*a*c. Якщо d > 0, то обчислити x1 = x2 =

−b + d 2a

−b − d , 2a

та написати ці числа;

інакше, якщо d = 0, то обчислити x =

−b й написати це число; 2a

інакше написати «дійсних коренів немає». У цьому алгоритмі вхідними даними є коефіцієнти a, b, c, проміжними — дискримінант d, вихідними — два корені (можливо, один) або текст «дійсних коренів немає». За цим алгоритмом, залежно від конкретних вхідних даних, можливе виконання однієї з трьох таких послідовностей дій. 1. Прочитати коефіцієнти, обчислити d, перевірити, що d > 0 (і це так), обчислити x1, x2 й написати ці числа. 2. Прочитати коефіцієнти, обчислити d, перевірити, що d > 0 (і це не так), перевірити, що d = 0 (і це так), обчислити x і написати це число. 3. Прочитати коефіцієнти, обчислити d, перевірити, що d > 0 (і це не так), перевірити, що d = 0 (і це не так), і написати, що дійсних коренів немає. Дуже часто алгоритм створюється шляхом поступового уточнення понять, пов’язаних із об’єктом, та потрібних дій. Приклад. Розглянемо алгоритм одягання дитини. Спочатку нам відомо, що є верхній та нижній одяг, і на першому кроці опис одягання має такий вигляд. Надягти нижній одяг. Надягти верхній одяг. 7


Îñíîâí³ ïîíÿòòÿ àëãîðèòì³çàö³¿

Потім уточнюємо, що таке нижній одяг і що треба зробити, щоб його надягти. Надягти трусики. Надягти майку. Надягти шкарпетки. Далі уточнюємо, що таке верхній одяг і в чому різниця в одязі хлопчиків та дівчат. Згадуємо про взуття. Надягти сорочку. Якщо дитина є хлопчиком, то надягти штанці, інакше надягти спідничку. Взути сандалії. Алгоритм одягання дитини має такий остаточний вигляд. Надягти трусики. Надягти майку. Надягти шкарпетки. Надягти сорочку. Якщо дитина є хлопчиком, то надягти штанці, інакше надягти спідничку. Взути сандалії. Очевидно, що цей алгоритм задає два можливих процеси, які відрізняються надяганням штанців або спіднички. ‹ Алгоритми повинні мати кілька загальних властивостей: зрозумілість, результативність, однозначність, дискретність, масовість та виконуваність. Розглянемо їх. Зрозумілість. Для виконання алгоритму завжди потрібен виконавець. Це може бути людина або деяка технічна система, зокрема й комп’ютер. Наприклад, виконувати арифметичні дії, щоб розв’язати квадратне рівняння (і не тільки), може людина. Але вона може перекласти цю роботу на комп’ютер, якщо створить відповідну програму й примусить комп’ютер її виконати. Дії з одягання дитини здатна виконувати людина, а зі збирання приладів — спеціальна автоматична лінія. Зрозумілість алгоритму полягає в тому, що виконавець може правильно зрозуміти та виконати команди, записані в алгоритмі. Але команди завжди записуються згідно з деякою системою позначень. Система позначень деякого змісту називається мовою. Мова містить елементарні позначення, правила утворення складніших позначень із простіших та правила, за якими зміст і позначення відповідають одне одному. Отже, виконавець має розуміти мову запису алгоритму. 8


2. Àëãîðèòìè òà ¿õí³ âëàñòèâîñò³

Результативність. Виконання будь-якого алгоритму повинно приносити його виконавцеві або іншій особі відчутні результати. Наприклад, «корені рівняння визначено», «дитину одягнуто», «прилад зібрано» тощо. Однозначність. Алгоритм не повинен містити команд, зміст яких можна сприйняти неоднозначно. Наприклад, якби в алгоритмі одягання дитини була команда «надягти штанці або спідничку», виконавець не знав би, що ж саме йому робити. Окрім того, після виконання кожної команди виконавець має точно знати, що йому робити далі. Дискретність. Дискретність алгоритму полягає в тому, що він задає послідовність дій, чітко відокремлених одна від одної. Отже, дії, задані командою, мають починатися лише після закінчення дій за попередньою командою. Окрім того, виконання кожної команди повинно займати обмежений проміжок часу. Масовість. Конкретні об’єкти, до яких застосовуються дії під час виконання алгоритму, визначають конкретні задачі, які часто називаються екземплярами задачі. Наприклад, конкретна трійка чисел 3, 10, 2, відповідна квадратному рівнянню, яке треба розв’язати, або конкретний хлопчик Вася, якого треба одягнути. Масовість алгоритму полягає в тому, що він описує не один, а деяку множину процесів, які відбуваються при розв’язанні всіх можливих екземплярів задачі. Переважна більшість алгоритмів є масовими, хоча існують і алгоритми, що задають тільки один процес. Виконуваність. Алгоритм має бути таким, щоб на кожному екземплярі задачі його можна було виконати до кінця (й отримати результат). Кожен процес, заданий алгоритмом, має бути скінченним і тривати скінченний час. Окрім того, процес не повинен обриватися без отримання результату. Приклад. Ділення чисел у стовпчик, тобто утворення десяткового дробу, може бути, залежно від конкретних чисел, як скінченним, так і нескінченним (десятковий дріб для 5/4 є скінченним, для 5/3 — ні). Ще приклад: якщо серед дій, заданих деяким алгоритмом, є ділення, і дільником є 0, то виконати це ділення неможливо, і незрозуміло, що робити далі. ‹ Алгоритм має враховувати можливість нескінченного виконання дій або неможливість виконати наступну дію й містити команди, які забезпечують закінчення дій з деяким результатом за будь-яких умов. У деяких ситуаціях розглядають реальну виконуваність, враховуючи вимоги до тривалості процесу, які висуваються в конкретних задачах. Існують такі задачі, для яких секунда — це занадто довго, а є й такі, що розв’язуються протягом кількох діб. 9


Îñíîâí³ ïîíÿòòÿ àëãîðèòì³çàö³¿

3. МОДЕЛЬ КОМП’ЮТЕРА Дамо спрощений погляд на виконавця програм — комп’ютер. Його загальну структуру наведено на рис. 1 (насправді вона набагато складніша). З основних елементів комп’ютера назвемо лише материнську плату, центральний процесор, оперативну пам’ять та зовнішні пристрої. На материнській платі розташовані: центральний процесор, оперативна пам’ять (ОП), центральна магістраль для зв’язку між усіма пристроями комп’ютера, гнізда для підключення інших плат керування зовнішніми пристроями та деякі інші елементи. Зовнішні пристрої (пристрої уведення-виведення, або ПУВ) — це дисплей (монітор), клавіатура, маніпулятор «миша», дисководи та інші (сканер, модем тощо). Вони керують обробкою даних на зовнішніх носіях, наприклад, магнітних дисках. ПУВ мають власні процесори, які можуть переносити дані з зовнішніх носіїв до оперативної пам’яті або навпаки. Програма для комп’ютера являє собою послідовність команд, основний зміст яких — обробка даних. Центральний процесор зчитує дані й команди програми з оперативної пам’яті та виконує їх. Команди задають зчитування даних з пам’яті, створення нових даних і запис їх у пам’ять. Є також команди, за якими дані надходять до зовнішніх пристроїв або зчитуються з них.

Оперативна пам’ять

Екран (дисплей)

Центральний процесор

Клавіатура

Материнська плата «Миша»

Дисковод Дисковод Рис. 1. Загальна схема комп’ютера

10


4. Ìîâè ïðîãðàìóâàííÿ âèñîêîãî ð³âíÿ

Усі дані в комп’ютері представлено у двійковій системі, яка має лише дві цифри — 0 і 1. Значення 0 і 1 відповідають двом стійким станам елемента пам’яті, що називається біт (bit, або binary digit — двійкова цифра). Вісім послідовних бітів утворюють байт. Байт може мати 28 = 256 станів і представляти кожним з них одне з 256 значень, наприклад, ціле число від 0 до 255. Ці самі стани байта можуть розглядатися як числа від – 128 до 127 (їх кількість теж 256) або щось інше. Оперативна пам’ять являє собою послідовність байтів, в якій кожен байт має свій номер — адресу. Деяке значення (число, символ тощо) у пам’яті, як правило, займає кілька сусідніх байтів і вказується адресою першого з них. Представлення чисел в оперативній пам’яті докладніше описано в наступному розділі. У байтах вимірюють обсяги пам’яті та передавання даних, для чого також використовують такі одиниці, як Kбайт («кейбайт», 210 = 1024 байт, або не зовсім точно «кілобайт»), Мбайт («мегабайт», 2 20 = 1048576 байт) і Гбайт («гігабайт», 230 = 1073741824 байт). Розмір оперативної пам’яті вимірюється, як правило, десятками й сотнями Мбайт, і середній розмір пам’яті комп’ютерів щороку відчутно зростає. Машинні команди, як і дані, також записуються послідовностями 0 і 1. Спрощено можна сказати, що команда містить код машинної операції та адреси даних, до яких застосовується операція. Система команд, які може виконувати процесор, називається машинною мовою. Для людини машинні мови дуже незручні, вони також вимагають глибоких знань про устрій вузлів комп’ютера та подробиці виконання програми. Цими мовами користуються лише розробники комп’ютерів та деякі інші спеціалісти.

4. МОВИ ПРОГРАМУВАННЯ ВИСОКОГО РІВНЯ Приблизно у середині 50-х років XX століття було розпочато пошук мов програмування, які мали бути ближчими за формою до мови математичних формул та людських мов, а також вільними від «машинних» подробиць. Водночас, ці мови мали бути такими, щоб записані ними програми можна було перекласти у відповідні машинні програми за допомогою самої машини. І такі мови програмування невдовзі було розроблено. 11


Îñíîâí³ ïîíÿòòÿ àëãîðèòì³çàö³¿

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

5. ЕТАПИ СТВОРЕННЯ ПРОГРАМИ Процес створення програми в найзагальніших рисах має кілька основних етапів: • аналіз задачі та уточнення її постановки; • проектування програми; • розробка програми (кодування); • остаточна перевірка програми (тестування); • передача замовникові. Розкриємо зміст наведених етапів стосовно шкільного навчання. Аналіз задачі та уточнення її постановки. Спочатку умову задачі дає вчитель або її треба прочитати у задачнику. Дуже часто умову сформульовано недостатньо точно й однозначно, тому її необхідно уточнити, задаючи питання вчителеві. Точна постановка задачі дозволяє зрозуміти, які дії треба виконати для її розв’язання. Проектування програми. Тут поступово уточнюють дії з розв’язання задачі та уточнюють їх опис. З’ясовують і уточнюють дані, потрібні для розв’язанні задачі. Дуже часто в задачі можна виділити кілька підзадач і описати їх розв’язання окремо. Тоді й алгоритм складається зі зв’язаних та узгоджених між собою частин (допоміжних алгоритмів), які описують розв’язання підзадач. Розробка програми (кодування). Коли дії та дані уточнено до вигляду, в якому їх можна подати в мові програмування, починають розробку програми. Найчастіше програму записують мовою високого рівня (інколи окремі її частини різними мовами). Розроблюючи програму, можна припуститися помилок, які виявляються при трансляції або виконанні програми. Помилки необхідно виявляти й виправляти, тобто налагоджувати програму. Налагодження програми полягає в тому, що її багаторазово запускають зі спеціально підібраними вхідними даними, які допомагають віднайти помилки. 12


6. Êðîêè ðîáîòè ç ïðîãðàìîþ

Остаточна перевірка програми (тестування). В реальних виробничих процесах програму розробляють програмісти, а остаточно перевіряють інші спеціалісти — тестувальники. Тестування починається, коли програмісти упевнені, що програма (або деяка її частина) є правильною. Як правило, спочатку тестувальники все-таки виявляють помилки й цикл «розробка-тестування» повторюється. В умовах школи ситуація аналогічна, тільки в ролі програміста виступає учень, а тестувальника — вчитель. Передача замовникові. У реальному житті замовникові передається як програма, так і необхідна супроводжувальна документація з її використання. В умовах школи цьому відповідає те, що учень здає програму та звіт із виконаної роботи. Окрім представлених етапів, реальні програми мають ще етап супроводження — він полягає в тому, що розробник виправляє помилки, виявлені під час експлуатації програми, модернізує програму й передає її оновлені варіанти користувачам тощо.

6. КРОКИ РОБОТИ З ПРОГРАМОЮ Типова послідовність роботи з програмою містить такі кроки: набирання тексту; компіляція; компонування; завантаження та виконання або інтерпретація. Набирання тексту. Текст програми мовою високого рівня (вхідний текст) найчастіше набирають за допомогою спеціальної програми (текстового редактора) і, як правило, записують на диск у вигляді вхідного файла (рис. 2). Програма може складатися з кількох файлів — у великих програмах їх можуть бути десятки й сотні. Компіляція. Компілятор — це програма, при виконанні якої читається вхідний текст і створюється його машинний еквівалент — об’єктний код (див. рис. 2). ОП

Текстовий редактор

Текст програми, який набирається на клавіатурі та виводиться на екран

Транслятор

Текст програми

Об’єктний код Диск у файлі

Рис. 2. Створення та компіляція тексту 13


Îñíîâí³ ïîíÿòòÿ àëãîðèòì³çàö³¿

Як правило, об’єктний код програми містить далеко не всі необхідні команди — програма може складатися з частин; деякі з яких є стандартними. Компонування. Об’єктний код обробляє ще одна програма — компонувальник. Ця програма «збирає з частин» (компонує) виконуваний код (машинну програму) і записує його або в оперативну пам’ять (завантажує), або на диск у вигляді файла, готового до виконання (рис. 3). Такий файл можна завантажити пізніше. Виконується компонувальник

Бібліотека системи програмування та інші підпрограми

Програма (виконуванй код)

Об'єктний код

Виконуваний код

ОП

Диск

Рис. 3. Створення виконуваної машиною програми

Завантаження та виконання. Запис машинної програми в оперативну пам’ять називається завантаженням (рис. 4). Його здійснює спеціальна програма — завантажувач, який може входити до складу компонувальника. Якщо завантаження здійснено успішно, починається процес виконання завантаженої програми. Програмазавантажник

Завантажена копія програми

ОП

Диск

Програма у файлі Рис. 4. Завантаження програми

Інтерпретація. Це такий спосіб обробки високорівневої програми, за яким машинна програма не створюється (рис. 5). Вхідна високорівнева програма обробляється спеціальною програмою — інтерпретатором. При цьому дії вхідної програми, які вона задає, одразу виконуються. Як 14


6. Êðîêè ðîáîòè ç ïðîãðàìîþ

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

Інтерпретатор

Бібліотека системи програмування та інші підпрограми

Вхідний текст програми

Диск

Дані на вході та на виході програми

Рис. 5. Інтерпретація високорівневої програми

Інколи компіляцію та інтерпретацію узагальнюють словом трансляція, називаючи трансляторами усі види програм перетворення вхідних текстів до машинного чи проміжного вигляду. Інтерпретація програми використовується у такому інструменті, як налагоджувач. Він забезпечує інтерпретацію вхідної програми невеликими порціями (кроками) і дає можливість побачити результати виконання після кожного кроку. Це полегшує виявлення помилок у програмі. Описані засоби (текстовий редактор, компілятор та/або інтерпретатор, компонувальник, завантажувач і налагоджувач), як правило, утворюють систему програмування, або інтегроване середовище. Крім них, до складу цієї системи входить бібліотека стандартних підпрограм, які можна використовувати під час створення програми. Що краще розробник програми володіє технологіями, інструментом та бібліотеками, то його робота ефективніша. КОНТРОЛЬНІ ЗАПИТАННЯ 1. Що таке інформаційна модель? Назвіть приклади інформаційних моделей. 2. Що таке алгоритм? 3. Чим алгоритм відрізняється від процесу розв’язання задачі? 4. Назвіть властивості алгоритму. 5. Які функціональні блоки має комп’ютер? Яке їх призначення? 6. Що входить до складу зовнішньої пам’яті? 15


Îñíîâí³ ïîíÿòòÿ àëãîðèòì³çàö³¿

7. Що таке біт і байт? Скільки станів вони можуть мати? 8. Чому числа в комп’ютері подані у двійковій системі? 9. З яких елементів складається машинна програма? 10. Чим мови програмування високого рівня відрізняються від машинних мов? 11. Що таке трансляція? 12. Чим відрізняється компіляція від інтерпретації? 13. Назвіть основні етапи створення програми. 14. У чому полягає наладка програми? 15. Які засоби входять до складу інтегрованого середовища?

16


ðîçä³ë

2

ÏÐÅÄÑÒÀÂËÅÍÍß ×ÈÑÅË

1. ПОЗИЦІЙНІ СИСТЕМИ ЧИСЛЕННЯ Людина звикла до десяткової системи запису чисел (системи числення). Ця система поступово удосконалювалася протягом тисячоліть, починаючи з давніх Вавилону та Індії. У Середньовіччі вона стала відома арабам і завдяки їм прийшла в Європу. У десятковій системі є десять знаків — цифр, якими записують числа від 0 до 9. Великі числа записують тими самими знаками, але не одним, а двома й більше, тобто число записують як послідовність знаків. У цій послідовності знаки мають різні позиції і тому цифра праворуч позначає кількість одиниць, наступна — кількість десятків, і так далі. Отже, одна й та сама цифра залежно від позиції має «різну вагу». Наприклад, у записі 32 цифра 2 задає дві одиниці, а у записі 23 — два десятки. Тому цю систему запису чисел називають позиційною. Історія людства залишила у спадок не лише десяткову систему запису чисел. У деяких країнах люди й дотепер підраховують предмети дюжинами (12 предметів) та гросами (12 дюжин). Для запису чисел у такій системі потрібні 12 різних знаків. Деякі народи використовували 60 різних знаків, деякі — п’ять. Усі вказані системи мають різні кількості знаків (10, 12, 60,†5), які називаються основами. Окрім позиційних систем, відомі й непозиційні. Деяке уживання й дотепер має римська система числення, що виникла в Давньому Римі. У цій системі запис наступного числа утворюється не новою цифрою, а додаванням цифри: І, ІІ, ІІІ, тому її називають адитивною. Звичайно, 17


Ïðåäñòàâëåííÿ ÷èñåë

правила утворення записів чисел цим не вичерпуються; вони набагато складніші, ніж у позиційних системах, і розглядати їх докладніше не будемо. Особливо незручно в римській системі виконувати арифметичні операції, і недарма вона не стала «робочою» для людства. Повернемося до позиційної десяткової системи. Цифру праворуч у запису числа називають молодшою («її записа��о в молодшому розряді»), ліворуч — старшою. Розряд одиниць називають нульовим, розряд десятків — першим, сотень — другим тощо. За такої нумерації вага розряду відповідає степеню числа 10: одиниця — це 10 0 , десяток — це 101 , сотня — це 102 тощо. Отже, розташування цифри в тому чи іншому розряді є прямою вказівкою, на який степінь числа 10 треба помножити цифру. Звідси число можна записати як суму добутків цифр числа на відповідні степені десятки. Наприклад, 5834 = 5 ⋅ 103 + 8 ⋅ 10 2 + 3 ⋅ 101 + 4 ⋅ 100 . Якщо запис числа має дробову частину, то додаються цифри, ділені на число 10 у відповідному степені, наприклад,

1 1 1 + 3⋅ 2 + 4 ⋅ 3 1 10 10 10 Розглянемо яку-небудь позиційну систему числення з основою, відмінною від 10, наприклад, 7. Аналогічно до десяткової, ця система повинна мати такі властивості: • для запису чисел є сім цифр— 0, 1, 2, 3, 4, 5, 6; • значення цифри залежить від її розташування (позиції) в записі; • вага кожного розряду числа є відповідним степенем сімки. 0,234 = 2 ⋅

Отже, перші числа записуються як 0, 1, Ö, 6, а далі йдуть записи 10, 11, Ö , 16, 20, 21, Ö, 66, 100, … . Сімкове 10 — це звичне десяткове 7 (але ж немає такого знака в сімковій системі!), сімкове 11 — це звичне 8, 20 — це звичне десяткове 14, тобто двічі по 7, тощо. А сімкові 100 та 200 — це десяткові 49 та 98, тобто один та два рази по 7 у квадраті. А тепер подамо сумами степенів сімки такі числа (про сімковий запис свідчить маленька цифра 7 біля числа): 342 7 = 3 ⋅ 7 2 + 4 ⋅ 7 1 + 2 ⋅ 7 0

63017 = 6 ⋅ 73 + 3 ⋅ 7 2 + 0 ⋅ 71 + 1⋅ 7 0 18


2. Ïåðåòâîðåííÿ ÷èñåë ç íåäåñÿòêîâî¿ ñèñòåìè â äåñÿòêîâó

0,12 7 =

1 2 + 2 1 7 7

5 6 + 71 72 Цей спосіб запису використовується у позиційних системах числення з будь-якою основою (більше 1). Питання в тому, які знаки є цифрами. Якщо основа не більше десяти, використовують звичний набір десяткових цифр (з нього беруть стільки знаків, скільки треба). Проте для системи з основою більше десяти потрібні додаткові знаки, щоб позначати десяткові числа 10, 11, … . Де їх узяти? У XX столітті для цього стали використовувати послідовні великі літери латинського алфавіту — A, B, C тощо. Приклади. У двійковій системі числення лише дві цифри — 0 та 1. 32,567 = 3 ⋅ 71 + 2 ⋅ 70 +

Двійковий запис 1101 позначає число 1 ⋅ 2 3 + 1 ⋅ 2 2 + 0 ⋅ 21 + 1 ⋅ 2 0 = 13 , запис 0,101 — число 1⋅ 2 −1 + 0 ⋅ 2−2 + 1⋅ 2 −3 = 0,625 . У шістнадцятковій системі числа від 10 до 15 позначають так: 10†ó†A, 11†ó†B, 12†ó†С, 13†ó†D, 14†ó†E, 15†ó†F. Тоді 2 A16 = 2 ⋅161 + 10 ⋅ 16 0 = 42 , FF16 = 15 ⋅161 + 15 ⋅16 0 = 255 , 0, C16 = 12 ⋅ 16 −1 = 3 / 4 = 0,75 .‹ • Запис

xn … x1 x 0 .x −1 … x − k з P-ковими цифрами задає число

x n ⋅ P n + … + x1 ⋅ P 1 + x 0 ⋅ P 0 + x −1 ⋅ P −1 + … + x −k ⋅ P −k . Цифри x у цій сумі позначають числа від 0 до P–1.

2. ПЕРЕТВОРЕННЯ ЧИСЕЛ З НЕДЕСЯТКОВОЇ СИСТЕМИ В ДЕСЯТКОВУ У комп’ютерах числа подані за допомогою елементів, які мають два стійкі стани, відповідні значенням 0 і 1, тобто двійковим цифрам. Низка таких елементів своїми станами представляє послідовність двійкових цифр, тобто число у двійковому записі. Отже, у комп’ютері числа подані та обробляються в двійковій системі. Проте людям зручніше вводити числа в комп’ютер та отримувати їх від 19


Ïðåäñòàâëåííÿ ÷èñåë

нього в десятковій системі. Так виникає задача переведення запису числа з однієї системи в іншу, зокрема, з двійкової в десяткову та навпаки. Запис x n … x1 x 0 .x −1 … x −k з недесятковими (P-ковими) цифрами задає число, яке є сумою чисел, позначених цифрами й помножених на відповідні степені основи P. Отже, щоб знайти десяткове представлення числа, треба: представити в десятковій системі число P та цифри запису; обчислити суму добутків значень цифр та відповідних степенів числа P. Приклади

100112 = 1⋅ 2 4 + 0 ⋅ 23 + 0 ⋅ 22 + 1⋅ 21 + 1⋅ 20 = 19 10,0112 = 1⋅ 21 + 0 ⋅ 2 0 + 0 ⋅ 2−1 +1⋅ 2−2 + 1⋅ 2 −3 = 2,375 1BC16 = 1 ⋅ 16 2 + 11 ⋅161 + 12 ⋅ 16 0 = 444 1B,C 16 = 1 ⋅ 161 + 11 ⋅ 16 0 + 12 ⋅ 16 −1 = 27 ,75 ; 12,2 3 = 1 ⋅ 31 + 2 ⋅ 3 0 + 2 ⋅ 3 −1 = 5,666... , тобто представляється нескінченним періодичним десятковим дробом. ‹ Нагадаємо: якщо основа P має прості дільники, відмінні від 2 та 5, то дробове число зі скінченним P-ковим записом може мати нескінченне періодичне десяткове подання. Якщо ж простими дільниками P є лише 2 і 5, то й десятковий дріб скінченний.

3. ПЕРЕТВОРЕННЯ З ДЕСЯТКОВОЇ СИСТЕМИ В НЕДЕСЯТКОВУ Переведення цілих чисел. Розглянемо, як за натуральним числом T у десятковому записі отримати цифри P-кового представлення. Нам не відомі ні самі цифри, ні їх кількість. Але відомо, що P-ковий запис задає число як суму добутків значень цифр на відповідні степені числа P, тобто при деякому невідомому n і невідомих цифрах xi справджується рівність

T = xn ⋅ P n + … + x1 ⋅ P1 + x0 ⋅ P 0 . Помітимо: всі доданки, окрім останнього, мають множник P. Тоді значенням молодшої цифри x0 є остача від ділення T на основу P, а сума T1 = x n ⋅ P n −1 + ... + x1 ⋅ P 0 дорівнює цілій частці від ділення T на P. Поді20


3. Ïåðåòâîðåííÿ ç äåñÿòêîâî¿ ñèñòåìè â íåäåñÿòêîâó

ливши цю суму на P з остачею, знайдемо остачу x1 і наступну частку, і так далі, поки на якомусь кроці частка від ділення не стане менше P. Вона й буде старшою цифрою x n . Приклади.†Перекладемо 202 у вісімкову систему. 202 : 8 = 25 (остача 2 — молодша цифра), 25 : 8 = 3 (остача 1 — наступна цифра). 3 : 8 = 0 (остача 3 — остання старша цифра). Отже, отримано цифри 2, 1, 3 (від молодшої до старшої), тобто вісімковий запис 3128 . ‹ Перекладемо 202 у 16-кову систему. 202 : 16 = 12 (остача 10 — значення молодшої цифри A), 12 : 16 = 0 (остача 12 — значення старшої цифри С), Отже, отримано 16-ковий запис CA16 . ‹ Позначимо остачу від ділення T на P націло як T mod†P, частку — як T div†P. Опишемо утворення P-кового запису числа T таким алгоритмом. 1. Спочатку частка T дорівнює N, а запис порожній. 2. Поки T > 0, обчислювати остачу R як T mod P та нову частку T як T div P (шляхом ділення у стовпчик), представляти R у P-ковій системі числення як цифру C(R) та дописувати до запису ліворуч. Переведення дробових чисел. Розглянемо, як за додатним дійсним числом V < 1 отримати цифри його P-кового запису (принаймні кілька перших, оскільки в дійсності запис може бути й нескінченним). Припустимо, що V = x−1 ⋅ P −1 + … + x−k ⋅ P −k + … з невідомими значеннями цифр. Якщо обидві частини рівності помножити на P, отримаємо рівність V × P = x −1 + x −2 ⋅ P −1 + … + x−k ⋅ P −k +1 + … , або V × P = x −1 + P −1 ( x−2 + … + x −k ⋅ P −k +2 + …) . Звідси x−1 = [V × P ] , а P −1 ( x −2 + … + x−k ⋅ P − k + 2 + …) = {V × P} , де [V × P] та {V × P} позначають цілу та дробову частини V × P. Далі так само помножимо {V × P} на P, знову отримаємо цілу та дробову частину (ціла буде значенням x−2 ), і так далі. 21


Ïðåäñòàâëåííÿ ÷èñåë

Приклади. Одержимо двійковий запис десяткового дробу 0,75. 0,75×2†=†1,5,[1,5]†=†1 (перша цифра), {1,5} = 0,5; 0,5×2†=†1,[1]†=†1 (наступна цифра), {1} = 0. Усі подальші цифри будуть 0, тому 0,11 є скінченним двійковим представленням для десяткового 0,75. Одержимо двійковий запис десяткового дробу 0,1. 0,1×2†= 0,2, [0,2]†=†0 (перша цифра), {0,2} = 0,2; 0,2×2†= 0,4, [0,4]†=†0 (наступна цифра), {0,4} = 0,4; 0,4×2†= 0,8, [0,8]†=†0 (наступна цифра), {0,8} = 0,8; 0,8×2†= 1,6, [1,6]†=†1 (наступна цифра), {1,6} = 0,6; 0,6×2†= 1,2, [1,2]†=†1 (наступна цифра), {1,2} = 0,2. Далі цифри 0, 0, 1, 1 будуть нескінченно повторюватися, тобто точний двійковий запис десяткового 0,1 є періодичним 0,0(0011), а будь-який початок цього запису позначає наближення до десяткового 0,1, наприклад, 0, 00011=

1 1 + = 0,09375(помилка — приблизно шість тисячних). 16 32

Одержимо 16-ковий запис десяткового дробу 0,8. 0,8×16 = 12,8, [12,8] = 12 (перша цифра C), {12,8}†=†0,8. Далі цифра C буде нескінченно повторюватися, тому 0,(C) є точним 16-ковим записом десяткового 0,8. ‹ Якщо V є скінченним десятковим дробом і основа Р має обидва прості множники 2 та 5, то число V можна представити скінченним P-ковим записом. В іншому випадку P-ковий запис може бути нескінченним, і тоді за скінченну кількість кроків буде отримано наближене представлення числа V. Отже, за дійсним числом V, V < 1, можна одержати R перших цифр його P-кового представлення, виконуючи такий алгоритм. 1. Спочатку представленням є «0.». 2. Поки одержано менше за R дробових цифр, ×P, обчислити d як [V× ×P] (ціле обчислити V× ×P}. число від 0 до P–1) та V як {V× Представити значення d як P-кову цифру та дописати її до представлення праворуч. Цілі та дробові числа переводяться в недесяткову систему за різними алгоритмами. Якщо треба перевести число, що має цілу та дробову частини, треба виділити ці частини й перевести їх окремо. Взагалі, перевести запис числа з недесяткової системи числення в іншу недесяткову не22


4. Çâ’ÿçîê äâ³éêîâèõ, â³ñ³ìêîâèõ òà ø³ñòíàäöÿòêîâèõ çàïèñ³â

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

4. ЗВ’ЯЗОК ДВІЙКОВИХ, ВІСІМКОВИХ ТА ШІСТНАДЦЯТКОВИХ ЗАПИСІВ Двійкова система не дуже зручна для людини, оскільки числа записуються в ній досить довгими послідовностями нулів і одиниць. Наприклад, десяткове число 1024 в двійковій системі числення виглядає як 10000000000. Для скорочення записів у програмуванні традиційно використовують вісімкову та шістнадцяткову системи числення. Вісімковим цифрам від 0 до 7 взаємно однозначно відповідають усі можливі трирозрядні двійкові коди (з незначущими нулями). 0 000 1 001 2 010 3 011 4 100 5 101 6 110 7 111 Аналогічно, шістнадцятковим цифрам від 0 до F відповідають усі можливі чотирирозрядні двійкові коди (також із незначущими нулями). 0 1 2 3

0000 0001 0010 0011

4 5 6 7

0100 0101 0110 0111

8 9 A B

1000 1001 1010 1011

C D E F

1100 1101 1110 1111

Наведений зв’язок між цифрами та групами двійкових ци��р забезпечує дуже простий спосіб отримання вісімкового та шістнадцяткового запису за двійковим і навпаки. Для переведення з вісімкової в шістнадцяткову систему й навпаки можна використати двійковий запис як проміжний. У двійковому записі можна, починаючи з молодшої цифри, замінити трійки цифр відповідними вісімковими цифрами (остання трійка може бути неповною — до неї допишемо незначущі нулі). Навпаки, вісімковий запис перетворюється на двійковий заміною цифр відповідними їм трійками двійкових. Трійка, що відповідає старшій цифрі, може мати незначущі нулі — вони не записуються. Приклад. Двійковий запис 1111010 розбивається на групи 1, 111, 010. Група 1 доповнюється до трійки 001. Трійці 001 відповідає вісімкова циф23


Ïðåäñòàâëåííÿ ÷èñåë

ра 1, трійці 111†ó†7, і, нарешті, 010†ó†2. Результатом перетворення є вісімкове 172. Навпаки, у вісімковому 172 цифрі 1 відповідає група двійкових розрядів 001, 7 — група 111, 2 — група 010. Результат перетворення — двійкове 1111010 (незначущі старші 00 не записуємо).‹ Перетворення двійкового запису на шістнадцятковий аналогічне, лише двійковий запис розбивають на групи з чотирьох розрядів, які замінюють відповідними шістнадцятковими цифрами. Навпаки, шістнадцятковий запис перетворюють на двійковий заміною цифр відповідними четвірками двійкових цифр. Приклад. Двійкове число 1111010 розбивається на групи 111 і 1010. Старша група 111 доповнюється до 0111; їй відповідає шістнадцяткова цифра 7, групі 1010 — цифра A. Результат перетворення — шістнадцятковий запис 7A. У шістнадцятковому записі 7A цифрі 7 відповідає тетрада 0111, цифрі A†ó 1010. Результат перетворення — двійкове число 1111010.‹ Аналогічний зв’язок між цими трьома системами існує й у записі дробових чисел. Групи по три двійкові цифри, починаючи від старших (перших після коми), відповідають вісімковим цифрам, а групи по чотири — шістнадцятковим. Остання група може бути неповною — до неї дописують незначущі нулі, але праворуч. Приклад. Двійковий запис 0,0101111101 розіб’ємо на групи по три цифри: 010, 111, 110 та 1. Останню групу доповнимо до 100 та отримаємо вісімковий запис 0,2764. Цей самий двійковий запис розіб’ємо на групи 0101, 1111, 01, останню групу доповнимо до 0100 та утворимо шістнадцятковий запис 0,5F4. ‹

5. ДОДАВАННЯ ТА МНОЖЕННЯ У ПОЗИЦІЙНИХ СИСТЕМАХ ЧИСЛЕННЯ ДОДАВАННЯ Додавання одноцифрових чисел учні вивчають за допомогою таблиць, у яких подано результати додавання чисел від 1 до 9, наприклад, 7 + 8 = 15, 9 + 1 = 10, 9+9 = 18. Аналогічні таблиці додавання неважко скласти для будьякої системи числення.

24


5. Äîäàâàííÿ òà ìíîæåííÿ ó ïîçèö³éíèõ ñèñòåìàõ ÷èñëåííÿ

Приклад. Для сімкової системи з цифрами 0, 1, 2, 3, 4, 5 та 6 таблиця 1 додавання має такий вигляд. Таблиця 1

0 1 2 3 4 5 6

0 0 1 2 3 4 5 6

1 1 2 3 4 5 6 10

2 2 3 4 5 6 10 11

3 3 4 5 6 10 11 12

4 4 5 6 10 11 12 13

5 5 6 10 11 12 13 14

6 6 10 11 12 13 14 15

Таблиця демонструє переставний закон додавання — заповнивши рядок таблиці, можна відразу отримати відповідний стовпчик. Якщо сума чисел не менш ніж 7, утворюється двоцифрова сума, тобто з’являється перенос 1 до старшого розряду. Так само в десятковій системі перенос з’являється, якщо сума не менше ніж 10. У системах з основою більше 10 потрібні цифри A, B, C тощо. Наприклад, два останні рядки таблиці додавання однорозрядних чисел з основою 16 мають такий вигляд: E F

0 E F

1 2 3 4 5 6 7 8 9 А В C D E F F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 10 11 12 13 14 15 16 17 18 19 1А 1B 1C 1D 1E

При додаванні одноцифрових чисел A та B у будь-якій P-ковій системі значенням цифри є остача від ділення (A+B) на P, переносом до старшого розряду — частка від ділення (A+B) на P. Якщо A+B < P, перенос дорівнює 0 («переносу немає»), а якщо A+B ≥ P, він дорівнює 1. Наведене правило дає основу для алгоритму додавання чисел у стовпчик у довільній P-ковій системі. Починаючи від молодшого розряду, цифри та перенос від попереднього розряду додаються, у розряді залишається остача від ділення суми на P, а частка є переносом до наступного розряду. Перенос у молодший розряд дорівнює 0.

25


Ïðåäñòàâëåííÿ ÷èñåë

Приклади. Додамо числа в сімковому записі; у дужках нагорі запишемо переноси. (110 ) + 45

(100 ) + 561

(1110 ) + 2656

26 104

64 655

5614 11603

Аналогічно додамо числа в двійковому запису, де 0+0 = 0, 1+0 = 0+1 = 1, 1+1 = 10. (100) + 10

(1100 ) + 11

(1110 ) + 111

11 101

110 1001

1 1000

Так само додамо числа в шістнадцятковому запису. (100 ) + 87

( 010 ) + 1F

(1 1 1 0) + FFF

95 11C

12 E 14 D

1 1 00 0

МНОЖЕННЯ Множення в недесяткових системах числення виконується також за допомогою відповідних таблиць. Розглянемо, наприклад, таблицю множення однорозрядних чисел з основами 2, 3 та 4. 0 1

0 0 0

1 0 1

0 1 2

0 0 0 0

1 0 1 2

2 0 2 11

0 1 2 3

0 0 0 0 0

1 0 1 2 3

2 3 0 0 2 3 10 12 12 21

Таблиці демонструють переставний закон множення — заповнивши рядок таблиці, можна відразу отримати відповідну колонку. Наведемо також вісім останніх рядків таблиці 2 множення з основою 16.

26


5. Äîäàâàííÿ òà ìíîæåííÿ ó ïîçèö³éíèõ ñèñòåìàõ ÷èñëåííÿ

Таблиця 2

8 9 A B C D E F

0 0 0 0 0 0 0 0 0

1 8 9 A B C D E F

2 10 12 14 16 18 1A 1C 1E

3 18 1B 1E 21 24 27 2A 2D

4 20 24 28 2C 30 34 38 3C

5 28 2D 32 37 3C 41 46 4B

6 30 36 3C 32 48 4E 54 5A

7 38 3F 46 3D 54 5B 62 69

8 40 48 50 58 60 68 70 78

9 48 51 5A 63 6C 75 7E 87

А 50 5A 64 6E 78 82 8C 96

В 58 63 6E 79 84 8F 9A А5

C 60 6C 78 84 90 9C A8 B4

D 68 75 82 8F 9C A9 B6 C3

E 70 7E 8C 9A A8 B6 C4 D2

F 78 87 96 A5 B4 C3 D2 E1

При множенні одноцифрових чисел A та B у будь-якій P-ковій системі значенням цифри є остача від ділення A × B на P, переносом до старшого розряду — частка від ділення A × B на P. За A × B < P перенос дорівнює 0. Наведене правило дає основу для алгоритму множення кількарозрядного числа на однорозрядне число X у стовпчик. Починаючи з молодшого розряду, обчислюється добуток Y значення цифри числа та X. До Y додається перенос від попереднього розряду й отримується сума S. Остача від ділення S на P у P-ковому представленні записується в результат, а частка є переносом до наступного розряду. Приклад. Помножимо числа відповідно в трійковій, четвірковій та шістнадцятковій системах; у дужках вгорі запишемо переноси. (110 ) × 212

(1220 ) × 123

(8D10) + 8E 2

2 1201

3 1101

F 853E

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

27


Ïðåäñòàâëåííÿ ÷èñåë

Приклад. Помножимо десяткові 12 і 21 у десятковій, двійковій, четвірковій, вісімковій та шістнадцятковій системах відповідно. × 12

21 12 24 252

×

1100

1100 1100 11111100

30

× 14

111 30 30

25 74 30 374

×

10101 1100

30 3330

×

C

15 3C C FC

• У всіх наведених прикладах ми ніколи не змішували записи чисел у системах з різними основами. Якщо числа записано в різних системах числення, то виконувати дії з ними чи порівнювати їх не можна.

6. ВНУТРІШНЄ ПРЕДСТАВЛЕННЯ ЧИСЛОВИХ ДАНИХ Як уже зазначалося, вісім послідовних бітів утворюють байт, який може мати 256 різних станів (комбінацій з 0 і 1). Їм можна поставити у взаємно однозначну відповідність цілі числа від 0 до 255, цілі числа від –128 до 127, пари 16-кових цифр або елементи якоїсь іншої 256-елементної множини. ПРЕДСТАВЛЕННЯ ЦІЛИХ ЧИСЕЛ Цілі числа подаються в комп’ютері переважно у двох формах — беззнаковій та знаковій. Ці форми називаються кодами. Далі числа ототожнюються з їх представленням, хоча з погляду математики це є хибним. Беззнаковi коди займають декілька байтів (найчастіше, 1, 2, 4 або 8). Байти нумеруються, починаючи з 0. Біти всередині байтiв нумеруються від 0 до 7. N байтів містять 8N бітів, тому використовують також наскрізну нумерацію бітів — від 0 до 8N – 1 (від молодших до старших). Усі можливі послідовності бітів представляють числа від 0 до 28 N −1 . Відповідність між кодами та числами при різних значеннях N представлено у такій таблиці 3. Знакові коди також займають 1, 2, 4 або 8 байт. Старший біт представляє знак числа: 0† ó ´ + ª , 1†ó ´ñª. Додатні числа представляються так само, як i беззнакові, лише за рахунок знакового біта їх діапазон — від 0 до 28 N −1 −1 . Таке представлення називається прямим кодом. Наприклад, …1. число 28 N −1 −1 має прямий код 011… 28


6. Âíóòð³øíº ïðåäñòàâëåííÿ ÷èñëîâèõ äàíèõ

Таблиця 3

Коди 11…11 11…10 11…01 … 10…00 01…11 … 00…10 00…01 00…00

Числа

28 N − 1 28 N –2 28 N –3

… 28 N −1

28 N −1 –1 … 2 1 0

N=1 255 254 253 … 128 127 … 2 1 0

N=2 65535 65534 65533 … 32768 32767 … 2 1 0

N=4 4394967295 4394967294 4394967293 … 2147483548 2147483647 … 2 1 0

Від’ємні числа представлено так званим додатковим кодом. Код від’ємного числа A позначається D (A) й утворюється так. 1. За прямим кодом числа |A| заміною всіх 0 на 1 і всіх 1 на 0 будуємо обернений код R(A). 2. За R(A) як беззнаковим цілим числом обчислюємо код D(A) = R(A)+1. Приклад. Побудуємо двобайтовий додатковий код числа –144. Прямий код |A| Обернений код R(A) Додавання 1 Додатковий код D(A)

0000 0000 1001 0000 1111 1111 0110 1111 1 1111 1111 0111 0000

«Відновити» за додатковим кодом від’ємне число можна у зворотному порядку. 1. D(A) розглядаємо як беззнакове ціле та обчислюємо R(A) = D(A)–1. 2. Код, обернений до R(A), є прямим кодом числа |A|. Проте можна обійтися без віднімання. Очевидно, що D(A) = R(|A| – 1), тобто, наприклад, додатковий код числа –144 водночас є оберненим кодом числа –143. Тоді код R(D(A)) є прямим кодом числа |A| – 1. Тоді прямий код |A| можна отримати так.: 1. Будуємо код R(D(A)), обернений до D(A). 2. До R(D(A)) як беззнакового цілого додаємо 1. 29


Ïðåäñòàâëåííÿ ÷èñåë

Відповідність між кодами та числами при різних значеннях N представлено в такій таблиці 4: Таблиця 4

Коди 01…11 … 00…10 00…01 00…00 11…11 11…10 11…01 … 10…00

Числа 28Nñ1ñ1 Ö 2 1 0 ñ1 ñ2 ñ3 Ö ñ28Nñ1

N=1 127 Ö 2 1 0 ñ1 ñ2 ñ3 Ö ñ128

N=2 32767 Ö 2 1 0 ñ1 ñ2 ñ3 Ö ñ32768

N=4 2147483647 Ö 2 1 0 ñ1 ñ2 ñ3 Ö ñ2147483548

Як бачимо, від’ємних чисел на одне більше, ніж додатних. ПРИНЦИПИ ПРЕДСТАВЛЕННЯ ДІЙСНИХ ЧИСЕЛ Деякі особливості представлення дійсних чисел тут опущено, щоб не загромаджувати викладення. Для дійсних чисел найчастіше використовують 4, 6, 8 або 10 байт, поділених на поля (послідовності бітів) <знак><порядок><мантиса>. Ці поля означають таке. Поле <знак> має довжину 1 біт, його значення 0 представляє знак «+», 1 — знак «–». Поле <порядок> у деякий спеціальний спосіб задає показник степеня числа 2 (він може бути як додатним, так і від’ємним). Поле <мантиса> означає дробову частину — невід’ємне число строго менше 1. Поля представляють число в такий спосіб. Мантиса додається до 1, сума множиться на степінь числа 2, заданий порядком, і враховується знак. Отже, число є значенням виразу вигляду: ±(1+дробова частина ) × 2 показник. Сума в цьому виразі не менше 1 і строго менше 2, тому кажуть, що це представлення числа є нормалiзованим. Дійсні числа в описаному представленні утворюють обмежену підмножину множини раціональних чисел; зокрема, серед них є число, найменше за модулем і відмінне від 0, а також число, найбільше за модулем. Між двома послідовними числами в описаному представленні є нену30


6. Âíóòð³øíº ïðåäñòàâëåííÿ ÷èñëîâèõ äàíèõ

льова різниця. Вона дуже мала, якщо числа близькі до 0, і досягає десятків тисяч, якщо числа близькі до найбільшого за модулем. Що більше байтів займає представлення, то більше чисел воно дозволяє закодувати й то менше між ними різниці. КОНТРОЛЬНІ ЗАПИТАННЯ 1. З чим пов’язують виникнення десяткової системи числення? 2. Які ви знаєте системи числення? 3. Що таке позиційна система числення? 4. Наведіть приклад адитивної системи числення. 5. Чи можна використовувати позиційні системи числення з цифрами, що мають різні основи? 6. Як кодуються цифри в системах числення з основою більше десяти? 7. Подати алгоритм переведення десяткового запису числа в систему числення, відмінну від десяткової. 8. Розповісти алгоритм переведення запису числа з недесяткової системи числення в десяткову. 9. Складіть таблицю додавання для системи числення з основою 16. 10. Складіть таблицю множення для системи числення з основою 11. 11. Які системи числення використовують у роботі з комп’ютером? 12. Розповісти алгоритм переведення двійкового запису числа у вісімковий (шістнадцятковий). 13. Розповісти алгоритм переведення запису числа з основою 8 (16) у двійковий. 14. Чому квадрат числа P – 1 у P-ковому запису обов’язково має старшу цифру P – 2, а молодшу 1. ЗАДАЧІ 1. Запишіть число як суму степенів основи системи числення з коефіцієнтами: а) 5768; b)†842,413; c)†12,223; d) А09,7F16. 2. Переведіть десяткове число в римську систему запису: а) 55; b)†995; c)†1547. 3. Переведіть римський запис чисел в арабський (позиційний десятковий): а) LX; b)†CXI; c)†MDCCCXII; d)†MCMLXI. 4. Запишіть число як суму степенів основи системи числення з коефіцієнтами та визначте десятковий запис числа: а) 2,336; b)†291,4D615; c)†245,5678; d)†A09,С3513; 31


Ïðåäñòàâëåííÿ ÷èñåë

e)†B,1F416; f)†2743,1611. 5. Переведіть десятковий запис у двійкову, вісімкову та шістнадцяткову системи: а) 94,341; b)†13,55; c)†2948,77; d)†382,33. 6. Перетворити десяткові записи чисел на двійкові, вісімкові та шістнадцяткові: а) 100; b)†255; c)†1024; d)†32767; e)†256; f)†640; g)†16382; h)†65537. 7. Перетворити шістнадцяткові записи чисел на вісімкові та десяткові: а) F1; b)†FF; c)†4AB; d)†FFFE; e)†1FF; f)†8A1; g)†7FFF. 8. Перетворити вісімкові записи чисел на шістнадцяткові та десяткові: а) 377; b)†1200; c)†1232; d)†400; e)†1777; f)†3777; g)†7777. 9. Додайте числа в заданій системі числення: а) 36 6 + 42 6 ; b)† 1123 + 213 ;

c)† 10012 + 110 2 ;

d)† 358 + 75 8 ;

e)† 7912 + A612 ; f)† 4511 + A911 . 10. Перемножте числа в заданій системі числення: а) 366 ⋅ 426 ;

b)† 1123 ⋅ 213 ;

c)† 10012 ⋅110 2 ;

d)† 358 ⋅ 758 ;

e)† 7912 ⋅ A612 ; f)† 4511 ⋅ A911 . 11. Подати 36-кові записи чисел ZY, 100 у десятковому записі (36-кові цифри A, B, …, Y, Z позначають десятковi числа 10, 11, …, 34, 35, відповідно). 12. За основою P та P-ковими записами дробів Q вказати їх десятковий запис: а) P = 2; Q = 0,0001; 0,1111; b)†P = 3; Q = 0,22; 0,(11); c)†P = 16; Q = 0,1; 0,F; 0,8; 0,(7); d)†P =2; Q = 0,001; 0,1101; 0,000001; e)†P = 3; Q = 0,(20); 0,(02); 0,11; f)†P = 16; Q = 0,1F; 0,FE; 0,81; 0,3(F). 13. Записати P-ковий заприс десяткового дробу Q, де: а) Q = 0,5; P = 2, 3, 5, 8, 16, 20; b)†Q = 0,1; P = 3, 5, 8, 16, 20; c)†Q = 0,75; P = 2, 3, 5, 8, 16, 20; d)†Q = 0,2; P = 3, 5, 8, 16, 20. 14. Вказати двобайтовий додатковий код чисел: а) –1, –8, –9, –32767, –32768; b)†ñ255, ñ256, ñ640, ñ641. 15. Нехай при додаванні та відніманні чисел у знаковому записі перенесення із старшого цифрового розряду перетворюється на зміст знакового розряду, а перенос із знакового розряду втрачається. Указати значення виразу (maxi та mini позначають максимальне та мінімальне цілі числа): а) maxi + 1; b)†mini ñ 1. 32


ðîçä³ë

3 ÏÐÎÃÐÀÌÓÂÀÍÍß Ë²Í²ÉÍÈÕ ÀËÃÎÐÈÒ̲ ÌÎÂÎÞ TURBO PASCAL 7.0

1. АЛФАВІТ І СЛОВНИК МОВИ Вивчення будь-якої мови починається з вивчення алфавіту — скінченної множини символів, дозволених для використання. Алфавіт мови Turbo Pascal містить: • літери (букви) — великі й малі латинські A, B, … , Z, a, b, …, z, а також символ підкреслення _; • десяткові цифри 0, 1, 2, …, 9; • спеціальні символи + - * / = > < . , ; : ' ( ) [ ] { } ^ @ $ #. З символів алфавіту утворюються «слова» мови — лексеми (або лексичні одиниці). Це послідовності символів, які розглядаються як неподільні й самі по собі мають деякий зміст. У мові Turbo Pascal є чотири види лексем: • константи; • імена; • знаки операцій; • роздільники. Окрім них, у програмах записують ще коментарі та директиви компілятора. Константа — це позначення значення (числового або іншого типу). Числові константи мають звичний для нас вигляд, наприклад, 273, 3.1415926, 2.71828, лише відокремлення цілої частини від дробової задається крапкою, а не комою. Булеві константи false і true позначають те, про що 33


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

ми звикли говорити, відповідно, «хибність» та «істина». Існують також текстові константи — послідовності будь-яких ASCII-символів, обмежених апострофами, наприклад, 'Ліцей', '3.1415926', '-273'. Ім’я — це послідовність букв алфавіту й цифр, що починаються з букви, наприклад, A, x1, _1_2, jklmn. Великі й малі букви в іменах не різняться: Nam, nAM, nam — це одне й те саме ім’я. Імена можуть мати довільну довжину, але до уваги беруться лише перші 63 символи. Ім’я завжди іменує якийсь об’єкт; воно виділяє його з-поміж інших, тобто ідентифікує, тому ім’я ще називають ідентифікатором. Деякі імена (це англійські слова або їх скорочення) позначають певні складові частини програми, наприклад, program, const, var, begin, end. Вони називаються зарезервованими, ключовими або службовими словами. Існують також стандартні слова, наприклад, integer, write, readln, sin, Pi тощо, які мають певний зміст, але його за бажанням програміста можна змінити. Автор програми також сам створює та використовує користувацькі імена, позначаючи за їх допомогою певні об’єкти програми. Знак операції — це позначення операції, виконання якої над числами та іншими значеннями породжує нове значення. Значення, до яких застосовується операція, називаються її операндами, а породжуване значення — результатом. Знаки операцій записують як окремими символами, наприклад + або –, так і іменами або послідовностями інших символів, наприклад div або <=. Множина знаків уточнюється у параграфах 5–9. Роздільниками є дужки ( ) [ ], розділові знаки , ; . : та пропуск. Ними відокремлюються лексеми та інші, складніші, елементи програми. Коментар — це допоміжне пояснення, яке має на меті допомагати людині зрозуміти програму, не задає ніяких дій і при трансляції пропускається. Коментар — це «майже» довільна послідовність символів, що починається символом { і закінчується найближчим }, тобто не містить } всередині. «Майже» означає, що відразу за дужкою { не може бути символу $ (тоді це не коментар, а так звана директива компілятору). Замість дужок { та } можна записувати пари символів (* та *). Наприклад, { це коментар } (* це теж *) { і це } а це вже не коментар, а незрозуміло що} Коментарі можна записувати між будь-якими двома словами, проте краще розташовувати їх праворуч від тексту програми або в окремих рядках. Директиви компілятора — це записи, які задають ті чи інші режими 34


2. Òèï — çíà÷åííÿ òà îïåðàö³¿

компіляції програми і можуть суттєво впливати на зміст машинної програми. Як і коментарі, директиви записуються в дужках {}, однак відразу за дужкою { має йти символ $. Конкретний вигляд та зміст деяких директив буде розглянуто нижче. Суміжні імена й константи відокремлюються так званими порожніми символами — символами, які не мають зображення й з’являються в тексті програми, якщо при його створенні натискати клавіші Space (пропуск), Enter («кінець рядка») і Tab (символ табуляції). Порожні символи називаються пропусками. Пропуск між сусідніми лексемами не обов’язковий, якщо хоча б одна з них є роздільником, коментарем або знаком операції (але не іменем). Наприклад, знаки + або <= не є іменами, а div — є. Тому 1+2, 1 div–2 чи 1<=2 написати можна, а 1div2 — ні (треба 1 div 2).

2. ТИП — ЗНАЧЕННЯ ТА ОПЕРАЦІЇ Програми обробляють дані у вигляді чисел, символів та інших значень. Значення мають певне представлення в комп’ютері й до значень застосовують деякі операції. За способом представлення та допустимими операціями значення (дані) поділяються на типи. Поняття типу даних є одним із фундаментальних у програмуванні. Тип даних — це множина значень (даних) разом із множиною операцій, застосованих до цих даних. Множина значень називається носієм типу, а множина операцій — сигнатурою. У мовах програмування використовуються числові, символьні та булеві значення. Вони розглядаються як цілісні елементи, що не мають окремих складових частин, тому називаються скалярними на відміну від структурних, складених з окремих компонентів. Типи скалярних даних називаються скалярними. Практично кожна мова програмування забезпечує ціле сімейство скалярних типів. Вони називаються базовими типами мови. Імена базових типів можна вживати, не оголошуючи у програмі (вони є стандартними). Крім того, мови мають спеціальні конструкції для оголошення власних скалярних та структурних типів. У базових скалярних типах мови Turbo Pascal є цілі та дійсні числа, булеві значення «істина» та «хибність», а також символи. Будь-який тип, базовий чи створений програмістом, має своє ім’я й характеризується множиною значень та набором можливих операцій. 35


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

3. ТИПИ ЦІЛИХ ЧИСЕЛ Для представлення цілих чисел мова Turbo Pascal надає п’ять типів: Ім’я типу byte word shortint integer longint

Довжина, байт 1 2 1 2 4

Діапазон значень 0..255 0..65535 ñ128..127 ñ32768..32767 ñ2147483648..2147483647

Чому в цілих типів такі діапазони значень, висвітлено в розділі 1. Ціла константа — це, як і в математиці, послідовність десяткових цифр, можливо, зі знаком «+» або «–» попереду: 0, +1024, –273 тощо. Діапазони можливих констант різних цілих типів наведено в таблиці. Для найбільшого цілого числа типу integer виділено ім’я maxint (типу longint — іменем maxlongint). Найбільше за модулем від’ємне число типу integer можна задати виразом –maxint–1. Усі цілі типи мають однакові набори операцій. Розглянемо сигнатуру операцій на прикладі типу integer. Основні арифметичні операції з цілими числами (додавання, віднімання, множення, обчислення частки та остачі від ділення націло, а також ділення) позначають знаками. Більшість операцій є двомісними. Знак «–» задає застосування як двомісної операції віднімання, так і одномісної операції «мінус»: –32768, –(2+3). Знак «+» також може задавати як двомісну, так і одномісну операцію. Арифметичні операції з цілими числами Знак + – – (одномісний) * div mod /

Приклади застосування та результати 1+2 = 3 1–2 = –1 –(1) = –1 2*2 = 4 7 div 3 = 2, –7 div 3 = –2, 7 div –3 = –2, –7 div –3 = 2 7 mod 3 = 1, –7 mod 3 = –1, 7 mod –3 = 1, –7 mod –3 = –1 7 / 3 ≈ 2.3333333 (дійсне), 6/3 = 2.0 (дійсне) 36


3. Òèïè ö³ëèõ ÷èñåë

Арифметичні операції в цілих типах вважають означеними не для всіх можливих значень цих типів, тобто частково означеними. Неналежне застосування операцій може призводити залежно від налаштування компілятора до неправильних результатів або до аварійного закінчення програми. Результат додавання, наприклад 32767+1 = 32768 не представляється в типі integer, тому може залежати від типу комп’ютера та налаштування компілятора. Можливо, це буде значення 32768 типу longint, але не обов’язково. В операціях ділення /, div, mod дільник не може бути нулем, інакше програма закінчиться аварійно. Операції div та mod є такими, що за будь-яких значень a та b виконується рівність a div b + a mod b = a. Операція /, застосована до будь-яких цілих чисел, має результатом не ціле (дійсне) число. Операції порівняння цілих чисел задають знаками =, <>, >, <, >=, <= («дорівнює», «не дорівнює», «більше», «менше», «більше або дорівнює», «менше або дорівнює»). Результатом є значення «істина» або «хибність» булевого типу, який представлено в наступному параграфі. Наприклад, 1 = 2 — false («хибність»), 1 <> 2 — true («істина»), 1 >= 1 — true тощо. Деякі операції з цілими числами задають у вигляді f(…), де f — ім’я. Вирази такого вигляду називаються викликами функцій. Розглянемо три такі функції та відзначимо особливості, пов’язані з представленням та обробкою цілих чисел: Вигляд Що обчислюється Приклади виклику odd(x) odd(7)=true, ознака непарності x ó odd(12)=false false або true 2 sqr(x) sqr(2)=4, x sqr(181)=32761 abs(x) abs(–1)=1 |x| Наведені функції називаються вбудованими, оскільки їх застосування реалізується підпрограмами зі стандартної бібліотеки системи програмування. Інші вбудовані функції буде розглянуто нижче. Кількість байтів, відведених під значення числових типів, є фіксованою в системі Turbo Pascal, але вона може відрізнятися в інших системах. Для визначення кількості байтів, відведених під значення певного типу (окрім файлових, див. статтю 10 на с. 45), радимо користуватися функцією Sizeof. Наприклад Sizeof(Longint)=4, Sizeof(Word)=2. 37


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

4. БУЛІВ ТИП Булів тип має два логічних (булевих) значення «хибність» та «істина», які задано константами false (хибність) і true (істина). Цей тип має ім’я boolean на честь видатного англійця Джорджа Буля, засновника математичної логіки. До булевих значень застосовують логічні операції «і», «або», «не», які називають, відповідно, булевим множенням (кон’юнкцією), булевим додаванням (диз’юнкцією) та запереченням і позначають як and, or та not. Ще одна операція називається «виключне або» чи «додавання за модулем 2» і позначається знаком xor. Результати застосування цих операцій до булевих значень наведено в таблиці: A false false true true

B false true false true

A and B false false false true

A or B false true true true

A xor B false true true false

not A true true false false

Окрім булевих, є ще операція «порядковий номер » o r d : ord(false) = 0, ord(true) = 1. Порядковим номерам булевих значень відповідає результат їх порівняння: false < true. Очевидним шляхом означено всі інші порівняння =, <>, >, <=, >=. Булеве значення представляється його номером в одному байті, тобто Sizeof(boolean)=1.

5. ТИП СИМВОЛІВ Тип символів (літерний тип) має ім’я char. У ньому 256 значень, які представляються в одному байті. Зауважимо, що у відносно нових мовах програмування, наприклад Java, символи займають два байти, тому їх кількість — 65536. Символ позначається символьною константою, тобто символом у апострофах: 'A', '1', '.' тощо. Сам символ «апостроф» задається символьною константою ''''. Підкреслимо: символьна константа — це не символ, а його позначення в мові програмування. Символам взаємно однозначно відповідають номери від 0 до 255. Будьяке символьне значення можна задати виразом chr(i), де i має значення від 0 до 255, тобто номер. Наприклад, chr(48) позначає те ж, що й 38


6. Ïîíÿòòÿ ïåðåë³÷óâàíîãî òèïó

константа '0', chr(48+1) — те ж, що '1', chr(65) є синонімом константи 'A', chr(97) — 'a'. Запис вигляду #i позначає те ж, що й chr(i); наприклад, #48 = chr(48). Відповідність символів і номерів від 0 до 127 зафіксовано в Американському стандартному коді для обміну інформацією (ASCII), а номерам від 128 до 256 можуть відповідати різні набори символів. Розглянемо операції з символами. Цілий номер символа породжується викликом функції «порядковий номер», тобто виразом ord(c), де значенням c є символ. Наприклад, ord('0') = 48, ord('A') = 65, ord('a') = 97. За означенням, функції c h r і o r d взаємно обернені, тобто chr(ord(c)) = c за будь-якого символу c, а ord(chr(n)) = n за будьякого n від 0 до 255. Для символів означено всі порівняння, причому символи упорядковано так само, як і відповідні їм номери. Зокрема, справджуються такі нерівності. ' ' < '_' < '0' < '1' < … < '9' < 'A' < 'B' < … < 'Z' < 'a' < 'b' < … < 'z' Результатом операції конкатенації (дописування) зі знаком «+» є послідовність з двох символів, або рядок. Наприклад, '1'+'2' — це послідовність символів, яку можна задати також виразом '12'.

6. ПОНЯТТЯ ПЕРЕЛІЧУВАНОГО ТИПУ Кожен елемент типу boolean, char або ціле число, окрім найбільшого в своєму типі, має елемент, наступний за ним, і кожен, окрім найменшого, — попередній перед ним. Наприклад, наступним за false є true, за chr(0) — chr(1), за chr(254) — chr(255), а попереднім перед true є false. Окрім того, елементи цих типів можна пронумерувати послідовними цілими числами. У мові Паскаль перелічуваним називається тип, у якому означено операції succ (наступний), pred (попередній) і ord (порядковий номер). Перелічувані типи також називаються дискретними. Операції succ, pred, ord задають у вигляді викликів вбудованих функцій: вирази pred(1), succ('a') та ord(true) мають значення, відповідно, 0, 'b' та 1. Для знакових числових типів операцію ord означено так, що ord(i) = i, тобто ord(0) = 0, ord(-1) = -1. В усіх перелічуваних типах означено також порівняння =, <>, <, >, <=, >=. Результат застосування succ до найбільшого елемента типу та pred до найменшого залежить від налаштування компілятора. 39


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

7. ТИПИ ДІЙСНИХ ЧИСЕЛ Для представлення дійсних чисел використовують чотири типи: single, real, double, extended. Дійсні типи подано в таблиці:

single

4

real

6

Кількість Кількість Найменше Найбільше біт біт за модулем за модулем мантиси порядку 23 8 ≈ 1.5 ⋅10 −45 ≈ 3.4 ⋅ 1038 39 8 ≈ 2.9 ⋅10−39 ≈ 1.7 ⋅1038

double

8

52

11

≈ 5.0 ⋅ 10 −324

64

15

≈ 3.4 ⋅ 10 −4932 ≈ 1.1⋅ 10 4932

Назва

Довжина, байт

extended 10

≈ 1.1 ⋅10308

Як видно з таблиці, тип extended має найбільший діапазон та найвищу точність представлення. У процесорі числа обробляються в представленні саме типу extended, і під час запису в регістри процесора числа з інших типів перетворюються в цей. Тип real є стандартним (доступним для використання незалежно від встановленого режиму компіляції). Щоб використовувати інші дійсні типи, треба встановити спеціальний режим компіляції за допомогою меню системи програмування або директиви компілятора. Дійсні числа позначаються дійсними константами. Розглянемо приклад. Число 1,2345 можна позначити кількома способами, наприклад, 123, 45 ⋅ 10−2 . У цьому записі воно має цілу частину «123», дробову частину «,45» і десятковий порядок «–2». Запису відповідає константа мови Паскаль 123.45E-2, в якій 123 — ціла частина, .45 — дробова, а E–2 — порядок. Це саме число можна представити також константами 0.12345E1, 0.012345E+2, 1.2345, 12345e-04 та іншими. Дійсні константи мають обов’язкову цілу частину, за якою записано дробову частину і порядок (можливо, одне з них). Ціла частина — це непорожня послідовність цифр, дробова — послідовність цифр, можливо, порожня, із крапкою на початку, а порядок — латинська буква E або e з однією або кількома цифрами, можливо, зі знаком + або -. Перед константою може стояти знак –, і тоді вона задає від’ємне число: –12.345E–1. Представлення дійсного числа константою, у якій ціла частина містить одну цифру від 1 до 9, називається нормалізованим, наприклад, 9.81 або 1.0E2 (для числа 0 таким є 0.0). Множина чисел, які можна представити у дійсних типах, уточнюється за поданою вище таблицею. За будь-якого дійсного типу вона є скінчен40


7. Òèïè ä³éñíèõ ÷èñåë

ною обмеженою підмножиною множини раціональних чисел і містить усі цілі числа, які представлено в типі longint (тип single містить не всі, але містить носій типу integer). Різниця між двома «сусідніми» дійсними числами змінюється в їх діапазоні, для великих чисел досягаючи порядку 104 . Операції. До дійсних значень застосовують ті ж арифметичні операції, що й до цілих, за винятком odd, div, mod, а також succ, pred, ord. Результатом цих операцій завжди є дійсне число. Порівняння дійсних чисел, як і цілих, має відповідний булів результат: результатом 1.0<=2.5 є значення true, 1.0=0.99 — значення false. Дійсні типи є «машинними представниками» множини дійсних чисел, яка є всюди щільною (між довільними двома дійсними числами існує безліч інших дійсних). Пронумерувати ці числа неможливо, тому дійсні типи не є перелічуваними. Для дійсних типів означено так звані вбудовані математичні функції. Дійсне значення | x | , arctg x, cos x, e x , ln x, sin x, x 2 , x повертається з виклику функції, ім’я якої, відповідно, abs, arctan, cos, exp, ln, sin, sqr, sqrt. Усі ці функції застосовуються й до чисел цілих типів (але лише abs та sqr у цьому випадку породжують значення цілого типу). Значення аргументу у викликах функцій cos і sin виражає кількість радіан, а не градусів. Означено також нульмісну функцію Pi (її значення є наближенням до числа π ≈ 3,14159265358979 ). Наприклад, cos(Pi/2) = 0.0, sin(Pi/2) = 1.0. Значення математичних функцій обчислюються наближено, і для деяких аргументів можуть відрізнятися від справжніх математичних. Для дійсних типів означено декілька специфічних операцій, які задаються викликами функцій. Їх подано в таблиці (означення trunc, int і frac для від’ємних чисел відрізняється від математичного): Функції, специфічні для дійсних типів Вигляд Що обчислюється Приклади та примітки виклику round(x) найближче до x ціле round(4.12)=4, round(1.5)=2, число (значення round(2.14748364749E9) = типу longint) 2147483647. при x ≥ 2.1474836475E 9 не визначено (тоді round(x) не можна представити в longint) 41


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

Закінчення

Вигляд виклику trunc(x)

int(x) frac(x)

Приклади та Що обчислюється примітки ціла частина x (значення типу trunc(ñ4.12) = ñ4, longint) trunc(1.9) = 1, trunc(x) = 2147483647 при x ≥ 2147483647 ціла частина x (значення того ж int(1.9) = 1.0, типу, що й x) int(ñ1.9) = ñ1 дробова частина x (значення того frac(1.6) = 0.6, ж типу, що й x) frac(ñ1.6) = ñ0.6

8. ЗМІННІ ВЕЛИЧИНИ Поняття змінної числової величини вперше з’явилося в роботах Декарта в XVII ст. Пізніше виявилося, що змінна величина може бути не лише числовою. У найширшому розумінні змінна величина — це узагальнення, абстракція якогось реального чи уявного об’єкта (або його окремої характеристики), який може перебувати в різних станах. Змінні величини задають іменами, наприклад, у записі другого закону Ньютона a = F/m імена m, a, F позначають змінні величини — масу тіла, прискорення його руху та силу, що діє на нього. У програмуванні змінна також є представником об’єкта або його моделлю у програмі. Вона представляє його в пам’яті комп’ютера, тому змінна в програмуванні — це ділянка пам’яті, яка своїми станами представляє стани об’єкта. У мовах високого рівня змінні позначаються іменами, або ідентифікаторами.1 Імена можна вибирати будь-які, окрім службових слів. Наприклад, ABRACADABRA, temperature, number, f123qq тощо. Нагадаємо: великі й малі букви в іменах розглядаються як однакові. Імена змінних та інших об’єктів бажано обирати так, щоб вони були пов’язані з поняттями, які вони представляють, наприклад, tCels, tKelv або KelvC (температура за Цельсієм, за Кельвіном, константа Кельвіна).2 Змінна величина у математиці вважається заданою, якщо визначено множину значень, яких вона може набувати. У Паскаль-програмі змінна 1 2

Змінні можуть позначатися також складнішими виразами (див. статті 7, 9 та 11). Інколи перші букви імені змінної обираються так, щоб вони вказували на її тип. 42


8. Çì³íí³ âåëè÷èíè

задається оголошенням, яке вказує ім’я та тип змінної й має такий вигляд. var ім’я змінної : ім’я типу; Наприклад, var even : boolean;. Ключове слово var є скороченням англійського variable — змінна. Кілька однотипних змінних можна оголосити разом, указавши їх імена через кому. var tCels, tKelv : integer; Тип змінної задає множину її можливих значень (що залежить від довжини ділянки пам’яті, яку займає змінна) та операцій, які можна застосувати до неї. Кожна ділянка пам’яті комп’ютера має адресу, якою ділянка позначається в машинній програмі. Ім’я змінної у Паскаль-програмі в результаті трансляції перетворюється на адресу деякої ділянки пам’яті. При виконанні програми ця ділянка і є змінною, ім’я якої записано в програмі. Ім’я змінної вказує або посилається на ділянку пам’яті під час виконання програми (рис. 1). Як бачимо, змінна (ділянка пам’яті) та її позначення (ім’я) — це різні поняття. Ім’я

Змінна

Рис. 1. Змінна та її ім’я

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

9. ВИРАЗИ Застосування операцій до числових та інших значень, у результаті якого утворюється нове значення, у мовах програмування високого рівня задається за допомогою виразу. Найпростіші вирази — це константи й імена змінних базових типів. Складніший вираз утворюється з простіших як: • вираз у дужках; • два вирази і знак двомісної операції між ними; • вираз із знаком одномісної операції перед ним; • виклик функції з виразом у дужках. Приклади: ((1)), true and false, 1-(2+3), (1-2)+3, (1+2)<>3, -(5+3), odd(2), ord(odd(15)). 43


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

Значення, до якого застосовується операція, називається її операндом. Операнди позначаються виразами. Послідовність застосування операцій до операндів утворює процес обчислення виразу, результатом якого є значення виразу. Тип значення виразу називається типом виразу. Як бачимо, вираз має подвійний зміст (семантику): задає процес обчислення й має значення певного типу. Приклади. Вираз 2*2 = 5 задає процес, у якому: 1) обчислюється добуток 2*2 і виникає ціле значення 4; 2) порівнюються цілі значення 4 і 5 і виникає значення false, яке є значенням цього виразу; його тип — boolean. Нехай a, b, c — імена числових змінних, які представляють коефіцієнти квадратного рівняння ax 2 + bx + c = 0 . При обчисленні виразу b*b– 4*a*c>0 спочатку обчислюється квадрат значення змінної b, потім добуток числа 4 та значень змінних a і c, потім різниця двох отриманих добутків. Ця різниця порівнюється з нулем, і булів результат цього порівняння (значення true або false) є значенням усього виразу. Отже, цей вираз задає обчислення ознаки того, що вказане рівняння має саме два корені. Припустимо, що c — ім’я символьної змінної, значенням якої може бути один із символів '0', '1', …, '9', тобто десяткова цифра. Вираз ord(c)–ord('0') задає обчислення порядкових номерів значення змінної c та символа '0'. Потім обчислюється різниця цих номерів — ціле число від 0 до 9, тобто числове значення цифри. ‹ Мова Turbo Pascal, в основному, відповідає угодам математики про порядок застосування операцій у виразах. Це дозволяє не записувати зайві дужки, наприклад, 1–2*3 означає те саме, що й 1-(2*3). На порядок застосування операцій за відсутності дужок впливає їх старшинство, або пріоритет. Якщо поруч із позначенням операнда записано знаки двох операцій, то спочатку виконується старша з них (із вищим пріоритетом). У таблиці, поданій нижче, усі операції розбито на чотири групи, розташовані у порядку спадання пріоритету (деякі операції означено у подальших главах). Операції всередині кожної групи мають однакові пріоритети. Наприклад, вираз 1+(3+2)*2 задає, що після обчислення 3+2, тобто значення 5, воно множиться на 2, а не додається до 1. Окрім пріоритетів, операції мають властивості право- або лівобічного зв’язування. У мові Turbo Pascal усі двомісні операції мають властивість лівобічного зв’язування: якщо ліворуч і праворуч від позначення операнда записано знаки операцій з однаковим старшинством, то операнд 44


10. Ïðèñâîþâàííÿ çíà÷åííÿ çì³íí³é

зв’язується з операцією, вказаною ліворуч (ця операція застосовується спочатку). Наприклад, 1–2*4+3 задає те саме, що й(1–2*4)+3, але не те, що 1–(2*4+3). Старшинство операцій Загальна назва операцій Унарні Мультиплікативні Адитивні Відношення

Символи операцій –, *, +, =,

not, ^, @ /, and, mod, div, shl, shr –, or, xor <>, <, >, <=, >=, in

У системі програмування Turbo Pascal застосовується так зване ледаче, або скорочене, обчислення булевих операцій and і or. Спочатку обчислюється їх перший операнд. Якщо для операції and це false, то другий операнд не обчислюється, оскільки результатом все одно буде false. Аналогічно, якщо перший операнд операції or є true, то другий операнд не потрібен. Наприклад, вираз ( 2 * 2 = 5 ) a n d 323345 div 17 = 0) задає обчислення лише 2*2=5, а (2*2=4)or (323345 div 17 = 0) — лише 2*2=4. Повністю булеві вирази обчислюються в спеціальному режимі компіляції (див. додатки А і В).

10. ПРИСВОЮВАННЯ ЗНАЧЕННЯ ЗМІННІЙ Змінна своїми значеннями представляє стани об’єктів, які моделюються в програмі. Основний спосіб надати значення змінній — це оператор присвоювання. У літературі оператори ще називають командами, вказівками та інструкціями. Оператор присвоювання має такий вигляд: ім'я змінної := вираз; Знак присвоювання := — це окрема лексема, яку не слід плутати зі знаком операції порівняння =. Оператор присвоювання позначає такі дії. 1. Обчислити значення виразу, записаного праворуч від знака :=. 2. Записати обчислене значення в змінну, позначену іменем ліворуч. 3 Наприклад, якщо ім’я z оголошено як var z:integer, то оператор присвоювання z:=7*(3+1) задає обчислення значення 28 і запис його в змінну z. Після виконання присвоювання змінна z має значення 28. Оператор присвоювання задає зміну значення (стану) змінної. У вира3 Нагадаємо, що змінні можуть позначатися також складнішими виразами (див. статті 7, 9 та 11).

45


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

зах в операторах присвоювання можна записувати імена змінних, яким присвоєно значення за попередніми операторами програми. Значенням виразу, що є іменем змінної, є її значення, присвоєне їй раніше. Наприклад, якщо у програмі змінній z присвоєно значення 2, то вираз z+1 задає додавання 2 та 1 і має значення 3. При іншому значенні z вираз z+1 мав би інше значення. Якщо цей вираз написати, наприклад, в операторі z:=z+1, то оператор задає збільшення значення z на 1, яким би воно не було (звичайно, якесь значення змінній z треба присвоїти перед виконанням цього оператора). Сукупність змінних, імена яких оголошено у програмі, називається пам’яттю програми,4 а відповідність між іменами змінних та їх станами — станом пам’яті програми. Присвоювання значення змінній означає зміну стану пам’яті програми. Ім’я змінної у виразі задає її значення на момент обчислення виразу. Отже, вираз обчислюється при поточних значеннях вказаних у ньому змінних, тобто на поточному стані пам’яті. Далі ми розглянемо інші види операторів, але всі оператори задають зміну станів пам’яті програми. Ця зміна й є їхньою семантикою. Для перелічуваних типів існує специфічний різновид оператора присвоювання. Він записується як inc(z) або dec(z), де z — ім’я змінної перелічуваного типу. У дійсності ці оператори є викликами вбудованих процедур (про процедури йдеться у розділі 6). Виклик inc(z) тотожний оператору z:=succ(z), dec(z) — оператору z:=pred(z). Після імені змінної та коми у виклику inc та dec можна записати цілий вираз. Наприклад, виклик inc(z,2) задає збільшення z на дві «одиниці» того типу, до якого належить z. Якщо змінна z типу char має значення 'A', то після виконання inc(z,2) її значенням буде 'C'. Значення виразу у виклику може бути й від’ємним — тоді z зменшиться. Аналогічно, виконання dec(z,2) зменшує значення z у його типі, наприклад, з 3 до 1 або з 'C' до 'A'.

11. СТРУКТУРА ТА ПРИКЛАДИ ПРОГРАМ Програма мовою Turbo Pascal має такий загальний вигляд: program ім'я; розділ підключення модулів 4 Далі ми побачимо, що у процесі виконання програми до її пам’яті змінні можуть додаватися або, навпаки, вилучатися з пам’яті.

46


11. Ñòðóêòóðà òà ïðèêëàäè ïðîãðàì

оголошення імен та підпрограми begin опис виконуваних дій end. У першому рядку записано заголовок програми; він містить ім’я програми й не є обов’язковим. Проте краще взяти собі за правило завжди його записувати. Розділ підключення модулів (він теж не є обов’язковим) починається службовим словом uses і містить перелік імен модулів. Докладніше про модулі йдеться в статті 10, а зараз дамо дуже стисле пояснення. Програми часто створюються у вигляді кількох програмних одиниць — власне програми та цілого набору модулів, які в ній використовуються. Модулі зберігаються й транслюються окремо, а їхні «машинні» варіанти підключаються до програми при компонуванні. Щоб забезпечити підключення потрібних модулів, на початку програми вказують їхні імена. Оголошення імені — це опис того, що позначає ім’я у програмі. Ім’я може позначати деяке значення або ділянку пам’яті, в якій зберігаються значення, а також інші, складніші об’єкти. Конкретні види оголошень імен розглядатимуться разом із уточненням відповідних понять (змінних, констант, типів та підпрограм). Кожне з оголошень закінчується роздільником «;». Підпрограма — це спеціальним чином оформлена частина програми. Якщо програма описує дії з розв’язання деякої задачі, то підпрограма описує дії з розв’язання деякої частини цієї задачі, тобто підзадачі. Цей термін буде уточнено в статті 6. Кожне ім’я, що використовується в програмі, має бути оголошеним. Деякі імена оголошуються в системі програмування або в інших програмних одиницях. Опис виконуваних дій разом із «дужками» begin–end називається тілом програми. Після тіла обов’язковою є крапка — символ кінця програмної одиниці. Текст програми між її заголовком та крапкою, тобто оголошення та тіло, називається блоком. Дії в програмі задаються послідовно записаними командами, або операторами. У літературі оператори ще називають вказівками та інструкціями. Приклади. Найкоротша програма, яка не задає жодних дій, має такий вигляд: begin end. У першому рядку наступної програми записано заголовок з її іменем First (тобто «програма Перша»). Запис writeln('Привіт!') оз47


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

начає дію: надрукувати на екрані комп’ютера символи, вказані в апострофах. Ім’я writeln є скороченням англійського write line — записати рядок. program First; begin writeln('Привіт!') end. Наступна програма містить пояснення у фігурних дужках — коментарі, призначені для читача. program Second; { «програма Друга» } const KelvC = 273; {ім’я KelvC далі позначає число 273} var tCels, tKelv : integer; {оголошення імен змінних} begin writeln('Обчислення температури за Кельвіном.'); writeln('Температура за Цельсієм (ціле число)>'); {запросити до введення цілого числа} readln(tCels);{прочитати з клавіатури число} {й запам’ятати у змінній tCels} tKelv:=tCels+KelvC;{присвоїти суму змінній tKelv} writeln('Температура за Кельвіном: ', tKelv); { надрукувати текст і суму } end. За цією програмою комп’ютер спочатку має вивести на екран текст: Обчислення температури за Кельвіном. Температура за Цельсієм (ціле число)> Далі указано дію з коментарем «прочитати з клавіатури ціле число й запам’ятати у змінній tCels». При виконанні цієї дії комп’ютер має чекати, поки ми не наберемо якесь ціле число (наприклад, 127) і не натиснемо на клавішу Enter. Потім він має обчислити суму введеного числа з числом 273, для якого на початку програми введено позначення KelvC. Цю суму 400 (на 273 більше, ніж ми набрали) він має присвоїти змінній tKelv. Нарешті, він має вивести текст і значення змінної tKelv: Температура за Кельвіном: 400 Отже, за цією програмою комп’ютер отримує від клавіатури ціле число, що задає температуру за Цельсієм, обчислює й запам’ятовує температуру за Кельвіном і виводить її значення на екран. ‹ 48


12. Âèâåäåííÿ çíà÷åíü âèðàç³â íà åêðàí

Як бачимо, оператори присвоювання та інші оператори записуються один за одним і відокремлюються роздільником «;». Оператори, записані один за одним, утворюють послідовність операторів. Виконання операторів програми можна проімітувати, указавши послідовність операторів і станів пам’яті програми, після виконання операторів. Якщо в процесі виконання програми змінна ще не отримала значення, будемо вважати його невизначеним і позначати як «?». Розглянемо таку програму: program a2; var x, y, z : integer; begin z:=1; x:=3+z; y:=15-x; x:=x+1 end. Її імітацію подамо таблицею: Виконано оператор z:=1 x:=3+z y:=15-x x:=x+1

Утворено x ? ? 4 4 5

стан пам’яті y z ? ? ? 1 ? 1 9 1 9 1

‹

• Імітацію програми можна провести за допомогою її покрокового виконання. Замість невизначеності початковими значеннями змінних будуть нулі. Проте на цю властивість системи програмування покладатися не варто.

12. ВИВЕДЕННЯ ЗНАЧЕНЬ ВИРАЗІВ НА ЕКРАН Мови програмування, як правило, не мають спеціальних операторів для виведення значень з оперативної пам’яті на зовнішні носії та уведення їх з носіїв до оперативної пам’яті. Ці операції здійснюються за спеціальними підпрограмами (процедурами) уведення-виведення. Розглянемо підпрограми виведення. 49


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

Виведення, або запис, значення виразу на екран задається у вигляді виклику процедури writeln(вираз).5 Виклик процедури, на відміну від виклику функції, записується не у виразі, а як окремий оператор. При виконанні підпрограми writeln обчислюється значення виразу і за ним створюється відповідна константа, тобто послідовність символів. Константа передається пристрою, частиною якого є екран, і виводиться на нього. Типом виразу може бути лише базовий тип. Наприклад, при виконанн�� операторів програми з цілою змінною z z:=1; writeln(1+z); writeln(z<=1); на екрані з’являються такі символи: 2 TRUE На екрані майже завжди наявна світлова позначка, що миготить, — курсор. При виконанні підпрограми виведення константа друкується, починаючи з того місця на екрані, де перебуває курсор. Після виведення за допомогою процедури writeln курсор переходить до наступного рядка екрана. Всередині дужок writeln можна написати кілька виразів через кому. Вони обчислюються один за одним, і відповідні їм константи друкуються підряд в одному рядку; курсор пересувається в наступний рядок після виведення останньої константи. Наприклад, у результаті виконання операторів z:=1; writeln(1+z, z<=1); на екрані з’явиться 2TRUE. Разом із виразами можна записувати послідовності символів між апострофами, наприклад: 'x=', '123' тощо. Вони називаються рядковими константами, або літералами, і виводяться без апострофів. Наприклад, за x цілого типу при виконанні x:=2; writeln(1, ') x = ', x, '; x**2 = ', x*x, '; x^2 > 2 : ', x*x > 2); на екран виводиться такий текст: 1) x = 2; x**2 = 4; x^2 > 2 : TRUE За виразом можна написати двокрапку й цілу константу, наприклад, x+y:10. Константа задає так звану ширину поля виведення, до якого виводяться символи, що представляють значення виразу. Якщо символів більше, ніж ширина, то друкуються всі символи, а якщо менше, то спочат5 Нагадаємо: ім’я writeln є скороченням англійського write line — записати рядок.

50


13. Óâåäåííÿ çíà÷åíü ó çì³íí³

ку будуть виведені пропуски (доповнивши кількість символів до заданої ширини поля виведення), а потім символи. Наприклад, при виконанні операторів x:=10; writeln('x = ', x:5, ', x**2 = ', x*x:1) на екран перед 10 виводиться три пропуски та всі цифри числа 100. x = 10, x**2 = 100 Процедура write відрізняється від writeln лише тим, що після виведення останньої константи курсор не пересувається на наступний рядок екрана. Наприклад, при виконанні операторів x:=2; write('x = ',x); write(', x**2 > 2 : ',x*x>2) на екран виводиться такий текст: x = 2, x**2 > 2 : TRUE Курсор залишається праворуч від останньої букви. На початку тіла програми варто записувати виклик процедури виведення з повідомленням щодо самої програми та її призначення. За допомогою процедур writeln і write можна виводити значення не лише на екран, а й на інші зовнішні носії, наприклад, диск. Про це йдеться в статті 7.

13. УВЕДЕННЯ ЗНАЧЕНЬ У ЗМІННІ Уведення даних із зовнішніх носіїв до змінних програми задається процедурами уведення, або читання. Виклик однієї з них у найпростішому випадку має вигляд readln(ім’я-змінної). Ім’я readln є скороченням англійського read line — прочитати рядок. При виконанні такого виклику комп’ютер зупиняється й чекає, що на клавіатурі буде набрано константу того ж типу, що й тип указаної змінної. У відповідь слід набрати якусь константу на клавіатурі (її буде показано на екрані) та натиснути на клавішу Enter. Набрана константа після натискання на Enter передається процедурі, яка за константою створює відповідне значення та присвоює вказаній змінній. Типом змінної може бути лише базовий тип. Якщо натиснути Enter, не набравши нічого, окрім пропусків, то комп’ютер і надалі чекатиме. Перед числовою константою можна набрати довільне число пропусків (система їх ігнорує). Якщо замість потрібної константи набрати інші символи (наприклад, недопустиму константу або замість числової константи символьну), то ви51


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

конання програми на цьому буде завершено (аварійно) і на екрані з’явиться повідомлення про те, що вхідні символи були неправильними. Нехай, наприклад, ім’я z позначає змінну типу integer. Якщо при виконанні readln(z) набрати 1024, то після натискання на Enter значенням z стане 1024. Якщо ж набрати 50.9 або z=1024, програму буде аварійно завершено, оскільки число 50.9 не подається в типі integer та ціла константа не може починатися символами «z=». Якщо ж набрати ціле число, яке виходить за межі типу, наприклад, 50000000, результат залежить від встановлених директив компіляції і може бути цілком неочікуваним. У виклику процедури читання можна написати кілька імен змінних, відокремивши їх комами. При виконанні цього виклику треба набрати відповідну кількість констант, відокремивши їх пропусками або натисканнями на Enter у довільній кількості. Поки всі константи не буде набрано, виконання виклику не закінчується. Нехай, наприклад, x:integer, y:real. При виконанні readln(x,y); слід набрати цілу константу, додати хоча б один пропуск або натискання на Enter, а потім набрати будь-яку цілу або дійсну константу (між цілою та дробовою частиною набрати десяткову крапку) і натиснути на Enter. • Практично перед кожним викликом процедури читання з клавіатури варто записати виклик процедури виведення із запрошенням до введення значень і вказівкою, скільки й яких типів. Наприклад, указати виведення запрошення writeln('Температура за Цельсієм (ціле число)>'), а потім виклик readln(tCels);. Процедура readln задає читання не лише з клавіатури, але й з інших зовнішніх носіїв, наприклад, з диску (див. статтю 7).

14. ІМЕНУВАННЯ ВИРАЗІВ З КОНСТАНТАМИ ТА ІНІЦІАЛІЗАЦІЯ ЗМІННИХ Вираз із константами, записаний у програмі, обчислюється не при виконанні програми, а у процесі трансляції. Значення такого виразу можна позначити іменем (іменувати) і використовувати це ім’я далі в програмі. Іменування має такий вигляд: const ім’я константи = вираз із константами; Ключове слово const означає «константа»; остання «;» обов’язкова.6 6

В іменуваннях можна викликати лише 14 з усіх системних функцій. 52


14. ²ìåíóâàííÿ âèðàç³â ç êîíñòàíòàìè òà ³í³ö³àë³çàö³ÿ çì³ííèõ

Іменування виразу є оголошенням імені, і це ім’я можна записувати нижче у програмі замість виразу. Корисно іменувати вирази, що записуються у багатьох місцях програми. Якщо вираз іменований, то зміни слід внести лише в іменування, а якщо ні — доведеться змінювати вираз усюди, де він зустрічається. За словом const можна записати кілька іменувань, відокремивши їх «;», причому у виразах можна використовувати імена вже іменованих виразів із константами. Ось приклад: const a=12; b=2*a; tt=a+b; Ім’я tt після цього позначатиме число 36. Оголошення імен у Паскаль-програмі часто починають саме з іменування виразів. Імена констант, як і константи та імена змінних, записують у виразах. Вони також розіменовуються, наприклад, нехай ім’я Kelvc оголошено як const Kelvc=273. Тоді в операторі tKelv := tCels+Kelvc; розіменовуються і tCels, і Kelvc (тільки Kelvc ще під час трансляції, а не за виконання програми). Оголошення змінної з присвоюванням їй початкового значення називається ініціалізацією. У мові Turbo Pascal ініціалізація має дещо дивний вигляд. Оголошення змінної починається словом const і замість знака присвоювання записується знак «=», тому ініціалізовані змінні ще називають типізованими константами. Ось приклад: const bool:boolean=true; kb8:integer=1024*8; Тут оголошено ім’я булевої змінної bool з початковим значенням true та ім’я цілої змінної kb8 з початковим значенням 1024*8 = 8192. Вираз, який ініціалізує змінну, обчислюється під час трансляції програми, тому може містити константні вирази7 та імена константних виразів, оголошені вище у програмі. Імена змінних (зокрема, ініціалізованих раніше) у ньому заборонені. Наприклад, перший з двох наступних рядків містить правильну послідовність оголошень, а другий — помилкову. const kb1=1024; kb8:integer=8*kb1; const kb1:integer=1024; kb8:integer=8*kb1;{???} 7

З урахуванням обмежень на виклики функцій. 53


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

15. СУМІСНІСТЬ ТИПІВ Сумісність числових типів у виразах. У всіх типах цілих та дійсних чисел означено одні й ті самі операції: +, -, /, *, порівняння та інші. Проте, наприклад, додавання цілих та дійсних чисел відбувається по-різному, тобто однакові знаки позначають у дійсності різні операції. Наприклад, виразу 1+2 відповідає додавання цілих чисел, а виразу 1.0+2.0 — дійсних. Яку саме операцію слід додати до машинної програми, компілятор визначає за знаком операції та типами операндів. Мова Turbo Pascal дозволяє задавати у виразах дійсні та цілі операнди разом, наприклад, 1+2.0. При трансляції таких виразів додаються команди породження дійсного значення за цілим операндом. Отже, якщо один із операндів має дійсний тип, при обчисленні виразу спочатку цілий операнд перетворюється на дійсний і потім виконується вказана операція над дійсними значеннями. Так, при обчисленні 1+2.0 спочатку 1 перетворюється на 1.0 і потім додаються 1.0 і 2.0. Можливість указування операндів різних типів у виразах називається сумісністю цих типів. Типи цілих і дійсних є сумісними. Цілі типи між собою також сумісні. Проте результат перетворення цілого значення до іншого типу може залежати від того, змінні чи константи записано у виразі, а також від конкретних значень та дії директив компілятора. Правила перетворення цілих типів тут не наводяться. Сумісність типів за присвоюванням — це можливість значення одного типу присвоювати змінним іншого типу. Дійсні типи сумісні за присвоюванням із цілими, але не навпаки. Наприклад, якщо діє оголошення a:real; b:integer;, то присвоювання b:=a; є хибним, але можна написати a:=b. Ціле значення b перед присвоюванням дійсній змінній a перетворюється на дійсне. Так само при виконанні readln(a); можна набрати на клавіатурі не дійсну, а цілу константу — a одержить дійсне значення. Зворотні перетворення пр��граміст повинен задавати явно за допомогою функцій trunc або round, наприклад, b:=round(a). Дійсні типи сумісні за присвоюванням між собою. Проте значення більшого типу перетворюється на значення меншого за допомогою округлення. Якщо після округлення значення не подається в меншому типі, програма аварійно закінчується. Цілі типи також сумісні за присвоюванням, проте з рядом особливостей, які тут не подано. Зазначимо лише, що значення меншого розміру (у байтах) утворюються за рахунок відкидання старших байтів у значенні, 54


16. Ïîá³òîâ³ îïåðàö³¿ ç ö³ëèìè ÷èñëàìè

більшому за розміром, тобто можливі помилкові результати або аварійне завершення програми.

16. ПОБІТОВІ ОПЕРАЦІЇ З ЦІЛИМИ ЧИСЛАМИ Для цілих типів означено двомісні операції and , or, xor та одномісну not. Вони застосовуються як відповідні булеві операції, але окремо по бітах операндів, і називаються побітовими булевими. Булевому значенню false відповідає біт 0, true — біт 1. Приклад. Припустимо, змінні x і y мають тип shortint та значення 5 і 6. Тоді x and y = 4, x or y = 7, x xor y = 3, not x = -6. Якби ці змінні мали тип byte, то при тих самих значеннях not x = 250. Щоб зрозуміти ці результати, запишемо двійкові записи значень змінних та виразів, а на їх основі й десяткові записи: Вираз x not x

Двійковий запис значення 0 0 0 0 0 1 0 1 1 1 1 1 1 0 1 0

Десятковий запис 5 250 (як byte), ñ6 (як shortint) y 0 0 0 0 0 1 1 0 6 x and y 0 0 0 0 0 1 0 0 4 x or y 0 0 0 0 0 1 1 1 7 x xor y 0 0 0 0 0 0 1 1 3 Для цілих чисел є ще операції зсуву shl і shr (shift left та shift right — зсув ліворуч та зсув праворуч). При виконанні операції i shl k (i shr k), де k ≥ 0, біти в значенні i зсуваються ліворуч (праворуч) на k розрядів. Молодші (старші) біти при цьому заповнюються значенням 0, а старші (молодші) — втрачаються. Наприклад: 6 shl 2 = 24, 6 shr 2 = 1. Операції зсуву цілих чисел виконуються у процесорі набагато швидше, ніж операції множення та ділення, тому, наприклад, цілочислове ділення невід’ємного значення i на 2 варто задавати як i shr 1. КОНТРОЛЬНІ ЗАПИТАННЯ 1. Які основні групи символів є в алфавіті мови Turbo Pascal? 2. Які види лексичних одиниць є в мові Turbo Pascal? 3. Які різновиди імен є в мові Turbo pascal? Чим вони відрізняються? 4. Чи можна створити змінну або константу з іменем Pi, sin, program, integer, const? 55


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

5. З яких основних структурних частин складається програма, записана мовою Turbo Pascal? 6. Що таке тип даних? 7. Що таке переповнення? 8. Які набори операцій є сигнатурами типів Boolean, integer, real та char? 9. Які вбудовані функції означено для типів Boolean, integer, real та char? 10. Чому типи Boolean, integer та char називаються дискретними, а real — ні? 11. Які пріоритети мають операції у виразах? 12. Яка арифметична операція (відсутня в мові Turbo Pascal) повинна мати властивість правобічного зв’язування? 13. Чим відрізняється іменування константи від ініціалізації змінної? 14. Що таке команда (оператор) присвоювання? 15. Що є семантикою послідовності операторів присвоювання? 16. Чи можна імена типізованих констант вказувати у викликах процедур уведення? 17. Що таке сумісність типів? Назвіть приклади сумісних типів даних. 18. Що таке сумісність типів за присвоюванням? Назвіть приклади типів, сумісних за присвоюванням. 19. Чому цілі типи не сумісні за присвоюванням з дійсними? ЗАДАЧІ 1. Подати булеву операцію xor через інші булеві операції. 2. Знайти найбільше натуральне число, квадрат якого подається в типі Longint. 3. Обчислити значення виразу: а) (2*2 = 4) and true; b) (2*2 = 4) or false; c)(not true) or false; d) (ord(true) = 1) xor (ord(false) = 0). 4. Обчислити значення виразу: а) 2*ord(true) + 3*ord(false); b) (false = true) = false; c) 58 mod 13 div 10; d) 9 mod 5 * 12 div 16. 56


16. Ïîá³òîâ³ îïåðàö³¿ ç ö³ëèìè ÷èñëàìè

5. Якщо вираз не є помилковим, обчислити його значення, а якщо є, пояснити, чому: а) 1 = 2 = 3; b) false = true and false. 6. Пояснити різницю між записами 0 і '0', A і 'A', - і '-'. 7. Обчислити значення виразу: а) chr(ord('0') + 9); b)chr(ord('A') + 25); c) chr(ord('0') – 16); d) 'Z' > 'a'; e) ord('9') – ord('0'); f) ord('F') – ord('A'). 8. Написати вираз, який задає обчислення: а) цілого числа від 0 до 9 за значенням символьної змінної ch від '0' до '9'; b) символа від '0' до '9' за цілим значенням змінної dg від 0 до 9. 9. У шістнадцятковій системі числення літерами 'A', 'B', …, 'F' позначаються числа, десятковий запис яких 10, 11, …, 15. Написати вираз, що задає обчислення цілого числа від 0 до 15 за значенням символьної змінної ch, яким може бути цифра від '0' до '9' або буква від 'A' до 'F'. 10. Намалюйте чотири кола, позначених іменами типів boolean, integer, real та char. Проведіть стрілки між колами — стрілка веде від кола А до кола Б, якщо існують операції з операндами типу А й значеннями типу Б, наприклад, від кола integer до кола boolean (порівняння). Позначте стрілки знаками відповідних операцій. 11. Нехай a та b — імена цілих змінних. Написати арифметичний вираз, значенням якого є: а) більше з двох значень a і b; b) значення останньої цифри в десятковому записі a, наприклад, при a = 789 це 9; c) сума значень десяткових цифр двозначного a при a = 83 це 11; d) сума значень десяткових цифр тризначного a при a = 123 це 6. 12. Нехай a, b — імена цілих змінних із додатними значеннями. Написати булів вираз, значенням якого є true тоді й тільки тоді, коли: а) ціле a ділиться на ціле b без остачі; b) цілі значення a i b мають однакову парність. 13. Нехай k — ім’я цілої змінної з додатним значенням, яке виражає кількість секунд від початку доби. Написати арифметичний вираз, значенням якого є: а) кількість повних годин, які пройшли до цього моменту; b) кількість повних хвилин, які пройшли до цього моменту. 57


Ïðîãðàìóâàííÿ ë³í³éíèõ àëãîðèòì³â ìîâîþ Turbo Pascal 7.0

14. Нехай n — ім’я цілої змінної. Написати вираз, який задає обчислення значення ( −1)n . 15. Написати вираз, який задає обчислення відстані між двома точками площини. 16. За допомогою стандартних математичних функцій написати вираз, який за дійсними змінними з іменами a та b задає обчислення значення: ab , log b a , e, π / 4 . 17. Написати програму, яка задає читання значень трьох однотипних змінних a, b, c, “циклічний” обмін їх значень ( a → b, b → c, c → a ) та друкування одержаних значень. 18. Нехай Z — числова змінна. Використовуючи лише операції множення та оператори присвоювання змінним із будь-якими іменами, написати програму читання значення z та обчислення: а) z10 ;

б) z12 ;

в) z15 ;

г) z31 ;

ґ) z 48 ;

д) z 62 .

Множень повинно бути якомога менше. Наприклад, обчислення z 6 можна задати так: z2:=z*z; z4:=z2*z2; z6:=z4*z2. Тут лише три множення замість п’яти у рівності z6:=z*z*z*z*z*z.

58


ðîçä³ë

4

ÐÎÇÃÀËÓÆÅÍÍß, ÂÈÁ²Ð ÂÀвÀÍҲ ÒÀ ÑÊËÀÄÅͲ ÎÏÅÐÀÒÎÐÈ

1. УМОВНІ ВИРАЗИ Вирази з булевими значеннями називаються умовними, або умовами. Вони відіграють особливу роль у програмуванні, оскільки є основою для прийняття рішень з вибору подальшого шляху обчислень. Наприклад, нам знайома умова того, що квадратне рівняння ax 2 + bx + c = 0 , задане коефіцієнтами a, b, c (значеннями числових змінних), при a ≠ 0 буде мати два корені: b*b–4*a*c>0. Залежно від її значення true або false можна, наприклад, надрукувати ці два корені або з’ясувати, чи має рівняння один корінь чи не має жодного. Для цього з’ясування може знадобитися ознака b*b–4*a*c=0. Часто умову використовують як ознаку деякої властивості, істинну, якщо властивість має місце, й хибну, якщо це не так. Наприклад, при a ≤ 0 умови b*b–4*a*c>0 та b*b–4*a*c=0 є ознаками того, квадратне рівняння має відповідно два та один корінь. Найпростіші умови, головним чином, записують як рівності або нерівності. Приклади. Умову того, що значення цілої змінної x є парним, можна записати по-різному. 1. Парне число при діленні на 2 дає остачу 0: x mod 2 = 0. 2. Нагадаємо: вбудована функція odd повертає true за непарного аргументу. Тоді умова парності може виглядати так: not odd(x). Розглянемо умову того, що значення цілої змінної x є квадратом цілого числа, тобто x — ціле число. У цій ситуації ціла частина кореня при 59


Ðîçãàëóæåííÿ, âèá³ð âàð³àíò³â òà ñêëàäåí³ îïåðàòîðè

піднесення до квадрату дорівнює x, в іншій — не дорівнює. Скористаємося тим, що функція trunc за дійсним значенням породжує ціле, а sqr за цілим аргументом обчислює ціле значення. Звідси маємо такий вираз: sqr(trunc(sqrt(x))) = x. ‹ Складніші умови формулюють як системи або сукупності, складені з простіших умов. Системи умов записують мовою Паскаль за допомогою операції and. Розглянемо ознаку того, що значення числової змінної x належить a ≤ x проміжку [a; b]. Математичні вирази a ≤ x ≤ b або  є системами, x ≤ b і їм відповідає такий вираз (дужки в ньому обов’язкові!). (a <= x) and (x <= b) Математичний вираз a = b = c, що виражає рівність трьох значень, теж є системою, й ознака рівності значень змінних a, b, c має аналогічний вигляд. (a = b) and (b = c) Зауважимо, що записувати як ознаку вираз (a=b) and (b=c) and (a=c) є зайвим, оскільки за будь-яких значень змінних указані два вирази матимуть однакові булеві значення. Припустимо, що значення числових змінних a, b, c представляють довжини відрізків. Запишемо умову того, що з цих відрізків можна утворити трикутник. Математика вимагає такого: a > 0, b > 0, c > 0, a+b > c, a+c > b, b+c > a. Умова, записана мовою Паскаль, може виглядати так. (a>0) and (b>0) and (c>0) and (a+b>c) and (a+c>b) and (b+c>a).‹ Сукупності умов записують мовою Паскаль за допомогою операції or. Приклади. Припустимо, що значення числових змінних a, b, c представляють довжини відрізків, з яких можна утворити трикутник. Умова того, що цей трикутник рівнобедрений, мовою математики записується як сукупність: a = b a = c   b = c , тобто a = b або b = c або a = c. Їй відповідає такий вираз. (a = b) or (a = c) or (b = c) За тих самих припущень запишемо ознаку прямокутності трикутника. Гіпотенузою може бути будь-яка сторона, тому ознака виглядає так. (a*a+b*b=c*c) or (a*a+c*c=b*b) or (b*b+c*c=a*a) 60


2. Îïåðàòîðè ðîçãàëóæåííÿ

2. ОПЕРАТОРИ РОЗГАЛУЖЕННЯ Пригадаймо алгоритм обчислення дійсних коренів рівняння ax 2 + bx + c = 0 за його коефіцієнтами a, b, c (a ≠ 0). Залежно від конкретних значень коефіцієнтів, виконання алгоритму може відбуватися одним із трьох шляхів обчислення. Шлях обирається в результаті визначення, чи справджується умова d > 0; якщо це не так, то чи дійсна умова d = 0. Розглянемо засоби, за допомогою яких у програмі вказують вибір шляху обчислень залежно від тих або інших умов, а потім повернемося до розв’язання квадратного рівняння. Вибір одного з двох можливих шляхів обчислення задають за допомогою операторів розгалуження (умовних операторів), що мають такий вигляд: if умова then оператор1 else оператор2; Нагадаємо: умова — це вираз типу boolean. Дослівний переклад цього запису виглядає так: якщо умова то оператор1 інакше оператор2 При його виконанні спочатку обчислюється значення умови. Якщо отримано значення true, то виконується оператор, записаний після слова then, і на цьому виконання закінчується. Якщо отримано значення false, виконується оператор, записаний після else. Обидва оператори довільні (присвоювання, розгалуження або інші, представлені нижче). Наведений оператор називається оператором розгалуження у повній формі. Йому відповідає блок-схема на рис. 1, а. true

false

true

Умова оператор 1

false Умова

оператор 2

оператор

а) б) Рис. 1. Блок-схеми двох форм оператора розгалуження

Приклад 1. Розглянемо такі оператори: readln(x); if x>=0 then z:=1 else z:=-1; 61


Ðîçãàëóæåííÿ, âèá³ð âàð³àíò³â òà ñêëàäåí³ îïåðàòîðè

=0 Якщо прочитано невід’ємне значення x, то обчислення виразу x>= (перевірка умови) дає значення true, і змінна z отримує значення 1. =0 має значення Якщо ж прочитане значення є від’ємним, то вираз x>= false, і z отримує значення -1. Щоб програму було легше сприймати, оператор розгалуження часто записують так: if умова then оператор1 else оператор2; При розв’язанні багатьох задач оператори розгалуження вкладаються, тому рекомендуємо записувати їх «східцями», наприклад, ось так: if умова then if умова then оператор else оператор else ... Інколи виникають довгі ланцюги розгалужень, у яких після слова else записується наступний оператор розгалуження зі словом if на початку. У цих ситуаціях краще записувати оператори у такому вигляді. if умова then оператор else if умова then оператор else if умова ...‹ Приклад 2. Напишемо програму обчислення коренів. Нам потрібні дійсні змінні для коефіцієнтів a, b, c та дискримінанта d. Вирази для коренів тільки обчислюються й друкуються, тому оголошувати змінні для них не обов’язково. Читання коефіцієнтів задається оператором readln(a,b,c), а об=b*b-4*a*c. Потім алгоритм числення дискримінанта — оператором d:= розгалужується залежно від значення d. Якщо d>0, то друкуються два =0. Якщо вона істинна, треба друкукорені, інакше перевіряється умова d= вати значення −b// 2a, інакше —повідомлення про відсутність коренів. Отже, програма буде такою: program roots; var a, b, c, d : real; begin writeln('Корені квадратного рівняння'); 62


2. Îïåðàòîðè ðîçãàëóæåííÿ

writeln('Уведіть дійсні числа a, b, c (a<>0): ') readln(a,b,c); d:=b*b-4*a*c; if d > 0 then writeln((-b-sqrt(d))/(2*a ),' ', (-b+sqrt(d))/(2*a)) else if d = 0 then writeln(-b/(2*a)) else writeln('Дійсних коренів немає') end. Якщо при виконанні цієї програми увести значення змінних a, b, c >0 буде істинною, тому об(наприклад, відповідно, 1, 3 і 2), то умова d> числюються й друкуються корені -2 та -1. Якщо увести значення 1, 2 і 3, >0 хибна і обчислюється умова d= = 0. Її значенням є false, і то умова d> друкується повідомлення Дійсних коренів немає. Якщо ж задати >0 буде хибною, умова d= =0 істинною; друзначення 1, 2 і 1, то умова d> кується значення -b// (2a), тобто -1. ‹ Приклад 3. Припустимо, за віком особи чоловічої статі визначається її вікова категорія. Якщо вік менше двох років, це немовля. Від двох до п’яти — дитина, від 6 до 14 — хлопець, від 15 до 22 — юнак, від 23 до 60 — чоловік, більше — дід. Нехай вік є значенням змінної age. Тоді вікова категорія друкується при виконанні такого оператора розгалуження: if age < 2 then writeln('немовля') else if age <= 5 then writeln('дитина') else if age <= 14 then writeln('хлопець') else if age <= 22 then writeln('юнак') else if age <= 60 then writeln('чоловік') else writeln('дід');‹ Оператор розгалуження має ще одну, скорочену, форму: if умова then оператор; Якщо обчислення умови дає значення false, то на цьому його виконання закінчується. Йому відповідає блок-схема на рис. 1, б. 63


Ðîçãàëóæåííÿ, âèá³ð âàð³àíò³â òà ñêëàäåí³ îïåðàòîðè

Наприклад, замість наведеного вище оператора if x >= 0 then z:=1 else z:=-1; можна записати такі: z:=-1; if x >= 0 then z:=1; Якщо значення x від’ємне, z залишиться зі значенням -1. Приклад. Виведення коренів квадратного рівняння можна задати за допомогою скороченої форми розгалуження. if d>0 then writeln((-b-sqrt(d))/(2*a),' ', (-b+sqrt(d))/(2*a)); if d=0 then writeln(-b/(2*a)); if d<0 then writeln('Дійсних коренів немає'); Зазначимо недолік цього методу: якщо умова d > 0 істинна, то в цьому прикладі після друкування все одно обчислюються умови d = 0 і d < 0, а це збільшує час виконання програми. Друкування коренів не можна описати в такий спосіб: if d>0 then writeln((-b-sqrt(d))/(2*a),' ', (-b+sqrt(d))/(2*a)); if d=0 then writeln(-b/(2*a)); else writeln('Дійсних коренів немає'); =0 або d< <0, то все буде гаразд, але якщо d> >0, то друкуються Якщо d= =0, яка є хибною, і друкується два корені, потім обчислюється умова d= текст Дійсних коренів немає, а це, вочевидь, неправильно.

3. СКЛАДЕНИЙ ОПЕРАТОР Щоб написати кілька операторів там, де за правилами мови має бути один, наприклад, як гілку в умовному операторі, використовують складений оператор. Складений оператор — це послідовність операторів у «операторних дужках» begin-end. Він має такий загальний вигляд: begin оператор; ... оператор end; 64


3. Ñêëàäåíèé îïåðàòîð

Виконання складеного оператора полягає у послідовному виконанні операторів, записаних у ньому. Тіло будь-якої програми також є складеним оператором. Приклад. Дано трикутник зі сторонами a, b, c. Визначити, який це трикутник: гострокутний, тупокутний чи прямокутний. Для розв’язання цієї задачі слід урахувати таке: 1) довжини відрізків мають бути додатними; з відрізків заданої довжини можна утворити трикутник, тільки якщо сума довжин будь-яких двох з них більше довжини третього; 2) якщо трикутник можна побудувати, він буде прямокутним тоді й тільки тоді, коли (за теоремою Піфагора) сума квадратів двох сторін дорівнює квадрату третьої, причому ця рівність може виконуватися лише для однієї пари сторін; 3) у гострокутному трикутнику сума квадратів кожних двох сторін більше квадрата третьої, а в тупокутного є одна сторона, квадрат якої більше суми квадратів двох інших. Задамо перевірку ознак типів трикутника за допомогою скорочених умовних операторів. Доведеться вкласти послідовність цих операторів у else-гілку, відповідну можливості побудови трикутника, а для цього необхідний складений оператор. Отже, програма матиме такий вигляд: Var a,b,c:real; Begin write('Введіть довжини сторін: '); readln(a,b,c); if (a<=0) or (b<=0) or (c<=0) then writeln('Помилка вхідних даних.') else if (a+b<c) or (a+c<b) or (c+b<a) then writeln('З відрізків такої довжини ', 'утворити трикутник неможливо.') else begin { утворити трикутник можна } if (sqr(a)+sqr(b)=sqr(c)) or (sqr(a)+sqr(c)=sqr(b)) or (sqr(c)+sqr(b)=sqr(a)) then writeln('Трикутник прямокутний'); if (sqr(a)+sqr(b)>sqr(c)) and (sqr(a)+sqr(c)>sqr(b)) and (sqr(c)+sqr(b)>sqr(a)) then writeln('Трикутник гострокутний'); 65


Ðîçãàëóæåííÿ, âèá³ð âàð³àíò³â òà ñêëàäåí³ îïåðàòîðè

if (sqr(a)+sqr(b)<sqr(c)) or (sqr(a)+sqr(c)<sqr(b)) or (sqr(c)+sqr(b)<sqr(a)) then writeln('Трикутник тупокутний'); end End.

4. ОПЕРАТОР ВИБОРУ ВАРІАНТІВ Приклад 1. Продовжимо приклад про вікову категорію. Наведене розв’язання є правильним, але достатньо громіздким. Розв’язання буде набагато наочнішим і коротшим, якщо використати оператор вибору варіантів, або case-оператор (від англ. case — випадок): case age of 0, 1 : writeln('немовля'); 2..5 : writeln('дитина'); 6..14 : writeln('хлопець'); 15..22: writeln('юнак'); 23..60: writeln('чоловік'); else writeln('дід') end;‹ Після слова case записують вираз довільного перелічуваного типу, який називається селектором варіантів (тут це ім’я age). Після слова of записують оператори-варіанти, відмічені деякими з можливих значень селектора. Ці мітки відокремлюють двокрапками. Варіант може відмічатися списком з кількох констант або діапазонів відповідного типу, записаних через кому. У наведеній тут повній формі оператора після слова else записано альтернативний варіант. У скороченій формі його разом зі словом else немає. При виконанні оператора вибору спочатку обчислюється значення селектора. Потім воно послідовно порівнюється з мітками варіантів. Тількино значення селектора збігається з міткою, виконується відповідний оператор, і все закінчується. Якщо значення селектора немає серед міток варіантів, то виконується альтернативний варіант, якщо він є. А якщо немає, то виконання оператора вибору закінчується. Множини значень у списках можуть перетинатися — буде виконано варіант, у списку якого значення селектора знайдено вперше. Приклад 2. Розглянемо таку задачу: увести з клавіатури число, знак операції (+, -, * або /), ще одне число і надрукувати результат застосу66


4. Îïåðàòîð âèáîðó âàð³àíò³â

вання операції до цих чисел. Наприклад, після читання 2, +, 3 друкується 5, а після 2, /, 3 — приблизно 0.667. Для спрощення припустимо, що користувач програми задає числа й знак в окремих рядках і що він пра��ильно вводить числа, але може набрати недопустимий знак операції. Для чисел оголосимо дійсні змінні op1 та op2, а для знака операції — символьну sign. Ознаку відсутності помилки користувача збережемо в булевій змінній ok. Спочатку прочитаємо операнди та знак. Потім за значенням sign оберемо одну з операцій: додавання, віднімання, множення або ділення. Результат запам’ятаємо на місці першого операнда. Якщо знак операції недопустимий або користувач хоче ділити на 0, повідомимо про це та присвоїмо false змінній ok. Якщо помилок не було, то після застосування операції надрукуємо значення першого операнда; якщо були, то нічого не надрукуємо. Отже, гілка обчислень обирається за значенням змінної sign — вона й буде вказана як селектор варіантів. Усі описані дії можна задати за допомогою case-оператора в такий спосіб: writeln('Задайте дійсне число'); readln(op1); writeln('Задайте символ операції (+, -, *, /)'); readln(sign); writeln('Задайте дійсне число'); readln(op2); ok := true; {поки все гаразд} case sign of '+': op1 := op1+op2; '-': op1 := op1-op2; '*': op1 := op1*op2; '/': if op2 <> 0 then op1 := op1/op2 else begin { спроба ділення на 0} writeln('Ділення на 0'); ok:=false end else begin { це “else” відповідає “case” } writeln('Недопустимий знак операції'); ok:=false end end; {це “end” відповідає “case”} if ok then writeln('Результат: ', op1) 67


Ðîçãàëóæåííÿ, âèá³ð âàð³àíò³â òà ñêëàäåí³ îïåðàòîðè

Вправа. Напишіть усю програму. Щоб перевірити її, треба задати не менше чотирьох наборів вхідних даних (по одному для кожного знака операції) і ще два набори, пов’язані з помилками (ділення на 0 та недопустимий знак операції).‹ У case-операторі перед словом else можна (але не обов’язково) писати «;» — на відміну від повної форми умовного оператора, де «;» перед else є синтаксичною помилкою. КОНТРОЛЬНІ ЗАПИТАННЯ 1. Що таке умова? Які існують види умов і як вони формуються? 2. Який тип мають умови в умовних операторах? 3. Синтаксис і семантика команди розгалуження. 4. Що таке складений оператор? 5. Як працює оператор вибору варіантів? 6. Чи може селектор варіантів мати дійсний тип? 7. Чи може селектор варіантів бути іменем змінної або виразом перелічуваного типу? 8. Чи можна в якості міток варіантів у case-операторі використовувати імена змінних або вирази перелічуваних типів? ЗАДАЧІ 1. Написати вираз, який задає перевірку, чи є значення символьної змінної ch: а) цифрою від '0' до '9'; b) малою латинською буквою; c) латинською буквою (великою чи малою); d) цифрою або латинською буквою. 2. Нехай a, b, c, … — імена цілих змінних із додатними значеннями. Написати умову того, що: а) a, b, c задають сторони прямокутного трикутника; b) a, b, c задають сторони гострокутного трикутника; c) a, b, c задають сторони рівнобедреного трикутника; d) a, b, c задають сторони різнобічного трикутника; e) a, b, c, d задають сторони паралелограма; f) a1, b1, c1 і a2, b2, c2 задають сторони двох рівних трикутників; g) цеглину a × b × c можна просунути у прямокутне вікно x × y так, щоб її грані були паралельні сторонам вікна. 3. Нехай a, b — імена цілих змінних із додатними значеннями. Написати умову того, що: 68


4. Îïåðàòîð âèáîðó âàð³àíò³â

а) значення a ділиться на значення b без остачі; b) значення a i b обидва парні або обидва непарні. 4. Підлога в кімнаті складається з квадратних клітин і має розміри n × m. На двох клітинах поставлено стовпи. Написати умову того, що підлогу можна покрити дощечками розмірами 2 × 1. 5. Поле шахівниці визначається парою натуральних чисел від 1 до 8: перше число — номер вертикалі, друге — горизонталі. Нехай k, l, m, n — імена цілих змінних зі значеннями від 1 до 8. Написати умову того, що: а) з поля (k, l) одним ходом тури можна потрапити на поле (m, n); b) поля (k, l) і (m, n) є полями одного кольору; c) з поля (k, l) одним ходом слона можна потрапити на поле (m, n); d) ферзь, розташований на полі (k, l), загрожує полю (m, n); e) кінь, розташований на полі (k, l), загрожує полю (m, n). 6. Використовуючи побітові булеві операції та порівняння, запишіть умову того, що беззнакове ціле X ділиться на 4 без остачі. 7. Змінні a, b, c, d мають дійсні значення. Написати умову того, що відрізки [a; b] і [c; d] прямої Ox мають хоча б одну спільну точку (гарантовано, що a ≤ b та c ≤ d). 8. Написати умову того, що значення дійсних змінних з іменами a, b, c задають рівняння ax + by + c = 0: а) прямої на площині; b) прямої, яка проходить через І, ІІ та ІІІ чверті площини; c) прямої, яка проходить через І, ІІ та ІV чверті площини; 9. Написати умову того, що дві прямі на площині, які задано цілими коефіцієнтами двох рівнянь вигляду ax + by + c = 0: а) паралельні й не збігаються; b) паралельні (можливо, збігаються); c) збігаються; d) перетинаються; e) перпендикулярні. 10. Імена x, y — це імена змінних цілого типу. Імітувати виконання таких операторів, якщо при уведенні x отримує значення: а) 1; b) 2; c) 3; d) 4. readln(x); if x = 1 then y:=16 else if x = 2 then y:=256 else if x = 3 then y:=4096 else y:=10000; writeln(y) 69


Ðîçãàëóæåííÿ, âèá³ð âàð³àíò³â òà ñêëàäåí³ îïåðàòîðè

Переписати розгалуження за допомогою оператора case. 11. За чотирицифровим цілим числом визначити: а) чи є сума його цифр парною; b) чи є воно «щасливим» (сума перших двох цифр дорівнює сумі двох останніх, наприклад, 3407); c) чи є воно паліндромом (читається зліва направо та справа наліво однаково, наприклад, 2992); d) чи збігаються його перша й друга половини, наприклад, 4545; e) чи утворюють його цифри неспадну послідовність, наприклад, 4588; f) чи має воно хоча б дві однакові цифри, наприклад, 3953; g) чи є якісь дві його цифри парними, а дві — ні, наприклад, 2376. 12. Написати програму дослідження, тобто обчислення кількості дійсних коренів рівняння ax 2 + bx + c = 0 за його коефіцієнтами (можливо, a = 0). 13. Дано коефіцієнти a, b, c рівняння ax 2 + bx + c = 0 . Визначити, чи є хоча б один його корінь парним числом. 14. Написати програму, за якою вводяться три цілих числа, перевіряється, чи задають вони довжини сторін трикутника, і, якщо це так, визначається його вигляд: рівнобічний, рівнобедрений і не рівнобічний, різнобічний; гострокутний, прямокутний, тупокутний. 15. Дано координати двох відрізків на координатній площині. Визначити: а) чи належать вони одній прямій і якщо так, то чи мають вони хоча б одну спільну точку; b) чи збігаються відрізки хоча б одним із своїх кінців; c) чи перетинаються ці відрізки. 16. Дано координати кінців відрізка на координатній площині та координати ще двох точок. Визначити, чи лежать ці точки: а) на прямій, яка містить відрізок; b) по один бік прямої або по різні боки прямої, яка містить відрізок. 17. Дано три цілих числа, які задають час, що показує стрілковий годинник (наприклад, 12 30 45 означає 12 год, 30 хв, 45 с). Визначити: а) чи збігаються годинна та хвилинна стрілки у цей час; b) чи знаходяться годинна та хвилинна стрілки під кутом 90 градусів; c) через який найменший час збігатимуться годинна та хвилинна стрілки. 18. У банці знаходяться білі та чорні зернини. З банки виймають навмання дві зернини. Якщо вони мають той самий колір, то їх викидають, а до банки кладуть чорну зернину (чорних зернин достатньо). Якщо ж зернини мають різні кольори, то чорну викидають, а білу повертають до бан70


4. Îïåðàòîð âèáîðó âàð³àíò³â

ки. Ці дії повторюють доти, поки не залишиться одна зернина. Напишіть програму, яка за відомою кількістю чорних та білих зернин визначає колір останньої зернини. 19. Найближча крамниця працює з 7:00 до 19:00 з перервою на обід з 13:00 до 15:00. Гастроном, що знаходиться в 20 хв ходьби, працює з 8:00 до 20:00 та має перерву з 14:00 до 16:00. На відстані, подолати яку можна тільки за 45 хв, знаходиться супермаркет, який працює з 8:00 до 23:00 без перерви. За заданим часом визначити, що краще: a)відвідати найближчу крамницю; b)дійти до гастронома (з урахуванням часу на дорогу); c)рушати в супермаркет (теж з урахуванням часу на дорогу); d)залишитися вдома, оскільки всі магазини зачинено. Час уводиться так: години — ціла частина числа, а хвилини — дробова, тобто 15:30 означає 15 год 30 хв. 20. Дано ціле число, що визначає вік людини. Дописати до нього слово «рік», «роки», «років» відповідно до правил української граматики. Наприклад, 21 рік, 34 роки, 14 років. 21. Розробити програму з використанням команди вибору, яка за введеною датою народження людини визначає, до якого знаку Зодіака вона належить. 20.01 – 18.02 Водолій 19.02 – 20.03 Риби 21.03 – 19.04 Овен 20.04 – 20.05 Телець 21.05 – 21.06 Близнюки 22.06 – 22.07 Рак 23.07 – 22.08 Лев 23.08 – 22.09 Діва 23.09 – 22.10 Терези 23.10 – 22.11 Скорпіон 23.11 – 21.12 Стрілець 22.12 – 19.01 Козеріг

71


Öèêë³÷í³ àëãîðèòìè

ðîçä³ë

5

ÖÈÊ˲×Ͳ ÀËÃÎÐÈÒÌÈ

1. ОПЕРАТОР ЦИКЛУ З ПАРАМЕТРОМ Почнемо з прикладу. Функція факторіала n! невід’ємного цілого числа n визначається формулою n! = (n–1)! · n при n > 0, а 0! = 1. Звідси n! = 1 · 2 · … · (n–1) · n. Щоб обчислити цей добуток при n ≥ 1, треба спочатку присвоїти добутку значення 1, а далі перебрати всі цілі множники від 1 до n та домножити добуток на них, щоразу зберігаючи результат у добутку. Уточнимо ці міркування. Подамо добуток у змінній fact. Отже, якщо n > 0, треба перебрати множники k = 1, 2, …, n і для кожного виконати fact:=fact*k. Іншими словами, треба виконати такі дії: fact:=1; для k = 1, 2, …, n виконати fact:=fact*k Тепер запишемо це мовою Паскаль: fact:=1; for k:=1 to n do fact:=fact*k; Отже, множення fact на k відбувається за вказаних значень від 1 до n (for — це англійське «для», to — «до», do — «виконати»). Загальний вигляд оператора циклу з параметром такий: for ім’я := вираз1 to вираз2 do оператор; Ім’я позначає параметр циклу — змінну перелічуваного типу, сумісного за присвоюванням з типами виразів. Частина оператора від слова for до слова do називається заголовком циклу, а оператор — тілом. 72


2. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ïàðàìåòðîì

У прикладі параметром була змінна k, виразами — 1 та n, тілом — fact:=fact*k. Оператор циклу з параметром ще називають for-оператором. Опишемо дещо спрощено його виконання (в дійсності воно складніше й має деякі «підводні камені»). Спочатку обчислюються й порівнюються вирази в заголовку. Якщо значення першого виразу A більше ніж другого B, на цьому виконання оператора циклу закінчується. Інакше параметр циклу послідовно отримує значення A, A+1, …, B, і при кожному з них виконується тіло циклу. Оператор циклу з параметром має ще одну форму. for ім’я := вираз1 downto вираз2 do оператор; Усі позначення мають той самий зміст, що й у першій формі. Слово downto («униз-до») замість слова to означає, що при A ≥ B тіло циклу виконується зі значеннями параметра циклу, які послідовно зменшуються: A, A-1, …, B. Відповідно, при A < B тіло циклу не виконується жодного разу. Оператори циклу з переліком застосовують, коли кількість виконань тіла циклу відома до початку його виконання. Мова Turbo Pascal не забороняє змінювати параметр циклу в тілі циклу, але наслідки цього можуть бути непередбачуваними, тому варто вважати, що змінювати параметр заборонено. Граничні значення, задані виразами в заголовку оператора, обчислюються один раз і запам’ятовуються. Якщо ці вирази містять змінні, що отримують нові значення при виконанні циклу, ці зміни не впливають на граничні значення. Варто вважати, що за межами оператора циклу значення параметра невизначено і там його не використовувати.

2. ПРИКЛАДИ ОРГАНІЗАЦІЇ ЦИКЛІВ З ПАРАМЕТРОМ Приклад 1. За натуральним значенням змінної n обчислити значення виразу 2 + 2 + … + 2 (n знаків кореня). Результат можна обчислити як n-й елемент послідовності 2 , 2 + 2 , 2 + 2 + 2 , … . На кожному кроці до попереднього результату додаєть-

ся 2 й береться квадратний корінь із цієї суми. Зазначимо, що 73

2 утво-


Öèêë³÷í³ àëãîðèòìè

рюється як корінь з суми 2 та попереднього результату 0, тому перше значення 2 теж можна обчислити в циклі. rez:=0; for i:=1 to n do rez:=sqrt(rez+2); Приклад 2. Скласти програму піднесення до квадрата натурального числа n, використовуючи таку закономірність: 12 = 1 , 22 = 1 + 3 , 32 = 1 + 3 + 5 , 42 = 1 + 3 + 5 + 7 , …, n2 = 1 + 3 + 5 + 7 + ... + (2n − 1) . У цій закономірності бачимо: квадрат числа n є сумою n непарних натуральних чисел від 1 до 2n – 1. Отже, накопичимо числа вигляду 2i–1 при i = 1,2,…,n в сумі s, яка спочатку дорівнює 0. s:=0; for i:=1 to n do s:=s+2*i-1; Приклад 3. Знайти суму дільників заданого натурального числа n. «Лобове» розв’язання задачі є очевидним. Перебрати всі числа від 1 до n і, якщо n ділиться на поточне число, додати це число до накопичуваної суми. S:=0; for i:=1 to n do if n mod i = 0 then S:=S+i; Кількість повторень циклу можна зменшити приблизно наполовину, адже кожне натуральне число ділиться на 1 і на себе, а наступний за величиною після n множник не більше половини n. Звідси візьмемо 1 + n за початкове значення суми, а далі переберемо числа від 2 лише до половини n. S:=1+n; for i:=2 to n div 2 do if n mod i = 0 then S:=S+i; Проте кількість повторень можна зменшити ще, врахувавши таку особливість: якщо добуток двох дільників числа n дорівнює n, то хоча б один з них не більше

n . Це дозволяє, додавши до суми спочатку 1 і n, потім

перебрати числа i від 2 до [ n ] і додати до суми кожен дільник i разом з n div i. S:=1+n; 74


2. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ïàðàìåòðîì

for i:=2 to round(sqrt(n)) do if n mod i = 0 then S:=S+i+S div i; Значення квадратного кореня округлюється функцією round, оскільки в заголовку циклу for можна використовувати вирази тільки перелічуваного типу. Приклад 4. Формула бінома Ньютона ( a + b)n =

n

∑ Cnk ak bn −k — одна

k =0

з найбільш відомих у математиці. Коефіцієнти

Cnk

=

n! називаютьk !(n − k )!

ся біномними. Обчислимо біномний коефіцієнт за заданими n і k. Можна написати три цикли для обчислення факторіалів (див. параграф 4.1), але це дуже нераціонально. По-перше, одні й ті самі значення, що виникають у процесі обчислення факторіалу, обчислюються тричі. По-друге, функція m! швидко зростає при збільшенні m, тому існує чимало значень n і k, при яких значення Cnk подається в цілих типах, а n! і k! — ні. Подивимося на формулу біномного коефіцієнта: n! n (n − 1) ⋅ ... ⋅ ( n − k + 1) = . k !(n − k )! 1⋅ 2 ⋅ ... ⋅ k У ній закладено обчислення послідовності значень 1, n/1, n · (n–1)/(1 · 2), ... , n(n–1) · … · (n–k+1)/(1 · 2 · ... · k), в якій кожне наступне значення отримується з попереднього шляхом множення на дріб вигляду (n–i+1)/i. Усі члени цієї послідовності цілі, тому ці обчислення можна задати так: d:=1; for i:=1 to k do d:=d*(n-i+1) div i; З урахуванням тотожності Cnk = Cnn − k при k ? n/2 кількість повторень циклу можна зробити рівним k, інакше — n – k. Не забудемо також, що Cnk = 0 при k > n, k < 0 або n < 0. Оголосимо змінні n і k типу integer, C типу longint. Опишемо обчислення такими операторами (значення n і k вважаються вже заданими, обчислений коефіцієнт зберігається в змінній C). 75


Öèêë³÷í³ àëãîðèòìè

if (k>n) or (k<0) or (n<0) then C:=0 else begin if k > n div 2 then k:=n-k; { C(n,k) = C(n,n-k) } C:=1; for i:=1 to k do C:=C*(n-i+1) div i; end; Для перевірки, чи правильно запрограмовано обчислення, треба задати три пари значень n і k, при яких результатом має бути 0, а також пари, при яких цикл з параметром виконується один і кілька разів. Особливої уваги потребують випадки, коли k = n div 2 і k = n div 2 + 1, оскільки за таких значень k при фіксованому n біномні коефіцієнти максимальні. Варто також дослідити граничні пари значень n і k, за яких правильний результат подається в типі longint. Приклад 5. Відомо, що коробка цукерок коштує B гривень, шоколадка — C, а пачка печива — P (B > C > P, усі числа натуральні). У покупця є N гривень. Чи можна купити на ці гроші T предметів, витративши гроші повністю? Спочатку розглянемо простий і не надто раціональний спосіб. Організуємо повний перебір всіх варіантів, враховуючи, що коробок з цукерками може бути не більше N div B, шоколадок — не більше N div C, пачок печива — не більше N div P. Отже, утворимо три вкладених цикли й у внутрішньому вкажемо перевірку умови задачі. for box:=0 to N div B do for chock:=0 to N div C do for pack:=0 to N div P do if (box*B+chock*C+pack*P = N) and (box+chock+pack = T) then writeln('Коробок: ',box,'; шоколадок: ', chock,'; пачок: ',pack); Проте кількість повторень тіла внутрішнього циклу повторень можна суттєво зменшити. По-перше, якщо придбано box коробок, то залишається сума грошей N-box*B, на яку можна придбати не більше (Nb o x * B ) d i v C шоколадок. Між іншим, це гарантує, що box*B+chock*C ≤ N. По-друге, якщо придбано box коробок і chock шоколадок, то за умовою задачі кількість пачок має дорівнювати T76


2. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ïàðàìåòðîì

(box+chock). Тоді замість перебору значень pack достатньо виконати присвоювання pack:=T-(box+chock), зменшивши цим кількість вкладених циклів. for box:=0 to N div B do for chock:=0 to (N-box*B) div C do begin pack:=T-box-chock; if (box*B+chock*C+pack*P = N) then writeln('Коробок: ',box, '; шоколадок: ', chock,'; пачок: ',pack); end; Можна було б утворити зовнішній цикл по кількості pack пачок печива або по кількості chock шоколадок, а не по кількості box коробок з цукерками. Але тоді кількість виконань тіла була б більшою, оскільки B > C > P. Наведений фрагмент можна удосконалити, якщо врахувати, що розв’язок задачі існує лише за умови T*P ≤ N. Приклад 6. Послідовність чисел 1, 1, 2, 3, 5, 8, 13, … , у якій перші два числа дорівнюють 1, а кожне наступне, починаючи з третього, дорівнює сумі двох попередніх, називається послідовністю чисел Фібоначчі. Обчислити за номером n (n > 0) n-е число Фібоначчі. Якщо n = 1 або 2, результатом є 1. Якщо n > 2, будемо визначати числа одне за одним, поки не отримаємо числа із заданим номером. За першими двома обчислимо третє, за другим і третім — четверте, за третім і четвертим — п’яте, і так далі. Як бачимо, для визначення нового числа з усіх попередніх чисел потрібні тільки два останніх. Отже, у нас з’являються поняття: передостаннє число, останнє та наступне, а також номер числа, визначеного останнім. Представимо їх змінними, відповідно, fa, fb, fc та i. Розглянемо, як мають змінюватися їх значення: i fa fb fc На початку першого кроку 2 1 1 ? Обчислення суми 2 1 1 2 На початку другого кроку 3 1 2 2 Обчислення суми 3 1 2 3 На початку третього кроку 4 2 3 3 Обчислення суми 4 2 3 5 На початку четвертого кроку 5 3 5 5 77


Öèêë³÷í³ àëãîðèòìè

Щоб реалізувати ці зміни, треба на кожному кроці виконувати fc:=fa+fb, після чого збільшувати номер i та готуватися до наступного кроку: fa:=fb; fb:=fc. Зауважимо, що перестановка цих двох присвоювань була б грубою помилкою. Реалізуємо наведені міркування в такій програмі. program fibonacci;{обчислити n-е число Фібоначчі} var fa, fb, fc : longint; {два попередні й нове числа Фібоначчі} n, i : integer; Begin writeln('Задайте номер числа Фібоначчі: '); readln(n); if n<=2 then writeln(1) else begin fa:=1; fb:=1; {пара останніх чисел} {цикл переходу до нової пари останніх чисел} for i:=3 to n do begin fc:=fa+fb ; {наступне останнє число} fa:=fb; {підготовка до наступного кроку } fb:=fc; {або «переприсвоювання»} end; writeln(fb) end { гілки else } End; Щоб перевірити цю програму, задайте номери 1, 2 й деякі інші так, щоб цикл виконувався один і декілька разів, наприклад, номери 3 й 8. Результатами мають бути числа, відповідно, 1, 1, 2 та 21. Цікаво також побачити, що 144 — це 12-е число Фібоначчі (1 і 144 — єдині числа Фібоначчі, рівні квадратам своїх номерів). Вправа. Змініть програму так, щоб можна було визначити найбільше число Фібоначчі типу integer (word, longint) та його номер. ‹ Числа Фібоначчі мають величезне значення в усій математиці й носять ім’я видатного італійського матем��тика Леонардо Пізано Фібоначчі (точніше, Боначчі-молодший). Він сформулював кілька цікавих задач; найвідомішою з них стала така «задача про розмноження кролів». Першого січня ми маємо пару кролів. Ця пара може народити нову пару через два місяці, тобто у перший день березня, а потім на перший день кожного наступного місяця. Кожна народжена пара кролів виростає 78


2. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ïàðàìåòðîì

за місяць і ще через місяць народжує нову пару кролів. Скільки пар кролів буде через 12 місяців, тобто через рік? Неважко зрозуміти, що першого січня, лютого, березня тощо буде саме 1, 1, 2, 3, 5, 8, … пар кролів. Продовживши цей ряд, отримаємо, що першого грудня буде 144 пари кролів, а першого січня наступного року — 233. Цікаво, що багато інших задач, зовні не схожих на задачу про кролів, мають такий самий розв’язок. Наведемо приклади. 1. Треба пройти по сходах, які мають п східців. З чергового східця можна ступати або на наступний, або через один. Скільки існує варіантів пройти по сходах? 2. Визначити кількість послідовностей довжини п, складених з символів 0 і 1, в яких жодні дві одиниці не стоять поруч. 3. Шлях бджоли. Бджола починає свій рух у клітину 1 або 2 і прямує до клітини з номером п. Пересуватися бджола може лише по сусідніх клітинах від меншого номера до більшого. Скількома різними шляхами бджола може потрапити до клітини з номером п?

4. Цеглина має висоту 2 й ширину 1. Треба збудувати стіну висотою2 та шириною п. Визначити кількість способів укладки цеглини залежно від п (рис. 1, а). 5. Дві скляні пластини накладено одна на одну. Скільки існує способів проходження променя світла крізь пластини або відбиття від них, якщо промінь змінює свій напрямок n разів (рис. 1, б)?

а) цеглини в стіні б) проходження променя Рис. 1. Кількості способів є числами Фібоначчі 79


Öèêë³÷í³ àëãîðèòìè

3. ОПЕРАТОР ЦИКЛУ З ПЕРЕДУМОВОЮ Продовжимо приклад з обчисленням факторіала. Після виконання тіла циклу зі значеннями параметра 1, 2, …, n змінна fact отримує послідовні значення 1!, 2!, …, n!. Щоб перейти від попереднього значення fact до наступного, треба збільшити значення k та домножити fact на нове значення k. Але це можна повторювати, поки k не досягло n. Отже, в циклі ще повинна бути перевірка умови k < n. Уточнимо опис цих дій, врахувавши, що починати треба зі значенням k = 0. k:=0; fact:=1; Поки k < n виконувати: k:=k+1; fact:=fact*k Мовою Паскаль ці дії описуються так. { ініціалізація множника та факторіала } k:=0; fact:=1; while k < n do begin { «поки k < n виконувати» } k:=k+1; { наступний множник } fact:=fact*k; { нове значення факторіала } end; { k = n, fact = n! } Оператор циклу з передумовою (while-оператор) має такий загальний вигляд: while умова do оператор; Тут while умова do — заголовок циклу, а оператор — тіло (слова while та do зарезервовані). Оператор циклу виконується так. Спочатку обчислюється умова в заголовку. Якщо вона істинна, виконується тіло циклу й знов обчислюється умова. Якщо вона істинна, все повторюється. Виконання циклу закінчується, коли при обчисленні умови утворюється false. Отже, останній раз в циклі тільки обчислюється умова, а тіло циклу не виконується. Якщо при першому обчисленні умови вона виявляється хибною, тіло циклу не виконується жодного разу. Оператору циклу з передумовою відповідає блок-схема, подана на рис. 2. Умову в операторі циклу називають умовою продовження, оскільки, якщо вона істинна, виконання оператора циклу продовжується. Виконання циклу починається саме з обчислення умови, тому її ще називають передумовою. 80


4. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ïåðåäóìîâîþ

true

false Умова

Оператор

Рис. 2. Блок-схема оператора циклу з передумовою

Оператори циклу з передумовою застосовують, як правило, коли кількість повторень циклу заздалегідь невідома. Після while-оператора варто записати коментар із запереченням умови продовження. Це допомагає відстежити значення змінних після закінчення оператора циклу.

4. ПРИКЛАДИ ОРГАНІЗАЦІЇ ЦИКЛІВ З ПЕРЕДУМОВОЮ Приклад 1. Обчислити суму цифр заданого натурального числа. Нехай задане число є значенням змінної n. Щоб знайти суму його цифр, можна діяти так. Спочатку сума sum дорівнює 0. Далі беремо молодшу цифру числа, додаємо її до суми та викреслюємо з числа. Повторюємо ці дії, доки в числі є цифри. Молодша цифра числа n є значенням виразу n mod 10, її «викреслення» з числа можна подати як n := n div 10. Наприклад, 123 mod 10 = 3, 123 div 10 = 12. Отже, маємо такі оператори: sum := 0; { початкова сума цифр } while n <> 0 do begin sum := sum + n mod 10;{додамо молодшу цифру} n := n div 10 {та «викреслимо» її} end; { n=0, значення sum є сумою всіх цифр } Зауважимо, що застосувати цикл for у цій ситуації неможливо, оскільки кількість цифр наперед не відома.

81


Öèêë³÷í³ àëãîðèòìè

Приклад 2. Число називається автоморфним, якщо його цифри збігаються з останніми цифрами його квадрата. Наприклад, 52 = 25 , 6 2 = 36 , 252 = 625 . Визначити, чи є натуральне число N автоморфним. Спочатку обчислимо N 2 , а потім будемо циклічно порівнювати останні цифри обох чисел і, якщо вони рівні, вилучати їх з обох чисел. Процес обірветься, коли від початкового числа залишиться 0 або останні цифри не будуть збігатися. var N, sqr_N : longint; Begin write('Введіть число '); readln(N); sqr_N:=sqr(N); while (N<>0) and (N mod 10=N_sqr mod 10) do begin N := N div 10; N_sqr := N_sqr div 10; end; if N=0 then writeln('Число автоморфне.') else writeln('Число не автоморфне.'); End. Приклад 3. Довжини сторін прямокутника є натуральними числами. Від прямокутника відрізають квадрати максимальної площі, поки від нього не залишиться квадрат. Знайти кількість утворених квадратів. Нехай a та b — довжини сторін прямокутника. Очевидно, що сторона квадрата максимальної площі рівна меншій стороні прямокутника. Нехай це буде a. Відрізаючи квадрат, ми зменшуємо більшу сторону прямокутника (b:=b–a). Якщо a > b, то зменшується a: a:=a–b. Відрізання припинимо, коли прямокутник «виродиться» у квадрат. Кількість квадратів — це кількість відрізань плюс один, оскільки останнє відрізання утворює два квадрати. Отже, при кожному відрізанні будемо збільшувати лічильник відрізань count, почавши з 1. var a, b, count : longInt; Begin write('Введіть сторони прямокутника: '); readLn(a,b); count:=1; while a <> b do begin if a > b then a := a-b 82


4. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ïåðåäóìîâîþ

else b := b-a; inc(count) end; writeLn('Кількість квадратів: ', count) End. Проімітуємо підрахунок квадратів при a = 5, b = 3. Операція count := 1 a := a–b; inc(count) b := b–a; inc(count) a := a–b; inc(count)

a 5 2 2 1

b 3 3 1 1

count 1 2 3 4

Якщо з цього алгоритму вилучити підрахунок кількості квадратів, отримаємо добре відомий алгоритм Евкліда, за яким можна обчислити найбільший спільний дільник (НСД) двох натуральних чисел. НСД є остаточним значенням змінних a та b. Наведений алгоритм працює дуже довго, якщо одне з чисел велике, а друге мале. Для наочності запустіть програму й уведіть, наприклад, 2147483647 (найбільше число типу longint) та 1. Проте роботу можна суттєво прискорити. Якщо a > b, то в результаті послідовних віднімань b від a в a залишається остача від ділення a на b. Отже, замість віднімання обчислимо a:=a mod b, збільшивши count відразу на a div b. При цьому, якщо a кратне b, то a mod b стане рівним 0, а кількість a div b враховує як квадрати, що відрізаються, так і квадрат-залишок. Отже, змінимо умову продовження на (a<>0) and (b<>0) та почнемо з count = 0. Оператори між уведенням та виведенням набудуть такого вигляду: count:=0; while (a<>0) and (b<>0) do begin if a > b then begin a:=a mod b; inc(count,a div b) end else begin b:=b mod a; inc(count,b div a) end end; 83


Öèêë³÷í³ àëãîðèòìè

Розглянувши уважно остачі від ділення на двох послідовних кроках, неважко переконатися, що більше з двох чисел за два кроки алгоритму зменшується більш ніж удвічі. Завдяки цьому алгоритм працює швидко за будь-яких вхідних чисел. Цікаво, що якщо вхідними є два послідовних числа Фібоначчі, то всі остачі від ділення a mod b дорівнюють різницям a – b, тобто другий алгоритм працює не краще ніж перший. При цьому послідовними значеннями змінних a та b є числа Фібоначчі, а результатом — номер меншого з вхідних чисел у послідовності чисел Фібоначчі. Якщо в другому алгоритмі вилучити обробку лічильника count, отримаємо алгоритм Евкліда в сучасній версії. НСД вхідних чисел — це значення тієї зі змінних a та b, яке після закінчення циклу не дорівнює 0. Приклад 4. Напишіть програму визначення останньої ненульової цифри в запису числа N! (N — типу integer). Наприклад, у 5! = 120 цією цифрою є 2, у 7! = 5040 — 4. Безпосередньо обчислити N! не можна, оскільки факторіали швидко зростають. Наприклад, найпотужніший цілий тип longint «вміщує» лише 12! = 518918400. Існує цікавіше розв’язання. Увага: старші цифри факторіалу не впливають на молодші цифри наступного факторіалу, а нулі в кінці факторіалу залишаються і у подальших факторіалів. Ці міркування дозволяють замість факторіалів зберігати лише декілька їх останніх ненульових цифр та відкидати молодші нулі. var f, i, N : longInt; Begin write('Введіть натуральне число: '); readln(N); f:=1; for i:=1 to N do begin f:=f*i; while f mod 10 = 0 do f:=f div 10; f:=f mod 10000; end; writeln('Остання ненульова цифра: ', f mod 10); End. Приклад 5. Розкласти натуральне число більше 1 на прості множники, тобто подати у вигляді добутку простих чисел. Наприклад, 10 = 2 * 5; 16 = 2 * 2 * 2 * 2. 84


4. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ïåðåäóìîâîþ

Будемо перебирати всі натуральні числа k, починаючи з 2, та, поки задане число N ділиться на k, виводити k й ділити N на нього. Процес продовжимо, поки N більше 1. Помітимо: після всіх можливих ділень на простий дільник він відсутній в N. Звідси, коли дійдемо до складеного k, N не поділиться на k, оскільки всі прості дільники числа k вже відсутні в N. Наприклад, 20 двічі ділиться на 2 й потім не ділиться на 4, а тільки на 5: 20 = 2*2*5. Отже, запропоноване розв’язання не потребує перевірки числа на простоту. var N, k : longint; Begin write('Введіть число: '); readln(N); write(N, ' = '); k:=1; while N > 1 do begin inc(k); { збільшуємо дільник на 1 } while N mod k = 0 do begin write(k); {поділилося - виводимо множник} N:=N div k; {якщо залишилося більше 1,то буде продовження} if N > 1 then write('*'); end; end; End. Перевірте цю програму для чисел 2, 5, 20, 105, 1073741824 (це 2 в степені 20). Додайте обробку ситуації N = 1. Спробуйте написати програму, яка організовує виведення результату у іншому форматі з використанням позначки степеня «^». Тоді розклади виглядатимуть так: 48=2^4*3, 200=2^3*5^2. Приклад 6. Відома формула для обчислення числа π: π 1 1 1 1 = 1 − + − + −…. 4 3 5 7 9 Узявши суму п доданків (п — натуральне число), отримаємо наближення до π. Знайти п, при якому наближення відрізняється від π не більше ніж на 0.001. 85


Öèêë³÷í³ àëãîðèòìè

Варіант 1. Скористаємося стандартною функцією Pi: значення, яке вона повертає, вважатимемо еталонним. Будемо обчислювати суму, додаючи або віднімаючи дроби по одному, поки різниця між сумою та еталоном не стане менше 0.001. Нумерацію доданків почнемо з 1. Перший доданок відразу додамо до суми. Далі в циклі дроби з парними номерами віднімаються, з непарними — додаються. Знаменники доданків щоразу збільшуються на 2. program p4_1; const difference = 0.001; { різниця } var p4 : real; { наближена сума } k, n : integer; { знаменник та номер дробу } Begin k:=1; n:=1; { перші знаменник та номер } p4:=1; { перший доданок 1 враховано в сумі } while abs(Pi-p4*4) > difference do begin inc(k,2);inc(n);{наступні знаменник і номер} if odd(n) then p4 := p4+1/k else p4 := p4-1/k; end; { abs(Pi-p4*4) <= difference } writeln(n); End. Варіант 2. Спробуємо обійтися без еталонного значення. Послідовні доданки в сумі за абсолютною величиною монотонно зменшуються та по черзі додаються й віднімаються. Звідси знак нескінченного «хвоста» суми у формулі збігається зі знаком першого доданка «хвоста». Тому число π обов’язково знаходиться між двома сусідніми значеннями суми, причому ближче до останнього з них. Заведемо дві змінні: p41 має значеннями суми парного числа дробів, p42 — непарного. На парному кроці за значенням p41 обчислюється p42, на непарному — навпаки, за p42 обчислюється p41. Якщо різниця між ними буде не більше 0.002, це гарантує, що різниця між останньою з них та π не більше 0.001. program pi4_2; const difference = 0.001; var p41, p42 : real; k, n : longint; begin p41:=1; p42:=2/3; n:=2; k:=3; while 4*abs(p41-p42) > 2*difference do 86


5. Îïåðàòîð öèêëó ç ï³ñëÿóìîâîþ

begin inc(k,2); inc(n); if odd(n) then p41:=p42+1/k else p42:=p41-1/k; end; { abs(Pi-p4*4) <= 2*difference } if odd(n) then p42 := p41; writeln(n); end.

5. ОПЕРАТОР ЦИКЛУ З ПІСЛЯУМОВОЮ Оператор циклу з післяумовою, або repeat-оператор, має такий загальний вигляд: repeat послідовність операторів until умова; Слова repeat і until («повторювати» та «доти, поки не») є ключовими; вони відіграють роль операторних дужок, у які вкладено тіло циклу — послідовність операторів. Оператор циклу з післяумовою виконується так. Спочатку виконуються оператори тіла циклу, потім обчислюється умова. Якщо вона істинна, цикл завершується, інакше повторюється тіло й знову обчислюється умова. На відміну від оператора з передумовою, цикл починається діями в тілі й закінчується обчисленням умови. Циклу з постумовою відповідає блок-схема на рис. 3.

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

Умова

false

true Рис. 3. Блок-схема оператора циклу з постумовою

87


Öèêë³÷í³ àëãîðèòìè

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

6. ПРИКЛАДИ ОРГАНІЗАЦІЇ ЦИКЛІВ З ПІСЛЯУМОВОЮ Приклад 1. Знову повернемося до обчислення факторіалу та опишемо його за допомогою оператора repeat-until. Цикл домноження на новий множник треба починати за n>0. Цикл закінчується, коли множник k досягне верхньої межі n, тому умовою завершення буде вираз k=n. k:=0; fact:=1; if n > 0 then repeat inc(k); fact:=fact*k; until k = n; { k = n, fact = n! } Приклад 2. Прочитати три числа, які задають довжини сторін трикутника, та обчислити його периметр і площу. Якщо трикутник з такими сторонами не існує, треба повторити введення. Нехай a, b, c — дійсні змінні, яким присвоюються введені значення, p — змінна для півпериметра. Значення змінних треба спочатку прочитати, і лише потім перевіряти умову того, що вони додатні й задовольняють нерівності трикутника. Якщо ця умова задовольняється, повторювати введення не треба, тому вона має стати умовою виходу з циклу. Отже, скористаємося repeat-оператором: Repeat writeln('Задайте довжини сторін трикутника '); readln(a, b, c); until (a>0) and (b>0) and (c>0) and (a+b>c) and (a+c>b) and (b+c>a);

88


6. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ï³ñëÿóìîâîþ

p := (a+b+c)/2; { півпериметр } writeln('Периметр: ', 2*p); writeln('Площа: ', sqrt(p*(p-a)*(p-b)*(p-c))); 1 1 1 + + … + зі збільшенням числа 2 3 n доданків n необмежено зростає. Визначити мінімальну кількість доданків Приклад 3. Відомо, що сума 1 +

n

n, за яких

1

∑k

більше від заданого додатного дійсного числа Т.

k =1

Будемо обчислювати суми одного, двох, трьох тощо доданків, поки сума не стане більше T — кількість доданків у сумі й буде потрібним числом. На кроці обчислення переходимо до наступного номера k та додаємо 1/k до суми. k:=0; sum:=0; repeat inc(k); sum:=sum + 1/k; until sum > T; { k має потрібне значення } Приклад 4. Перевірити, чи утворюють цифри заданого натурального числа спадну послідовність. Наприклад, для числа 96521 відповідь буде «так», а для 9768 — «ні». Розв’язок цієї задачі базується на таких міркуваннях. Якщо взяти останню цифру числа за еталон, то наступна ліворуч цифра повинна бути більшою за нього. При справдженні цієї умови, можна відкинути останню цифру числа шляхом ділення на 10 і повторити процес. Процес закінчиться, якщо знайдено цифру, що не перевищує еталон, або всі цифри числа розглянуті (число перетворилося на нуль). var N : longint; etalon : byte; Begin write('Введіть число: '); readln(N); repeat etalon := N mod 10; N := N div 10;

89


Öèêë³÷í³ àëãîðèòìè

until (N=0) or (N mod 10 <> etalon); if N = 0 then writeln('Так.') else writeln('Ні.'); End. Приклад 5. Визначити всі прямокутники, що мають натуральні довжини сторін і задану площу S (натуральне число). Очевидно, що принаймні один прямокутник з площею S існує (одна з довжин сторін дорівнює 1, друга — S). Отже, будемо перебирати довжини k, починаючи від 1, і для кожної перевіримо, чи ділиться S на k. Якщо ділиться, виведемо k та S div k. Значення k більше від кореня квадратного з S, задають прямокутники, в яких сторони просто помінялися місцями, тому розглядати їх не потрібно. var S, k : longint; Begin write('Введіть площу: '); readln(S); k := 1; repeat if S mod k = 0 then writeln(k, '*', S div k, ' = ', S); inc(k); until k > sqrt(S); End. Приклад 6. За двома натуральними числами N і M визначити, чи можна N подати як суму двох ненульових доданків, сума квадратів яких дорівнює M. Наприклад, 10 = 7+3, 58 = 7 2 + 3 2 . Незважаючи на те, що необхідно підбирати два доданки, достатньо одного циклу, адже другий доданок визначається на основі умови. Якщо позначити доданки через x і y, то y = M − x2 . Цикл перебору працює, поки не знайдено розв’язок та x не стало більше y. Якщо вихід з циклу відбудеться за першою умовою, то розв’язок знайдено, інакше розв’язків немає. var x, y, N : longint; begin write('Введіть два числа: '); 90


6. Ïðèêëàäè îðãàí³çàö³¿ öèêë³â ç ï³ñëÿóìîâîþ

readln(N,М); x := 0; repeat inc(х); y := trunc(sqrt(M-sqr(x))); until (х>y) or (sqr(x)+sqr(y)=M); if x>y then writeln('Розв'язок відсутній.') else writeln('Розв'язок: ', x, ' ', y); End. Приклад 7. За натуральним числом N визначити всі трійки натуральних чисел x, y, z, за яких x2 + y 2 + z2 = N . Використаємо вкладені цикли. У зовнішньому циклі (зі змінною x) переберемо цілі числа від 0 до кореня квадратного з N (це найбільше значення, якого можуть набувати змінні). Діапазон значень у середньому циклі (зі змінною y) можна дещо скоротити, оскільки на початку його виконання відоме значення «зовнішньої» змінної x. Аналогічно скорочується й діапазон значень у внутрішньому циклі (зі змінною z), оскільки на початку його виконання відомі значення і x, і y. Задача може не мати розв’язків, тому використаємо змінну булевого типу, яка фіксує, чи знайдено хоча б один розв’язок. Спочатку цій змінній присвоїмо false (розв’язок ще не знайдено). var x, y, z, N : longint; flag : Boolean; Begin write('Введіть число: '); readln(N); flag:=false; writeln('Розв''язки рівняння x^2+y^2+z^2 = ', N); x:=-1; repeat inc(x); y:=-1; repeat inc(y); z:=trunc(sqrt(N–x*x–y*y))-1; repeat inc(z); if sq(x)+sqr(y)+sqr(z) = N 91


Öèêë³÷í³ àëãîðèòìè

then begin writeln(x,y,z); flag:=true; end until sqr(z) > N – sqr(x) – sqr(y); until sqr(y) > N – sqr(x); until x > sqrt(N); if not flag then writeln('Розв''язків немає.'); End.

7. ПОНЯТТЯ СТРУКТУРНОГО ПРОГРАМУВАННЯ Структурне програмування — це написання коду, який має певну структуру й завдяки цьому є зручним для розуміння, наладки, внесення змін та використання. Структурне програмування полягає у використанні невеликого набору простих керуючих структур (структурних операторів), правильність яких легко проаналізувати й установити. При цьому оператори складаються з інших операторів, вкладених у них. Властивість структурності операторів полягає в тому, що кожен оператор має один вхід і один вихід. Програма, записана структурними операторами, називається структурованою. Терміни «структурний оператор» і «структурована програма» з’явилися в 60-ті роки. Спочатку структурних операторів було три — складений begin-end, розгалуження if-then-else і циклу while. Їх структурність очевидна (див. рис. 4.1, 5.2). Було доведено, що будь-яку неструктуровану програму можна перетворити на структуровану, яка задає ті самі обчислення, хоча й за рахунок додавання нових змінних і дублювання фрагментів програми. У мові Паскаль до структурних операторів додано оператори вибору варіантів case та циклу repeat-until і for. Вони теж мають один вхід і один вихід і є структурними. За допомогою структурних операторів мови Паскаль утворюються структуровані програми. У мові Паскаль є засіб, який порушує структурованість програми — це оператор безумовного переходу на мітку. Але в цій книжці мітки та оператори переходу не використовуються.

92


7. Ïîíÿòòÿ ñòðóêòóðíîãî ïðîãðàìóâàííÿ

КОНТРОЛЬНІ ЗАПИТАННЯ 1. Яким може бути тип параметра for-циклу? 2. У чому різниця між операторами for-to і for-downto? 3. В якому випадку значення параметра циклу після завершення виконання оператора for залишається невизначеним? 4. Яке значення має параметр for-циклу після його закінчення, якщо тіло циклу було виконано не менше одного разу й не переривалося операторами переходу? 5. Чи може цикл з параметром for виконуватися нескінченно? 6. Якими можуть бути причини зациклювання програми? 7. У чому різниця між операторами циклу з передумовою та з післямовою? 8. В якому випадку тіло циклу з передумовою не виконується? 9. Опишіть загальний вигляд операторів циклів у мові Паскаль. 10. Чи будь-який циклічний процес можна запрограмувати, з різних форм циклів використовуючи тільки одну? ЗАДАЧІ 1. Імітувати виконання послідовності операторів (усі змінні типу longint). Пояснити зв’язок між значеннями i та x і y: а) i:=1; x:=1; y:=2; while x < y do begin inc(i); x := x*i; y := y*2; writeln(i, ' ', x, ' ', y) end; b) i:=1; x:=1; y:=2; while i <= 10 do begin inc(i); x := x*i; y := y*2; writeln(i, ' ', x, ' ', y) end; 2. Знайти найбільше додатне ціле число n, для якого виконується умова 3n 2 − 730 n < 5 . 3. За заданими a, b і h (або m) надрукувати значення функції arctg(sin x) у точках a, a+h, …, a+mh, де: а) m обчислюється за h, виходячи з того, що a+mh ? b і a+(m+1)h > b; b) h обчислюється за m з рівності a+mh = b. 93


Öèêë³÷í³ àëãîðèòìè

4. За заданим n обчислити значення виразу 3 + 6 + … + 3(n − 1) + 3n (n коренів). 5. Трикутник Паскаля має такий початок.: 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 Числа в трикутнику — це біномні коефіцієнти, тобто в i-му рядку (i = 0, 1, 2, …) на j-му місці (j = 0, 1, …, i) записано Ci j . Написати програму друкування перших 13 рядків трикутника Паскаля (їх номери від 0 до 12). Числа повинні відокремлюватися не менш ніж одним пропуском. 6. За цілим A > 0 обчислити: а) найменше n, при якому n! > A; b) найбільше n, при якому n! ? A. 7. Надрукувати перші k дробових знаків двійкового представлення дійсного числа x. 8. Дано натуральне число N. Визначити: a) чи зустрічається в ньому задана цифра; b) яка цифра в ньому зустрічається частіше a чи b; c) яка в ньому цифра мінімальна (максимальна). 9. Написати програму, яка вводить два натуральних числа M і N і виводить кожне ціле число від M до N, що має таку властивість: а) всі десяткові цифри числа парні; b) послідовність десяткових цифр числа є монотонною (як неспадною, так і незростаючою); c) p-кові цифри числа утворюють арифметичну прогресію; d) p-кові цифри числа утворюють геометричну прогресію; 10. Дано натуральне число N. Визначити, чи є це число: a) таким, що різниця його максимальної та мінімальної цифр число парне; b) «щасливим» (сума цифр першої половини числа дорівнює сумі цифр другої половини, наприклад, 1203, 205007, 23004001); c) таким, що сума його цифр на непарних місцях дорівнює сумі цифр на парних місцях, наприклад, 1210, 432410);

94


7. Ïîíÿòòÿ ñòðóêòóðíîãî ïðîãðàìóâàííÿ

d) симетричним (перша половина числа співпадає з другою, наприклад, 22, 1414, 453453); e) паліндромом (читається в обидва боки однаково, наприклад, 232, 456654); f) Армстронга (число дорівнює сумі своїх цифр, піднесених до степеню, що дорівнює кількості цифр в числі, наприклад, 153 = 13 + 53 + 33 ); g) досконалим (число дорівнює сумі всіх своїх натуральних дільників, крім самого числа, наприклад, 6=1+2+3, 28=1+2+4+7+14). 11. Дано два натуральних числа N та М. Знайти всі спільні дільники цих чисел. 12. Дано натуральне число N. В інтервалі від 1 до N знайти натуральне число, яке має максимальну суму дільників. 13. За заданим n обчислити значення виразу i

n

∑ i =1

∑ cos j j =1 i

∑ sin

. j

j =1

Тригонометричні функції для кожного аргументу мають обчислюватися тільки по одному разу. 14. Значення функцій sin x та cos x треба вивести на екран у два стовпчики для значень x = a, a+h, a+2h, a+3h, … у такий спосіб. Спочатку друкуються перші m значень і виводиться запит, чи продовжувати друкування. Після відповіді користувача «1» друкуються наступні m значень і запит, і так далі. Робота завершується після відповіді «0». Написати програму введення значень a, h, m і описаного друку значень «сторінками». 15. Дано два натуральних числа N (N<100000) та M (M<10). Розробити власний навчальний алгоритм множення у стовпчик даних чисел, демонструючи по кроках процес виконання дій: запис одиниць результату у поточний розряд та перенесення десятків у наступний розряд. Перехід до наступного кроку здійснюється натисканням будь-якої клавіші. 16. Дано два натуральних шестицифрових числа N та M. Розробити власний навчальний алгоритм додавання (віднімання) у стовпчик даних чисел, демонструючи по кроках процес виконання дій: запис одиниць результату у поточний розряд та перенесення десятків у наступний роз-

95


Öèêë³÷í³ àëãîðèòìè

ряд. Перехід до наступного кроку здійснюється натисканням будь-якої клавіші. 17. Дано два натуральних числа N та M. Знайти НСК (найменше спільне кратне) цих чисел. 18. Трикутники, в яких довжини сторін і площ — натуральні числа, називають трикутниками Герона (наприклад, трикутник зі сторонами 13, 14, 15 та площею 84 є трикутником Герона). Скласти програму визначення трикутників Герона, довжини сторін яких не перевищують даного натурального N. 19. Дано натуральне число N. Визначити всі нескорочувані дроби із знаменником N в інтервалі (0,1). 20. Задано натуральне число k. Вивести k-ту цифру послідовності 12345678910111213..., в якій виписані підряд усі натуральні числа.

96


ðîçä³ë

6

ÄÎÏÎ̲ÆͲ ÀËÃÎÐÈÒÌÈ, ÀÁΠϲÄÏÐÎÃÐÀÌÈ

1. ПІДПРОГРАМА — ОПИС РОЗВ’ЯЗАННЯ ПІДЗАДАЧІ Програма — це опис розв’язання деякої інформаційної задачі. Процес побудови програми починається з аналізу задачі. Практично в кожній задачі, аналізуючи її, можна виділити окремі підзадачі, допоміжні до неї. Розв’язання підзадачі можна описати в окремій, спеціально оформленій, частині програми. Такий опис розв’язання підзадачі називають підпрограмою. Щоб вказати виконання підпрограми, у програмі достатньо записати її виклик. Деякі підзадачі доводиться розв’язувати майже в усіх задачах, наприклад, уведення даних із зовнішніх носіїв у змінні програми та їх виведення на носії. Для таких стандартних підзадач кожна система програмування надає великий набір готових підпрограм. Нам уже знайомі підпрограми уведення/виведення readln, writeln, а також обчислення математичних функцій sin, abs тощо. У їх викликах ми лише вказуємо вирази або змінні, що мають оброблятися під час їх виконання. Уявіть собі програму читання двох чисел, їх додавання та друкування суми, якби в ній замість викликів підпрограм readln та writeln був опис усіх дій з уведення та виведення даних. Її текст був би у багато разів довшим, а її сутність, додавання чисел, у цьому тексті просто б загубила97


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

ся. Отже, згадана програма додавання двох чисел є простою саме завдяки стандартним підпрограмам. У практичному програмуванні знати стандартні підпрограми корисно й необхідно, адже застосовувати готові деталі набагато легше, ніж створювати їх самому. Стандартні підпрограми у системах програмування зібрано у спеціальні набори — бібліотеки. Ці підпрограми додаються до машинної програми у процесі компонування. Проте користуватися можна не лише стандартними підпрограмами. Мови програмування дозволяють створювати власні підпрограми, тобто описувати розв’язання потрібних підзадач у окремих програмних одиницях. Виділення окремих підзадач суттєво полегшує проектування та розробку програми для розв’язання всієї задачі. Використання підпрограм для розв’язання підзадач дозволяє зробити програму добре структурованою й зрозумілою. Алгоритм, реалізований підпрограмою, може бути довгим і складним, але він «захований» у підпрограму. Завдяки цьому програма стає простою й короткою, її легше написати й налагодити.

2. ПРОЦЕДУРА — ПІДПРОГРАМА, ЯКА РЕАЛІЗУЄ ОКРЕМИЙ ОПЕРАТОР У мові Turbo Pascal є два види підпрограм — процедури та функції. Вони відрізняються формами запису. Окрім цього, виклики процедур являють собою окремі оператори (як виклики writeln або readln), а виклики функцій — вирази (арифметичні, булеві тощо — як виклики sin, ord або odd). Спочатку розглянемо процедури. Приклад 1. Прочитати два числа типу integer, визначити мінімальне з них та вивести його на екран. У цій задачі легко виділити підзадачі: прочитати числа, визначити мінімальне з двох чисел, вивести відповідь. Перша з них розв’язується за допомогою стандартних підпрограм writeln і readln — спочатку треба вивести на екран запрошення до введення чисел, а потім отримати їх. Щоб вивести відповідь, достатньо одного виклику writeln. Для розв’язання підзадачі «визначити мінімальне з двох чисел» напишемо власну процедуру. Звичайно, щоб визначити мінімальне з двох чи-

98


2. Ïðîöåäóðà — ï³äïðîãðàìà, ÿêà ðåàë³çóº îêðåìèé îïåðàòîð

сел, можна обійтися без підпрограм, адже треба лише перевірити нерівність вигляду a<b та виконати одне з двох присвоювань вигляду c:=a або c:=b. Проте на цьому простому прикладі ми розглянемо важливі поняття, пов’язані з підпрограмами. Підпрограми, як і програми, складаються із заголовка й блоку, лише заголовок має дещо інший вигляд, а після блоку замість «.» має бути «;». Блок підпрограми, як і блок програми, складається з оголошень імен та тіла підпрограми. Заголовок підпрограми є оголошенням її імені, оскільки визначає спосіб використання цього імені у подальшому. У нашій підзадачі присутні три величини — два «вхідних» числа та одне «результатне» (мінімальне з тих двох). Вони є параметрами підзадачі. Ці величини вкажемо як параметри процедури в її заголовку. Дії з ними опишемо в тілі процедури. Програма з процедурою та її викликом має такий вигляд: program MinOfTwoNumbers; var a, b, c : integer; { змінні програми } { процедура обчислення мінімуму } procedure min(x, y : integer; var z : integer); begin if x<y then z:=x else z:=y end; { тіло програми } Begin writeln('Задайте два цілих числа:') readln(a,b); min(a,b,c); { виклик процедури min } writeln('Мінімальне з них: ',c); End. У першому рядку підпрограми записано її заголовок. Службове слово procedure свідчить, що це саме процедура, min є іменем процедури. Вхідні числа при виконанні виклику процедури є значеннями параметрів x і y, результат запам’ятовується як значення параметра z. Ніяких інших імен, окрім параметрів, у нашій підзадачі немає, тому оголошення імен у цьому блоці відсутні. Що означає слово var перед іменем z, уточнимо нижче. При виконанні програми після читання значень змінних a та b виконується виклик процедури min; у виклику вказано величини, з якими треба виконати процедуру (a, b та c).

99


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

На початку виконання виклику утворюються нові окремі змінні, відповідні параметрам x і y (але не z!). Значення a та b присвоюються цим новим змінним. Далі виконуються оператори, записані в тілі процедури: змінна з іменем z отримує потрібне значення. Завдяки слову var перед z у заголовку підпрограми, ця змінна не є новою — цю саму змінну позначає ім’я c, оголошене у програмі. Отже, присвоювання змінній z при виконанні процедури в дійсності є присвоюванням змінній c! Завдяки цьому значення, отримане z, після закінчення виклику підпрограми залишається в змінній c. Проілюструємо виконання програми таблицею, припустивши, що змінні a та b під час уведення отримують значення 1 і 2. У стовпчиках таблиці, що показують стани пам’яті, подано змінні, тобто ділянки пам’яті. Змінні, відповідні параметрам процедури, позначимо, додавши ім’я процедури: min.x, min.y, min.z. Як бачимо, імені c та параметру min.z відповідає один і той самий стовпчик, тобто одна й та сама змінна: Виконувані дії Пам’ять програми Утворення змінних програми a b c ...readln(a, b) 1 2 ? min(a, b, c) 1 2 ? 1 2 min.z min.x min.y Утворення нових змінних Присвоювання x та y 1 2 ? 1 2 Обчислення x < y 1 2 ? 1 2 z := x 1 2 1 1 2 Закінчення виклику min 1 2 1 writeln(…, c) 1 2 1 Зауважимо: якби в тілі процедури було змінено x або y, це не вплинуло б на значення a та b, оскільки іменам x та y відповідають «власні» змінні. Приклад 2. Прочитати три числа типу integer, визначити мінімальне з них та надрукувати його. Скористаємося тим, що у нас уже є процедура обчислення мінімального з двох значень. Спочатку визначимо мінімальне з перших двох чисел, а потім знайдемо мінімальне з цього «першого мінімуму» та третього числа. Отже, двічі викликаємо процедуру min, указавши у викликах відповідні величини. 100


2. Ïðîöåäóðà — ï³äïðîãðàìà, ÿêà ðåàë³çóº îêðåìèé îïåðàòîð

program MinOfThreeNumbers; var a, b, c, m1, m2 : integer; procedure min(x, y : integer; var z : integer); begin if x<y then z:=x else z:=y end; Begin writeln('Задайте три цілих числа:') readln(a,b,c); min(a,b,m1); { m1 -- перший мінімум } min(m1,c,m2); { m2 -- другий мінімум } writeln('Мінімальне з них: ',m2); End. Другий виклик можна записати як min(m1,c,m1) і потім надрукувати значення m1, а не m2, тобто взагалі позбутися змінної m2. На початку цього виклику значення змінної m1 присвоюється параметру x, а потім значення m1 змінюється при виконанні z:=x або z:=y. Приклад 3. Прочитати три цілих числа та видати їх, упорядкувавши за неспаданням. Уведемо числа в змінні a, b, c та за потреби обміняємо їх значення так, щоб справджувалися нерівності a ≤ b ≤ c, а потім виведемо значення a, b, c. Уточнимо дії з упорядкування значень. if a>b then обміняти значення змінних a і b; { нерівність a<=b істинна } if b>c then обміняти значення змінних b і c; { нерівності b<=c і a<=c істинні, тобто значення c - найбільше, } <= { але a<= <=b може стати хибним, тому: } if a>b then обміняти значення змінних a і b. У цьому алгоритмі явно виділяється підзадача «обміняти значення двох змінних», яку треба розв’язати тричі, лише з різними парами змінних. Для розв’язання цієї задачі напишемо процедуру; її параметри вказують на змінні, значення яких обмінюються. program reorder3; var a, b, c : integer; { процедура обміну значень її параметрів }

101


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

procedure exchange(var x, y : integer); var t : integer; { допоміжна змінна } begin t:=x; x:=y; y:=t end; Begin writeln('Задайте три цілих числа'); readln(a,b,c); if a>b then exchange(a,b); { a <= b } if b>c then exchange(b,c); { c – найбільший } if a>b then exchange(a,b); { a <= b } writeln('За неспаданням: ',a,' ',b,' ',c) End. У процедурі exchange оголошено допоміжну змінну t, яка використовується в тілі процедури. При виконанні першого та третього викликів exchange її параметри x та y посилаються на змінні a та b, при виконанні другого — на b та c. ПІДСУМКИ ТА УТОЧНЕННЯ У мові Turbo Pascal підпрограми записуються серед оголошень імен програми (в інших мовах може бути по-іншому). Процедура (один з двох різновидів підпрограм) має такий загальний вигляд. procedure ім’я(оголошення параметрів); оголошення імен та підпрограми begin послідовність операторів end; Імена, оголошені в заголовку підпрограми, називаються параметрами, або формальними параметрами. Вирази або імена змінних, записані у виклику підпрограми, називаються аргументами, або фактичними параметрами. Параметри, оголошені без слова var перед їх іменем, називаються параметрами-значеннями, а оголошені зі словом var перед їхнім іменем — параметрами-змінними. Оголошення параметрів у заголовку підпрограми записують у дужках як послідовність секцій. У секції оголошено або однотипні параметризначення, або однотипні параметри-змінні (зі словом var на початку

102


3. Äâà ñïîñîáè ï³äñòàíîâêè àðãóìåíò³â íà ì³ñöå ïàðàìåòð³â

секції). Секції відокремлюють знаком «;». Проте у виклику незалежно від секцій усі аргументи відокремлюються комами. Якщо підпрограма не має параметрів, дужки () в її заголовку не записують. Тоді її викликом є її ім’я.

3. ДВА СПОСОБИ ПІДСТАНОВКИ АРГУМЕНТІВ НА МІСЦЕ ПАРАМЕТРІВ ПІДСТАНОВКА ЗА ЗНАЧЕННЯМ Параметр-значення позначає у підпрограмі «власну» змінну. На початку виконання виклику підпрограми цій змінній присвоюється значення аргументу. Відповідним аргументом може бути довільний вираз того ж типу, що й тип параметра (або типу, сумісного за присвоюванням з типом параметра). Приклади аргументів, що є виразами, нам знайомі з викликів стандартних підпрограм: s q r t ( b * b - 4 * a * c ) , c h r ( o r d ( ' 0 ' ) + 1 ) , writeln(x*x) тощо. У викликах нашої процедури min перші два аргументи теж можуть бути довільними виразами типу integer, наприклад min(10+x,3*x,v), де x, v — імена цілих змінних. Цей спосіб передачі даних у підпрограму називається підстановкою аргументу на місце параметра за значенням. ПІДСТАНОВКА ЗА ПОСИЛАННЯМ Параметр-змінна під час виконання підпрограми позначає ту саму змінну, яку позначає відповідний йому аргумент, вказаний у виклику. Щоб забезпечити це, до пам’яті підпрограми передається посилання на цю змінну. Відповідним аргументом може бути ім’я змінної того ж типу, що й у параметра. Приклади аргументів, що відповідають параметрам параметрамзмінним: readln(a,b,c) або inc(x), а також третій аргумент у викликах нашої процедури min. Цей спосіб передач даних у підпрограму називається підстановкою аргументу на місце параметра за посиланням. ЩО ВИБИРАТИ? Якщо після виклику підпрограми використовується старе значення аргументу або аргументом може бути довільний вираз, йому має відповідати параметр-значення.

103


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

Якщо після виклику підпрограми має використовуватися нове значення аргументу, отримане при виконанні виклику, то параметр треба оголосити як параметр-змінну. Перше правило має винятки, пов’язані з масивами (див. розділ 7). Параметри x та y процедури min (див. вище) ілюструють перше правило, параметр z — друге. ПАРАМЕТРИ"КОНСТАНТИ У мові Turbo Pascal є ще один різновид параметрів — параметриконстанти; вони оголошуються зі словом const на початку. Аргументом для них може бути довільний вираз того ж типу, що й параметр. Підстановка відбувається так. Значення аргументу обчислюється й запам’ятовується в ділянці пам’яті підпрограми, яка викликає (аналогічно до параметров-значень), а у підпрограму, яку викликано, передається посилання на цю ділянку (як для параметрів-змінних). Отже, цей механізм дозволяє використовувати аргументи-вирази й не вимагає копіювання їх значень до пам’яті виклику підпрограми. Присвоювання параметрамконстантам заборонено; їх виявляє транслятор. Параметри-константи зручні, коли потрібні параметри великого розміру, не змінювані у підпрограмі; приклади їх застосування наведено в розділі 7.

4. ФУНКЦІЯ — ПІДПРОГРАМА ОБЧИСЛЕННЯ ЗНАЧЕННЯ, ПОТРІБНОГО У ВИРАЗІ Функція є другим різновидом підпрограми. Виклик функції є виразом (числовим, булевим тощо). Значенням цього виразу стає значення, яке обчислюється при виконанні виклику функції та в деякий спеціальний спосіб повертається у програму. Приклад 1. Згадаємо обчислення мінімального з двох цілих значень. Результатом цього обчислення є одне скалярне значення (число), яке зручно використовувати як вираз, наприклад, вказати його як аргумент у виклику процедури writeln. У наведеній вище процедурі min потрібне значення поверталося у пам’ять програми як значення аргументу, відповідного параметру-змінній. Проте це повернення можна організувати інакше. Розглянемо програму обчислення мінімального з двох заданих цілих чисел за допомогою функції.

104


4. Ôóíêö³ÿ — ï³äïðîãðàìà îá÷èñëåííÿ çíà÷åííÿ, ïîòð³áíîãî ó âèðàç³

program MinOfTwoNumbers; var a, b : integer; { функція обчислення мінімуму } function min(x,y : integer) : integer; begin if x<y then min:=x else min:=y end; Begin writeln('Задайте два цілих числа:') readln(a,b); {виклик функції-аргумент у виклику процедури} writeln('Мінімальне з них: ', min(a,b)); End.‹ Підпрограма, що є функцією, починається службовим словом function. Після дужок з параметрами записується двокрапка та тип значення, яке повертається з виклику функції. Повернення значення відбувається за допомогою спеціальної змінної, яку позначає ім’я функції (у наведеному прикладі це min). У тілі функції обов’язково мають бути оператори присвоювання з іменем функції в лівій частині, причому при виконанні виклику має виконуватися хоча б один з них. Остаточне значення цієї змінної повертається з виклику функції. Проілюструємо виконання наведеної програми, вважаючи, що змінні a та b під час уведення отримують значення 1 і 2. Змінна, однойменна з функцією, має «власну» ділянку пам’яті й у таблиці позначена min.min. Коли закінчується виклик min, її значення присвоюється додатковій змінній, яка є у «машинній» програмі й позначена «Аргумент writeln» (див. табл. на с. 106). Виклик функції є виразом і може бути частиною складнішого виразу (ми бачили це раніше, використовуючи стандартні функції). Приклади використання викликів min 1. Якщо з якоюсь метою нам треба обчислити квадрат значення, що повертається з виклику min, або додати до нього 1, можна написати вираз sqr(min(a,b)) або, відповідно, min(a,b)+1. 2. Для обчислення мінімального з трьох цілих значень a, b, c можна записати такі виклики функції min. m:=min(a,b); m:=min(m,c)

105


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

Таблиця

Виконувані дії Утворення змінних програми ... readln(a, b) min(a, b) Утворення нових змінних Присвоювання x та y Обчислення x < y min := x Закінчення виклику min writeln(...)

Пам’ять програми a

b

Аргумент writeln

1

2

?

1

2

?

1

2

1

2

1

min.x

min.y

min.min

?

1

2

?

2

?

1

2

?

1

2

?

1

2

1

1

2

1

1

2

1

Водночас, мінімальне з трьох цілих значень a, b, c є значенням виразу min(min(a,b),c). При його обчисленні, коли починає виконуватися «зовнішній» виклик min, треба обчислити значення першого аргументу, а ним є «внутрішній» виклик min(a,b) . Тому виконується цей «внутрішній» виклик. Лише потім значення, що повертається з нього, присвоюється першому параметру у виконанні «зовнішнього» виклику. Аналогічно мініма льне з чотирьох значень задає вираз min(min(a,b),min(c,d)). Приклад 2. Прочитати довжини чотирьох відрізків (гарантовано, що це попарно різні додатні цілі числа) та обчислити кількість трикутників, які з них можна утворити. Щоб обчислити кількість трикутників, треба для кожної з чотирьох можливих трійок відрізків перевірити, чи можна утворити з них трикутник. Отже, нам треба одні й ті самі обчислення провести чотири рази, тільки з різними числами. Напишемо функцію, яка обчислює ознаку, чи можна з трьох відрізків утворити трикутник. Але повертати з неї будемо не булеве значення цієї ознаки, а ціле (0 відповідає false, 1 — true). Це дозволить просто надрукувати суму цих ознак: 106


5. Îáëàñòü 䳿 îãîëîøåíü ³ìåí

program NumberOfTriangles; var a, b, c, d : integer; function triangle(x, y, z : integer) : integer; begin triangle:=ord((x+y>z) and (x+z>y) and (y+z>x)) end; Begin writeln('Задайте чотири додатних цілих числа:') readln(a,b,c,d); writeln('Кількість трикутників: ', triangle(a,b,c)+triangle(a,b,d)+ triangle(a,c,d)+triangle(b,c,d)) End. Ім’я функції, записане на місці виразу (праворуч у операторі присвоювання, в умові розгалуження або циклу, як аргумент у виклику тощо), позначає виклик цієї функції. Якщо ця функція має параметри, а у виразі записано лише її ім’я, це є синтаксичною помилкою. Записувати ім’я функції у викликах процедур уведення заборонено (на відміну від запису в лівих частинах операторів присвоювання).

5. ОБЛАСТЬ ДІЇ ОГОЛОШЕНЬ ІМЕН Під час використання програм з підпрограмами постає природне питання: чи можна у програмі та підпрограмах використовувати одні й ті самі імена, що позначають різні об’єкти (константи, змінні, типи, підпрограми)? Відповідь на це запитання пов’язана з поняттям «область дії оголошення імені». Розглянемо його. Область дії оголошення імені — це сукупність місць у програмі, в яких ім’я позначає об’єкт, заданий саме в цьому оголошенні. За правилами мови Паскаль, оголошення імені діє від місця запису у програмі або підпрограмі до кінця її блоку. Якщо в цій області є підпрограми, то воно діє і в них. Але якщо вони містять своє власне оголошення цього ж імені, то за тим же правилом до кінця їх блоків діють їх власні оголошення. Отже, власне оголошення у підпрограмі «перекриває» оголошення, записане вище. Наприклад, оголошення імен x, y у заголовку функції min діють до кінця тіла цієї функції та перекривають можливі оголошення

107


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

цих імен у програмі. У процедурі exchange оголошення імені t діє до кінця тіла exchange, тобто у програмі змінну з іменем t «не видно». Різним об’єктам у різних підпрограмах (однією з них вважається програма) можна давати однакові імена. Це дозволяє оголошувати імена у підпрограмах, не замислюючись, чи вже оголошено ці імена у програмі або інших підпрограмах. Заголовок підпрограми є оголошенням її імені, розташованим у програмі (або іншій підпрограмі). Тому підпрограму можна викликати не тільки у програмі, а й у підпрограмах, які записано нижче в цій самій програмі. Заголовок підпрограми передує її блоку, тому викликати підпрограму можна в ній самій (такі виклики називаються рекурсивними — див. нижче). Також не можна оголошувати ім’я підпрограми в її ж блоці. Ім’я, оголошене у підпрограмі, називається локальним у цій підпрограмі, а ім’я, яке використовується у підпрограмі без оголошення — глобальним у ній. Змінні з глобальними іменами називаються глобальними. Щоб при імітації програми відрізняти локальні імена підпрограми, перед локальним іменем додають ім’я підпрограми й крапку (див. імітацію процедури та функції min у прикладах вище). Тож, якщо в підпрограмі S оголошено ім’я N, його при імітації позначають S.N. Під час виклику підпрограми її параметрам-змінним відповідає пам’ять, «належна» аргументам. При імітації програми параметр-змінна підпрограми «накладається» на аргумент, а імена інших змінних, оголошених у підпрограмі, вказують на власні ділянки пам’яті (див. приклади імітації вище).

6. ВИКОНАННЯ ВИКЛИКУ ПІДПРОГРАМИ ПАМ’ЯТЬ ВИКЛИКУ ПІДПРОГРАМИ Сукупність змінних, які утворюються при виконанні виклику підпрограми, має назву пам’ять виклику підпрограми. Змінні в цій пам’яті відповідають параметрам-значенням та локальним іменам змінних підпрограми. Цю пам’ять часто ще не зовсім точно називають локальною пам’яттю підпрограми. Змінні в пам’яті виклику підпрограми називаються локальними. Якщо підпрограма є функцією, то її локальна пам’ять містить також змінну, однойменну з функцією; її значення повертається з виклику. Наприклад, локальну пам’ять процедури exchange (див. приклад вище) утворює

108


6. Âèêîíàííÿ âèêëèêó ï³äïðîãðàìè

змінна з іменем t, а функції min (див. приклад вище) — змінні, відповідні параметрам x, y та імені функції min. Локальна пам’ять містить ще один елемент — посилання на місце, з якого треба продовжити виконання програми після виклику. Це місце називається точкою повернення з підпрограми, а посилання на неї зберігається до закінчення виконання виклику підпрограми. ПРОЦЕС ВИКОНАННЯ ВИКЛИКУ ПІДПРОГРАМИ 1. Виділяється пам’ять для точки повернення та параметрів-значень підпрограми. Посилання на точку повернення з підпрограми запам’ятовується. 2. Обчислюються значення аргументів для параметрів-значень, посилання на пам’ять аргументів для параметрів-змінних (а також і перше, і друге для параметрів-констант). Аргументи підставляються. 3. Виділяється пам’ять, відповідна локальним іменам змінних. 4. Виконуються оператори тіла підпрограми. Зокрема, в локальній пам’яті функції запам’ятовується значення, що повертається з виклику. 5. Якщо підпрограма є функцією, то значення, що повертається з її виклику, копіюється з її пам’яті до пам’яті програми (або підпрограми, яка викликає). 6. Програма (підпрограма, яка викликає) пр��довжується з точки повернення. Змінні в локальній пам’яті підпрограми не відповідають іменам програми (підпрограми, яка викликає), тобто ця пам’ять недоступна у програмі і вважається звільненою; її можна використовувати для наступного виклику цієї ж або іншої підпрограми. Відбувається так зване логічне звільнення: зміст локальної пам’яті не змінюється й перетворюється на «сміття». АВТОМАТИЧНА ПАМ’ЯТЬ, АБО ПРОГРАМНИЙ СТЕК Змінні, оголошені на рівні програми, існують протягом усього часу виконання програми й тому називаються статичними. Область пам’яті з ними також називається статичною. Локальним іменам і параметрамзначенням підпрограм відповідають змінні з іншої області пам’яті — автоматичної. Назва «автоматична» пояснюється тим, що при виконанні викликів підпрограм пам’ять виділяється і звільняється без явних вказівок у програмі, написаній мовою високого рівня, тобто «автоматично».

109


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

Локальні імена, оголошені у підпрограмах, можуть збігатися з іменами у програмі та інших підпрограмах, оскільки вони вказують на цілком різні об’єкти в різних областях пам’яті. Ось чому на локальні імена у підпрограмах немає ніяких обмежень. При виконанні викликів підпрограм ділянки автоматичної пам’яті виділяються й звільняються за принципом «останньою зайнято — першою звільнено». Якщо складати аркуші паперу в стос і брати їх тільки зверху, то аркуш, що потрапив до стосу останнім, забирають першим. Англійською мовою такий стос називається stack (стек), а кладуть і беруть аркуші за принципом «Last In — First Out» (LIFO), тобто «останнім прийшов — першим пішов». Тому автоматичну пам’ять програми також називають програмним стеком. Аналогічно потрапляють набої в магазин автомата й вистрілюються з нього. Останній вилітає першим, а перший (він на дні магазина) — останнім. Можна вистрілити набій з магазина й додати згори новий. Аналогічно закінчується виконання виклику підпрограми, записаного в деякому тілі (програми або підпрограми), і починається виконання наступного виклику з цього ж тіла.

7. ПРИКЛАДИ ВИКОРИСТАННЯ ПІДПРОГРАМ Приклад 1. Прочитати два натуральних числа a і b, де 0 ≤ a ≤ b, та надрукувати всі прості числа від a до b. Простий спосіб розв’язання цієї задачі — перебрати всі натуральні числа від a до b і для кожного визначити, чи є воно простим. Відразу виділимо підзадачу «визначити, чи є задане натуральне число простим». Результат цього визначення, тобто відповідь «так» або «ні», представимо булевим значенням, яке повертається з функції. Ця функція матиме такий заголовок. function isPrime(n : longint) : boolean; Спочатку напишемо програму з викликом цієї функції, не уточнюючи її: program primes; function isPrime(n : longint) : boolean; ... { див. нижче } end;

110


7. Ïðèêëàäè âèêîðèñòàííÿ ï³äïðîãðàì

var a, b, { межі пошуку простих чисел } i : longint; { поточне натуральне число } begin write('Задайте два натуральних числа ', '(друге не менше першого): '); readln(a,b); for i:=a to b do if isPrime(i) then write(' ', i) end. Як бачимо, програма дуже проста. Її можна трохи прискорити, якщо врахувати, що всі парні числа, більші за 2, є складеними. Тоді треба окремо перевірити, чи міститься 2 у заданих межах, а потім перебрати лише непарні числа. Розглянемо алгоритм визначення, чи є задане число простим, який буде реалізовано функцією isPrime. Число n, відмінне від 1, є простим, якщо має тільки два додатних дільники — 1 і n. Число 2 є простим, а n, n > 2, — якщо не ділиться без остачі на жодне з чисел 2, 3, …, n – 1; в іншому випадку число є складеним. Отже, треба перебрати всі дільники k від 2 до n – 1 і перевірити подільність n на k. Для цього знадобиться цикл з умовою продовження k < n, у якому значення k збільшується. Проте обчислення можна скоротити. Якщо n є складеним, то n = k * m, де й k, й m більше одиниці, а менше з них обов’язково не більше n . Отже, щоб дізнатися, чи є простим число n, достатньо перевірити його подільність лише на числа від 2 до [ n ] ([x] позначає цілу частину x). Це дозволяє зменшити кількість виконань тіла циклу приблизно в n разів. Якщо n ділиться на деяке k, то очевидно, що воно складене, і подальші перевірки не потрібні. Отже, умова продовження матиме вигляд (k<=t) and (n mod k<>0), а тіло циклу міститиме лише збільшення k. Оголосимо цілі змінні t і k для верхньої межі та поточного дільника й реалізуємо модифікований алгоритм. Особливий випадок значень n — 0 і 1, які не є простими; перебирати для них можливі дільники не потрібно. function isPrime(n:longint):boolean; { визначити, чи є простим значення параметра } var k, { поточний дільник } t : longint; { верхня межа } Begin if n<2 then isPrime:=false 111


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

else begin k:=2; {перший дільник 2} t:=round(sqrt(n)); { sqrt(n) обчислюється в дійсному типі, і навіть для квадратів цілих чисел може виникнути похибка, тому для гарантії використовуємо не trunc, а round } while (k<=t)and(n mod k <> 0) do inc(k); { (k > t) або (n mod k = 0) } {якщо k>t, то число просте, інакше складене} isPrime := k > t end End; Як бачимо з цього прикладу, розробку підпрограми відокремлено від розробки програми, в якій ця підпрограма використовується. Цей принцип є дуже важливим у програмуванні. Приклад 2. Увести два дроби, обчислити та вивести їх суму. Кожен дріб задають парою цілих чисел a і b, де b ≠ 0. Результатом має бути нескоротний дріб, тобто дріб вигляду a/b, де b > 0 і НСД(|a|, |b|) = 1. Алгоритм розв’язання простий — увести два дроби, додати їх і надрукувати результат. У ньому легко виділити підзадачі «увести дріб», «вивести дріб», «додати два дроби». Реалізуємо розв’язання цих підзадач за допомогою підпрограм з такими заголовками (імена num і den є скороченнями від numerator — чисельник, denumerator — знаменник, Fract є скороченням fraction — дріб). Процедура уведення дробу в пару параметрів. Параметри мають бути параметрами-змінними. procedure readFract(var num, den : integer); Процедура додавання дробів, заданих першими двома парами параметрів-значень, та збереження суми в третій парі параметрів-змінних. procedure plusFract(num1,den1,num2,den2:integer; var num3,den3:integer); Процедура виведення дробу, заданого парою параметрів-значень. procedure writeFract(num, den : integer); Використаємо їх у такій очевидній програмі: program SumFractions; ...підпрограми, які представлено нижче...

112


7. Ïðèêëàäè âèêîðèñòàííÿ ï³äïðîãðàì

var a1,b1, a2,b2, a3,b3 : integer; {три дроби} begin writeln('Додавання двох дробів'); write('Дріб (два цілих, друге не 0)>'); readFract(a1,b1); { уведення першого дробу } write('Дріб (два цілих, друге не 0)>'); readFract(a2,b2); { уведення другого дробу } plusFract(a1,b1, a2,b2, a3,b3); { a3/b3 = a1/b1 + a2/b2 } writeFract(a3,b3); { виведення результату } end. Після того, як зафіксовано заголовки та виклики підпрограм, можна переходити до їх розробки. Але спочатку звернемо увагу на те, що одне й те саме дробове число має безліч записів у вигляді дробів і лише один у вигляді нескоротного дробу з додатним знаменником, зокрема, дріб з чисельником 0 подамо як 0/1. Тому дроби будемо подавати тільки як нескоротні. Обробка дробів саме як нескоротних ставить ще одну підзадачу — «скоротити дріб». Її треба розв’язувати при уведенні та додаванні дробів. Для неї напишемо додаткову процедуру скорочення дробу, заданого парою параметрів-змінних. procedure reduceFract(var num, den : integer); Почнемо розробку з процедур, які забезпечують уведення та виведення даних. Процедура введення дробу. Тут треба лише ввести з клавіатури два цілих числа та скоротити дріб. Але якщо знаменник дорівнює 0, введення треба повторити. procedure readFract(var num, den : integer); Begin repeat read(num); readln(den); if den=0 then write('Знаменник 0. Задайте інший дріб: ') until den<>0; if den<0 then begin { забезпечимо додатність знаменника }

113


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

den:=-den; num:=-num; end; reduceFract(num,den) { скорочення дробу } End; Процедура виведення дробу. Надрукуємо дріб із символом «/» між чисельником і знаменником. procedure writeFract(num, den : integer); begin writeln(num, '/', den) end; Процедура додавання. Найпростіший спосіб додати два дроби — реалізувати формулу

n1 n2 n1d 2 + n2 d1 + = і скоротити цей дріб. d1 d 2 d1d 2

procedure plusFract(num1,den1, num2,den2 : integer; var num3,den3 : integer); var m:integer; begin den3:=den1*den2; num3:=num1*den2+num2*den1; reduceFract(num3,den3) { скорочення дробу } end; Допоміжна процедура скорочення дробу. Скоротити дріб –– означає поділити його чисельник і знаменник на їх найбільший спільний дільник (НСД). Отже, маємо ще одну допоміжну підзадачу — «обчислити НСД двох натуральних чисел». Її розв’язання реалізуємо у вигляді функції GCD (це ім’я є скороченням від англійського greatest common divisor — найбільший спільний дільник). procedure reduceFract(var num, den : integer); var t : integer; begin if num=0 then den:=1 end else begin t:=GCD(abs(num),abs(den)); {обчислення НСД} num:=num div t; den:=den div t end end;

114


7. Ïðèêëàäè âèêîðèñòàííÿ ï³äïðîãðàì

Допоміжна функція обчислення НСД. Скористаємося сучасною версією алгоритму Евкліда. function GCD(a, b : integer) : integer; begin while (a<>0) and (b<>0) do if a>b then a:=a mod b else b:=b mod a; { a=0 або b=0 } if a=0 then GCD:=b else GCD:=a end; Розташування створених підпрограм у програмі. Підпрограму можна використовувати після її оголошення. Тому процедуру скорочення треба записати після функції обчислення НСД, а процедури введення та додавання — після процедури скорочення. Розташування процедури виведення дробу не має значення. Отже, у програмі SumFractions, наведеній вище, розташуємо підпрограми в такій послідовності: program SumFractions; function GCD ... див. вище ... end; procedure reduceFract ... повний текст див. вище ... end; procedure plusFract ... повний текст див. вище ... end; procedure readFract ... повний текст див. вище ... end; procedure writeFract ... повний текст див. вище ... end; var a1,b1, a2,b2, a3,b3 : integer; { три дроби } Begin writeln('Додавання двох дробів'); write('Дріб (два цілих, друге не 0)>'); readFract(a1, b1); { уведення першого дробу } write('Дріб (два цілих, друге не 0)>'); readFract(a2, b2); { уведення другого дробу }

115


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

plusFract(a1,b1, a2,b2, a3,b3); { a3/b3 = a1/b1 + a2/b2 } writeFract(a3, b3); { виведення результату } end. Вдале розбиття задачі на підзадачі дозволяє отримати просту програму й декілька простих підпрограм, налагодити які набагато легше, ніж одну складну й громіздку програму. Розбиття програми на підпрограми є одним із втілень принципу «розділяй і пануй», який у програмуванні має дуже велике значення. РЕКУРСИВНІ ПІДПРОГРАМИ Підпрограми, які містять виклики самих себе, називаються рекурсивними, а використання таких підпрограм — рекурсією. Приклад 1. Функція «факторіал» від натурального n позначається n! і задається такими рівностями: 0! = 1, n! = n · (n–1)! при n > 0. Згідно з ними, 1! = 1 · 0! = 1, 2! = 2 · 1! = 2, 3! = 3 · 2! = 6 тощо. Як бачимо, щоб обчислити значення функції для n, де n > 0, треба знайти її значення для n–1. У цьому зверненні до значення функції з меншим аргументом і полягає рекурсивність. Безпосередньо за наведеним означенням дуже просто написати таку рекурсивну функцію (вважаємо, що n! = 1 при n < 0): function f(n : byte) : longint; begin if n<=0 then f:=1 else f:=n*f(n-1) {f(n-1) – рекурсивний виклик} end; Виклик рекурсивної підпрограми виконується так само, як виклик будьякої іншої підпрограми. Розглянемо схематично виконання виклику f(2). Спочатку виділяється локальна пам’ять для n і f, тобто програмний стек починає заповнюватися. Значення аргументу 2 більше 0, тому починається рекурсивний виклик f(1). Знову виділяється локальна пам’ять для n і f, тобто у програмний стек додаються нові значення. У цьому виклику значення аргументу 1 знову більше 0, тому починається рекурсивний виклик f(0), і зайнята частина програмного стека збільшується втретє. У цьому третьому виклику значення аргументу 0 не більше 0, тому виклик закінчується поверненням 1. Програмний стек починає звільнятися. Далі за-

116


7. Ïðèêëàäè âèêîðèñòàííÿ ï³äïðîãðàì

кінчується рекурсивний виклик f(1), і з нього повертається 1. Програмний стек звільняється вдруге. Нарешті, закінчується «зовнішній» виклик f(2), повертається 2, і програмний стек звільняється повністю. З кожним рекурсивним викликом зайнята частина програмного стека збільшується, а із закінченням виклику — зменшується. Оскільки розмір стеку обмежений, можлива ситуація (особливо при виконанні рекурсивних підпрограм), коли пам’яті в стеку не вистачить. Тоді програма аварійно завершується з повідомленням Stack overflow. Приклад 2. На клавіатурі набираються цілі числа, не рівні нулю. Поява 0 означає кінець уведення. Прочитати числа та видати їх у зворотному порядку (кінцевий 0 не виводити). Перше число треба вивести останнім, друге — передостаннім і так далі. Отже, для обробки послідовності чисел треба прочитати перше число і, якщо це не 0, так само обробити решту чисел і потім вивести перше число. А якщо прочитано 0, то нічого робити не треба. Звідси отримуємо рекурсивну процедуру revertInput у такій програмі: program printNumbers; procedure revertInput; var x : integer; begin read(x); { уведення числа } if x<>0 then begin revertInput; { обробка решти чисел } write(x, ' '); { виведення числа } end end; Begin revertInput; End. Уведене число запам’ятовується в локальній змінній x процедури і з кожним викликом у програмному стеку з’являється новий екземпляр x. Після уведення 0 виклики закінчуються у порядку, зворотному до того порядку, в якому починалися. Звідси й значення локальних змінних x виводяться у зворотному порядку. Приклад 3. Прочитати натуральне число (у звичайному десятковому записі) та основу системи числення р, p ≤ 36. Вивести р-ковий запис

117


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

числа. Значення цифр 10, 11, 12, …, 35 представити цифрами A, B, C, …, Z. Значення молодшої цифри у р-ковому запису числа є остачею від ділення числа на р. Далі, узявши замість числа частку від його ділення на р, так само отримаємо значення наступної цифри, і так далі, поки не залишиться число менше р. Але цифру, отриману першою, треба вивести останньою, тому запам’ятаємо значення молодшої цифри, потім рекурсивно виведемо старші цифри, а потім виведемо молодшу цифру. Можливим значенням цифр від 0 до 35 мають відповідати цифри — символи від '0' до '9' та від 'A' до 'Z'. Для отримання цифри за її значенням напишемо допоміжну функцію digit. program numberOutput; function digit(v : byte) : char; begin { повернення цифри за її значенням v } case v of 0..9 : digit:=chr(v+ord('0')); 10..35 : digit:=chr(v-10+ord('A')); end; end; procedure writeNP(n : longint; p : byte); begin if n>=p then writeNP(n div p, p); { за n < p заглиблення в рекурсію немає } write(digit(n mod p)); end; var n : longint; p : byte; { число та основа } Begin readln(n,p); writeNP(n,p); end. Для перевірки програми обов’язково задайте число 2147483647 (найбільше число типу longint) та основи 2, 8, 16, 32. Відповідними результатами мають бути 11…1 (31 «1»), 17777777777, 7FFFFFFF, 1VVVVVV. Перевірте також декілька чисел з максимальною основою 36, наприклад, 35, 71 та 1295 (результатами мають бути Z, 1Z та ZZ). У рекурсивній підпрограмі обов’язково повинна бути умова, за істинності якої відбувається повернення з виклику (див. приклади, наведені вище). Ця умова визначає «дно рекурсії», яке при виконанні підпрограми

118


7. Ïðèêëàäè âèêîðèñòàííÿ ï³äïðîãðàì

обов’язково має досягатися, інакше виклики призведуть до переповнення програмного стека або інших непередбачуваних наслідків. Приклад 4. Розглянемо процедуру, в якій умову повернення з рекурсії сформульовано невдало. procedure badProc(x : integer); begin if x<>2 then badProc(x-2); { заглиблення та } write(x); { вихід з рекурсії } end; При виконанні виклику badProc(6) відбудуться рекурсивні виклики з аргументами 4 та 2. При x=2 заглиблення в рекурсію не буде, тому буде надруковано 2, а потім виклики закінчаться з друкуванням 4 та 6. Проте виклик badProc(5) призведе до рекурсивних викликів з аргументами 3, 1, –1, –3, … . Отже, умова повернення x=2 ніколи не стане істинною, тому виконання рекурсивних викликів переповнить програмний стек. Приклад 5. Найбільший спільний дільник НСД(a, b) натуральних a і b можна обчислити рекурсивно на основі таких рівностей: якщо b = 0, то НСД(a, b) = a, якщо a mod b = 0, то НСД(a, b) = b, якщо a mod b > 0, то НСД(a, b) = НСД(b, a mod b). Наведемо відповідну рекурсивну функцію GCD (Greatest Common Divisor — Найбільший Спільний Дільник). Вважається, що значення аргументів у виклику невід’ємні. function GCD(a, b : integer) : integer; begin if b=0 then GCD:=a else if a mod b = 0 then GCD:=b else GCD:=GCD(b, a mod b) end; Як бачимо, «дно рекурсії» утворюють ситуації, в яких b = 0 або a кратне b, і тоді НСД обчислюється просто без рекурсії. Приклад 6. Напишемо функцію, яка обчислює натуральний степінь дійсного числа a на основі такого алгоритму. a 2n = ( a 2 )n ; a 2n +1 = a · ( a 2 )n ; a 0 = 1. 119


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

Цей алгоритим був відомий ще давнім індійцям, тому часто називається індійським. Його основна мета — скоротити кількість множень. На-

( )

приклад, за цим алгоритмом a5 = a × a2

2

, тобто з трьома множеннями

замість чотирьох: a · a · a · a · a. Одне множення буде зекономлено за рахунок того, що a 2 зберігається як проміжне значення і множиться само

( )

на себе. Так само a10 = a 5

2

, що потребує лише чотирьох множень (три

5

з них для обчислення a ). Тут зберігається спочатку a 2 , а потім a5 . function pow(a:extended; n:longint):extended; begin if n=0 then pow:=1.0 else if odd(n) then pow:=a*pow(a*a,n div 2) else pow:=pow(a*a,n div 2) end; Проміжні множники зберігаються в локальній пам’яті викликів функції, точніше в змінних, які відповідають імені pow. З’ясуємо, як максимальна кількість арифметичних дій залежить від n. У кожному наступному вкладеному виклику значення аргументу n менше попереднього значення принаймні вдвічі. При n = 0 відбувається повернення з виклику, тому кількість цих зменшень значення аргументу n не більше, ніж log 2 n , тобто виклик з аргументом n породжує не більше ніж log 2 n рекурсивних викликів. При кожному виконанні виклику відбувається не більше одного ділення, піднесення до квадрата і множення, тому загальна кількість арифметичних операцій не більше 3 ⋅ log 2 n . Наприклад, при n = 1000 це приблизно 30. У цьому посібнику розглядається тільки так звана пряма рекурсія, коли підпрограма містить виклики самої себе. У програмуванні також використовується непряма рекурсія, коли підпрограми містять взаємні виклики. Приклад 7. На дошці — три стрижні: 1, 2, 3. На першому розміщено вежу з n дисків; нижній диск має найбільший діаметр, а діаметр кожного наступного менший за діаметр попереднього. За один хід із будь-якого стрижня можна взяти верхній диск і перемістити на інший стрижень, але дозволено класти диск лише на дошку або на диск більшого діаметра. Треба перемістити усю вежу зі стрижня 1 на стрижень 3. 120


7. Ïðèêëàäè âèêîðèñòàííÿ ï³äïðîãðàì

Ця гра називається «Ханойські вежі». За легендою, цю гру почали понад тисячу років тому ченці в одному монастирі поблизу Ханою у В’єтнамі; вони мали n = 64 диски. Коли вони закінчать гру, настане кінець світу. Розв’язком цієї гри-задачі є послідовність перенесень дисків. Напишемо програму друкування позначень цих переносів. Для перенесення вежі з n дисків зі стрижня 1 на стрижень 3 необхідно перенести вежу з n – 1 диска на стрижень 2, потім перенести нижній диск на стрижень 3 і вежу з 2 на 3. При перенесенні вежі з 1 на 2 допоміжним буде стрижень 3, а при перенесенні з 2 на 3 — 1, причому ніяка інша послідовність дій неможлива. Отже, розв’язання задачі для вежі висотою n описан�� за допомогою розв’язання для вежі висотою n–1, тобто рекурсивно. Нехай disk(a,b) позначає перенос одного диска зі стрижня a на стрижень b, а tower(h,a,b,c) — перенесення вежі висотою h з a на b з використанням стрижня c як допоміжного. При h > 1 виконання tower(h,a,b,c) буде таким. tower(h-1,a,c,b); disk(a,b); tower(h-1,c,b,a) При h = 1 переносять тільки один диск, тобто виконують disk(a,b). Отже, очевидною є така програма. program HanoiTowers; procedure disk(diskFrom, diskTo : integer); begin writeln(diskFrom, '->', diskTo) end; procedure tower(H:byte; F,T,V:byte); {Height – висота, From - з, To - на, Via – через} begin if H=1 then disk(F,T) else begin tower(H-1,F,V,T); disk(F,T); tower(H-1,V,T,F) end end; var n : byte; begin writeln('Імітація гри «Ханойські вежі»'); write('Натуральна кількість дисків>'); readln(n);

121


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

tower(n,1,3,2) end. Визначимо кількість перенесень дисків у вигляді функції s(n), де n — висота вежі. Очевидно, що s(1) = 1 і s(n) = 2 · s(n–1)+1. Як бачимо, s(2) = 3, s(3) = 7, s(4) = 15 тощо. Кожне наступне значення подвоюється, тому неважко переконатися, що s( n) = 2 n − 1 . Значення s(64) приблизно дорівнює 1019 . Якщо припустити, що ченці переносять один диск щосекунди, то для перенесення такої вежі потрібно більше 1012 років! У ченців ще багато роботи… . За рекурсивною підпрограмою без жодного оператора циклу можуть легко ховатися величезні обсяги обчислень. Як і будь-який потужний засіб, рекурсія вимагає обережності у використанні.

8. ГЛИБИНА РЕКУРСІЇ ТА ЗАГАЛЬНА КІЛЬКІСТЬ РЕКУРСИВНИХ ВИКЛИКІВ З рекурсивними підпрограмами пов’язано два важливих поняття — глибина рекурсії та загальна кількість викликів, породжених викликом рекурсивної підпрограми. Глибина рекурсії виклику підпрограми — це кількість рекурсивних викликів, розпочатих і не закінчених у момент початку цього виклику. Наприклад, при обчисленні n! виклик з аргументом n починається першим, тому має глибину 0. Рекурсивний виклик з аргументом n – 1 має глибину 1, з аргументом n – 2 — глибину 2 тощо. Найбільшу глибину n має виклик з аргументом 0. Аналогічно в задачі «Ханойські вежі» виклик процедури tower з висотою h має глибину рекурсії n – h. Коли виконується виклик підпрограми з глибиною рекурсії m, одночасно «існує» m + 1 екземпляр локальної пам’яті підпрограми. Кожен екземпляр займає ділянку певного розміру, тому збільшення глибини, як було сказано вище, може призвести до переповнення програмного стеку. Загальна кількість рекурсивних викликів, породжених викликом рекурсивної підпрограми, — це кількість викликів, виконаних між початком і завершенням цього виклику. Наприклад, у задачі «Ханойські вежі» кожен виклик задає одне перенесення диску та, окрім найбільш заглибленого, породжує два рекурсивних

122


8. Ãëèáèíà ðåêóðñ³¿ òà çàãàëüíà ê³ëüê³ñòü ðåêóðñèâíèõ âèêëèê³â

виклики. Нам уже відомо, що перенесення вежі висотою n вимагає 2n − 1 перенесень дисків, тому між початком і закінченням виклику з аргументом n виконується 2n − 2 рекурсивних викликів. Загальна кількість вкладених викликів визначає тривалість виконання виклику. Як бачимо, кількість викликів у задачі «Ханойські вежі» за значень n порядку кількох десятків призводить до неприпустимо довгого виконання програми. Використання рекурсивних підпрограм потребує уміння оцінити можливу глибину рекурсії, розмір пам’яті виклику підпрограми та загальну кількість рекурсивних викликів. Приклад. Повернемося до чисел Фібоначчі, які визначаються рівностями f1 = f2 = 1 , f n = f n −2 + f n −1 за n > 2. Безпосередньо за цими рівностями дуже просто написати таку функцію визначення числа Фібоначчі за його номером (вважається, що додатність n гарантовано). function f(n:integer):longint; begin if (n=1)or(n=2) then f:=1 else f:=f(n-2)+f(n-1) {два рекурсивних виклики} end; ЦЕ ДУЖЕ ПОГАНА ФУНКЦІЯ!!! Її головний недолік: вона породжує абсолютно не потрібні багаторазові обчислення тих самих величин. Розглянемо, наприклад, обчислення f(6). Очевидно, що виклик f(4) виконується двічі, f(3) — тричі, f(2) — п’ять разів, f(1) — тричі. У загальному ж випадку за n > 3 обчислення f n породжує два виклики f(n-2), три виклики f(n-3), п’ять викликів f(n-4), …, f n −1 викликів f(2) та f n −2 викликів f(1). Для виконання всіх цих дій потрібно часу не менше ніж для перенесення Ханойської вежі висотою n. Але, на відміну від задачі про вежі, усі ці дії не є обов’язковими, адже циклічний алгоритм у главі 5 вимагає лише одного додавання та двох присвоювань на кожне з чисел f 3 , f 4 ,..., f n (плюс присвоювання, додавання та порівняння номерів). КОНТРОЛЬНІ ЗАПИТАННЯ 1. Що таке підпрограма? 2. Які типи підпрограм ви знаєте? Чим вони відрізняються? 3. Яким елементом програми є заголовок підпрограми?

123


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

4. Чим відрізняються заголовки функцій та процедур? 5. Яким елементом програми є виклик процедури, а яким – виклик функції? 6. Як ви вважаєте, процедури readln і writeln мають параметри-значення чи параметри-змінні? 7. Що таке формальні та фактичні параметри (параметри та аргументи)? 8. Чим відрізняється підстановка аргументів на місце параметрівзначень та на місце параметрів-змінних? 9. Чому пам’ять, утворену локальними змінними викликів підпрограм, називають програмним стеком? 10. За якої умови у підпрограмі можна використовувати імена, не оголошені в ній? 11. Яке ім’я називається у підпрограмі локальним, а яке — глобальним? 12. Чи можна використовувати локальне ім’я підпрограми в інших підпрограмах, записаних після цієї підпрограми? 13. Яка підпрограма називається рекурсивною? 14. Що таке глибина рекурсії? На яку характеристику програми вона впливає? 15. Що таке загальна кількість рекурсивних викликів, породжених даним викликом? На яку характеристику програми вона впливає? ЗАДАЧІ 1. Написати функцію визначення максимального зі значень двох її цілих параметрів. 2. Написати функцію even, тобто «парне», яка повертає ознаку парності свого цілого параметра. 3. Написати функцію з дійсним параметром x, яка обчислює: а) –1, 0, 1, відповідно, при x < 0, x = 0, x > 0; б) найменше ціле, що не менше за значення параметра. 4. Проімітувати виконання такої програми: а) var a,b:integer; function f(x:byte; var y:byte):byte; begin b:=b+1; y:=a+b; f:=y end; begin for a:=2 to 4 do begin b:=a; writeln(f(a,b)); writeln(b)

124


8. Ãëèáèíà ðåêóðñ³¿ òà çàãàëüíà ê³ëüê³ñòü ðåêóðñèâíèõ âèêëèê³â

end; writeln(a) end. b)var a,b:byte; function f(var x:byte; y:byte):byte; begin b:=b+1; y:=a+b; f:=y end; begin for a:=2 to 4 do begin b:=a; writeln(f(a,b)); writeln(b) end; writeln(a) end. c) function f(a:byte):byte; begin a:=a+1; f:=a end; function g(var x:byte):byte; begin x:=2; g:=x end; var a,b:byte; begin a:=3; b:=4; writeln(f(a)); writeln(g(b)); writeln(a,b) end. d) var a,b:byte; function f(var a : byte) : byte; begin a:=a+4; f:=a end; function g(x:byte):byte; begin x:=3; g:=x end; begin a:=2; b:=1; writeln(f(a)); writeln(g(b)); writeln(a,b) end. e) var a,b,c:byte; function f(x:byte; var y:byte):byte;

125


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

begin x:=x+1; y:=y+2; c:=a+b; f:=x*y end; begin a:=1; b:=1; writeln(f(a,b)); writeln(a,b,c) end. f) var a,b,c:byte; function f(var x:byte; y:byte):byte; begin x:=x+1; y:=y+2; c:=a+b; f:=x+y end; begin a:=2; b:=2; writeln(f(a,b)); writeln(a,b,c) end. 5. Написати підпрограму, яка за трьома дійсними додатними числами, що задають довжини відрізків, визначає, чи можна з них утворити трикутник, і якщо так, повертає периметр, площу, радіуси вписаного та описаного кіл (якщо не можна, повертає нулі). Написати програму, яка вводить трійки чисел, визначає, чи можна з них утворити трикутники, та обчислює вказані вище параметри цих трикутників. Ознака кінця введення — хоча б одне з трьох уведених чисел дорівнює 0. 6. Написати програму друкування всіх «близнюків», тобто пар простих чисел вигляду 6m – 1 і 6m + 1 при m > 0, наприклад, 5 і 7, 11 і 13, 17 і 19 (але не 23 і 25, оскільки 25 — складене число). Обмежитися числами типу integer (longint). 7. Написати процедуру друкування розкладу значення її натурального параметра на прості множники (див. приклад з розділу 5) так, щоб між множниками ставився знак * і кожен з них друкувався один раз, але зі знаком ^ і показником степеня, якщо той більше 1. Наприклад, 169 = 13^2, 144 = 2^4*3^2, 50 = 2*5^2. 8. Написати функцію, яка знаходить найменше спільне кратне двох цілих чисел. 9. Вказати, що буде виведено при виконанні програми, якщо на вході задати: а) 10; b) 7; c) 5.

126


8. Ãëèáèíà ðåêóðñ³¿ òà çàãàëüíà ê³ëüê³ñòü ðåêóðñèâíèõ âèêëèê³â

program P; var x:integer; procedure S(N:integer); begin write(N,' '); if N>7 then S(N-1); write(N,' '); end; Begin writeln('ціле число: '); readln(x); S(x); End. 10. Написати рекурсивну функцію обчислення біноміального коефіцієнта Cnk =

n! , при виконанні якої виконувалося б не більше k/2 k !⋅ (n − k )!

рекурсивних викликів.

127


Äîïîì³æí³ àëãîðèòìè, àáî ï³äïðîãðàìè

Науково-виробниче видання Бібліотека «Шкільного світу» Ставровський Андрій, Скляр Ірина Програмуємо правильно Посібник Частина 1 Художній редактор О. Голик Літературний редактор Ю. Желєзна Коректор І. Бірюкович Комп’ютерна верстка К. Яскевич Набір Т. Корольова Підписано до друку 06.06.07. Формат 60х84/16. Папір офсетний № 1. Гарнітура Таймс. Друк офсетний. Умовн. друк. арк. 7,44. Обл.-вид. арк. 7,2. Тираж 1300 пр. Зам. № ТОВ Видавнцтво «Шкільний світ» 01014, м.Київ, вул. Тимірязєвська, 2 Свідоцтво про внесення суб’єкта видавничої справи до Державного реєстру видавців, виготівників і розповсюджувачів видавничої продукції серія ДК № 775 від 21.01.2002 р. Видрукувано з готових діапозитивів в ОП «Житомирська облдрукарня» 10014, Житомир, вул. Мала Бердичівська, 17 Свідоцтво про внесення суб’єкта видавничої справи до Державного реєстру видавців, виготівників і розповсюджувачів видавничої продукції серія ЖТ № 1 від 06.04.2001 р. 128


Á³áë³îòåêà «Øê³ëüíîãî ñâ³òó» Çàñíîâàíà ó 2003 ð.

Андрій Ставровський Ірина Скляр ПРОГРАМУЄМО ПРАВИЛЬНО Посібник У двох частинах

Частина 2 ²íôîðìàòèêà. Á³áë³îòåêà

Êè¿â «Øê³ëüí��é ñâ³ò» 2007


ББК 32.973-018я721 С76

Редакційна рада: Н. Вовковінська, М. Мосієнко — канд. філол. наук, Г. Кузьменко, О. Шатохіна

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

С76

Ставровський, Андрій. Програмуємо правильно: Посібник. : У 2 ч. / А. Ставровський, І. Скляр. — К. : Шк. світ, 2007. — Ч. 2. — 128 с. — (Б-ка «Шк. світу»). ISBN 978-966-451-000-1. ISBN 978-966-451-075-9. (Ч. 1) ISBN 978-966-451-091-9. (Ч. 2) Посібник містить другу частину початкового курсу програмування в рамках шкільної дисципліни «Інформатика». У першій чатсині представлено основи алгоритмізації та програмування мовою Паскаль, у другій частині — роботу з основними структурами даних (масиви, файли, записи, множини). Необхідний теоретичний матеріал супроводжується практичними прикладами, докладним розв’язанням типових задач, контрольними запитаннями та задачами для самостійної роботи. Посібник призначено для учнів, які вивчають інформатику за програмою фізико-математичних шкіл, а також для вчителів інформатики, студентів педагогічних ВНЗ і всіх, хто бажає навчитися програмувати. ББК 32.973-018я721

ISBN 978-966-451-000-1(б-ка «Шк. ISBN 978-966-451-075-9 (Ч. 1) ISBN 978-966-451-091-9 (Ч. 2)

світу») © А. Ставровський, І. Скляр, 2007 © ТОВ Видавництво «Шкільний світ», дополіграфічна підготовка, 2007


ЗМІСТ Розділ 1. Масиви та файли 1. Поняття масиву .............................................................................................. 5 2. Приклади утворення та обробки масивів .................................................... 7 3. Двовимірні масиви. Приклади використання ............................................. 15 4. Файл, файлова змінна, файлові типи ........................................................... 21 5. Зв’язування та відкриття файла .................................................................. 23 6. Запис у текстовий файл ................................................................................ 24 7. Уведення даних базових типів із текстового файла .................................... 27 8. Приклади введення послідовностей даних ................................................. 29 Розділ 2. Сортування лінійних масивів 1. Поняття сортування ..................................................................................... 41 2. Поняття складності алгоритму .................................................................... 43 3. Обмінне сортування ..................................................................................... 46 4. Сортування вибором ................................................................................... 47 5. Сортування вставками ................................................................................. 48 6. Швидкі алгоритми сортування ................................................................... 50 7. Сортування «злиттям» ................................................................................. 51 8. Швидке сортування ..................................................................................... 55 9. Пірамідальне сортування ............................................................................ 59 Розділ 3. Рядки та множини 1. Рядки ............................................................................................................. 67 2. Уведення та виведення рядків ..................................................................... 69 3. Кілька стандартних підпрограм обробки рядків ....................................... 72 4. Приклади використання підпрограм обробки рядків ............................... 74 5. Множини та їх представлення ..................................................................... 77 6. Операції з множинами .................................................................................. 79 7. Приклади використання множин ................................................................. 81 Розділ 4. Арифметика багатоцифрових чисел 1. Представлення багатоцифрових чисел зі знаком ....................................... 92 2. Уведення та виведення ................................................................................. 94 3. Порівняння ................................................................................................... 96 4. Додавання та віднімання .............................................................................. 97 5. Множення ................................................................................................... 100 6. Цілочислове ділення ................................................................................... 102 7. Скорочення дробу ...................................................................................... 105 8. Десяткове ділення ....................................................................................... 106 9. Модуль для роботи з багатоцифровими числами ................................... 109 Додаток Окремі можливості середовища Turbo Pascal .............................................. 115 Деякі службові слова мови Turbo Pascal ...................................................... 123 Директиви компілятора Тurbo Pascal ............................................................ 124 Кодування символів ....................................................................................... 126


Ìàñèâè òà ôàéëè

ВІД АВТОРІВ Посібник містить теми шкільного курсу «Масиви» та «Рядки», а також теми, які в загальноосвітній школі не вивчаються, але є необхідними для підготовки до олімпіад з інформатики всіх рівнів: від районної до Всеукраїнської. Тут представлено оброб% ку текстових файлів, різноманітні методи сортування, зокрема, швидкого, роботу з багатоцифровими числами (так звана «довга арифметика»), складність алгоритму та її оцінки, нестандартні задачі з використанням множин. Усі програми супроводжуються детальним розбором. У цьому посібнику, як і в попередньому, не всі задачі є ав% торськими — деякі взято з Інтернету або інших посібників. На% ведені тут задачі є досить цікавими, мають нескладне оригіналь% не розв’язання й дозволяють учням поступово готуватися до участі у різних конкурсах та олімпіадах Юних програмістів. Посібник також містить довідкові матеріали, корисні для на% буття практичних навичок роботи з системою програмування Turbo Pascal.

4


ðîçä³ë

1 ÌÀÑÈÂÈ ÒÀ ÔÀÉËÈ

1. ПОНЯТТЯ МАСИВУ У багатьох задачах у пам’яті програми треба зберігати великі набори однотипних даних. Для цього використовують масиви Масив — це змінна, утворена послідовністю значень, які на% зиваються елементами (компонентами), є однотипними й іден% тифікуються номерами (індексами). Множина індексів (діапазон) фіксується в оголошенні масиву та при виконанні програми не змінюється. Кількість елементів індексної множини називають довжиною масиву. • Елементи масиву є рівнодоступними, тобто можливість об% робки елемента не залежить від його місця в послідовності. • Тип масиву задають виразом, у якому вказують множину індексів I та тип елементів T: array [I] of T . Слова array (масив) і of — ключові. • Типом елементів може бути довільний тип, а типом індексів — перелічуваний тип, указаний іменем або діапазоном. Типи integer, word та longint як типи індексів у Turbo Pascal заборонено, оскільки ця мова програмування накладає обмежен% ня на загальний розмір масиву 64К. • Якщо тип індексів оголошено як діапазон, то його задають двома константами, можливо, іменованими. Записувати імена змінних у діапазоні заборонено.

5


Ìàñèâè òà ôàéëè

Приклад. 1. Багаточлен з дійсними коефіцієнтами, який має степінь не більше 99, можна подати масивом типу array [0..99] of real. Індекси його дійсних елементів — від 0 до 99. 2. Щоб підрахувати, скільки разів у деякому тексті зустрівся кожен символ, можна заповнити масив типу array[char] of word. Індексами його 256 цілих змінних є символи, значення% ми — кількості цих символів. 3. Оголошення типу масиву на кшталт array[1..n] of real з іменем змінної n — типова помилка початківців. Вираз, що задає тип масиву, можна записати в оголошенні змінної, але, як правило, краще окремо оголосити ім’я типу ма% сивів, а потім цим іменем задавати тип змінних. Приклад. Можливі обидва такі оголошення. 1) type aReal = array [0..99] of real; var X : aReal; 2) var X : array [0..99] of real; Перша буква a імені aReal вказує на те, що ім’я позначає саме тип масивів (array). Перший варіант оголошень краще, оскільки для типу оголошено ім’я, яке можна використовувати надалі замість виразу array[0..99] of real. В обох випадках ма% сив X містить 100 дійсних змінних X[0], X[1] , …, X[99]. Еле% менти масиву позначають виразами вигляду X[індексний_вираз], де індексний_вираз має значення від 0 до 99. Їм, як і всім змінним, можна присвоювати значення або використовувати їх значення у виразах, тобто можливі оператори такого вигляду. A[0]:=1; A[1]:=A[0]+2; for k:=2 to 99 do readln(A[k]); writeln(A[0]+A[1]+A[2]+A[3]) Обробку масивів описують, головним чином, через обробку їх елементів, але однотипні масиви можна присвоювати. Наприклад, при дії оголошень var a,b:array[char] of integer; ch:char; замість оператора циклу for ch:=chr(0) to chr(255) do b[ch]:=a[ch] можна написати таке лаконічне присвоювання: b := a 6


2. Ïðèêëàäè óòâîðåííÿ òà îáðîáêè ìàñèâ³â

2. ПРИКЛАДИ УТВОРЕННЯ ТА ОБРОБКИ МАСИВІВ У конкретних задачах значення елементів масиву утворюють% ся в три основні способи: • шляхом уведення з зовнішнього джерела даних; • за допомогою генератора псевдовипадкових чисел; • як результат обчислень. Розглянемо приклад уведення значень за допомогою клавіа% тури. Приклад. Треба отримати від клавіатури послідовність цілих чисел довжиною не більше 10, визначити найбільше з них і по% зиції у послідовності, на яких було це число. На початку програми оголосимо тип масиву. Окремо оголо% симо ім’я константи, яка задає кількість елементів масиву. За умо% вою, потрібен масив, у якому використовуються не більше 10 еле% ментів. const maxN = 10; aInt = array[1..maxN] of integer; Розглянемо найпростіший варіант уведення значень елементів числового масиву: спочатку задано кількість значень, а потім самі значення в цій кількості. Реалізуємо введення процедурою readAInt . Масив і кількість елементів, які отримують значен% ня, мають використовуватися після виклику процедури, тому ого% лосимо їх як параметри%змінні. procedure readAInt(var a:aInt; var n:byte); var k:byte; {поточний індекс} begin repeat write('Кількість елементів(1..', maxN, ')>'); readln(n); until (1 <= n) and (n <= maxN); for k:=1 to n do begin write(k, '-е ціле значення>'); readln(a[k]); end; end; 7


Ìàñèâè òà ôàéëè

Після виклику цієї процедури значення одержують тільки перші n елементів масиву. Значення решти непередбачувані і, по су��і, є «сміттям». Саме тому необхідний параметр n, що представ% ляє справжню кількість заданих значень. Після того, як уведено числа, проходимо масивом і визначає% мо найбільше значення. Потім ще раз проходимо масивом і ви% водимо номери елементів із цим найбільшим значенням. Заува% жимо: якби було потрібно лише максимальне число, а не його позиції, можна було визначати його при введенні чисел, не ви% користовуючи масив. Отже, програма матиме такий вигляд: program maxIntNumber; const maxN=10; aInt=array[1..maxN] of integer; var x:aInt; {масив цілих чисел} max:integer;{максимальне число} n,k:byte; {кількість та поточна позиція} procedure readAInt(var a:aInt; var n:byte); ... {див. вище в тексті} end; begin readAInt(x,n);{x і n визначаються у виклику} max:=x[1]; for k:=2 to n do if x[k] > max then max := x[k]; writeln('Максимальне число: ', max); write('Його позиції: '); for k:=1 to n do if x[k]=max then write(k,' '); readln; end. Якщо за її виконання задати кількість чисел 8, а потім увести числа 21, 13, 21 , 20 , –1 , 0, 21 , 5, то відповідь має бути, що максимальним є число 21, а його позиції 1, 3, 7. ‹ У багатьох задачах (від моделювання природних або соціаль% них процесів до розкладання гральних карт) використовують по% слідовності чисел, що належать певній множині, але більше ніяк 8


2. Ïðèêëàäè óòâîðåííÿ òà îáðîáêè ìàñèâ³â

не пов’язані одне з одним. Такі числа називаються випадковими. Проте часто замість випадкових чисел використовують числові послідовності, в яких наступні елементи певним чином обчис% люються за попередніми, але виглядають як випадкові. Послідов% ності таких чисел називають псевдовипадковими. Їх отримують за допомогою спеціальних підпрограм — генераторів псевдовипадко вих чисел. Багаторазові виклики такої підпрограми породжують послідовність псевдовипадкових чисел. У системі Turbo Pascal функція%генератор псевдовипадкових чисел має ім’я random. Її виклик без аргументів повертає псев% довипадкове дійсне число з інтервалу [0; 1). Якщо у виклику вка% зано аргумент типу word зі значенням R, повертається ціле чис% ло від 0 до R–1. Корисна також процедура randomize без параметрів, яка ініціалізує генератор випадковим числом, отриманим за допомо% гою системного годинника комп’ютера. Якщо процедуру randomize не викликати, то кожне виконання послідовності викликів функції r a n d o m буде давати одну й ту саму по% слідовність чисел. Розглянемо заповнення масиву за допомогою псевдовипадко% вих чисел та на основі обчислень. Приклад. У школі вчаться учні, зріст яких від 110 до 220 см (ціле число). Треба отримати дані про їх зріст та вивести кількість учнів кожного можливого зросту. Утворимо дані про зріст за допомогою функції random. Діа% пазон значень зросту — від 110 до 220, тому потрібне значення утворимо як 110 плюс випадкове число в межах від 0 до 110. Отже, багаторазові виклики функції матимуть вигляд random(111). Перший спосіб. В умові не сказано про можливу кількість учнів. Припустимо, що їх не більше двох тисяч і запам’ятаємо числа в масиві, індекси якого від 1 до 2000 відповідають учням. Потім за цим масивом визначимо, скільки учнів мають зріст 110, скіль% ки — 111 тощо. Отже, оголосимо тип масиву з даними про зріст учнів. const maxN=2000; aSize=array[1..maxN] of word; 9


Ìàñèâè òà ôàéëè

Дані про зріст утворимо за допомогою такої процедури, на по% чатку виконання якої від клавіатури отримується кількість учнів. procedure genASize(var a:aSize; var n:word); var k:word; {поточний індекс} begin repeat write('Кількість учнів(1..', maxN, ')>'); readln(n); until (1<=n) and (n<=maxN); Randomize; for k:=1 to n do a[k]:=110+Random(111); end; Тоді програма з цією процедурою матиме такий вигляд: program maxIntNumber; const maxN=2000; aSize=array[1..maxN] of word; var S:aSize; {масив даних про зріст учнів} n,cnt,k:word; {кількість та лічильник учнів, номер учня} t:byte; {поточний зріст} procedure genASize(var a:aSize; var n:word); ... {див. вище в тексті} end; begin genASize(S,n);{S і n визначаються у виклику} for t:=110 to 220 do begin cnt:=0; for k:=1 to n do if S[k]=t then inc(cnt); if cnt>0 then {лише ненульові кількості} writeln('Зріст ',t,': ',cnt,' учнів'); end; readln; end.

10


2. Ïðèêëàäè óòâîðåííÿ òà îáðîáêè ìàñèâ³â

Другий спосіб. Насправді масив, індекси якого відповідають уч% ням, не потрібен. Замість його заповнення використаємо масив, індекси якого — можливі значення зросту, а значення — кількості повторень цих значень серед даних. Оголосимо тип та% кого масиву лічильників. type aCnt=array[110..220] of word; У процедурі утворення даних збільшуються лічильники, індек% совані значеннями зросту. Кількість учнів тепер обмежено тільки типом word. procedure genACnt(var a:aCnt); {genACnt – генерувати масив лічильників (скорочення від GENerate Array of CouNTers} var n:word; {кількість учнів} t:byte; {значення зросту} begin repeat write('Кількість учнів>'); readln(n); until (1<=n); {ініціалізація лічильників нулями} for t:=110 to 220 do a[t]:=0; randomize; for k:=1 to n do inc(a[110+random(111)]); end; Виведемо дані за допомогою процедури writeACnt. Масив лічильників задамо як її параметр. Під час її виконання параметр має залишатися без змін, тому оголосимо його як параметр%кон% станту. Програма з цими двома процедурами має такий вигляд: program maxIntNumber; type aCnt=array[110..220] of word; var C:aCnt; {дані про зріст учнів} procedure genACnt(var a:aCnt); ... {див. вище в тексті} end; procedure writeACnt(const a:aCnt); var t:byte; {поточний зріст} 11


Ìàñèâè òà ôàéëè

begin for t:=110 to 220 do if a[t]>0 then writeln('Зріст ',t,': ',a[t],' учнів'); end; Begin genACnt(C); writeACnt(C); readln; End. Приклад. Повернемося до прикладу статті 8 розділу 6 першої частини, де потрібно було прочитати натуральне число типу longint та вивести його р%ковий запис (p ≤ 36). За добре відомим алгоритмом значення цифр числа утворю% ються, починаючи від молодшої, а вивести їх треба, починаючи зі старшої. Запам’ятаємо ці значення в масиві байтів. Найбіль% ше число типу longint має 31 двійкову цифру, тому потрібен масив з 31 елемента. Заповнення масиву байтів реалізуємо про% цедурою m a k e D i g s , друкування цифр — процедурою writeDigs . Зауважимо: якщо вхідне число 0, то його запис ма% тиме 0 цифр, і це буде особливим випадком при друкуванні цифр. program numberOutput; var n:longint; p:byte; {число та основа} const maxND=31; {максимальна кількість цифр} type aDigs:array[0..maxND] of byte; var x:aDigs; {масив значень цифр} m:byte; {кількість цифр} function digit(v:byte):char; begin {повернення цифри за її значенням v} case v of 0..9: digit:=chr(v+ord('0')); 10..35: digit:=chr(v-10+ord('A')); end; end; procedure writeDigs(const x:aDigs; m:byte); {m - кількість цифр у масиві x} var k:byte; {поточний індекс цифри} 12


2. Ïðèêëàäè óòâîðåííÿ òà îáðîáêè ìàñèâ³â

begin if m=0 {особливий випадок: вхідне число 0} then write(0) else for k:=m-1 downto 0 do write(digit(x[k])); writeln; end; procedure makeDigs(n:longint; p:byte; var x:aDigs; var m:byte); {m - кількість цифр, які збережено в масиві x} begin m:=0; while n>0 do begin x[m] := n mod p; inc(m); n := n div p; end; end; Begin readln(n,p); makeDigs(n,p,x,m); writeDigs(x,m); End. ‹ Приклад. Повернемося до кролів Фібоначчі (статтю 2 розді% лу 5 першої частини). Припустимо, що пара кролів живе повних t місяців і ще декілька днів, а досягає зрілості й починає наро% джувати через b місяців після появи на світ (b < t ≤ 10). Першого січня є одна пара новонароджених кролів. Визначити кількість пар через задану кількість місяців m. Наприклад, за t = 4, b = 2, m = 5 відповідь буде 6 (якби кролі були «безсмертними», було б 8 пар, а так одна пара померла й одна не народилася). Зрозуміло, що кількість пар кролів змінюється залежно від того, скільки є пар того чи іншого віку. Подамо пари кролів у масиві R, індекси елементів якого відповідають віку кролів (у місяцях від 0 до t), а значення виражають кількість пар кролів цього віку. 13


Ìàñèâè òà ôàéëè

const maxAge=10; {максимальний вік кролів} var R:array[0..maxAge] of longint; Розглянемо, як за поточним значенням масиву визначається наступне. За місяць пари віку 0 (новонароджені) стануть парами віку 1, віку 1 — парами віку 2, …, віку t – 1 — парами віку t, а пари віку t вимруть. Отже, за кожного k від 0 до t – 1 значення еле% мента R[k] має стати значенням R[k+1]. Кількість новонаро% джених пар буде сумою кількостей пар, вік яких від b до t. Описаний перерахунок кількості пар кожного віку треба про% вести для кожного місяця, врахувавши у початковому значенні масиву, що «через 0 місяців» є одна новонароджена пара. На основі наведених міркувань побудуємо таку программу: program rabbits; const maxAge=10; {межа віку кролів} var R:array[0..maxAge] of longint; var b,t:byte; k:byte; m,month:word; s:longint; {сумарна кількість пар} begin writeln('Зрілість та тривалість (до 10)>',b,t); writeln('Кількість місяців>',m); for k:=1 to t do R[k]:=0; R[0]:=1; {спочатку є одна новонароджена пара} for month:=1 to m do begin for k:=t downto 1 do R[k]:=R[k-1]; {саме в такому порядку!!!} {підрахунок пар, які народяться} R[0]:=0; for k:=b to t do inc(R[0],R[k]); end; s:=0; for k:=0 to t do inc(s,R[k]); writeln('Загальна кількість пар: ', s); end.

14


3. Äâîâèì³ðí³ ìàñèâè. Ïðèêëàäè âèêîðèñòàííÿ

3. ДВОВИМІРНІ МАСИВИ. ПРИКЛАДИ ВИКОРИСТАННЯ Прямокутну таблицю з m × n однотипних елементів можна по% дати як складену з m рядків по n елементів у кожному. Її можна розглядати як масив, елементами якого є масиви, або як двови мірний масив. Якщо елементи двовимірної таблиці самі є маси% вами або таблиці утворюють послідовність, то виникає тривимі рний масив тощо. Оголошення двовимірних масивів, або матриць, і зображен% ня їх елементів у мові Паскаль опишемо за допомогою простого прикладу. Приклад. Позицію в грі «хрестики%нулики на полі 3 на 3» по% дано квадратною таблицею із символів 'x', '0' та ' ' (про% пуск). Пронумеруємо клітини поля, як у шахах, — буквами 'a', 'b', 'c' по горизонталі та числами 1, 2, 3 по вертикалі. Тоді рядки таблиці — це масиви такого типу. type Row=array['а'..'c'] of char; Таблиця — це масив, складений трьома рядками. type Table=array[1..3] of Row; Масиви типу Table мають два виміри: номер рядка та номер стовпчика в ньому. Вимір 1..3, що нумерує рядки, називається зовнішнім, вимір 'а'..'c' , що нумерує символи в рядках, — внутрішнім. Тип Table можна задати, не о��олошуючи імені типу Row . type Table=array [1..3] of array['а'..'c'] of char; У мові Паскаль є інша форма запису вимірів: через кому в спільних дужках. Наприклад, тип Table можна оголосити так. type Table=array[1..3,'a'..'c'] of char; За будь%якої форми оголошення типу Table елементи маси% ву цього типу, наприклад, A, можна позначати як A[i,j] або A[i][j] , де 1 ≤ i ≤ 3, 'a' ≤ j ≤ 'c'. Вираз вигляду A[i], де 1 ≤ i ≤ 3 , позначає рядок таблиці, тобто змінну типу array['а'..'c'] of char . • Елементи багатовимірних масивів розташовуються в па% м’яті послідовно, найшвидше в них змінюється внутрішній 15


Ìàñèâè òà ôàéëè

індекс, найповільніше — зовнішній. Зокрема, двовимірні маси% ви розташовуються рядками. Наприклад, послідовні елементи масиву типу array[1..2,'а'..'b'] of byte мають набори індексів [1,'a'], [1,'b'], [2,'a'], [2,'b'], а послідовні симво% ли в масиві типу Table — [ 1 , ' a ' ] , [ 1 , ' b ' ] , [ 1 , ' c ' ], [2,'a'], …, [3,'c']. Приклад. Треба прочитати два натуральних числа m і n не більше 20, утворити числову матрицю розмірами m × n (m рядків, n стовпчиків), вивести її на екран, а потім відобразити її симет% рично відносно вертикальної осі та вивести на екран. Спочатку оголосимо тип числової матриці, вважаючи, що зна% чення її елементів мають тип byte. const maxSz=20; {максимальний розмір} type Matrix=array[1..maxSz,1..maxSz] of byte; Уведемо розміри матриці та утворимо значення її елементів за допомогою генератора випадкових чисел у такій процедурі. procedure gMatr(var a:Matrix; var m,n:byte); var i,k:byte; {поточні індекси} begin write('Рядків та стовпчиків (1..',maxSz,')>'); readln(m,n); Randomize; for i:=1 to m do for k:=1 to n do a[i,k]:=Random(256); end; Процедура виведення значень у матриці на екран також дуже проста. Значення виводяться рядками; числа типу byte мають не більше трьох десяткових цифр, тому ширина поля виведення 4 забезпечить пропуски між числами на екрані. procedure wrMatr(const a:Matrix; m,n:byte); var i,k:byte; {поточні індекси} begin for i:=1 to m do begin for k:=1 to n do write(a[i,k]:4); {виведення в рядок} 16


3. Äâîâèì³ðí³ ìàñèâè. Ïðèêëàäè âèêîðèñòàííÿ

writeln; {перехід на новий рядок} end; end; Розглянемо симетричне відображення матриці відносно вер% тикальної осі. Перший стовпчик має помінятися місцями з ос% таннім, другий — з передостаннім тощо. Якщо кількість сто% впчиків непарна, то «серединний» стовпчик залишається без змін, а якщо парна, то такого стовпчика немає. Отже, кожен стовпчик з номером k від 1 до n div 2 треба поміняти місця% ми зі стовпчиком, номер якого — n-k+1. Цей самий процес мож% на розглядати інакше: у кожному рядку (від 1 до m) обмінюють% ся значення елементів з номерами k від 1 до n div 2 та відпо% відних їм елементів з номерами n-k+1. Реалізуємо цей обмін у такій процедурі: procedure vertSymm(var a:Matrix; m,n:byte); var i,k,t:byte; begin for i:=1 to m do for k:=1 to n div 2 do begin t:=A[i,k]; A[i,k]:=A[i,n-k+1]; A[i,n-k+1]:=t end end; Нарешті, напишемо програму розв’язання задачі, указавши в ній наведені процедури скорочено: program matrixes; const maxSz=20; {максимальний розмір} type Matrix=array[1..maxSz,1..maxSz] of byte; procedure gMatr(var a:Matrix; var m,n:byte); ... {див. вище в тексті} end; procedure wrMatr(const a:Matrix; m,n:byte); ... {див. вище в тексті} end; procedure vertSymm(var a:Matrix; m,n:byte); ... {див. вище в тексті} end; 17


Ìàñèâè òà ôàéëè

var x:Matrix; m,n:byte; Begin gMatr(x,m,n); writeln('Утворено таку матрицю'); wrMatr(x,m,n); writeln('Відображення зліва направо'); vertSymm(x,m,n); wrMatr(x,m,n); readln; End. Приклад. В умові попередньої задачі замість відображення матриці розглянемо її поворот на 180° (байдуже, за годиннико% вою стрілкою чи проти неї). Наведемо лише процедуру повороту матриці A на 180 . Неваж% ко переконатися, що при цьому за будь%яких i від 1 до m та k від 1 до n обмінюються значення A[i, k] та A[m – i + 1, n – k + 1]. Зна% чення всіх елементів верхньої половини матриці опиняються в нижній половині і навпаки. Отже, треба перебрати рядки матриці з номерами i від 1 до m div 2 та поміняти місцями значення A[i, k] та A[m – i + 1, n – k + 1] при всіх k від 1 до n. Проте, якщо m не% парне, залишиться «серединний» рядок, в якого i = m – i + 1. У ньому треба обміняти місцями перше значення з останнім, дру% ге — з передостаннім тощо, тобто при обміні A[i, k] та A[m – i + 1, n – k + 1] індекс k має прийняти значення від 1 до n div 2. procedure turn180(var a:Matrix; m,n:byte); var i,k,t:byte; begin for i:=1 to m div 2 do for k:=1 to n do begin t:=A[i,k]; A[i,k]:=A[m-i+1,n-k+1]; A[m-i+1,n-k+1]:=t end; if odd(m) then begin i := m div 2 + 1;{номер «серединного» рядка} for k:=1 to n div 2 do begin t:=A[i,k]; A[i,k]:=A[i,n-k+1]; 18


3. Äâîâèì³ðí³ ìàñèâè. Ïðèêëàäè âèêîðèñòàííÿ

A[i,n-k+1]:=t end; end; end; Перевірити цю програму треба як за парного, так і за непар% ного першого виміру матриці, оскільки дії в цих ситуаціях мають бути різними. ‹ Масиви, в яких кількості рядків і стовпчиків однакові, нази% ваються квадратними й мають дві діагоналі (головну та бічну). Бічна діагональ

Головна діагональ

Елементи на головній діагоналі мають індекси (1, 1), (2, 2), (3, 3), …, (і, і), …, (n, n), тобто номери рядка й стовпчика рівні. Індекси елементів на бічній діагоналі (1, n), (2, n – 1), (3, n – 2), …, (і, n + 1 – і), …, (n, 1) зв’язано формулою i + j = n + 1, де n — кількість рядків (стовпчиків), i та j — номер рядка та стовпчика масиву відповідно. Діагоналі визначають у масиві вісім «трикутників» (чотири ве% ликих, що є «половинами» масивів над кожною діагоналлю та під нею, та чотири малих між діагоналями). Будемо вважати, що еле% менти діагоналей належать відповідним трикутникам. Наприк% лад, при n = 4 «верхній лівий трикутник» при бічній діагоналі містить елементи з індексами (1, 1), (1, 2), (1, 3), (1, 4), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (4, 1), а «нижній трикутник» між діаго% налями — елементи з індексами (3, 2), (3, 3), (4, 1), (4, 2), (4, 3), (4, 4). При n = 3 аналогічні «трикутники» містять елементи з індексами (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (3, 1), та, відповідно, (2, 2), (3, 1), (3, 2), (3, 3). Традиційними є задачі пошуку елементів з деякими власти% востями в трикутниках, визначених діагоналями. Приклад. Знайти у відміченому трикутнику максимальне від’ємне значення.

19


Ìàñèâè òà ôàéëè

Розв’язання задачі розбивається на кілька підзадач: заповнен% ня масиву, пошук указаного елемента та виведення його на ек% ран. Масив заповнимо в процедурі InpMatr за допомогою ге% нератора псевдовипадкових чисел і для контролю відразу виве% демо на екран. Сам масив та кількість заповнених рядків повер% таються з виклику процедури як параметри%змінні. Пошук максимального від’ємного значення оформимо функ% цією. Звичайний алгоритм пошуку максимуму починається з того, що деякому еталону присвоюється початкове значення, наприк% лад, перше в масиві. Отже, в цій задачі треба врахувати область пошуку, визначити, в якому порядку йдуть її елементи й які індекси має перший з них. Але головна неприємність — необхід% но шукати максимальне від’ємне значення, а перший елемент може не бути від’ємним. Більше того, в області пошуку взагалі може не бути від’ємних чисел. Оформимо пошук функцією maxNeg. Домовимося, що вона повертає 0, якщо від’ємних чисел в області пошуку немає, і буде% мо обробляти цей особливий випадок при виведенні результату. Спочатку як еталонне значення візьмемо 0. Далі, якщо в області пошуку знайдено від’ємне значення й еталон рівний 0, то візьмемо це перше від’ємне значення як еталонне. Якщо ж знайдене від’ємне значення більше за еталонне, запам’ятаємо його як еталонне. program maxNegative; const maxSz=10; {максимальний розмір} type Matrix=array[1..maxSz,1..maxSz] of real; procedure inpMatr(var a:Matrix; var n:byte); var i, j : byte; begin Randomize; n := 1+random(10); for i:=1 to n do begin for j:=1 to n do begin a[i,j] := random*100-random*50; write(a[i,j]:7:2); end; writeln; end; end; 20


4. Ôàéë, ôàéëîâà çì³ííà, ôàéëîâ³ òèïè

function maxNeg(a:Matrix; n:byte):real; var i,j:byte; etalon:real; begin etalon:=0; for i := n div 2 + 1 to n do for j := n+1-i to i do if a[i,j]<0 then if (etalon=0)or(etalon<a[i,j]) then etalon:=a[i,j]; maxNeg:=etalon; end; var Mas : Matrix; n : byte; x : real; Begin inpMatr(Mas,n); x:=maxNeg(Mas,n); if x=0 then writeln('Від''ємних значень немає') else writeln('Max=',x:8:2); readln; End.

4. ФАЙЛ, ФАЙЛОВА ЗМІННА, ФАЙЛОВІ ТИПИ Під файлом прийнято розуміти деяку іменований набір даних на зовнішньому носії даних. У мовах програмування такий набір даних, як правило, називають фізичним файлом, а під словом «файл» розуміють файлову змінну, яка представляє файл у про% грамі Паскаль. Файл (файлова змінна), як і масив, являє собою послідовність однотипних елементів. Проте, на відміну від масиву, у будь%який момент виконання програми з усіх елементів файла можна оброб% ляти (читати або записувати) тільки один. Він називається дос тупним елементом; інші в цей момент недоступні. Номер дос% тупного елемента в послідовності елементів файла є значенням спеціальної неявної змінної — файлового вказівника (див. рис. 1 на с. 22). Номер доступного елемента (значення вказівника) змінюється при виконанні підпрограм обробки файлів

21


Ìàñèâè òà ôàéëè

Елемент 1

Елемент 2

Елемент k

Елемент N

Файловий вказівник Рис. 1. Фізичний файл і файловий вказівник

Основними діями з файлом є введення (читання) — копіюван% ня значення доступного елемента файла в «звичайну» змінну про% грами, і виведення (запис) — копіювання значення виразу в до% ступний елемент файла. Обробку файлів у мові Turbo Pascal задають за допомогою стан% дартних підпрограм, з яких нам знайомі процедури r e a d , readln, write, writeln. Ім’я файла записують як перший ар% гумент у викликах цих підпрограм. У мові Turbo Pascal є три основних різновиди файлів: типізо% вані, текстові та безтипові. Головне, чим вони відрізняються, — це набори підпрограм їх обробки. Кожен різновид має спе% цифічні підпрограми, означені тільки для нього. Спочатку пред% ставимо їх дуже стисло, а нижче розглянемо роботу тільки з тек% стовими файлами. Типізований файл — це послідовність елементів деякого ска% лярного або структурного типу. Підкреслимо: ці елементи (ком% поненти) файла є ділянками пам’яті на зовнішньому носії даних. Тип файла задають виразом вигляду file of тип, де тип є ти% пом елементів файла. Розглянемо приклади: type fByte = file of byte; {тип файла байтів} aByte = array[1..9] of byte; {тип масиву} faByte = file of aByte; {тип файла масивів} Текст — це послідовність символів, поділена на рядки. У мові Turbo Pascal для файлів%текстів означено тип з іменем text. Еле% ментами цих файлів є символи, проте тип text відрізняється від типу file of char. Поперше, для текстів означено процедури, незастосовні до ти% пізованих файлів, наприклад, readln та writeln. Подруге, в мові зафіксовано спеціальні символи, що позна% чають кінці рядків і кінець тексту та обробляються у специфіч% 22


5. Çâ’ÿçóâàííÿ òà â³äêðèòòÿ ôàéëà

ний спосіб. Кінець рядка позначається символом з номером 13 (chr(13) або #13), кінець тексту — символом #26 . Безтиповий файл розглядають як послідовність байтів; його тип має ім’я file . Наприклад, оголошення двох безтипових файлів може мати вигляд var f,g:file . Для них також озна% чено спеціальні підпрограми. • Одну й ту ж послідовність байтів можна розглядати та об% робляти як послідовність або значень деякого типу, або символів із розбиттям на рядки, або байтів. • Файли можуть бути елементами масивів, але не файлів. Наприклад, оголошення типу масиву type aText = array[1..2] of text; є допустимим, але такі оголошення типів недопустимі. type badTyp1=file of file of ...; type badTyp2=file of text; type badTyp3=file of array[1..2] of file of ...;

5. ЗВ’ЯЗУВАННЯ ТА ВІДКРИТТЯ ФАЙЛА Нехай нижче ім’я f позначає файлову змінну (у реальних про% грамах краще давати файловим змінним змістовні імена, наприк% лад workFile, inputFile тощо). Робота з файловою змінною починається зі зв’язування її з конк% ретним фізичним файлом. Для цього ім’я файлової змінної та ім’я фізичного файла в операційній системі (зовнішнє ім’я) задають у вик% лику процедури assign, наприклад assign(f,'myfile.dat'). Зовнішнє ім’я записують у апострофах. Після виклику процедури assign ім’я файлової змінної позначає фізичний файл. Файлові змінні з іменами input і output неявно (без указі% вки у програмі) оголошено як змінні типу text і зв’язано зі стан дартними файлами введення та виведення — клавіатурою та ек% раном комп’ютера. Ці файли мають зовнішнє ім’я 'con' (це ско% рочення від console — консоль). • Імена input і output можна не записувати у викликах підпрограм обробки файлів. Саме такими були виклики readln і writeln у попередніх розділах. Перш ніж обробляти зв’язаний файл, його необхідно відкри ти. Відкрити файл можна, щоб потім читати з нього або щоб записувати в нього. 23


Ìàñèâè òà ôàéëè

Для читання створеного раніше файла його відкривають за до% помогою процедури reset. Після виклику reset(f) елемен% ти файла можна читати, починаючи з першого. Для запису файл відкривають за допомогою процедури rewrite («перезаписати»). Після виклику rewrite(f) по% слідовність елементів файла f стає порожньою. Цю процедуру викликають для створення нового фізичного файла або зміни ста% рого зі знищенням попередніх даних. Текстові файли можна відкривати, не знищуючи їхні попе% редні дані. Якщо файлову змінну f типу text зв’язано з уже існуючим фізичним файлом, то виклик append(f) забезпечує, що потім нові символи будуть дописуватися після наявних у файлі. Після обробки файл треба закрити за допомогою процедури close : close(f) . Ця процедура не розриває зв’язку імені f із фізичним файлом, але введення та виведення за допомогою імені f неможливі до наступного відкриття або нового зв’язування з подальшим відкриттям. • Якщо файл не закрити після записування в нього, то, мож% ливо, не всі дані, записані у файлову змінну під час виконання програми, насправді потраплять у фізичний файл. • Спроба закрити вже закриту або ще не відкриту файлову змінну призводить до аварійного завершення програми. Отже, стандартний порядок дій із файловою змінною та% кий: Зв’язування (assign) Відкриття (reset, rewrite або append) Обробка (read, write та деякі інші підпрограми) Закриття (close)

6. ЗАПИС У ТЕКСТОВИЙ ФАЙЛ Текстові файли доводиться створювати дуже часто. Наприклад, якщо програмі на вхід потрібні кілька числових констант, їх не% важко набрати на клавіатурі. Проте коли таких констант десятки й більше або під час налаштування програми вони вводяться ба% гаторазово, краще записати їх у текстовий файл і у програмі зчи% тувати дані з текстового файла. 24


6. Çàïèñ ó òåêñòîâèé ôàéë

Символи записуються в текст процедурами write і writeln. Їх перший аргумент — ім’я файлової змінної. При виконанні вик% лику write(f,вираз) , де вираз має скалярний або рядковий тип, обчислюється значення виразу, за ним утворюється констан% та, тобто послідовність символів, яка представляє значення, і ви% водиться в текст f. Наприклад, при виконанні виклику write(f,trunc(sqrt(9999))); обчислюється значення 99 і у файл дописуються символи '9' і '9'. У виклику можна записати кілька виразів через кому — write(f,вираз1,вираз2,...);. Такий виклик насправді ви% конується як послідовність викликів write(f,вираз1); write(f,вираз2); ... Процедура writeln відрізняється лише тим, що за виклику writeln(f, список_виразів_скалярних_типів); після останньої константи в текст додається позначення кінця рядка, точніше, символи #13#10. Список виразів може бути по% рожнім — тоді в текст записуються тільки #13#10. Приклад. Розглянемо створення тексту з цілими константами, які отримуються від клавіатури, копіюються в текстовий файл та відокремлюються в ньому пропусками. program CreateInts; var f:text; {файл-текст} x:integer; {число від клавіатури} begin writeln('Створення тексту.', 'Кінець - <Ctrl+Z> замість константи'); assign(f,'myfile.txt'); rewrite(f); write('Введіть цілу константу: '); while not eof do begin readln(x); write(f,' ',x); {перед константою пропуск} writeln('Введіть цілу константу: '); end; close(f) end. 25


Ìàñèâè òà ôàéëè

Для закінчення роботи замість введення нової константи тре% ба натиснути Ctrl + Z і Enter. Приклад. Розглянемо програму створення тексту, у першому рядку якого записано цілі числа m і n — кількості рядків і сто% впчиків числової матриці (не більше 20). Наступні m рядків тек% сту містять по n цілих чисел (елементів матриці), відокремлених пропусками. Розміри матриці задаються на клавіатурі, а її еле% менти утворюються за допомогою генератора псевдовипадкових чисел. program CreateMatr; const maxSz=20; {максимальний розмір} var f:text; {файл-текст} i,k:byte; {поточні індекси} begin assign(f,'matrix.txt'); rewrite(f); write('Рядків та стовпчиків(1..',maxSz,')>'); readln(m,n); writeln(f,m,' ',n); Randomize; for i:=1 to m do begin for k:=1 to n do write(f, Random(65535)-32768, ' '); writeln(f); {перехід на новий рядок} end; close(f) end. У викликах процедур write і writeln після виразу через двокрапку можна вказати ширину поля W для запису значення виразу, наприклад write(f,sqr(x):4). Тут W = 4. Нехай для запису значення виразу насправді потрібно L символів, наприк% лад, в останньому виклику L = 1 при x < 4, L = 2 при 3 < x < 10, L = 3 при 9 < x < 34 тощо. Якщо L < W, то перед символами зна% чення додається W – L пропусків; якщо ж L ≤ W, виводяться всі символи. Отже, при x = 3 друкуються три пропуски й 9, а при x = 100 — усі п’ять символів 10000. Після виразу дійсного типу можна також указати кількість N цифр дробової частини, які виводяться після десяткової крапки, наприклад write(f, sqrt(x):7:3). Якщо N указано, то ви% 26


7. Óâåäåííÿ äàíèõ áàçîâèõ òèï³â ³ç òåêñòîâîãî ôàéëà

водиться константа з фіксованою крапкою та N цифрами після крапки, інакше — нормалізована з порядком. У цьому випадку при x = 2 виводяться два пропуски та 1.414. Остання цифра є результатом округлення.

7. УВЕДЕННЯ ДАНИХ БАЗОВИХ ТИПІВ ІЗ ТЕКСТОВОГО ФАЙЛА Для введення даних з текстових файлів використовують про% цедури read та readln. Спочатку розглянемо процедуру read. Її виклик має такий найпростіший вигляд. read(f, ім’я_змінної_базового_типу); Уведення символа. При виконанні виклику read(f,X) змінній X типу char присвоюється доступний символ тексту, яким би він не був, а доступним стає наступний за ним. Виняток — якщо до% ступний #26, то X отримує значення #0 і символ #26 залишаєть% ся доступним. Уведення числової константи. Ціла константа — це по% слідовність цифр, можливо, зі знаком '+' або '-' на початку, яка задає ціле число відповідного цілого типу; між знаком та пер% шою цифрою в тексті не може бути пропусків. Дійсна констан% та — це послідовність цифр та інших символів зі структурою кон% стант мови Паскаль, наприклад 1 . 1 , 2 . , 0 . 9 9 , 1 e - 3 , -2.73E+02 . Числові константи в текстах відокремлюються пропусками в довільній кількості. Символи табуляції та кінця рядка також бу% демо називати пропусками. Виклик read(f,X), де X — ім’я цілої або дійсної змінної, виконується так. З тексту від доступного символа читаються про% пуски, а потім — символи константи до найближчого пропуску (можливо, до символу #26 або до кінця файла). Доступним після читання константи буде перший пропуск після неї (відповідно, #26 або кінець файла). Якщо символи утворюють константу по% трібного типу, то за ними обчислюється значення й присвоюється змінній. При дійсній змінній X у тексті може знаходитися й ціла константа — за нею обчислюється дійсне значення. Символи можуть не утворювати константу відповідного типу — тоді виникає помилкова ситуація і виконання програми аварійно 27


Ìàñèâè òà ôàéëè

завершується. Наприклад, помилковими є послідовності символів - 2 (пропуск між знаком і цифрою), 12345m, 123- (присутні нецифрові символи там, де їх не може бути) або 13., якщо чи% тається значення цілої змінної. Якщо доступний кінець файла або від поточної позиції в файлі до його кінця чи найближчого си��вола #26 записано лише про% пуски, то при спробі прочитати число відповідна змінна отри% мує значення 0. В одному виклику процедури read можна указати кілька змінних. read(f, список_позначень змінних_числових_типів); Цей виклик виконується так само, як і відповідна по% слідовність викликів. read(f, ім’я_змінної_1); read(f, ім’я_змінної_2); ... Читання констант базових типів за процедурою readln ана% логічно процедурі read. Відмінність полягає в тому, що після читання константи всі символи тексту, які залишилися до най% ближчого кінця рядка в тексті, пропускаються разом із ним. До% ступним стає перший символ наступного рядка тексту (якщо до кінця рядка зустрінеться символ #26 , доступним стане він). Якщо у виклику указано кілька імен змінних, то «хвіст» рядка пропускається тільки після останньої константи. Якщо список імен порожній, то виклик readln(f) пропускає поточний ря% док тексту. Приклад. Нехай у тексті f записано такі символи: 1 5

2 5

3

#13

#26

Нехай x, y, z, t — імена цілих змінних. Розглянемо прикла% ди викликів процедур і значень, яких набудуть ці змінні. Виклики read(f,x,y); read(f,z,t) readln(f,x,y); read(f,z,t) readln(f,x); readln(f,y,z,t) 28

x 1 1 1

y 2 2 55

z 3 55 0

t 55 0 0


8. Ïðèêëàäè ââåäåííÿ ïîñë³äîâíîñòåé äàíèõ

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

8. ПРИКЛАДИ ВВЕДЕННЯ ПОСЛІДОВНОСТЕЙ ДАНИХ Типовою є ситуація, коли вхід програми утворено послідовні% стю однотипних значень у текстовому файлі. Послідовність уво% диться в циклі, вигляд якого відповідає способу визначення кінця послідовності. Розглянемо три таких способи: • довжину послідовності задано на її початку; • кінець послідовності задано спеціальним значенням; • кінець даних визначено кінцем тексту. У деяких задачах під час уведення можуть виникати умови, за яких процес уведення вхідних даних слід закінчити ще до того, як буде прочитано всі дані. Розглянемо також деякі особливості введення символів. ДОВЖИНУ ПОСЛІДОВНОСТІ ЗАДАНО НА ЇЇ ПОЧАТКУ Спочатку задається кількість значень, а потім самі значення у відповідній кількості. Потрібна кількість виконань циклу вве% дення відома заздалегідь, тому в таких ситуаціях використовують for %оператор. Приклад. Повернемося до прикладу в параграфі 3 й припус% тимо, що числова матриця не створюється за допомогою генера% тора псевдовипадкових чисел, а вводиться з текстового файла matrix.txt. У його першому рядку записано кількості рядків і стовпчиків матриці, а далі задано числові значення у відповідній кількості. Тоді у програмі (див. параграф 3) треба на% писати й викликати замість процедури gMatr таку процедуру. procedure readMatr(var a:Matrix; var m,n:byte); var f:text; {файл-текст} i,k:byte; {поточні індекси} 29


Ìàñèâè òà ôàéëè

begin assign(f, 'matrix.txt'); reset(f); readln(f,m,n); for i:=1 to m do for k:=1 to n do read(f,a[i,k]); close(f); end; КІНЕЦЬ ПОСЛІДОВНОСТІ ЗАДАНО СПЕЦІАЛЬНИМ ЗНАЧЕННЯМ Якщо заздалегідь відомо спеціальне значення, яким у вхідній послідовності позначено її кінець, то введення зручно задати за допомогою repeat%оператора, оскільки треба ввести не менше одного значення перед тим, як буде виявлено ознаку кінця вве% дення. Приклад. У перукарні працює один перукар. Клієнти прихо% дять, займають чергу (якщо вона є) і стрижуться в порядку чер% ги. Для кожного клієнта відома тривалість його стрижки t: клієнт залишає салон через t одиниць часу після початку стрижки. Мо% менти приходу клієнтів задано відносно початкового моменту часу. Треба вивести моменти виходу клієнтів у інший текстовий файл (по одному на рядок). Уточнимо вигляд вхідних даних, які мають читатися з файла, зв’язаного зі змінною f типу text. Припустимо, що стрижка відбувається не миттєво, тому вхідними даними є пари цілих чи% сел x1 , t1 , x2, t2 , …, де числа x не спадають, а тривалості t додатні (t = 0 означає кінець вхідних даних). Пари чисел задано по одній на рядок; числа відокремлено пропуском. Перше наближення до розв’язання є очевидним. repeat readln(f,x,t); if t>t0 {значення t0=0 задає закінчення} then за x і t обчислити момент виходу y until t=t0; Припустимо, що клієнти стрижуться без пауз: стрижка ново% го клієнта, якщо він уже прийшов, починається відразу після ви% ходу попереднього. За x1 і t1 можна обчислити момент виходу 30


8. Ïðèêëàäè ââåäåííÿ ïîñë³äîâíîñòåé äàíèõ

y1: y1 = x1+t1. Проте кожен наступний клієнт починає стриг% тися тільки після того, як закінчив стригтися попередній, тому y i= max{y i–1 + ti , xi + ti }. Щоб не розглядати окремо першого й на% ступних клієнтів, приймемо початковий момент за час виходу «ну% льового» клієнта. Отже, програма набуває такого вигляду: program Barber; {Barber – перукар} var x,t,t0,y:word; f,g:text; {вхідний та вихідний тексти} begin t0:=0; assign(f, 'barber.in'); reset(f); assign(g, 'barber.out'); rewrite(g); y:=0; repeat readln(f, x, t); if t>t0 then begin if y < x then y := x+t else y := y+t; writeln(g, y); end until t = t0; close(f); close(g); end. Ще один спосіб задати кінець послідовності — повторити пер% ше або останнє значення послідовності. Схема розв’язання за% лишається аналогічною, тільки «особливе значення» отримуєть% ся після введення, а не присвоювання (t0:=0 на початку тіла щойно наведеної програми). КІНЕЦЬ ДАНИХ ВИЗНАЧЕНО КІНЦЕМ ТЕКСТУ Прочитати послідовність значень, записану в текстовому файлі, можна за допомогою функції eof у циклі такого вигляду. while not eof(f) do begin read(f,v); {v – ім’я змінної} обробка v end; 31


Ìàñèâè òà ôàéëè

З виклику eof(f) повертається значення true, якщо доступ% ний кінець файла f або символ #26. Читання в тілі циклу відбу% вається після того, як із виклику eof(f) повернулося значення false , тобто кожному введенню передує успішна перевірка, чи не прочитано файл. • Якщо в наведеному вище циклі змінна v має числовий тип, то необхідно забезпечити, щоб між останньою числовою кон% стантою та кінцем вхідного файла не було порожніх символів. Якщо цього не зробити, буде прочитано зайве нульове значення, що не завжди бажано. Взагалі, якщо всередині тексту з числови% ми константами присутній символ #26, то наведений цикл об% роблятиме лише частину тексту до символа #26. Приклад. Текстовий файл містить послідовність цілих кон% стант типу integer, відокремлених пропусками. Треба прочи% тати їх та надрукувати їх кількість і суму. Будемо вводити константи й накопичувати їх суму, поки не прочитаємо весь текст. program Summa; var f:text; {вхідний текст} v:integer; {поточне число} n,sum:longint; {кількість і сума} begin sum:=0; n:=0; assign(f, 'numbers.in'); reset(f); while not eof(f) do begin read(f,v); inc(n); inc(sum,v) end; writeln('Введено чисел:',n,'. Їх сума:',sum); close(f); end. Якщо між останньою константою та кінцем вхідного файла за% писано хоча б один пропуск, то буде враховано одне зайве чис% ло. Перевірте це. Приклад. Відрізок [a; b] прямої Ox задається парою чисел a, b, де a ≤ b. Перетином двох відрізків є або відрізок, або порожня 32


8. Ïðèêëàäè ââåäåííÿ ïîñë³äîâíîñòåé äàíèõ

множина точок, наприклад, [1;3] ∩ [ 2; 4] = [ 2;3] ,

[1; 2] ∩ [3; 4] = ∅ ,

[1; 2] ∩ [2;3] = [2; 2] . Треба ввести з текстового файла пари чисел, що задають відрізки, і знайти перетин заданих відрізків. Припустимо, що кожен рядок вхідного тексту задає кінці відрізка. Числа в рядку відокремлено пропуском. Для збереження поточного перетину означимо змінні lb і hb (скорочення від low bound і high bound — нижня й верхня межа). Ознакою того, що перетин став порожнім, буде умова lb > hb. Cпочатку відрізків немає, тому перетин порожній — вирази% мо це початковими значеннями lb = 1, hb = 0. Далі значення% ми lb та hb мають стати кінці першого відрізка. Потім у циклі вводяться інші відрізки та обчислюється перетин. Після того, як перетин уже прочитаних став порожнім, про% довжувати читання відрізків немає сенсу — треба відразу видати відповідь і закінчити роботу. Тому до умови продовження циклу обробки відрізків додамо умову того, що перетин не став по% рожнім. program segments; var f:text; a,b:real; {поточний відрізок} lb,hb: real; {поточний перетин} begin assign(f,'segments.txt'); reset(f); lb:=1; hb:=0; if not eof(f) then readln(f,lb,hb); while not eof(f) and (lb<=hb) do begin readln(f,a,b); if a>lb then lb:=a; if b<hb then hb:=b; end; {текстовий файл прочитано або перетин порожній} if lb>hb then writeln('перетин порожній') else writeln('[',lb,';',hb,']') end.

33


Ìàñèâè òà ôàéëè

ДЕЯКІ ОСОБЛИВОСТІ ВВЕДЕННЯ СИМВОЛІВ Приклад. Розглянемо програму копіювання будь%якого файла як текстового в інший файл. Символи файла вводяться з тексту та виводяться по одному. Власне копіювання для ілюстрації офор% мимо у вигляді процедури, параметризованої файлами. Параметри файлових типів оголошуються в заголовках підпрог% рам тільки як параметризмінні. program FCopy; var f,g:text; procedure copyText(var f,g:text); var c:char; begin reset(f); rewrite(g); while not eof(f) do begin read(f,c); write(g,c); end; close(f); close(g); end; begin assign(f,'inp.txt'); assign(g,'inpcopy.txt'); copyText(f,g); end. Ця програма буде без проблем копіювати файл, якщо в ньому немає символа #26. Проте, як тільки у вхідному тексті доступним буде #26, з виклику eof(f) повернеться true і копіювання зак% інчиться незалежно від подальших символів у вхідному файлі. Щоб копіювати фізичні файли з іншими іменами, перед вик% ликом програми треба поміняти їх імена у викликах assign. Зокрема, щоб копіювати символи з клавіатури в текстовий файл, замість імені 'inp.txt' треба указати 'con'. Для виведення на екран треба указати 'con' замість 'inpcopy.txt'. ‹ Особливу ситуацію при посимвольному введенні з тексту може складати кінець рядка. Для визначення цієї ситуації використо% вують функцію eoln. Якщо доступним �� тексті f є кінець рядка, з виклику eoln(f) повертається true , інакше повертається false . 34


8. Ïðèêëàäè ââåäåííÿ ïîñë³äîâíîñòåé äàíèõ

Приклад. На вхід програми подається текст, кожен рядок яко% го містить послідовність дужок ( і ), тобто дужковий вираз; інших символів у рядку немає. Довжини рядків можна представити в типі longint. Дужковий вираз у рядку є правильним, якщо це () , або якщо в ньому спочатку йде (, потім правильний вираз, потім ), або якщо він є послідовністю правильних виразів. На% приклад, вирази (()) , ()()() є правильними,)( — ні. По% рожній вираз також вважається правильним. З’ясувати, чи є ви% рази в рядках правильними, і вивести в один рядок іншого тек% сту послідовність із символів 0 і 1 (1, якщо вираз у рядку пра% вильний, інакше 0). Скористаємося лічильником NOpen : його значенням буде кількість відкритих і ще не закритих дужок у прочитаній частині рядка. На початку кожного рядка NOpen = 0. Рядок із дужковим виразом вводимо по одному символу. Кожна відкриваюча дужка «(» збільшує NOpen на 1, кожна закриваюча – «)»зменшує. Якщо з’являється «(», для якої не було попередньої «)», отри% маємо NOpen < 0. У цій ситуації вираз неправильний — решту рядка можна пропустити, не аналізуючи далі. Якщо по закінченні рядка NOpen <>0, тобто кількість дужок незбалансовано, то ви% раз не є правильним. Перед кожним введенням символу треба перевірити, чи не за% кінчився рядок. Для цього звернемося до функції eoln. Якщо з її виклику повертається false, тобто доступний символ тексту не позначає кінець рядка, то можна його вводити та обробляти. Інакше рядок закінчено й треба видати результат його обробки — ord(NOpen=0) . Окрім виведення результату, у кінці рядка треба ще підготу% ватися до обробки наступного рядка, тобто пропустити кінець рядка та присвоїти NOpen значення 0. Отже, оформимо дії з об% робки кінця рядка процедурою endLine. Особливістю в кінці тексту є те, що останній рядок може як мати, так і не мати позначення кінця. Якщо кінець рядка не по% значено, то, щоб видати результати обробки цього рядка, треба знати, чи було введено символи між останнім кінцем рядка та кінцем тексту. Для цього оголосимо змінну NInp — лічильник символів, прочитаних у поточному рядку. Спочатку та після закінчення кожного рядка йому присвоюється 0. Якщо в кінці 35


Ìàñèâè òà ôàéëè

тексту NInp>0, то останній рядок прочитано, але результат його обробки не виведено, тому треба викликати endLine . program Balance; var f,g:text; {файли входу та виходу} c : char; {поточний символ} NOpen:longint; {лічильник дужок} NInp :longint; {лічильник символів} procedure endLine; {обробка кінця рядка} begin readln(f); write(g,ord(NOpen=0)); NOpen:=0; NInp:=0; end; Begin assign(f, 'balance.txt'); reset(f); assign(g, 'balance.sol'); rewrite(g); NInp:=0; NOpen:=0; while not eof(f) do if eoln(f) then endLine {кінець рядка} else begin read(f,c); inc(NInp); if c='(' then inc(NOpen) else dec(NOpen); if NOpen<0 {баланс неможливий} then endLine end; {файл прочитано; в останньому рядку може не бути позначення кінця рядка} if NInp>0 then endLine; close(f); close(g); End. КОНТРОЛЬНІ ЗАПИТАННЯ 1. Що таке масив? Як він описується? 2. Яким може бути тип елементів масиву, а яким — індексів ма сиву? 3. Як здійснюється доступ до окремих елементів масиву? 36


8. Ïðèêëàäè ââåäåííÿ ïîñë³äîâíîñòåé äàíèõ

4. Як здійснюється обробка масивів? 5. Які обмеження накладаються на розмірність масиву в Turbo Pascal? 6. Як зберігається багатовимірний масив у пам’яті комп’ютера? 7. Які масиви називають квадратними і які вони мають особливості? 8. Що таке файл і що таке файлова змінна? 9. Що таке файловий вказівник і для чого він використовується? 10. Які різновиди файлів є в Turbo Pascal і чим вони відрізняються? 11. Файлами якого типу є клавіатура та екран? 12. Що таке текст? Чим відрізняється обробка файлів цього типу від інших? 13. Яким є стандартний порядок дій з файловою змінною? 14. Що відбувається при зв’язуванні файлової змінної з файлом? 15. Як можна відкрити текстовий файл? 16. Які наслідки можливі, якщо не закрити файл, відкритий для запису? 17. Як програмується зчитування з файла з невідомою наперед кількістю елементів? ЗАДАЧІ 1. Задано одновимірний числовий масив. Без використання допоміжного масиву: а) значення елементів масиву циклічно зсунути на одну пози% цію ліворуч; b) значення елементів масиву циклічно зсунути на одну по% зицію праворуч; c) значення елементів масиву циклічно зсунути на k позицій ліворуч; d) значення елементів масиву циклічно зсунути на k позицій праворуч; e) реалізувати його дзеркальне перетворення; f) перебудувати масив так, щоб спочатку підряд у тому ж по% рядку було розташовано всі ненульові значення елементів маси% ву, а потім усі нульові; g) за даним значенням x перебудувати масив так, щоб спочат% ку було розташовано всі значення елементів, що менші x, потім усі рівні x, а потім — решту значень. Порядок значень усередині групи можна змінювати. 37


Ìàñèâè òà ôàéëè

2. За лінійним масивом цілих чисел знайти k, при якому зна% чення виразу | A[1] + A[2] + … + A[k] – A[k + 1] – A[k + 2] – … – A[n]) | (модуль різниці сум елементів правої та лівої частини, на які k розбиває масив) є мінімальним. 3. У лінійному масиві цілих чисел знайти найдовшу довжину «пилки» — послідовності чисел, що чергуються за зростанням та спаданням, наприклад 2 5 3 7 4 6 5 9. 4. У лінійному масиві цілих чисел поміняти місцями два фраг% менти масиву від позиції k до позиції m та від позиції p до позиції s. Наприклад, якщо є масив 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 і k = 5, m = 8, p = 13, s = 19, то результатом обміну буде масив 1 2 3 4 13 14 15 16 17 18 19 9 10 11 12 5 6 7 8 20 5. Дано прямокутну матрицю цілих чисел розмірами M на N. В її кожному рядку вилучити елементи, значенням яких є номер їх рядка. Всі наступні елементи зсунути ліворуч, а останні еле% менти рядка заповнити нулями. 6. Дано двовимірний масив цілих чисел розмірами M на N. Для кожного рядка знайти найменший спільний дільник елементів цього рядка. 7. У прямокутному масиві цілих чисел знайти число, що зуст% річається найчастіше. Якщо таких чисел декілька, визначити най% менше з них. 8. У прямокутну матрицю цілих чисел розмірами M на N вста% вити після її k%го рядка перший з рядків, що містять максималь% ний елемент цього масиву. Врахувати, що рядків побільшає. 9. На прямокутному клітчастому полі розмірами N на M на% мальовано прямокутники, що містять по кілька цілих клітин, які не накладаються один на одного і не мають спільних меж. Неза% мальовані клітини поля подано значенням 0, кольори прямокут% ників — цілими числами від 1 до 255. Підрахувати кількість пря% мокутників та знайти площу найбільшого з них. 10. Перевірити, чи становить квадратний масив магічний квад% рат (його заповнено цілими числами від 1 до n2 так, що суми чисел у кожному рядку, кожному стовпчику та в обох діагоналях однакові). 11. Елемент матриці називається сідловою точкою, якщо він одночасно є найменшим у своєму рядку та найбільшим у своєму стовпчику. За матрицею цілих чисел розмірами M на N з’ясува% 38


8. Ïðèêëàäè ââåäåííÿ ïîñë³äîâíîñòåé äàíèõ

ти, чи містить вона сідлові точки, і, якщо так, обчислити індек% си рядка й стовпчика однієї з них. 12. За квадратною числовою матрицею вивести послідовність чисел обходом матриці: а) «змійкою», починаючи по горизонталі з лівого верхнього кута; b) «спіраллю» проти годинникової стрілки з лівого верхнього кута; c) «спіраллю» за годинниковою стрілкою з лівого верхнього кута. 13. Утворити два одновимірні масиви шляхом копіювання в них елементів заданої цілочислової квадратної матриці. В один масив переписати всі елементи вище головної діагоналі матриці, в інший — нижче її. Порядок елементів указано на рисунку.

14. Перетворити задану квадратну матрицю дійсних чисел так, щоб верхній над її бічною діагоналлю трикутник містив її мак% симальне значення, нижній — мінімальне. 15. Створити текстовий файл із таблицею степенів числа 2 від 1 до 30. 16. Підрахувати кількість рядків у текстовому файлі. 17. Дописати до одного текстового файла другий текстовий файл. 18. Прочитати текстовий файл і вивести кількості повторень кожного з символів, які в ньому зустрічаються. 19. Дано два текстові файли. Визначити, чи збігаються посим% вольно відповідні рядки першого та другого файлів. За збігу рядків вивести в рядок третього файла відповідь «OK», інакше вивести символ з рядка першого тексту, яким починається відмінність, та всі подальші символи до кінця рядка. Якщо довжини файлів різні, вивести про це окреме повідомлення. 20. Текстовий файл має рядки довжиною не більше 80. Про% читати та вивести його зміст на екрані «сторінками»: після виве% дення кількох рядків тексту треба запитати, чи продовжувати ви% ведення, і, залежно від відповіді користувача, продовжувати або завершити роботу. 39


Ìàñèâè òà ôàéëè

21. Прочитати натуральне число типу longint та вивести його р%ковий запис (p ≤ 36) у вигляді многочлена зі степенями числа р; піднесення до степеня задати знаком ^. Якщо цифра 0, відповідний степінь числа р виводити не треба, а якщо 1, то ви% вести його без цифри як множник, наприклад: 1407 = 10^3+4*10^2+7*10^0. 22. З текстового файла зчитується послідовність ненульових чисел a1 , a2 , a3 ,..., an , яка закінчується нулем (він у послідовність не входить). Кількість чисел нічим не обмежено. Написати програму, яка: а) визначає найбільший елемент послідовності; b) обчислює середнє арифметичне введених чисел; c) обчислює середнє геометричне введених чисел; d) визначає кількість додатних та від’ємних чисел; e) перев��ряє послідовність на неспадання (незростання); f) обчислює кількість суміжних чисел різного знака; g) підраховує суму a1 ⋅ a2 + a2 ⋅ a3 + ... + an−1 ⋅ an + an ⋅ a1 . 23. У текстовому файлі записано цілі числа; їх кількість нічим не обмежено. Відомо, що тільки одне з них повторюється непарну кількість разів. Знайти це число, зчитавши текстовий файл один раз. 24. Написати програму створення текстового файла з рядка% ми довжиною до одного мільйона символів. Текст має бути вхідним для програми (див. останні приклад розділу). 25. Є два непорожні текстових файли, у кожному рядку яких записано додатне ціле число, причому послідовність чисел не% спадні. Записати в третій текстовий файл неспадну послідовність чисел, яка є результатом злиття двох заданих. Числа вивести по 10 на рядок (останній рядок може бути неповним). Наприклад, при заданих послідовностях (2, 2, 4, 6), (1, 3, 6, 7) утворюється (1, 2, 2, 3, 4, 6, 6, 7). 26. Множину цілих чисел подано текстом, у кожному рядку якого записано цілі числа, упорядковані за зростанням. За дво% ма такими файлами створити третій файл, який також є упоряд% кованим і представляє їх: а) об’єднання; б) перетин; в) різницю. 27. У текстовому файлі записано послідовність цілих чисел; їх кількість нічим не обмежено. Відомо, що одне з них зустрі% чається у послідовності частіше ніж усі інші, разом узяті. Знайти це число. 40


ðîçä³ë

2

ÑÎÐÒÓÂÀÍÍß Ë²Í²ÉÍÈÕ ÌÀÑȲÂ

1. ПОНЯТТЯ СОРТУВАННЯ Загальне значення слова «сортування» — це розподіл елементів на групи за деякою ознакою, наприклад, розподіл яблук по сортах за їх якістю, листів за поштовими індексами, банок із фарбами за кольором тощо. Проте у програмуванні під сортуванням розу% міють упорядкування елементів (за деякою характеристикою), на% приклад, вишикування учнів за зростом на уроці фізкультури, розташування слів у словнику в алфавітному порядку, упорядку% вання точок площини за зростанням координати x тощо. Як бачимо, об’єктами впорядкування можуть бути найрізно% манітніші елементи. Головне, щоб їх можна було порівнювати, тобто існував би закон, який дозволяє довільні два елементи пра% вильно розташувати один відносно іншого («A має стояти після B або перед ним»). Величину, за якою сортуються елементи, називають ключем сор тування. Це може бути число, послідовність символів або складні% ша величина, наприклад, дата, яка містить рік, місяць та день. Як відомо, дані в комп’ютері подано в числовому вигляді, тому програмісти під сортуванням традиційно розуміють упорядкуван% ня за зростанням або спаданням числових величин. Сортування з’явилося у практичній діяльності задовго до по% яви комп’ютерів і є необхідним у багатьох практичних задачах. Мета сортування — прискорити подальший пошук та обробку даних. 41


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

Приклад 1. Якщо нам невідомо, де в словнику розташовано сло% во, що починається тією чи іншою буквою, ми розкриваємо слов% ник приблизно на середині. Якщо слово має бути в словнику далі, шукаємо його тільки в другій половині словника. Тепер ми розк% риваємо словник на середині другої половини. Аналогічно, якщо слово має бути розташовано в першій половині, шукаємо тільки в ній. Щоразу, заглядаючи в словник, ми поділяємо «простір пошу% ку» навпіл, зменшуючи його приблизно вдвічі. Описаний пошук називається бінарним. Він дозволяє нам знайти слово в словнику за кілька секунд завдяки саме тому, що словник упорядковано за алфавітом. Якби цього не було, довелося б перебирати всі слова підряд. А якщо їх сто тисяч, то скільки тривав би цей процес? Отже, упорядкування даних має дуже велике значення. Недар% ма за недовгу історію програмування було створено десятки ал% горитмів сортування та їх численні реалізації. Традиційно розрізняють внутрішнє та зовнішнє сортування. Пер% ше впорядковує масиви в оперативній (внутрішній) пам’яті комп’ю% тера, друге оперує з даними на зовнішніх носіях (файлами), об’єм яких не дозволяє записати їх цілком в оперативну пам’ять. Нижче подано тільки методи внутрішнього сортування. Їх мож% на класифікувати за кількома ознаками. Головною з них є швидкість виконання; важливим є також об’єм додаткової пам’яті. Усі алгоритми сортування поділяють на базові та швидкі. Ба% зові працюють порівняно повільно, проте їх легко записати та на% лагодити. Швидкі, як правило, є громіздкими та складними для розуміння, але сортують набагато швидше, особливо великі ма% сиви даних. Щоб уточнити, що означає «швидкий алгоритм», нам знадобиться поняття складності алгоритму, описане в наступно% му параграфі. Перш ніж розглядати поняття складності та алгоритми сорту% вання, зафіксуємо деякі деталі цих алгоритмів. Вважатимемо, що треба відсортувати за неспаданням лінійний масив цілих чисел з індексами від 1 до N_max, тобто діють такі оголошення. const N_max = 1000; type aInt = array [1..N_Max] of integer; У процедури сортування буде передаватися масив (var A:aInt) та кількість елементів у його початку, що мають сортуватися (n:word). 42


2. Ïîíÿòòÿ ñêëàäíîñò³ àëãîðèòìó

У алгоритмах сортування дуже часто обмінюються значення двох елементів. Будемо вважати, що цей обмін реалізує така до% поміжна процедура. procedure swap(var a, b : integer); var t : integer; begin t := a: a := b; b := t; end;

2. ПОНЯТТЯ СКЛАДНОСТІ АЛГОРИТМУ Нагадаємо: більшість задач у програмуванні є масовими, тоб% то вимога «обчислити…» або «визначити…» стосується деякої множини конкретних вхідних даних. Ці конкретні дані визнача% ють екземпляри задачі. Наприклад, конкретні трійки числових коефіцієнтів визначають екземпляри задачі розв’язання квадрат% ного рівняння. Або конкретний числовий масив визначає екзем% пляр задачі упорядкування значень у масиві за неспаданням. Екземпляри багатьох задач можна охарактеризувати значен% ням деякого числового параметра, який називають розміром ек земпляра задачі. Цим розміром прийнято вважати кількість біт у вхідних даних, якими подано екземпляр. Проте у багатьох за% дачах під розміром екземпляра розуміють деяку величину, пря% мо пропорційну вказаній кількості біт. Найчастіше це кількість скалярних значень, що визначають екземпляр задачі, наприклад, довжина масиву або кількість чисел, які треба ввести. • Отже, найчастіше (хоча й не завжди) екземпляр задачі має розмір n, якщо задається даними, складеними з n скалярних значень. Головна мета наших міркувань — познайомитися з тим, як розмір екземпляра задачі впливає на тривалість виконання того чи іншого алгоритму. Для цього нам знадобиться поняття елементарної дії. Узагальнимо операції (присвоювання, порівняння, додаван% ня, множення тощо) над скалярними значеннями (числами, бай% тами тощо) терміном елементарна дія. Вважатимемо, що час ви% конання будь%якої елементарної дії не залежить від її операндів і самої дії. Тоді час виконання програми буде прямо пропорцій% ним кількості елементарних дій. Якщо не звертати уваги на ко% ефіцієнт цієї пропорційності, то час виконання можна вимірюва ти кількістю елементарних дій. 43


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

• Головну роль у понятті складності алгоритму відіграє не кількість елементарних дій, а характер її зростання при збільшенні розміру екземплярів задачі. Пояснимо це твердження. Нехай A — алгоритм розв’язання де% якої масової задачі. При розв’язанні екземпляра задачі за цим алгоритмом виконується конкретна кількість елементарних дій. Кожному можливому значенню розміру n = 1, 2, 3, … відповідає найбільша кількість елементарних дій по всіх екземплярах цього розміру. Ця відповідність є функцією, яка виражає залежність кількості дій від розміру. • Функція FA (n ) , означена як найбільша кількість елементар% них дій при розв’язанні екземплярів задачі розміру n за алгорит% мом A, називається часовою складністю (складністю) алгоритму A. Аналітичне вираження функції FA (n ) для реальних алгоритмів, як правило, неможливе й не потрібне. Практичне значення має так званий порядок зростання FA (n ) відносно n. Його виражають за допомогою іншої функції, яка має простий аналітичний ви% раз і є оцінкою для FA (n ) . Функція G(n) називається оцінкою згори для функції F(n), якщо існують додатне число C 2 та натуральне m, для яких F (n ) ≤ C2 G(n ) при n > m. Цей зв’язок між функціями позначають як «O»: F(n) = O(G(n)). Функція G(n) називається оцінкою знизу для функції F(n), якщо існують додатне число C1 та натуральне m, для яких C1G(n ) ≤ F (n) при n > m. Цей зв’язок між функціями позначають як « Ω »: F (n ) = Ω (G ( n ) ) . Функція G(n) називається оцінкою для функції F(n), або F(n) є функцією порядку G(n), якщо існують додатні скінченні числа C1 , C 2 та натуральне m, для яких C1G(n ) ≤ F (n) ≤ C2 G( n) при n > m. Для позначення такого зв’язку між функціями вживають знак « Θ »: F (n ) = Θ ( G ( n )) . Інколи користуються такими означеннями: Функція F(n) називається функцією порядку G(n) при великих F ( n) = C , де 0 < C < ∞ . Функція F(n) називається n→∞ G ( n)

n, якщо lim

44


2. Ïîíÿòòÿ ñêëàäíîñò³ àëãîðèòìó

F ( n) = 0. n→∞ G ( n)

функцією порядку менше G(n) за великих n, якщо lim

Цей зв’язок між функціями позначають як «o»: F(n) = o(G(n)). Для оцінювання складності реальних алгоритмів використо% вують логарифмічну, степеневу та експоненціальну функції, а та% кож їх суми, добутки та підстановки. Усі вони монотонно зрос%

( )

тають і просто виражаються. Наприклад, n( n − 1) = Θ n 2 , оскіль% ки 0,5n 2 < n(n − 1) < n 2 при n > 2. Аналогічно неважко переконати% ся, що

( ) ( ) ( )

n3 + 100 n2 = Θ n3 = o n3,1 = o 2 n ,

100log2 n + 10000 =

Θ ( log 2 n ) = Θ ( lg n ) = o( n) . Очевидно також, що будь%яка додатна константа C має оцінки O(1) та Θ (1) . Приклад. Розглянемо задачу обчислення суми n + 1 доданків s = 1/ 0!+ 1/1!+ 1/ 2!+ ... + 1/ n !  . Екземпляр задачі визначається ска% лярним значенням n, яке й приймемо за розмір екземпляра за% дачі. Для обчислення суми треба додати n + 1 число, тому складність додавань має оцінку Θ( n) . Якщо кожен доданок ak = 1 k ! обчислювати безпосередньо за цією формулою, то складність буде Θ( k ) . Тому сумарна по k = 0, 1, 2, ..., n складність буде Θ(0) + Θ(1) + Θ(2) + ... + Θ( n) = Θ(1 + 2 + ... + n) = Θ( n( n − 1)) = Θ( n2 ) . Проте черговий доданок ak можна обчислити за допомогою попереднього ak −1 : ak = ak −1 / k . Для цього незалежно від номера k потрібно поділити та присвоїти, тобто виконати Θ(1) дій. Складність цього алгоритму має оцінку Θ( n) , що є o ( n 2 ) , причо% му за n порядку 1000 другий алгоритм працює приблизно в 1000 разів швидше!

45


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

3. ОБМІННЕ СОРТУВАННЯ Розглянемо алгоритм сортування, який вважається найпрос% тішим. Нехай A[1], A[2], ..., A[n] — елементи масиву, які треба відсортувати за неспаданням. Порівняємо A[1] з A[2] : якщо A[1]>A[2], поміняємо їх значення місцями. Потім порівняє% мо A[2] з A[3] і, якщо треба, поміняємо місцями їх значення. Тоді A [ 3 ] буде мати найбільше значення серед A [ 1 ] , A[2], A[3] . Продовжимо ці порівняння та обміни до кінця — A[n] одержить найбільше значення. Наприклад, послідовність значень <3, 4, 2, 1> перетвориться на <3, 2, 1, 4>. Якщо значен% ня елементів уподібнити розмірам бульбашок, то порівняння й обміни будуть схожі на те, як найбільша бульбашка спливає на% гору, відтісняючи інші. Тому цей метод називається бульбашко вим сортуванням. В аналогічний спосіб перемістимо друге за величиною значен% ня в елемент A[n-1], перетворивши, наприклад, <3, 2, 1, 4> у <2, 1, 3, 4>. Потім третє за величиною значення перемістимо в A[n-2] тощо. На останньому кроці порівняємо тільки A[1] з A[2] і, якщо треба, поміняємо місцями їх значення. Уточнимо бульбашкове сортування процедурою bubbleSort (bubble — бульбашка). procedure bubbleSort(var A:aInt; n:word); var i, k : word; begin for k:=n downto 2 do for i:=1 to k-1 do if A[i]>A[i+1] then swap(A[i], A[i+1]) end; Очевидно, що найбільша кількість елементарних дій прямо пропорційна загальній кількості порівнянь, яких у найгіршому випадку (n–1) + (n–2) + … + 1 = n (n–1)/2. Звідси складність сор% тування n%елементного масиву описаним способом має оцін%

( )

ку Θ n2 . • У задачах до цього розділу запропоновано реалізувати кілька ідей, які для багатьох конкретних масивів відчутно змен% 46


4. Ñîðòóâàííÿ âèáîðîì

шують кількість дій. Проте ці ідеї не дають жодного ефекту, якщо у початковому масиві значення «відсортовано навпаки», тобто розташовано за спаданням, наприклад, <4, 3, 2, 1>.

4. СОРТУВАННЯ ВИБОРОМ Переглянемо елементи масиву від першого до останнього, знайдемо елемент із найбільшим значенням і поміняємо місця% ми це значення зі значенням A[n]. Потім виберемо найменше значення серед A[1], ..., A[n–1] і поміняємо його з A[n–1] тощо. Оскільки після знаходження максимального елемента нам необхідно міняти його місцями з останнім елементом поточної частини масиву, доцільніше шукати не максимальне значення, а його індекс. procedure selectSort(var A:aInt; n:word); var iMax:word; {індекс максимального значення} i, k : word; begin for k:=n downto 2 do begin iMax:=1; for i:=2 to k do if A[i]>A[iMax] then iMax:=i; if iMax<>k then swap(A[k], A[iMax]) end end; Очевидно, що складність цього алгоритму також має оцінку

( )

Θ n2 . У порівнянні з бульбашковим сортуванням він потребує меншої кількості обмінів — не більше n – 1. • Наведений алгоритм має один непомітний недолік, який може бути важливим у деяких реальних задачах. Однакові значен% ня після сортування змінюють свій взаємний порядок, тому цей алгоритм називають нестійким. На відміну від нього, бульбаш% кове сортування упорядковує однакові значення, зберігаючи їхнє взаємне розташування, тому є стійким. 47


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

5. СОРТУВАННЯ ВСТАВКАМИ Алгоритми сортування вставками відрізняються від наведених вище алгоритмів тим, що шукаються не «елементи для місць», а «місця для елементів». З сімейства цих алгоритмів розглянемо ал горитм прямих вставок. У масиві виділяємо дві частини: відсортовану та невідсорто% вану. Спочатку відсортована частина містить тільки перший еле% мент (сам по собі впорядкований). Далі щоразу беремо перше значення з невідсортованої частини та «перетягуємо» його до відсортованої так, щоб вона не втратила впорядкованості. Для цього пересуваємо поточне значення ліворуч доти, доки воно не займе своє правильне місце у відсортованій частині масиву, тоб% то коли значення ліворуч і праворуч поточного стоятимуть «пра% вильно» відносно нього. Отже, елемент «вставляється» у відсор% товану частину масиву (звідки й назва методу). procedure insertSort(var A:aInt; n:word); var i, k : word; begin for k:=2 to n do begin i:=k; while (A[i]<A[i-1])and(i>1) do begin swap(A[i], A[i-1]); dec(i); end; end; end; Оскільки зсув елемента на одну позицію ліворуч «коштує» три команди присвоювання, рекомендуємо запам’ятати еталонний елемент (той, для якого шукається місце у відсортованій частині) у додатковій змінній. Тоді зсув можна виконати одним присво% юванням, а запам’ятований елемент потім одним присвоюванням поставити на звільнене місце. procedure insertSort(var A:aInt; n:word); var i, k : word; etalon : integer; begin for k:=2 to n do begin etalon:=A[k]; i:=k; while (i>1) and (A[i-1]>etalon) do begin A[i]:=A[i-1]; dec(i); 48


5. Ñîðòóâàííÿ âñòàâêàìè

end; A[i]:=etalon; end; end; Складність цього методу також має оцінку Θ( n2 ) . У ньому, як і в сортуванні «бульбашкою», виконується багато обмінів сусідніх елементів. У 1959 році Д.Шелл запропонував удосконалення алгоритму прямих вставок. Його ідея — порівнювати елементи, що знахо% дяться на певній відстані один від одного і покроково зменшува% ти цю відстань. Розглянемо приклад. Нехай масив має такий вигляд: 44 55 12 42 94 18 06 67 Спочатку окремо згрупуємо й упорядкуємо за допомогою пря% мих вставок елементи, які знаходяться на відстані 4 (четверне впорядкування). У цьому прикладі вісім елементів, тому групи містять по два елементи.

44 18 06 42 94 55 12 67 Тепер згрупуємо елементи на відстані 2 (подвійне впорядку% вання). Маємо дві групи по чотири елементи.  

06

18

12

42

44

55

94

67

Нарешті, виконаємо звичайне (одинарне) впорядкування, по% рівнюючи сусідні елементи. 06 12 18 42 44 55 67 94 З описаного зрозуміло, що початкова відстань h = n/2. Після кожного проходу вона зменшується вдвічі. procedure ShellSort(var A:aInt; n:word); var i, j, h : word; temp : integer; begin h := n div 2; while h>0 do begin for i := h to n-h+1 do 49


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

begin j:=i; temp:=A[i]; while (j>=h)and(temp<A[j-h]) do begin A[j]:=A[j-h]; dec(j,h); end; A[j] := temp; end; h := h div 2; end; end; Отже, ідея методу Шелла: змінити масив так, щоб кожна гру% па елементів на відстані h була впорядкованою. Це дозволяє за деякого ряду відстаней порівняння h, остання з яких дорівнює одиниці, отримати упорядкований масив. Але яку послідовність кроків слід обрати? Було винайдено багато рядів відстаней h, які добре зарекомен% дували себе, але найкращої серед них немає. Доведено лише, що кращі результати дають відстані, які не є дільниками одна одної. В основному використовують спадні геометричні прогресії, ос% кільки в цій ситуації кількість кроків близька до log n. Деякі з рядів відстаней наведено у задачах цієї глави.

6. ШВИДКІ АЛГОРИТМИ СОРТУВАННЯ По суті, єдиною перевагою наведених вище алгоритмів є простота й швидкість програмування та наладки. Проте майже всі вони мають оцінку складності Θ( n2 ) і за великих n працюють надто повільно. Нижче подано алгоритми, які мають оцінку складності Θ( n log n) і за великих n працюють значно швидше.1 Проте їх важ% че зрозуміти й налагодити, деяким з них потрібна додаткова па% м’ять великого розміру. Розглянемо такі методи швидкого сортування: • сортування «злиттям»; • швидке сортування; • пірамідальне сортування. 1 Існують навіть алгоритми, які за деяких обмежень на множину можли% вих значень у масиві мають оцінку складності Θ(n ) , але їх вивчення вихо% дить за межі цієї книжки.

50


7. Ñîðòóâàííÿ «çëèòòÿì»

7. СОРТУВАННЯ «ЗЛИТТЯМ» В основі алгоритмів сортування «злиттям» лежить об’єднання двох упорядкованих послідовностей в одну. Це схоже на перебу% дову двох колон учнів, вишикуваних за зростом, в одну. Учні, перші у своїх колонах, порівнюються; вищий стає у нову колону, інший залишається першим у своїй. Після цього в колоні, з якої пішов учень, наступний за зростом учень стає першим. Знову по% рівнюються перші, і так вони діють, доки в одній з колон нікого не залишиться — тоді решта іншої колони перейде у хвіст нової без зміни порядку. Нехай у масиві A з елемента A[L] починається упорядкована ділянка довжиною m - L + 1 , а з елемента A [ m + 1 ] — ділянка довжиною R-m. Під упорядкованою ділянкою (відрізком або се% рією) розуміємо ділянку, яка не є частиною іншої упорядкованої ділянки. Наприклад, довжина таких упорядкованих ділянок до% рівнює m–L+1 = 3 і R–m = 3. 1 L

3

13 m

2 m+1

5

19 R

Вони об’єднуються в таку ділянку довжиною R–L+1 у допом% іжному масиві B. 1 2 3 5 13 19 L m m+1 R Розглянемо процедуру злиття в масив Z пари суміжних діля% нок масиву X, в якому ліва містить індекси від L до m, а права — від m+1 до R. procedure merge(var X,Z:aInt; L,m,R:word); var k:word; {індекс у цільовому масиві} i,j:word; {індекси у половинах} begin i:=L; j:=m+1; for k:=L to R do {заповнення елементів Z[L], ..., Z[R]} if i>m then begin Z[k]:=X[j]; inc(j) end else if j>r then begin Z[k]:=X[i]; inc(i) end 51


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

else if X[i]<X[j] then begin Z[k]:=X[i]; inc(i) end else begin Z[k]:=X[j]; inc(j) end end; Очевидно, тіло циклу виконується R – L + 1 разів, і щоразу виконується Θ (1) елементарних дій. Отже, складність виконан% ня виклику процедури merge є Θ ( R − L + 1) . Алгоритм сортування злиттям полягає в повторенні таких кроків злиття пар. У масиві відбувається пошук пари суміжних упорядкованих ділянок, які об’єднуються в одну ділянку допо% міжного масиву, наприклад, за допомогою процедури merge . Потім відбувається пошук та об’єднується наступна пара тощо. Можливо, наприкінці залишиться ділянка, яка не має пари, — її буде скопійовано без змін. На наступному кроці відбувається подібне злиття пар ділянок допоміжного масиву в основний. Кро% ки повторюються, поки якийсь із масивів не перетвориться на одну упорядковану ділянку. Якщо це допоміжний масив, він ко% піюється в основний. Продемонструємо виконання алгоритму на масиві A = <11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1> довжиною n = 11. Упорядковані послідовності виділено дужками < >, пари діля% нок, які об’єднуються, відокремлені «;», B — ім’я допоміжного масиву. A = <<11><10>; <9><8>; <7><6>; <5><4>; <3><2>; <1>> B = <<10, 11><8, 9>; <6, 7><4, 5>; <2, 3><1>> A = <<8, 9, 10, 11><4, 5, 6, 7>;<1, 2, 3>> B = <<4, 5, 6, 7, 8, 9, 10, 11><1, 2, 3>> A = <1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11> Як бачимо, масив відсортовано за чотири кроки злиття. Після першого кроку злиття довжина упорядкованих ділянок не менше 2 (за винятком, можливо, «хвоста» довжиною 1), після другого — не менше 4 (крім, можливо, «хвоста» меншої довжи% ни) тощо. Отже, після i%го кроку довжина упорядкованих діля% нок, крім, можливо, «хвоста», не менше 2i . Нехай n — кількість елементів масиву. На останньому, k%му, кроці об’єднуються дві ділянки; перша з них має довжину не менше 2k −1 , причому 2k −1 < n . Отже, кількість кроків k < log n + 1 . За рахунок можливо% го додаткового копіювання кількість кроків треба збільшити на 1, 52


7. Ñîðòóâàííÿ «çëèòòÿì»

але оцінка Θ(log n) збережеться. На кожному кроці загальна кількість елементарних дій є Θ ( n ) , тому складність алгоритму — Θ( n ⋅ log n) . Реалізуємо алгоритм сортування злиттям за допомогою про% цедури sortByMrg. Крок злиття ділянок одного масиву в інший оформимо функцією mergeStep. Вона повертає ознаку того, що на кроці злиття було знайдено хоча б одну пару упорядкованих ділянок. Якщо пару не знайдено, то масив, початковий у викли% ку функції, відсортовано. На непарних кроках злиття функція mergeStep виконує злиття ділянок основного масиву A у допо% міжний масив B, на парних — навпаки. Якщо масив, початковий у виклику mergeStep, виявився відсортованим на парному кроці злиття, значить, це масив B, і його треба скопіювати в основний масив A. А якщо на непарному кроці, то це масив A, і більше нічо% го робити не треба. Пару суміжних упорядкованих ділянок n%елементного масиву (перший із них починається індексом l e f t ) шукає функція findPair. Вона повертає ознаку того, що пару знайдено. Праві межі ділянок зберігаються в її параметрах mid і right. Якщо після її виклику справджується умова (left=1)and(right=n), то пару не знайдено, тобто масив відсортовано. Для злиття використовуємо процедуру merge (див. вище). До% поміжна процедура копіювання ділянки масиву в інший масив copyAr є очевидною. procedure copyAr(var X,Y:aInt; left,right:word)); var i:word; begin for i:= left to right do Y[i]:= X[i] end; function findPair(var X:aInt; n,left:word; var mid, right : word): boolean; {функція повертає ознаку того, що пару суміжних упорядкованих ділянок знайдено} begin findPair := false; if left <= n then 53


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

begin mid := left; while (mid<n)and(X[mid]<=X[mid+1]) do inc(mid); {(mid=n) or (X[mid]>X[mid+1])} if mid=n {права межа – кінець масиву} then right:=mid else begin {пошук правої межі} findPair := true; right := mid+1; while(right<n)and(X[right]<=X [right+1]) do inc(right); {(right=n) or (X[right]>X[right+1]} end; end; end; function mergeStep(var X,Y:aInt; n:word): boolean; {функція повертає ознаку того, що злиття масиву X у масив Y відбулося} var left,mid,right:word; begin mergeStep:=true; left:=1; while findPair(X,n,left,mid,right) do begin merge(X,Y,left,mid,right); left:=right+1 end; {останній виклик findPair повернув false} if (left=1)and(right=n) then mergeStep:=false {масив X відсортовано} else if left<=n then copyAr(X,Y,left,n); {копіювання «хвоста»} end; procedure sortByMrg(var A:aInt; n:word); var B:aInt; {допоміжний масив} 54


8. Øâèäêå ñîðòóâàííÿ

step:word; {номер кроку} notSorted:boolean;{ознака невідсортованості} begin step:=0; notSorted:=true; while notSorted do begin inc(step); if odd(step) then notSorted:=mergeStep(A,B,n) else notSorted:=mergeStep(B,A,n) end; {після злиття один з масивів відсортовано} if not odd(step) then copyAr(B,A,1,n) end; • Алгоритм сортування злиттям сортує масиви з великою довжиною значно швидше, ніж базові методи. Проте його голов% ний недолік — йому потрібен додатковий масив розміром n. • Алгоритм сортування злиттям зберігає взаємне розташуван% ня однакових значень, тобто є стійким. • В алгоритмах, заснованих на злитті, елементи послідовності обробляються у порядку їх розташування, тобто, по суті, без пря% мого доступу. Тому саме ці алгоритми використовуються для зов% нішнього сортування.

8. ШВИДКЕ СОРТУВАННЯ Перший алгоритм швидкого сортування 1960 року розробив Ч.А.Р. Хоар (C.A.R.Hoare). Цей алгоритм є одним із найпопу% лярніших, оскільки порівняно нескладний у реалізації, добре працює на різноманітних видах вхідних даних і не вимагає вели% чезних обсягів додаткової пам’яті (окрім відносно невеликого стека). Ідея швидкого сортування така. У деякий спосіб вибирається еталонне значення e. Значення елементів масиву A обмінюються так, що масив розбивається на дві ділянки — ліву та праву. Еле% менти в лівій ділянці мають значення не більше e, а у правій — не менше. Після цього достатньо окремо відсортувати ці дві ділянки. 55


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

Існує простий, але досить ефективний спосіб вибору e в ділянці масиву A[L], …, A[R]: e = A[(L+R) div 2]. Для розбиття використовуються два індекси — «лівий курсор» i та «правий курсор» j. Спочатку i=L, j=R; далі вони рухаються на% зустріч один одному в такий спосіб. Значення менше e («хороші») в лівій частині пропускаються, а на «поганому» рух зупиняється. Аналогічно після цього у правій частині пропускаються значен% ня, що більші e. Якщо i ще не став більше j, то це означає, що обидва вони вказують на «погані» значення. Ці значення обміню% ються місцями, а курсори зсуваються назустріч один одному. Рух курсорів продовжується, поки i не стане більше j. Тоді всі еле% менти від A[L] до A[j] мають значення не більше e, а всі від A[i] до A[R] — не менше. Ці дві ділянки, якщо їх довжина більше 1, поділяються й сортуються рекурсивно. Якщо ж ділянка має довжину 1, то її вже відсортовано. Оформимо сортування частини масиву A[L], …, A[R] такою процедурою: procedure quickSort(var A:aInt; L,R:word); var e:integer; {еталонне значення} i,j:word; {лівий та правий курсори} begin i:=L; j:=R; e:=A[(i+j) div 2]; while i<=j do begin while A[i]<e do inc(i); while A[j]>e do dec(j); if i<=j then begin swap(A[i],A[j]); inc(i); dec(j); end; end; {i > j}; if L<j {сортувати ліву ділянку} then quickSort(A,L,j); if i<R {сортувати праву ділянку} then quickSort(A,i,R); end; 56


8. Øâèäêå ñîðòóâàííÿ

Оцінимо складність наведеної реалізації швидкого сортуван% ня. Нехай масив A має n елементів. У найгіршому випадку в кож% ному виклику процедури quickSort еталонним значенням є найменше в ділянці від A[L] до A[R], і розбиття ділянки маси% ву довжиною m дає ділянки довжиною 1 і m – 1. Оскільки m = n, n–1, ..., 2, глибина рекурсії досягає Θ ( n ) , і при кожному зна% ченні m розбиття ділянки довжиною m має складність Θ ( m) . Тоді сумарна складність має оцінку Θ ( n ) + Θ ( n − 1) + ... + Θ(2) = Θ( n2 ) . Проте описаний найгірший випадок у реальних даних прак% тично ніколи не трапляється. Для переважної більшості даних розбиття ділянки масиву дає дві ділянки з приблизно рівними довжинами. Тому розміри масивів, які сортуються, при переході на наступний рівень рекурсії зменшуються приблизно вдвічі. Отже, в середньому кількість рівнів рекурсії оцінюється як Θ(log n) . Очевидно, що на кожному рівні рекурсії сумарна складність є O(n), тому загальна складність в середньому має оцінку O(n log n) . • Численні практичні дослідження свідчать, що наведений алгоритм в середньому працює швидше, ніж інші алгоритми сор% тування, які мають складність найгіршого випадку O(n log n) . • Швидке сортування не зберігає взаємного порядку однако% вих значень, тобто є нестійким. Для вибору еталонного значення існує багато способів, а не тільки вибір A[(L + R)/2]. Якщо еталонним є одне зі значень у сортованій частині масиву, адже це дозволяє не турбуватися про запобігання ви% ходу за межі сортованої частину й не перевіряти додаткових умов. Вибір «золотої середини» (значення, яке має бути посередині, або медіана) вимагає чималих додаткових зусиль і може взагалі погіршити оцінку складності. Проте досить ефективним є вибір середнього з трьох значень — першого (з індексом L), останньо% го (R) та серединного ((L + R)/2). Для цього перед вибором ета% лону їх можна обміняти місцями у такий спосіб. if A[L] > A[(L+R) div 2] then swap(A[L],A[(L+R) div 2]); if A[(L+R) div 2] > A[R] then swap (A[(L+R) div 2],A[R]); if A[L] > A[(L+R) div 2] then swap(A[L],A[(L+R) div 2]); e := A[(L+R) div 2]; 57


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

Ще два способи вибору еталонного значення наведено у зада% чах до цієї глави. Ітеративна версія алгоритму. Запишемо ітеративну процеду% ру, в якій явно реалізуємо обробку стека, необхідну для виконан% ня рекурсивної процедури. Змоделюємо програмний стек за допомогою масиву Stack, в якому будемо зберігати ліві та праві межі сортованих підмасивів. Спочатку присвоїмо елементам масиву Stack значення 0 за до% помогою стандартної підпрограми fillchar. procedure QuickSort(var A:aInt; n:word); var i, j, L, R, top : word; e : integer; Stack : array[1..N_max,1..2] of word; Begin FillChar(Stack,SizeOf(Stack),0); top:=1; {індекс верхівки стека} Stack[top,1] := 1; Stack[top,2] := N; while top>0 do begin L:=Stack[top,1]; R:=Stack[top,2]; dec(top); e:=A[(L+R) div 2]; i:=L; j:=R; repeat while (A[i]<e) do inc(i); while (A[j]>e) do dec(j); if i<=j then begin Swap(A[i],A[j]); inc(i); dec(j); end; until i>=j; if L<j then begin inc(top); Stack[top,1]:=L; Stack[top,2]:=j; end; if i<R then begin inc(top); Stack[top,1]:=i; Stack[top,2]:=R; 58


9. ϳðàì³äàëüíå ñîðòóâàííÿ

end; End; End;

9. ПІРАМІДАЛЬНЕ СОРТУВАННЯ Описане тут сортування в середньому повільніше, ніж швид% ке, хоча й має складність у найгіршому випадку Θ( n log n) . Проте цей алгоритм застосовується для сортування файлів і розв’язан% ня деяких інших задач. Пірамідальне сортування (або сортування за допомогою купи) використовує бінарне дерево (піраміду або купу). Розглянемо це поняття. Розташуємо елементи масиву з індексами 1.. n рядками, по% 1 двоюючи кількість елементів у рядках: у першому рядку — пер% ший елемент (з індексом 1), у 2 3 другому — з індексами 2 і 3, у третьому — з індексами 4–7, далі — 8–15 тощо. Останній ря% 4 5 6 7 док може залишитися неповним. Наприклад, при n = 10 буде утво% рено піраміду індексів, як на ри% 8 9 10 сунку. Розглянемо піраміду як бінарне дерево з коренем нагорі та лис% тям унизу. Дерево утворено вузлами, що відповідають індексам, і зв’язками між ними — дугами. Коренем дерева є вузол 1. Вузли 2 і 3 назвемо синами вузла 1 (їх батька), вузли 4 і 5 — синами 2 тощо. Вузли, які не мають синів, називаються листками (на рисунку це вузли 6—10. Отже, якщо вузол i має синів, то ними є вузли з індек% сами 2i та 2i + 1, а його батьком є вузол з індексом i div 2. Далі будемо розглядати вузли дерева, які містять значення еле% ментів масиву з відповідними індексами. Для сортування важли% вим є упорядкування значень у масиві, за якого при кожному можливому i справджується нерівність A[i div 2] ≥ A[i], тобто «син не більше батька». Ця властивість не стосується лише кореня де% рева, оскільки він не має батька. Дерево з цією властивістю нази% 59


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

вають правильним, або правильною купою . Розглянемо приклад правильного дерева, яке відповідає послідовності значень <30, 12, 30, 12, 5, 29, 2, 11, 10, 3>. 30 12 30 12 5 29 2 11 10 3 Сортування за допомогою купи використовує те, що корінь правильної купи містить її максимальне значення. Спочатку в ма% сиві утворимо правильну купу, далі поміняємо місцями значен% ня першого елемента масиву (кореня дерева) та останнього. Най% більше значення займе «своє» останнє місце в масиві, і далі про нього можна забути. Серед решти елементів масиву відновимо основну властивість купи та обміняємо місцями значення пер% шого елемента та тепер уже передостаннього, і так діятимемо далі, доки дерево не скоротиться до одного елемента. procedure heapSort(var A:aInt; n:word); var j:word; {индекс останнього} begin build(A,n); {початкова побудова купи} for j:=n downto 2 do begin swap(A[1],A[j]); rebuild(A,1,j-1) {відновлення правильності} end end; Спочатку уточнимо процедуру rebuild відновлення правиль% ності купи. Якщо значення в кореневому вузлі змінилося, основ% на властивість у ньому може не виконуватися, тому за потреби більший з синів обмінюється з батьком, після чого основна вла% стивість перевіряється й відновлюється для сина. Дії з відновлен% ня правильності купи в частині масиву A[f], …, A[d] неважко уточ% нити рекурсивною процедурою. procedure rebuild(var A:aInt; f,d:word); var maxSon:word; {індекс максимального сина} begin maxSon:=f; if (2*f<=d)and(A[f]<A[2*f]) then maxSon := 2*f; if (2*f+1<=d)and(A[maxSon]<A[2*f+1]) 60


9. ϳðàì³äàëüíå ñîðòóâàííÿ

then maxSon:=2*f+1; if maxSon<>f then begin swap(A[f],A[maxSon]); rebuild(A,maxSon,d); end; end; Наведена процедура є прикладом так званої «хвостової ре% курсії», коли після рекурсивного виклику немає жодних дій. У подібних ситуаціях рекурсію можна замінити циклом, умова за% вершення якого відповідає умові «дна рекурсії». Очевидно також, що перебудову дерева можна закінчити, якщо значення макси% мального сина та батька місцями не обмінювалися. У цій ситу% ації, щоб припинити виконання циклу, присвоїмо індексу «бать% ка» f значення за межами частини масиву, що перебудовується. Отже, наведемо нерекурсивний варіант процедури rebuild. procedure rebuild(var A:aInt; f,d:word); var maxSon:word; {індекс максимального сина} begin while (2*f<=d)do begin maxSon:=f; if (A[f]<A[2*f]) then maxSon:=2*f; if (2*f+1<=d)and(A[maxSon]< A[2*f+1]) then maxSon:=2*f+1; if maxSon<>f then begin swap(A[f],A[maxSon]); f:=maxSon {далі перебудова піраміди сина} end else f:=d+1; end; {2*f > d або A[f] >= A[maxSon]} end; Нарешті уточнимо початкову побудову купи за процедурою build . Помітимо, що в масиві з n елементами максимальним індексом вузла%батька є n div 2. Отже, послідовно утворимо пра% вильні купи у піддеревах, коренями яких є вузли n div 2, n div 2 – 1, …, 1. Це дозволить стверджувати, що побудований ма% сив являє собою правильну купу. 61


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

procedure build(var A:aInt; n:word); var i:word; begin for i:=n div 2 downto 1 do rebuild(A,i,n); {перебудова частини масиву} end; Оцінимо складність алгоритму. Очевидно, що вона прямо пропорційна загальній кількості викликів rebuild. При вико% нанні build процедура rebuild викликається n div 2 рази, а при виконанні циклу for процедури treeSort — ще n – 2 рази, тобто загальна кількість викликів процедури rebuild з інших процедур є Θ ( n ) . Оцінимо складність виконання одного виклику процедури rebuild . Помітимо, що в циклі значення змінної f не менш ніж подвоюється, а цикл не буде продовжуватися, якщо це зна% чення стане більше d, яке не більше n. Отже, таких подвоювань не може бути більше ніж log 2 n  . Кількість дій у тілі циклу є кон% стантою, тому загальна складність має оцінку O(n log n) . • Висотою вузла дерева називають кількість ребер у найдов% шому шляху з цієї вершини вниз до листків; висоту кореня нази% вають висотою дерева. Дерево, яке утворено як піраміда з n вузлів, має висоту log 2 n  . КОНТРОЛЬНІ ЗАПИТАННЯ 1. Для чого потрібно сортувати послідовності даних? 2. Дайте словесний опис алгоритмів сортування. 3. Назвіть головний недолік базових алгоритмів сортування. 4. Назвіть переваги й недоліки алгоритмів сортування, наведе них у цьому розділі. 5. Дайте оцінки обсягів додаткової пам’яті, необхідної для сор тування масиву з n елементами за різними алгоритмами сортування. ЗАДАЧІ 1. Якщо в алгоритмі бульбашкового сортування на проході ма% сиву не відбулося жодного обміну, то масив уже відсортовано, і подальші проходи не потрібні. Модифікуйте алгоритм, щоб сор% тування закінчувалося, якщо на деякому проході не було обмінів. 62


9. ϳðàì³äàëüíå ñîðòóâàííÿ

2. В алгоритмі бульбашкового сортування на проході масиву після останнього обміну значень решта масиву є відсортованою. Реалізуйте покращення алгоритму: запам’ятати індекс останнього елемента, що підлягав перестановці на проході, та на наступно% му проході використати цей індекс як праву межу невідсортова% ної частини масиву. 3. Алгоритм бульбашкового сортування вдосконалюється у так званому методі «шейкера», який дає відчутний виграш, коли ба% гато значень у вхідному масиві стоять далеко від своїх правиль% них позицій. Наприклад, при сортуванні за зростанням набору 100 60 25 5 10 5 2 3 очевидно, що великі числа (100, 60) займуть свої місця за два проходи («спливуть» наче бульбаш% ки). Проте маленькі (2, 3) будуть просуватися на свої місця по% вільно, оскільки прохід масивом здійснюється зліва направо. Щоб прискорити просування маленьких чисел до початку маси% ву, реалізуйте на кожному кроці алгоритму два проходи — зліва направо та справа наліво. 4. Сортування вибором допускає модифікацію, аналогічну ме% тоду «шейкера» (див. задачу 3): на проході шукати як максималь% не, так і мінімальне значення, а потім «розкидати» їх на свої місця в кінцях масиву. 5. Алгоритм сортування простими вставками дозволяє ство% рити відсортований масив із значень, що поступово надходять, наприклад, від клавіатури. Перше значення присвоюється пер% шому елементу масиву. Друге значення порівнюється з першим і, якщо воно менше, то «витісняє» перше на друге місце. Іна% кше нове значення йде на друге місце. Потім третє порівнюєть% ся з другим та записується або на третє місце, або витісняє зна% чення з другого місця на третє та порівнюється з тим, що на пер% шому місці. Наприклад, за читання послідовності значень 3, 1, 2 створюються послідовності значень у масиві <3>, <1, 3>, <1, 2, 3>. Узагалі, після читання k – 1 елемента є відсортована частина масиву A[1]A[2]…A[k – 1]. Нове значення v порівнюємо зі зна% ченням A[k – 1]. Якщо A[k – 1]>v, то A[k – 1] зсуваємо на k%е місце. Після цього порівнюємо v з A[k – 1]: якщо A[k – 2]>v, то A[k – 2] зсуваємо на (k – 1)%е місце тощо. Коли за чергового по% рівняння A[i] ≤ v, то v записується на (i + 1)%е місце. Якщо всі 63


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

значення в масиві більше v, то вони зсуваються, а v записується на перше місце. Реалізувати наведений алгоритм. 6. Для модифікації методу Шелла Д. Кнут запропонував таку послідовність кроків: 1 4 13 40 121 364 1093 3280 9841… Починаючи з 1, наступне число утворюється множенням попе% реднього на 3 та додаванням 1. Першим кроком є найбільше з на% ведених чисел, менше за n, другим — попереднє з цього ряду тощо до кроку 1. Ці кроки забезпечують сортування, яке має оцінку складності O( N ⋅ N ). Якщо використати ряд кроків 1 8 23 77 281 1073 4193 16577…, утворений за законом 4 i + 3 · 2 i–1 + 1 при i > 0, то оцінка склад% ності в середньому буде O N ⋅ 3 N . Ще кращою в середньому є послідовність 1 5 19 41 109 209 505 929 2161 3905…, отримана чергуванням послідовностей 4 i+1 – 3 · 2 i+1 + 1 та 9 · (4 i – 2i) + 1 при i ≥ 1. Реалізуйте метод Шелла з указаними рядами кроків. 7. У методі швидкого сортування значення, рівні еталонному, можна зібрати так, щоб ліворуч від них були лише значення, менші за еталонне, а праворуч — більші. Визначивши межі «мен% шої» та «більшої» груп, можна далі сортувати лише їх, не оброб% ляючи більше еталонні значення, які вже зайняли свої місця. За великої кількості повторень еталонного значення таке звуження меж сортування дає відчутний виграш. 8. Оригінальний метод реалізації цієї ідеї запропонували в 1993 році Бентлі (Bentley) та Мак%Ілрой (McIlroy). Еталонні зна% чення накопичуються в кінцях підмасиву (сортованої частини масиву). Коли курсори перетнуться, стануть відомими права межа «меншої» групи та ліва межа «більшої». Тоді еталонні значення з початку підмасиву шляхом обмінів переміщаються в кінець «мен% шої» групи, а з кінця підмасиву — у початок «більшої». Ці пере% сування незначно уповільнюють розбиття, але коли значення елементів масиву часто повторюються, сортування в цілому при% скорюється. 9. Швидке сортування можна прискорити, якщо короткі части% ни масиву сортувати за допомогою нерекурсивних викликів, а іншо%

(

)

64


9. ϳðàì³äàëüíå ñîðòóâàííÿ

го способу, більш ефективного саме для коротких підмасивів. Од% ним з таких методів є метод вставки. Модифікуйте алгоритм швид% кого сортування так, щоб підмасиви, довжина яких менше деякої порогової довжини, сортувалися за методом вставки. Завдяки експе% риментам відомо, що оптимальна порогова довжина підмасивів ле% жить у інтервалі від 5 до 20, залежно від кількості елементів у ма% сиві. Варіант модифікації полягає в тому, що підмасиви з довжи% ною, меншою за порогову, просто ігноруються, а після завершення цього неповного сортування весь масив даних впорядковується най% кращим у цій ситуації методом — прямими вставками. 10. Прочитати n цілих чисел у масив та виконати такі дії: а) одержати всі числа, які входять до масиву по одному разу; b) з’ясувати, чи є в масиві хоча б два однакових значення; c) одержати всі числа, які входять до масиву більше ніж по од% ному разу; d) підрахувати кількість різних значень у масиві; e) одержати масив, який містить усі числа з початкового ма% сиву, але тільки по одному разу; f) з’ясувати, яке число зустрічається в масиві найбільшу кількість разів. Програма повинна мати складність O(n log n). 11. За n точками на дійсній прямій визначити всі пари точок, відстань між якими найменша. Програма повинна мати складність O(n log n). 12. Задано послідовність із n цілих чисел. Визначити номер її k%го за величиною елемента (можливо, різних значень у послідов% ності менше ніж k). 13. Відрізки на прямій задано парами кінців [ ai , bi ], i=1, …, n. Перевірити, чи є відрізком об’єднання відрізків і, якщо це так, обчислити його кінці. Програма повинна мати складність O(n log n). 14. Написати процедуру обчислення N (N ≤ 1000) найменших значень послідовності дійсних чисел, яка вводиться з файла й має необмежену довжину, за умови: а) числа враховуються по одному разу незалежно від кількості їх повторень; b) число враховується стільки разів, скільки зустрічається у файлі (але не більше ніж N); 65


Ñîðòóâàííÿ ë³í³éíèõ ìàñèâ³â

c) кожне число враховується один раз, але ведеться додатко% вий облік повторень. 15. Дано послідовність n натуральних чисел. Знайти мінімаль% не натуральне число, яке не дорівнює жодній з сум чисел цієї по% слідовності (числа включаються до суми без повторень). Наприк% лад, за послідовності 1, 3 цим числом є 2. 16. Дано дві послідовності натуральних чисел a[1], a[2], …, a[n] та b[1], b[2], …, b[n]. Визначити перестановку i1 , i2 , …, in чисел 1, 2, …, n, при якій сума добутків a[1]b[ i1 ] + a[2]b[ i2 ] + … + a[n]b[ in ] мінімальна. 17. Задано n натуральних чисел, які задають точки на прямій. Задано довжину відрізка. Визначити найменшу можливу коорди% нату початку відрізка, за якої відрізок накриває щонайбільше то% чок. Наприклад, за точок 14, 24, 6, 18, 12, 20 та довжини відрізка 10 найменшою координатою початку є 10.

66


ðîçä³ë

3 ÐßÄÊÈ ÒÀ ÌÍÎÆÈÍÈ

1. РЯДКИ Рядок у загальному значенні цього слова — це скінченна по слідовність символів. У мові Turbo Pascal значення%рядки запи% суються за допомогою апострофів, наприклад, ' A B C ' або '12345'. Порожній рядок, тобто послідовність символів довжи% ною 0, позначається ''. Для подання та обробки рядків мова Turbo Pascal надає спеці% альний тип string[n], де n — ціла константа (можливо, іме% нована) не більше 255. Значення типу string[ n] — це по% слідовності символів довжиною від 0 до до n. Змінна типу string[n] являє собою масив символів з індекса% ми від 0 до n. Нехай s — змінна типу string[n]. Елемент s[0] відіграє особливу роль. Значенням L = ord(s[0]) може бути чис% ло від 0 до n — довжина рядка, що є значенням s. Сам рядок%зна% чення представлено елементами з індексами від 1 до L. Наприклад, якщо змінна s має тип string[5], то такі послідовності байтів #3 #5 #0

'A' '1' ?

'B' '2' ?

'C' '3' ?

? '4' ?

? '5' ?

представляють її значення відповідно 'ABC', '12345' та ''. • Елементи масиву%рядка з індексами від L + 1 до n невизна чені; спроба їх використання може призвести до неочікуваних 67


Ðÿäêè òà ìíîæèíè

наслідків. Їх значення варто розглядати як «сміття», що позна% чено в наведених байтах знаком ?. • Довжина рядка зберігається в одному байті й не переви% щує 255. Замість string[255] можна писати string. Найпростішими виразами рядкового типу є ім’я змінної%ряд% ка, рядкова константа (літерал) та вираз типу char. Для рядків означена двомісна операція конкатенації (дописування) +. Ре% зультат утворюється шляхом дописування правого операнда до лівого: значенням '12'+'3' є '123' . • Ціла довжина рядка повертається з виклику функції length із аргументом%рядком: length('123') = 3. Рядковий вираз можна присвоїти рядковій змінній. Символи присвоюються елементам змінної, починаючи з першого. Довжи% на значення виразу стає довжиною значення змінної. Якщо ця довжина більша за максимально можливу довжину n змінної, то елементам змінної присвоюються n перших символів. Приклади. Нехай змінна s має тип string[3] . Після при% своювання s:='12345' її послідовні елементи мають значен% ня #3, '1', '2', '3'; після присвоювання s:='12' вони ма% ють значення #2 , '1', '2', а елемент s[3] невизначений. Не% хай за s='12' виконуються оператори s:=s+'7';s:='9'+s. Тоді s послідовно одержить значення '127' та '912'. Після s:='' маємо: s[0] =#0, а всі інші елементи невизначені. Рядки, як і інші масиви, допускають використання та оброб% ку їх окремих елементів, але тільки в межах значення, поданого масивом. Наприклад, за s типу string[3] і s='12' можна при% своїти s[1] і s[2] символьні значення або використати їх, але s [ 3 ] є невизначеним. Можна також використовувати та змінювати s [ 0 ] . Наприклад, якщо за s = ' ' виконати s[0]:=chr(2), то довжиною значення s стане 2, тобто s[1] і s[2] будуть визначеними. • Явне використання окремих елементів рядків, особливо елемента з індексом 0, не рекомендується. Для рядків означено операції порівняння =, <> , <, <= , >, >= . Рядки рівні, якщо мають однакову довжину і в її межах відповідні символи збігаються; інакше рядки не рівні. Рядки порівнюються відповідно до їх лексикографічного поряд 68


2. Óâåäåííÿ òà âèâåäåííÿ ðÿäê³â

ку, тобто як слова в словнику, тільки символами алфавіту є #0 , #1 , …, #255 , упорядковані за номерами. Точніше, для не по% рожніх рядків s1 < s2, якщо існує i, 1 ≤ i ≤ min{length(s1), length(s2)}, при якому s1[i] < s2[i], а всі відповідні елементи з індексами менше i у рядках рівні. Крім того, s1 < s2, якщо s1 є початком s2. Наприклад, ' A A A ' < ' A Z ' , ' 1 2 ' < ' 9 ' , 'ZZZ'<'Za' , '12'<'123', а порожній рядок '' є найменшим.

2. УВЕДЕННЯ ТА ВИВЕДЕННЯ РЯДКІВ • Ім’я рядкової змінної можна вказати у виклику read або readln , а рядковий вираз — write або writeln . Уведення. Якщо змінна s має рядковий тип, а f є файловою змінною типу text, то при виконанні read(f,s) символи до найближчого кінця рядка або до кінця файла чи його ознаки #26 читаються та присвоюються елементам рядка s; доступним буде кінець рядка #13 (або кінець тексту #26). Сам символ #13 або #26 у рядок%змінну не записується. Якщо символів тексту до кінця рядка або тексту більше, ніж уміщається в s, то s запов% нюється до кінця, і доступним стає перший символ після прочи% таних у s. Якщо перед читанням рядка s за допомогою процеду% ри read доступний кінець рядка, то він залишається доступним, а значенням s стає порожній рядок. • Застосовувати read при введенні рядків небезпечно, особ% ливо в циклі. Якщо після введення рядка доступним символом стане #13 , то подальші спроби читання рядків залишатимуть його доступним, і виконання програми може «зациклитися». Приклад. Нехай змінні s1, s2 , s3 мають тип string[3] , а текст f містить таку послідовність символів.

'1' '2' '3' '4' '5' #13 #10 'a' #26 'c' Тоді після read(f,s1,s2,s3) змінні мають такі значення: s1='123' , s2 ='45', s3 ='' ; доступним залишається сим% вол #13. Подальші спроби вводити з цього тексту в рядкові змінні будуть надавати їм значення '' й залишати доступним сим% вол #13. ‹ Уведення рядків з клавіатури є аналогічним. Щоб виконання виклику read було закінчено, треба натиснути клавішу <Enter>. 69


Ðÿäêè òà ìíîæèíè

Наприклад, якщо в умовах наведеного прикладу на клавіатурі на% брати символи 12345 і натиснути клавішу <Enter>, то змінні отримають ті ж самі значення. • Уведення рядків з клавіатури за допомогою процедури read не рекомендується, оскільки має додаткові «підводні камені» (їх розгляд виходить за межі цієї книжки). Виконання процедури readln аналогічно read, але після за% повнення рядкової змінної або досягнення кінця рядка в тексті частина тексту разом із найближчим кінцем рядка пропускаєть% ся й доступним стає перший символ наступного рядка. В умовах попереднього прикладу після виконання readln(f,s1,s2,s3) змінні матимуть такі самі значення, але доступним буде символ ' a ' . Якби виконувалися виклики r e a d l n ( f , s 1 ) ; readln(f,s2); readln(f,s3) , то s1 мало б значення '123', s2 — 'a', s3 — '', а доступним став би символ #26. • Уведення рядків з текстових файлів та з клавіатури за про% цедурою readln надійніше, ніж за процедурою read. Виведення. Виведення рядкових виразів не має особливостей — символи значення виразу виводяться в текстовий файл або на ек% ран. Наприклад, якщо рядкова змінна s має значення '12' , то при виконанні виклику write(s+'*'+s) виводяться символи 12*12 . Приклад. Розглянемо програму, яка отримує рядки тексту від клавіатури та виводить у текстовий файл. program CreateText; var f:text; {файл-текст} s:string; {рядок для введення з клавіатури} begin writeln('Створення тексту.'); assign(f,'myfile.txt'); rewrite(f); write('Рядок і <Enter>: '); while not eof do begin readln(s); writeln(f,s); writeln('Рядок і <Enter>: '); end; 70


2. Óâåäåííÿ òà âèâåäåííÿ ðÿäê³â

close(f) end. Для закінчення роботи замість введення нового рядка треба натиснути <Ctrl+Z> і <Enter>. • Внаслідок особливостей системи Turbo Pascal довжина рядків, що набираються на клавіатурі, не більше 127. Приклад. Розглянемо «копіювання» тексту за таких умов: ряд% ки тексту мають довжину не більше 255, а порожні рядки або ті, що містять лише пропуски, не копіюються. Власне «копіювання» опишемо процедурою; вхідний і цільо% вий файли%тексти задамо як її параметри. За умовою, рядки тек% сту «уміщуються» в змінних типу string, тому для введення тек% сту використаємо стандартну процедуру readln та змінну%рядок. Обробляючи рядок, визначимо, чи є в ньому хоча б один сим% вол, відмінний від пропуску. Якщо є, рядок копіюється в цільо% вий текст. program copyPress; var f,g : text; fName : string; procedure copyNonEmpty(var f,g:text); var s:string; {поточний рядок тексту} k:byte; {лічильник позицій} empty:boolean; {ознака порожності} begin reset(f); rewrite(g); while not eof(f) do begin readln(f,s); {обробка рядка s} k:=1; empty:=true; while (k<=length(s)) and empty do if s[k] <> ' ' then begin writeln(g,s); empty:=false end else inc(k); 71


Ðÿäêè òà ìíîæèíè

{рядок оброблено} end; close(f); close(g); end; begin write('Вхідний текст: '); readln(fName); assign(f,fName); write('Цільовий текст: '); readln(fName); assign(g,fName); copyNonEmpty(f,g); end. Для перевірки цієї програми достатньо вхідного тексту, в яко% му лише три рядки — порожній, складений кількома пропуска% ми та рядок, що містить, крім пропусків, хоча б один інший сим% вол. У цільовий текст має скопіюватися тільки останній рядок.

3. КІЛЬКА СТАНДАРТНИХ ПІДПРОГРАМ ОБРОБКИ РЯДКІВ • Рядки є єдиним нескалярним типом, значення якого мо жуть повертатися функціями. У системі Турбо Паскаль означено чимало корисних підпрог% рам обробки рядків. Розглянемо шість із них, позначивши іме% нем string будь%який можливий рядковий тип. Функція copy з параметрами s:string; start,count:integer повертає підрядок рядка s, що почи% нається з символа s [ s t a r t ] і має довжину c o u n t . Якщо start+count–1>length(s), то повертається «хвіст» рядка від ≤ 0, то повертається підрядок, s[start] до кінця. Якщо start≤ що починається з s[1]. Наприклад, copy('abcd',2,2)='bc', copy('abcd',2,4)='bcd', copy('abcd',-1,2)='ab'. Функція pos із параметрами subs,s:string повертає най% менший номер того елемента в рядку s, починаючи з якого subs входить у s як підрядок (якщо не входить, повертається 0). На% приклад, pos('bc','abcabc')=2, pos('aa','abca')=0. Процедура d e l e t e з параметрами v a r s : s t r i n g ; start,count:integer знищує count символів, починаючи 72


3. ʳëüêà ñòàíäàðòíèõ ï³äïðîãðàì îáðîáêè ðÿäê³â

з позиції start у рядку s. За умов start = 0 або count = 0 або s t a r t >l e n g t h ( s ) рядок не змінюється. За умови start+count>length(s) із s вилучається підрядок до кінця рядка. Наприклад, якщо s =' a b c d e f ' , то після виклику d e l e t e ( s , 3 , 3 ) значенням s буде ' a b f ' , а після delete(s,3,5) — 'ab'. Процедура i n s e r t з параметрами s u b s : s t r i n g ; var s:string; start:byte вставляє subs як підрядок в s, починаючи з позиції start. Наприклад, якщо s='abf' , то після виклику i n s e r t ( ' c d e ' , s , 3 ) значенням s буде 'abcdef' . Якщо довжина значення s після вставки перевищує максимально можливу, «хвіст» обрізається. Наприклад, якщо s:string[5] має значення ' a b c ' , то після insert('ffff',s,1) її значенням буде 'ffffa'. Процедура str з параметрами v; var s:string перетво% рює значення першого аргументу (вираз цілого або дійсного типу) на рядок s так само, як при виконанні writeln. Зокрема, дійсні значення можна вивести в нормалізованому вигляді. Для цього після виразу через «:» можна вказати ширину поля виведення, а для дійсного виразу — ще кількість дробових цифр. Якщо зобра% ження значення виходить за межі рядка, в ньому залишаються тільки старші цифри. Наприклад, рядок s:string[3] після виклику str(1234,s) одержить значення '123'. Процедура val з параметрами s:string, var v; var ErrCode:integer перетворює зображення числа в рядку s на відповідний числовий тип і присвоює його змінній v. Якщо пе% ретворення можливе, то значенням ErrCode буде 0, інакше — позиція із символом у рядку, починаючи з якого перетворення неможливе. Тип аргументу для параметра v має відповідати змісту рядка s, а зміст рядка повинен задавати число, яке представ% ляється в типі аргументу. Наприклад, за s='1.3' або s='1E2' другий аргумент має бути дійсним, а не цілим. Аналогічно при його типі, наприклад, integer у рядку не може бути значень, що представляють числа більше 32767 або менше -32768. Про% пуски перед константою в рядку ігноруються. • Процедуру val часто використовують для перевірки, чи є деяка послідовність символів числовою константою потрібного типу. 73


Ðÿäêè òà ìíîæèíè

4. ПРИКЛАДИ ВИКОРИСТАННЯ ПІДПРОГРАМ ОБРОБКИ РЯДКІВ Приклад. Прочитати з клавіатури два рядки та визначити всі позиції у першому рядку, починаючи з яких другий входить у нього як підрядок. Наприклад, у рядку 'abababa' підрядок 'aba' починається у позиціях 1, 3 та 5. Нехай s і p — перший та другий рядки. Позицію першого входження поверне виклик pos(p,s). Щоб отримати наступне входження, треба вилучити з рядка s початок з першим симво% лом знайденого входження, і вже до скороченого рядка s засто% сувати pos(p,s). При цьому треба врахувати кількість символів, вилучених з s. Запам’ятаємо цю кількість в змінній deleted. Повторювати пошук і вилучення будемо, поки pos(p,s)>0. program allSubs; var s,p:string; {перший та другий рядки} deleted, {кількість вилучених символів} start:byte; {позиція початку підрядка} begin readln(s); readln(p); deleted:=0; start:=pos(p,s); while start>0 do begin inc(deleted,start); s:=copy(s,start+1,length(s)); write(deleted,’ ‘); start:=pos(p,s); end; end. Приклад. Прочитати з клавіатури три рядки та замінити всі входження другого рядка у перший третім рядком. Врахувати, що входження заміняються від початку рядка й після заміни пошук продовжується з символу, що йде першим за вставленим рядком. Наприклад, у рядку 'ababac' після заміни 'aba' рядком '*' залишається '*bac'. Якби входження замінялися з кінця ряд% ка, то залишилося б 'ab*c' . Нехай s, p, r — задані рядки. Як і у попередньому прикладі, знайдемо входження p в s за допомогою виклику pos(p,s) . 74


4. Ïðèêëàäè âèêîðèñòàííÿ ï³äïðîãðàì îáðîáêè ðÿäê³â

Нехай start — позиція, з якої починається входження, len — довжина рядка p. Починаючи з позиції start в s, знищимо len символів та вставимо r у рядок s, починаючи з позиції start. Продовжимо, якщо після цього pos(p,s)>0. program replaceSubs; var s, p, r : string; {рядки} start, {позиція початку входження} len : byte; {довжина третього рядка} begin readln(s); readln(p); readln(r); len := length(p); start := pos(p,s); while start>0 do begin delete(s,start,len);{вилучимо другий рядок} insert(r,s,start); {та вставимо третій} start := pos(p,s); end; writeln(s); end. Приклад. Прочитати з клавіатури рядок, у якому записано дійсні константи, розділені пропусками в довільній кількості. Деякі з констант можуть бути помилковими. Вивести в двох ок% ремих рядках правильні константи в нормалізованому вигляді та помилкові константи, відокремлені одним пропуском. Наприк% лад, за вхідного рядка 11.E2 mmm 1E2E2 0 виводяться два таких рядки. Right: 1.1000000000E+03 0.0000000000E+00 Wrong: mmm 1E2E2 Щоб виділити константу з рядка, вилучимо пропуски перед нею. Вилучення пропусків на початку рядка оформимо процеду% рою з заголовком delBlanks(var s:string). Після виклику delBlanks символи рядка до першого про% пуска або до кінця рядка утворюють константу або рядок по% рожній. Якщо рядок порожній, константи немає. Інакше визна% чимо позицію першого пропуска за допомогою функції pos . Якщо це 0, символи константи займають весь рядок, інакше його початок. Скопіюємо їх та вилучимо з рядка. Описані дії офор% 75


Ðÿäêè òà ìíîæèíè

мимо функцією, що повертає виділену константу (можливо, по% рожню), з таким заголовком. function getConst(var s:string):string; Для того щоб перевірити, чи є дійсна константа правильною, застосуємо процедуру val. Вона присвоює своєму другому аргу% менту значення типу real і третьому аргументу 0, якщо констан% та правильна. За помилкової ж константи третій аргумент отри% мує ненульове значення, що й є ознакою помилки. За умовою, правильні та помилкові константи треба вивести окремо, тому до закінчення обробки входу будемо дописувати константи до двох рядків right і wrong («правильні» та «по% милкові»). Нормалізовану константу, відповідну числу типу real, отримаємо за допомогою процедури str. program getcons; procedure delBlanks(var s:string); var i, len : byte; {поточний індекс та довжина} begin i:=1; len:=length(s); while (i<=len) and (s[i]=' ') do inc(i); delete(s,1,i-1) end; function getConst(var s:string):string; var p:byte; {поточна позиція} begin delBlanks(s); if s='' then getConst := '' else begin p:=pos(' ',s); {позиція пропуска} if p=0 then begin {пропуску немає} getConst:=s; {скопіювати константу} s:='' {й вилучити її} end else begin {за константою є пропуск} getConst:=copy(s,1,p-1); delete(s,1,p-1) end; 76


5. Ìíîæèíè òà ¿õ ïðåäñòàâëåííÿ

end; end; procedure select(s:string; var r,w:string); var sNum:string ; {рядок для константи} num:real; err:integer; {ознака помилки} begin r:=''; w:=''; while (s<>'') do begin sNum:=getConst(s); if sNum<>'' then begin val(sNum,num,err); if err<>0 then w:=w+' '+sNum else begin str(num,sNum); r:=r+' '+sNum end; end; end; end; var s, right, wrong : string; Begin readln(s); select(s,right,wrong); writeln('Right:', right); writeln('Wrong:', wrong); readln; End.

5. МНОЖИНИ ТА ЇХ ПРЕДСТАВЛЕННЯ Поняття «множина» є одним із первинних у математиці; воно не має точного означення, а лише розуміється як деякий набір або сукупність елементів. У математиці та інших науках множи% ни утворюються з елементів, які мають деяке спільне походження. 77


Ðÿäêè òà ìíîæèíè

Наприклад, можна розглядати множини учнів класу, школи або країни, різні множини чисел, але множини, в яких були б одно% часно й учні, й числа, насправді ніколи не розглядаються. Будь%який елемент може належати або не належати множині. Елементи в множині не повторюються, тобто всі елементи мно жини є попарно різними. Множини вважаються рівними, якщо ут% ворені одними й тими самими елементами. Мова Паскаль надає зручні засоби для представлення та об% робки множин, які складено значеннями перелічуваних типів — символів, цілих чисел (щоправда, не всіх) та типів, оголошених програмістом. Спочатку розглянемо, як множини позначаються у програмі. Константу%множину задають явним переліком її елементів у дуж% ках [ ] . Наприклад, множина чисел {1, 2, 3, 9} має вигляд [1,2,3,9], порожня множина ∅ — []. Якщо множина містить кілька значень, що йдуть у перелічуваному типі поспіль, можна записати їх як діапазон, наприклад, {1, 2, 3, 9} можна задати як [1..3,9] , а множину символів {'a' , 'i', 'j' , 'k' , 'l', 'm', 'n'} — як ['a','i'..'n']. Якщо T — перелічуваний тип, то вираз set of T визначає тип множини (множинний тип). Його значеннями є всі можливі множини значень типу T. Наприклад, значеннями типу set of Boolean є множини булевих значень: [], [false], [true], [false,true] , а значеннями типу set of 'a'..'z' — усі можливі множини, утворені з 26 малих латинських букв (цих мно% жин 226 ). Тип T називається базовим для типу set of T. Коли створювалася мова Паскаль, множини спочатку призна% чалися для подання лише множин символів. Цей тип має 256 еле% ментів, і це обмеження було поширено на всі типи, що можуть бути базовими для множин. • Базовим типом для множини може бути лише перелічува ний тип, кількість елементів у якому не більше 256. У машинній програмі множину представляє бітовий масив (бітове поле), у якому кожен біт відповідає елементу базового типу й своїм значенням 0 вказує, що цей елемент не належить мно% жині, а значенням 1 — що належить. Наприклад, дані типу set of [0..9] — це поля довжиною 10 біт, відповідних чис% лам від 0 до 9; множину [1..3,9] представляє таке бітове поле. 78


6. Îïåðàö³¿ ç ìíîæèíàìè

Число Наявність

0 0

1 1

2 1

3 1

4 0

5 0

6 0

7 0

8 0

9 1

• Номери бітів відповідають номерам елементів базового типу (починаючи з 0), тому елементи множини автоматично впо% рядковано за зростанням. • Насправді множини займають цілу кількість байтів, тому для наведеного прикладу кількість біт збільшується до 16. Змінні множинних типів оголошуються у звичайний спосіб. Тип у заголовку підпрограми задається іменем, а не виразом, тому множинні типи варто оголошувати окремо десь на початку про% грами. Розглянемо приклади. type set_number = set of byte; set_char = set of char; var Numbers : set_number; Symbols : set_char; Змінній множинного типу можна присвоювати значення, од% нотипні зі змінною. Такі значення задаються за допомогою опе% рацій з множинами.

6. ОПЕРАЦІЇ З МНОЖИНАМИ Об’єднанням двох множин називається множина, якій належать елементи хоча б однієї з цих множин. У мові Паскаль операція об’єднання позначається знаком +.

А

А+В

В

Наприклад: ['A','S']+['P','Q'] = ['A','P','Q','S']; [1..10]+[5..15,20..30] = [1..15,20..30]. За допомогою операції об’єднання до множин можна додава% ти нові елементи. Наприклад, якщо змінна А типу set of byte має значення [2,3,5,7], то після виконання присвоювання A:=A+[5,9] множина А матиме значення [2,3,5,7,9]. Перетином двох множин називається множина, складена еле% ментами, які належать одночасно обом цим множинам, тобто є спільними. У мові Паскаль ця операція позначається знаком *. 79


Ðÿäêè òà ìíîæèíè

А В А*В Наприклад, ['C','H']*['R','W'] = [] (спільних еле% ментів немає); [1..5,10..15]*[3..12] = [3..5,10..12]. Різниця двох множин — це множина, складена елементами, які належать першій з цих множин і не належать другій. Цю опера% цію позначають знаком -.

А

А-В

В

Наприклад, ['A','B']-['R','W'] = ['A','B'] (всі еле% менти першої множини не належать другій); [1..5]-[4..9] = [1..3] . Операція визначення, чи належить елемент множині, позна% чається зарезервованим словом in . Її результатом є true, якщо елемент належить множині, інакше false . Наприклад, вираз 13 in [10..20] має значення true, оскільки 13 належить діа% пазону цілих чисел 10..20 ;  вираз 'я' in ['a'..'z'] має значення false, оскільки українська літера «я» не належить мно% жині латинських літер. У мові Паскаль є операції порівняння множин, які мають булів тип. Операція = визначає, чи є дві множини рівними, тобто скла% деними з тих самих елементів. Наприклад, значенням виразу [1..3]=[1,2,3] є true, а виразу [1..3]=[1,3] — false. Операція <> визначає, чи є дві множини нерівними. Наприк% лад, значенням виразу [1..3]<>[1,2,3] є false, а виразу [1..3]<>[1,3] — true. Ще дві операції пов’язані з поняттям включення множин. Ка% жуть, що перша множина включається в другу, або є підмножи ною другої, якщо кожен її елемент належить другій множині. По% 80


7. Ïðèêëàäè âèêîðèñòàííÿ ìíîæèí

мітимо: множини, які включаються одна в одну, є рівними. Операція зі знаком <= визначає, чи включається множина, вказана ліворуч від знака, у множину, вказану праворуч. Наприк% лад, вирази [1,2]<=[1..3] та [1,2]<=[1..2] мають значен% ня true, а вираз [2..3]<=[1..2] — false . Значення вира% зу A<=B, де A та B — множини, залежно від включення множин вказано на рисунку. B

A

True

В

А

False

А В

True

Операція зі знаком >= визначає, чи включається множина, вказана праворуч від знака, у множину, вказану ліворуч. Наприк% лад, вирази [1..3]>=[1..2] та [1,2]>=[1..2] мають зна% чення true, а вираз [2..3]>=[1..2] — false. • Операції об’єднання, перетину, різниці та порівняння за% стосовують лише до однотипних множин.

7. ПРИКЛАДИ ВИКОРИСТАННЯ МНОЖИН Наведемо приклади задач, пов’язаних з обробкою текстів та рядків, у розв’язанні яких природно й зручно користуватися мно% жинами символів. Приклад. Прочитати від клавіатури два слова, складені мали% ми латинськими літерами, та визначити, чи можна утворити друге з них, використовуючи лише букви першого слова. Кількість по% вторень букв у словах не має значення. Наприклад, з букв слова bao можна утворити слова boa та baobab, а слово bay — ні. Утворимо множини літер, з яких складено слова. Тоді зали% шиться тільки визначити, чи включається друга множина у пер% шу. Будемо вважати, що слова можна прочитати в змінні типу string. Утворення множини літер за рядком оформимо проце% дурою LatSet. Щоб додати елемент до множини, подамо його як одноелементну множину та застосуємо об’єднання множин. 81


Ðÿäêè òà ìíîæèíè

program TwoStrings; type Latines = set of 'a'..'z'; procedure LatSet(var S:string; var L:Latines); var i : byte; begin L:=[]; {спочатку множина порожня} for i:=1 to length(S) do L:=L+[A[i]] {додавання літери} end; var S1, S2 : string; L1, L2 : Latines; begin writeln('Уведіть два рядки'); readln(S1); readln(S2); LatSet(S1,L1); LatSet(S2,L2); writeln('З букв слова ', S1); writeln('побудувати слово ', S2); if L2<=L1 then writeln(' можна') else writeln(' не можна'); end. • Функція не може повертати значення структурного типу «множина», тому множина повертається за допомогою парамет% ра%змінної. Приклад. Прочитати текст і визначити, які з латинських букв зустрічаються в ньому, а які — ні. Розглядати малі й великі літе% ри окремо. Прочитаємо текст посимвольно й утворимо множину латинсь% ких літер, які в ньому зустрічаються. Потім двічі переберемо всі символи та першого разу виведемо ті літери, що належать мно% жині, а другого — що не належать. Множини літер подамо в типі CharS = set of char. Мно% жину латинських літер, що зустрічаються в тексті, утворимо шля% хом додавання кожної прочитаної літери до множини, а множи% ну решти літер — як різницю між множинами всіх латинських літер та літер першої множини. Утворення множини латинських літер тексту оформимо про% 82


7. Ïðèêëàäè âèêîðèñòàííÿ ìíîæèí

цедурою textLets . Її додатковим параметром буде множина літер, які взагалі можуть додаватися до утворюваної множини. Перебір символів і друкування тих із них, що належать деякій множині, оформимо процедурою writeSet . Її параметром буде множина символів. program LatSets; type CharS = set of char; procedure textLets(var S:CharS; const GlobSet:CharS); var f : text; c : char; begin assign(f,'latsets.pas'); reset(f); S := []; while not eof(f) do begin read(f,c); if c in GlobSet then S:=S+[c]; end; close(f); end; procedure writeSet(const S:CharS); var c:char; begin for c := #0 to #255 do if c in S then write(c); writeln; end; const Lats:CharS=['A'..'Z','a'..'z']; var Present,Absend:CharS; begin textLets(Present, Lats); Absend := Lats-Present; writeSet(Present); writeSet(Absend); end. При виконанні наведеної програми, якщо входом є її власний текст у файлі 'latsets.pas', буде визначено такі множини присутніх та відсутніх латинських літер. 83


Ðÿäêè òà ìíîæèíè

ACGLPSZabcdefghilmnoprstuvwxyz BDEFHIJKMNOQRTUVWXYjkq Приклад. Знайти всі розв’язки ребуса МУХА + МУХА = СЛОН. Найпростіший спосіб розв’язання — перебрати всі можливі чотирицифрові числа та перевірити їх на відповідність вказано% му ребусу. У цій задачі можливі числа належать діапазону 1023..4987 — мінімальне та максимальне чотирицифрові чис% ла з попарно різними цифрами, які при множенні на 2 дають чо% тирицифровий результат. Для кожного числа та його подвоєння створимо множини цифр; відповідність чисел умові означає, що ці множини мають по чотири цифри, а їх перетин порожній. Для реалізації наведеного алгоритму використаємо тип «мно% жина цифр». type setDigit = set of 0..9; Створення множини цифр чотирицифрового числа опишемо процедурою createSet, у якій цифри виділяються як остачі від цілочислового ділення на 10 та додаються до множини, що є па% раметром%змінною. Кількість елементів у множині цифр обчислимо за допомогою функції nDigits, яка підраховує, скільки з цифр 0, 1, …, 9 на% лежать множині цифр. З використанням наведених підпрограм розв’язання стає оче% видним. program rebus; type setDigit = set of 0..9; procedure createSet(a:longint; var M:setDigit); begin {побудова множини цифр числа a} M:=[]; while a>0 do begin M:=M+[a mod 10]; a:=a div 10; end; end; function nDigits(M:setDigit):byte; var p,i:byte; 84


7. Ïðèêëàäè âèêîðèñòàííÿ ìíîæèí

begin {підрахунок елементів у множині цифр} p:=0; for i:=0 to 9 do if i in M then inc(p); nDigits:=p; end; var M1,M2:setDigit; {множини цифр МУХА,СЛОН} i:word; {перевіряється як МУХА} Begin For i:=1023 to 4987 do begin createSet(i,M1); if nDigit(M1)=4 then begin createSet(2*i,M2); if (nDigit(M2)=4)and(M1*M2=[]) then writeln(i,'+',i,'=',2*i); end; end; End. Приклад. Знайти всі прості числа на проміжку [2..n], де n — деяке натуральне число. Одне з розв’язань — перебрати всі числа від 2 до n та визначи% ти, чи є вони простими, для чого перевірити, чи має число дільник у діапазоні від 2 до квадратного кореня з числа. Якщо число має дільник, його відкидають, інакше воно є простим і його друкують. Проте існує інший метод визначення простих чисел, який має назву «решето Ератосфена». Випишемо всі числа заданого діа% пазону, а потім почнемо викреслювати ті з них, що мають менші за них дільники, відмінні від одиниці. Покажемо це на числах першого десятку. 2 3 4 5 6 7 8 9 10 Перше число 2 є простим. Викреслимо всі числа, кратні 2.

2 3 4 / 56 /78 / 9 1/0 Серед чисел, що залишилися (2 не враховуємо), перше число 3 просте. Викреслимо серед наступних усі, кратні йому.

2 34 /5 \ 6 /78 /\ 9 1/0 85


Ðÿäêè òà ìíîæèíè

Останнім кроком для цього набору чисел викреслимо числа, кратні 5, оскільки це перше зліва невикреслене число. Втім, чис% ло 10 уже викреслено, тому список чисел не зміниться. Він містить числа 2, 3, 5 та 7. За більшого діапазону числа треба викреслювати далі, поки не дійдемо до кінця діапазону. Насправді роботу можна скоротити. Помітимо, що будь%яке складене число m діапазону має простий дільник, не більший за m . Звідси з діапазону [2..n] в якості про% стих чисел, які можуть приводити до викреслювання кратних їм чисел, достатньо брати числа, не більші за n . Окрім того, якщо знайдено нове просте число i, то всі числа вигляду ki, де k < i, уже були викреслені на попередніх кроках. Звідси найменше крат% не, яке слід викреслювати, дорівнює i 2 . Для реалізації алгоритму скористаємося множинами. Спочат% ку розглянемо дуже обмежений діапазон чисел 2..255, а потім розглянемо спосіб його розширення. Утворимо множину чисел Primes, у яку спочатку включимо всі числа від 2 до 255. Потім, знайшовши перше ліворуч невик% реслене число, вилучимо з множини числа, кратні йому. Наступ% ним простим буде невикреслене число, яке є першим у множині праворуч від знайденого. Процес повторюється, доки не буде пе% ревірено всі числа, присутні в множині. program PrimeNumbers; var i, {кандидат у прості числа} multiple:word; {число, кратне простому} Primes:set of byte; begin Primes:=[2..255]; i:=1; while i<=sqrt(255) do begin inc(i); while not(i in Primes) do inc(i); multiple:=i*i; while (multiple<=255) do begin Primes:=Primes-[multiple]; multiple:=multiple+i; 86


7. Ïðèêëàäè âèêîðèñòàííÿ ìíîæèí

end; end; {виведення невикреслених чисел} for i:=2 to 255 do if i in Primes then write(i:5); end. Зверніть увагу, що змінні i та multiple оголошено з типом word , щоб запобігти зацикленню програми при отриманні зна% чення multiple більше 255. Якби multiple мала тип byte , результати були б непередбачуваними. Розширимо діапазон чисел за допомогою масиву множин, індек% сованого, починаючи з 0, кожен елемент якого відповідає за свій діапазон чисел (0..255, 256..511, 512..767 тощо). За цього розпо% ділу чисел по діапазонах число m подано множиною (елементом масиву) з номером m div 256 та її елементом m mod 256. Якщо ого% лосити масив з індексами 0..2000, то він уміститься в 64 K та яв% лятиме собою понад півмільйона чисел (точніше, 511999 чисел, починаючи з 0). program PrimeNumbers; var n, {межа діапазону} i, multiple, {кандидат у прості та кратне} mDiv,mMod:longint; {множина й число в ній} Primes : array[0..2000] of set of byte; begin write('ціле не більше 511999>'); readln(n); for i:=0 to 2000 do Primes[i]:=[2..255]; i:=1; while i<=sqrt(n) do begin inc(i); while not((i mod 256) in Primes[i div 256]) do inc(i); multiple:=i*i; while (multiple<=n) do begin mDiv := multiple div 256; mMod := multiple mod 256; 87


Ðÿäêè òà ìíîæèíè

Primes[mDiv]:= Primes[mDiv]-[mMod]; multiple:=multiple+i; end; end; {виведення невикреслених чисел} for i:=2 to n do if (i mod 256) in Primes[i div 256] then write(i:5); end. Якщо на вхід цієї програми дати максимально можливе зна% чення 511999, то отримаємо доволі довгий перелік простих чи% сел, останнім з яких буде 511997. • Діапазон чисел, серед яких шукаються прості, можна збільшити вдвічі, якщо врахувати, що парні числа, окрім 2, не є простими. Тоді кожен елемент множини відповідає за непарне число, починаючи з 3. Доступ до множин (елементів масиву) та їх елементів задається аналогічно — число m ≥ 3 подано множиною з номером (m div 2 – 1) div 256 та її елементом (m div 2 – 1) mod 256. Звичайно, перед виведенням невикреслених чисел треба окремо вивести 2. КОНТРОЛЬНІ ЗАПИТАННЯ 1. Чому довжину рядків у мові Turbo Pascal обмежено числом 255? 2. Чи можна оголосити тип «множина чисел від 1 до 1000» за допомогою конструктора set? 3. Скількі байтів займають дані типу: а) set  of  0..15 ; б) set of 'a'..'z' ; в) set of byte; г) set of char ? ЗАДАЧІ 1. Що друкується в результаті виконання такої програми? program StrConc; var a:integer; c:char; s:string; begin s:=''; for a:=0 to 2 do begin c:=chr(ord('0')+a); s:=s+c+s; writeln(s); end; end. 88


7. Ïðèêëàäè âèêîðèñòàííÿ ìíîæèí

2.У тексті, зв’язаному з файловою змінною f:text, містять% ся такі символи:

'1' '2' '3' '4' '5' #13 #10 '6' '7' #13 #26 '*' Вказати значення, яких набудуть змінні s1, s2, s3 , s4 типу string[3] після виконання таких операторів: а) readln(f,s1,s2);readln(f,s3);readln(f,s4); b) readln(f,s1);readln(f,s2,s3);readln(f,s4); c) read(f,s1);read(f,s2);readln(f,s3,s4). 3. Перевірити, чи є рядок паліндромом, тобто симетричною послідовністю символів (що читається однаково зліва та справа). 4. Рядок містить латинські літери та пропуски. В алфавітному порядку надрукувати всі літери, які: а) зустрічаються в рядку, та кількості їх входжень; b) зустрічаються в рядку тільки один раз; c) мають найбільшу кількість входжень. 5. Словом вважається непорожня послідовність символів, які не є пропусками. Слова в рядку розділено пропусками в довільній кількості: а) підрахувати кількість слів у рядку; b) знайти всі слова, що стоять на парних місцях; c) знайти найдовше слово в рядку та його довжину; d) знайти всі слова заданої довжини; e) замінити кожну послідовність пропусків одним пропуском (стиснути пропуски); f) вилучити з рядка всі однолітерні слова; g) вилучити з рядка всі слова, які містять не більше двох літер; h) вилучити з рядка всі слова непарної довжини; i) дзеркально відобразити кожне слово; j) підрахувати кількість слів%паліндромів; k) рівномірно доповнити рядок пропусками до фіксованої довжини. Звернути увагу на знаки пунктуації (після крапки, коми тощо повинен бути хоча б один пропуск). 6. Рядок містить запис числа, яке виражає кількість копійок. Побудувати в іншому рядку відповідний словесний запис у грив% нях та копійках. 7. Дано рядок символів. Підрахувати в ньому кількість знаків арифметичних дій: '+', '-', '*', '/' та '^' (піднесення до степеня). 89


Ðÿäêè òà ìíîæèíè

8. Дано рядок, що містить текст та арифметичні вирази типу a ⊕ b , де a та b — довільні числа, а ⊕ — знак додавання, віднімання, множення або ділення. Виписати всі арифметичні вирази та обчислити їх. 9. Дано рядок символів. Знайти (тобто вивести на екран) у ньому всі слова, що починаються та закінчуються голосною (при% голосною) літерою. 10. Дано рядок символів. Знайти у ньому всі цілі числа (числа можуть містити до 200 цифр, яким може передувати знак). 11. Дано два текстових файли. У першому з них кожен рядок містить слово%абревіатуру й через тире його розшифровку (слів не більше 50). У другому файлі є текст. У третій файл вивести цей текст, причому після того, як у тексті перший раз зустрічається якась розшифровка, записати у дужках відповідну абревіатуру, а всі інші такі ж розшифровки замінити їх абревіатурами. Враху% вати, що розшифровку в другому файлі може бути розбито на кілька рядків. 12. Дано два текстових файли. Перший з них містить довіль% ний текст, другий — не більше 30 слів, відокремлених комами. У другому файлі слова утворюють пари: кожне парне (за порядком запису) слово є синонімом записаного перед ним непарного. За% мінити у першому файлі слова синонімами та записати резуль% тат у новий файл. 13. Дано рядок, що містить ім’я файла, яке може мати або не мати повний шлях доступу до файла. Визначити, чи коректне воно з точки зору операційних систем DOS або WINDOWS. 14. У заданому рядку знайти найдовшу послідовність про% пусків та вивести її довжину словесним повідомленням у грама% тично правильній формі, наприклад, «12 пропусків», або «2 про% пуски», або «21 пропуск». 15. В заданому рядку знайти слово, що містить найбільшу кількість різних букв. Якщо їх кілька, вивести всі. 16. Дано рядок символів. Вивести в алфавітному порядку всі латинські літери, що повторюються: а) не менше двох разів; b) не більше двох разів; c) в точності два рази. 17. Отримати всі розв’язки для таких ребусів: а) ТРИ + САМ = ЛОБ; b) ІСК + ІСК = КСІ; c) АВ + ВС + СА = АВС; d) ТОЧКА + КРУГ = КОНУС; 90


7. Ïðèêëàäè âèêîðèñòàííÿ ìíîæèí

e) СОН + СОН = НІЧ; f) ONE + TWO + TWO + TWO + TWO = NINE; g) ЛІНІЯ + ЛІНІЯ = ФІГУРА; h) TEN + TEN + FORTY = SIXTY. 18. У двох рядках тексту записано хімічне рівняння: у першо% му рядку — хімічні елементи й дії, у другому — коефіцієнти при атомах. П��иклади: 2) CaO+H 2 O=Ca(OH) 2 1) 2H 2 +O 2 =2H 2 O Кожному пропуску першого рядка відповідає не пропуск дру% гого, і навпаки. Перевірити, чи правильно розставлено коефіціє% нти при доданках у рівнянні. Додати до цього ж тексту рядок з відповіддю ТАК або НІ . 19. У кожному рядку текстового файла записано правильний арифметичний вираз, який містить круглі дужки, цілі числа та знаки арифметичних дій + та –. Обчислити значення кожного ви% разу.

91


6. Ïðèêëàäè Àðèôìåòèêà îðãàí³çàö³¿ áàãàòîöèôðîâèõ öèêë³â ç ï³ñëÿóìîâîþ ÷èñåë

ðîçä³ë

4

ÀÐÈÔÌÅÒÈÊÀ ÁÀÃÀÒÎÖÈÔÐÎÂÈÕ ×ÈÑÅË

При розв’язуванні деяких задач виникають числа, які не можна подати у стандартних типах мови Паскаль, адже дані цих типів мають невеликий діапазон значень. Наприклад, уже 13! не мож% на подати в типі longint, а дійсні типи, хоча й дозволяють по% дати набагато більші числа, але далеко не всі підряд. Отже, щоб розширити діапазон чисел або підвищити точність їх запису, не% обхідні власні, нестандартні числові типи. Нижче розглянемо представлення цілих чисел зі збільшеною кількістю цифр на основі масивів та реалізуємо операції з цими числами: введення та виведення, порівняння, додавання та віднімання, множення, цілочислове ділення (обчислення част% ки та остачі від ділення, аналогічне операціям div та mod), де% сяткове ділення (обчислення результату як періодичного десят% кового дробу).

1. ПРЕДСТАВЛЕННЯ БАГАТОЦИФРОВИХ ЧИСЕЛ ЗІ ЗНАКОМ Будемо обробляти десяткові числа, в яких може бути до 1000 знаків. Нижче називатимемо ці числа довгими. Подати довге чис% ло можна рядком символів (цифр), якщо кількість цифр не пере% вищує 255, або масивом цілих чисел від 0 до 9 (нижче вони нази% ваються цифрами). Числа в рядковому записі легко вводити й ви% водити, але виконання арифметичних операцій з ними відчутно

92


1. Ïðåäñòàâëåííÿ áàãàòîöèôðîâèõ ÷èñåë ç³ çíàêîì

ускладнюється. З масивом чисел, навпаки, легко виконувати ариф% метичні дії, але введення%виведення ускладнено. Арифметичні дії «важать більше», тому зберігатимемо числа за допомогою масивів цілих чисел, які представляють десяткові цифри. Отже, десяткові цифри числа подамо в масиві типу a r r a y [ 1 . . m a x ] o f s h o r t i n t , де max — максимальна кількість цифр у числі. Звичним є запис, коли молодшу цифру числа розташовують праворуч, тому молодшій цифрі числа відпо% відатиме елемент масиву з індексом max. Якщо число має мен% ше ніж max цифр, скажімо, i цифр, то їх зберігають елементи масиву з індексами від max-i+1 до max, а елементи з індексами від 1 до max-i представляють незначущі нулі. Незначущі нулі не беруть участі у виконанні арифметичних операцій і не виводяться. Щоб не займатися щоразу визначен% ням, де в масиві починаються значущі цифри, зручно зберігати кількість цифр числа в окремій змінній. У нашому представленні для цього достатньо типу word. Нарешті, число може бути додатним або від’ємним, тобто треба ще зберігати його знак «+» або «–» в окремій змінній типу char. Отже, число мають представляти три різнотипні частини — знак, довжина, масив цифр. Числові величини у програмах моде% люються змінними, тому нам потрібен тип даних, значення яко% го мали б кілька різнотипних частин. У мові Паскаль типи, значення яких мають кілька різнотип% них частин, оголошують як типи записів, або структур. Ці типи описують виразом, в якому вказано імена частин (вони назива% ються полями) та їх типів. record ім’я1 : тип1; ім’я2 : тип2; ... ім’яN : типN end; Тип для представлення довгих чисел назвемо Long, поле знака числа назвемо sign, поле довжини числа — len, поле цифр — number. Отже, оголосимо такий тип даних. const max = 1000; type

93


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

Long = record sign : char; len : word; number: array[1..max+1] of shortint; End; За цим описом, у типі L o n g можна подати цілі числа від -99…9 до +99…9 (кількість дев’яток числі дорівнює max). До% датковий (max+1)%й елемент масиву є допоміжним; він буде ви% користовуватися для прискорення деяких операцій із числами. Поле змінної типу запис позначається його іменем, яке запи% сують після імені змінної та крапки. Наприклад, якщо змінна a має тип Long, то a.sign позначає змінну типу char , що збе% рігає знак числа, a.len — змінну типу word, що задає довжину числа, a.number — масив цифр, a.number[i] — елемент цього масиву з індексом i. • Усі поля запису повинні мати різні імена. • У мові Паскаль запис як єдине ціле можна присвоювати. Інших операцій із записами як цілісними даними немає, тому операції з ними необхідно програмувати самостійно. • Дані типу запис не можна повертати з функції, але пара% метри у підпрограмах можуть бути довільними структурами. • У мові Turbo Pascal тип параметра в заголовку підпрограми можна задавати тільки іменем, тому типи записів треба оголошу% вати вище у програмі.

2. УВЕДЕННЯ ТА ВИВЕДЕННЯ Уведення. Реалізуємо порозрядне уведення довгого числа з клавіатури, за яким визначаються поля запису типу Long. Спочатку перевіримо, чи не є перший уведений символ зна% ком «+» або «-» (запис додатного числа може не мати знаку). Якщо першим символом є знак, він заноситься в поле sign; циф% ри починаються з другого символа. Інакше число є додатним (у поле sign записується «+») і вже перший символ є цифрою чис% ла. Масив цифр заповнюється, починаючи з першого елемента, причому цифри (значення від 0 до 9) утворюються за символами за допомогою функції ord. Уведення триває до появи символа кінця рядка #13, який виявляє функція eoln .

94


2. Óâåäåííÿ òà âèâåäåííÿ

Після введення число зсувається й «притискається» до право% го кінця масиву, тобто молодша цифра (кількість одиниць) стає значенням елемента з індексом max. Кількість позицій зсуву виз% начається кількістю цифр числа X.len, яка підраховується при їх уведенні. Після зсуву незначущі розряди з індексами від 1 до max-X.len заповнюються нулями. procedure input(var X:Long); var i:word; {змінна циклу} S:char; {символ, що уводиться} Begin read(S); if (S='-') or (S='+') then begin {перший символ є знаком числа} X.sign:=S; X.len:=0; end else begin {перший символ є цифрою} X.sign:='+'; X.len:=1; X.number[1]:=ord(S)-ord('0'); end; while not eoln do begin {уведення цифр} read(s); inc(X.len); X.number[X.len]:=ord(S)-ord('0'); end; for i:=X.len downto 1 do {зсув цифр} X.number[max-X.len+i]:=X.num[i]; for i:=1 to max-X.len do {незначущі розряди} X.number[i]:=0; End; Виведення. Виведення довгого числа на екран ще простіше: спочатку, якщо число від’ємне, виводиться знак, а потім усі зна% чущі цифри числа (їх кількість є значенням поля len). procedure output(X:Long); var i:word; Begin if X.sign='-' then write('-'); for i:=max-X.len+1 to max do write(X.number[i]); End;

95


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

3. ПОРІВНЯННЯ Реалізуючи порівняння довгих чисел, зручно скористатися до% поміжною функцією, яка порівнює модулі двох довгих чисел (без урахування їх знаків). Отже, спочатку напишемо цю функцію. Нагадаємо: значущі цифри числа притиснуто до правого краю масиву, тому порівняти їх дуже просто, якщо почати ліворуч. Буде% мо рухатися по масивах і пропускати рівні цифри (спочатку, мож% ливо, це незначущі нулі). Зупинитися потрібно на перших зліва не% рівних цифрах. Проте, якщо числа рівні, то пропускання рівних цифр виведе за межі масивів. Щоб уникнути цього, можна під час руху перевіряти, що поточний індекс i в масивах не більше max. while (i<=max)and(A.number[i]=B.number[i]) do inc(i); {рух зліва направо} Замість перевірки умови i<=max, яка дещо уповільнює робо% ту, скористаємося методом так званого бар’єрного елемента. У на% прямку проходу масивами забезпечимо наявність елементів, які обов’язково припиняють виконання циклу. За руху масивами зліва направо ці елементи повинні бути в масивах праворуч, тоб% то у позиції з номером max+1 , і мати будь%які, але обов’язково нерівні значення, наприклад, 1 і 2. Якщо рух масивами зупинився на бар’єрних елементах, то це означає, що числа рівні. Інакше порівняння нерівних цифр виз% начає, яке з чисел більше. Отже, результатів може бути три: -1 — перше число менше, 0 — числа рівні, 1 — перше число більше. function comp_abs(A,B:Long):shortint; var i:word; Begin {бар’єрні елементи} A.number[max+1]:=1; B.number[max+1]:=2; i:=1; while (A.number[i]=B.number[i]) do inc(i); {A.number[i]<>B.number[i]} if i>max then comp_abs:=0 else if A.number[i]<B.number[i] then comp_abs:=-1 else comp_abs:=1; End;

96


4. Äîäàâàííÿ òà â³äí³ìàííÿ

Порівняння чисел з урахуванням знаків. Якщо знаки чисел різні, то меншим є від’ємне. З двох додатних меншим є число, модуль якого менший, а з двох від’ємних — модуль якого більший. Функ% ція порівняння також повертає -1 , 0 або 1, якщо, відповідно, перше число менше, числа рівні або перше більше. function compare(A,B:Long):shortint; Begin if A.sign=B.sign then if A.sign='-' then compare:=-comp_abs(A,B) else compare:=comp_abs(A,B) else if (A.sign='-') then compare:=-1 else compare:=1; End;

4. ДОДАВАННЯ ТА ВІДНІМАННЯ Знову спочатку напишемо допоміжні функції додавання та віднімання модулів довгих чисел (без урахування їх знаків). Вони реалізують алгоритми «у стовпчик», відомі з початкових класів, за якими розряди обробляються справа наліво; при додаванні в сумі двох розрядів враховується перенесення із попереднього роз% ряду, а при відніманні — позика. • У додаванні та відніманні беруть участь лише значущі розряди; на перший погляд, в реалізації варто це врахувати й не обробляти незначущі нулі, але це дещо ускладнює підпрог% рами. Окрім того, бажано присвоювати 0 незначущим розря% дам, щоб гарантувати, що в можливих подальших операціях число представлено правильно. Отже, будемо обробляти зна% чущі й незначущі цифри однаково. Тоді розряди, які в резуль% таті операції стають незначущими, автоматично отримують значення 0. Нам знадобиться також функція обчислення довжи��и числа, тобто кількості його значущих цифр. Щоб обчислити довжину, пропустимо в масиві цифр всі нулі ліворуч, застосувавши згадану вище техніку «бар’єрного елемента». Не забувайте: число нуль має довжину 1! .

97


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

function longLen(a:Long):word; var i:word; begin i:=1; a.number[max+1]:=1; {бар’єрний елемент} while a.number[i]=0 do inc(i); if i<=max then longLen:=max-i+1 else longLen:=1; end; Додавання модулів. При додаванні можливе ненульове перене% сення p зі старшого розряду. Це свідчить про те, що суму чисел не можна подати в типі Long. У цій ситуації в якості найпрості% шої реакції виведемо повідомлення 'Addition overflow' на екран, але продовжимо обчислення, ніби нічого не трапилося. Реальні програми у таких ситуаціях, як правило, хоча й не завж% ди, аварійно завершуються. • При роботі з довгими числами бажано передбачати мож% ливу довжину чисел і резервувати такий розмір представлення чисел, щоб воно вміщувало результати будь%яких можливих опе% рацій з числами, навіть множення. procedure plus_abs(A,B:Long; var Res:Long); var i:word; p:byte; s:shortint; Begin p:=0; for i:=max downto 1 do begin s := A.number[i]+B.number[i]+p; Res.number[i] := s mod 10; p := s div 10; end; if p>0 then writeln('Addition overflow'); {визначення довжини результату} Res.Len:=LongLen(Res); End; Віднімання модулів. Виконується також порозрядно, причому на деякому кроці може виникнути ситуація віднімання від мен% шого розряду більшого і тоді розряд результату буде від’ємним. В цьому випадку необхідно взяти позику — одиницю старшого

98


4. Äîäàâàííÿ òà â³äí³ìàííÿ

розряду, яка у поточному розряді буде десяткою, а потім враху% вати її на наступному кроці. Позику подамо змінною z. procedure minus_abs(A,B:Long; var Res:Long); var i:word; z:byte; Begin z:=0; for i:=max downto 1 do begin Res.number[i]:=A.number[i]-B.number[i]-z; if Res.number[i]<0 then begin inc(Res.number[i],10); z:=1; end else z:=0; end; {визначення довжини результату} Res.Len:=LongLen(Res); End; Віднімання модулів даватиме хибний результат, якщо перший модуль менше другого. Отже, перед викликом наведеної проце% дури треба забезпечити, щоб перший модуль був не менше за дру гий. Додавання довгих чисел з урахуванням знаків. Реалізуємо оче% видні, відомі з курсу математики, міркування. Якщо знаки чисел однакові, необхідно обчислювати суму мо% дулів і надавати результату знак доданків. Якщо знаки різні, то від більшого з модулів чисел необхідно відняти менший і надати результату знак доданка, більшого за модулем. Якщо модулі однакові, то знаком результата буде '+'. procedure plus(A,B:Long; var Res:Long); var s:shortint;{результат порівняння модулів} Begin if A.sign=B.sign then begin plus_abs(A,B,Res); Res.sign:=A.sign; end

99


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

else begin s:=comp_abs(A,B); if s>=0 then begin minus_abs(A,B,Res); if s>0 then Res.sign:=A.sign else Res.sign:='+'; end else begin minus_abs(B,A,Res); Res.sign:=B.sign; end end End; Віднімання довгих чисел з урахуванням знаків. Віднімання — це додавання від’ємника з протилежним знаком, тобто A–B = = A+(–B). procedure minus(A,B:Long; var Res:Long); Begin if B.sign='-' then B.sign:='+' else B.sign:='-'; plus(A,B,Res); End;

5. МНОЖЕННЯ Щоб помножити два числа, треба помножити перше на кожен розряд другого, а потім додати результати порозрядного множен% ня, зсуваючи кожен з наступних результатів ліворуч на один роз% ряд. Щоб реалізувати цей алгоритм, можна написати процедуру, яка множить перше число на цифру другого, зсуває результат на відповідну кількість розрядів (залежно від розряду цифри) та додає отриманий результат до попередньо накопиченої суми. Проте для множення на одну цифру не будемо писати окрему процедуру, а реалізуємо всі необхідні дії разом, враховуючи зсу% ви за рахунок індексування. Помітимо: добуток цифр у останніх розрядах max множників дає цифру в останньому розряді max результату та, можливо, пе% ренос у розряд max-1 . Узагалі, за алгоритмом множення «в сто%

100


5. Ìíîæåííÿ

впчик» та представленням чисел неважко переконатися, що до% буток цифр у розрядах i та j множників додається до значення в розряді i+j-max результату та, можливо, створює перенесення у розряд i+j-max-1 . Отже, спочатку масив цифр результату rez заповнимо нуля% ми. Потім у зовнішньому циклі пройдемо по розрядах j другого множника b (від молодших до старших), у внутрішньому — по розрядах i першого множника a. Щоб не множити на незначущі нулі, врахуємо реальну довжину чисел. Суму добутку i%ї та j%ї цифр, можливого переносу p від попереднього розряду та значен% ня (i+j-max)%го розряду результату збережемо в змінній multi. Тоді multi mod 10 є новим значенням у (i+j-max)%му роз% ряді результату, а multi div 10 — переносом у наступний роз% ряд. Перенос у розряд max є нульовим. Проте при множенні можлива ситуація, коли добуток чисел типу Long має стільки цифр, що його не можна подати в цьому типі. Появу «зайвих» цифр відстежимо за допомогою булевої змінної over . Якщо вона отримала значення true , результат множення обчислено з помилкою, тому видається повідомлення Multipl. overflow. Нарешті, знак добутку визначається дуже просто: за однако% вих знаків множників результат додатний, інакше від’ємний. procedure multiple(a,b:Long; var Res:long); var i, j : word; p, multi : byte; over : boolean; {ознака переповнення} begin for i:=1 to max do Res.number[i]:=0; over:=false; for j:=max downto max-b.len do begin {множення на j-у цифру другого множника} p:=0; for i:=max downto max-a.len do if i-max+j<1 then over:=true else begin multi:=Res.number[i-max+j] + a.number[i]*b.number[j] + p; Res.number[i-max+j]:=multi mod 10;

101


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

p:=multi div 10; if (i-max+j=1) and (p>0) then over:=true; end; end; {визначення довжини та знака результату} Res.len:=LongLen(Res); if a.sign=b.sign then Res.sign:='+' else Res.sign:='-'; if over then writeln('Multipl. overflow'); end;

6. ЦІЛОЧИСЛОВЕ ДІЛЕННЯ Ділення цілих чисел можна виконувати по%різному, а саме, як: • цілочислове ділення з обчисленням частки та остачі, ана% логічне операціям div та mod мови Паскаль; • скорочення дробу; • десяткове ділення з обчисленням періоду десяткового дробу. Розглянемо обчислення цілої частки та остачі від ділення «в сто% впчик». Спочатку частку й остачу покладемо рівними 0. Далі виз% начимо, чи не дорівнює дільник 0. Якщо це так, видамо повідом% лення Division by zero. Інакше в діленому візьмемо таку кількість цифр, яка є в дільнику, й запишемо їх у додаткове довге число — остачу від ділення. Якщо цифр у діленому менше ніж у дільнику, то частка дорівнює 0, а остачею повинно бути ділене. Далі, якщо довжина діленого не менше довжини дільника, повто римо такі дії (кількість повторень — це різниця довжин діленого та дільника плюс 1): 1) поки остача не менше дільника, віднімаємо від неї дільник; кількість віднімань дає нову цифру частки; 2) допишемо отриману цифру до частки як молодшу (для цьо% го значення частки зсунемо ліворуч, звільнивши місце для нової цифри); 3) допишемо до остачі наступну цифру діленого як молодшу (на останньому кроці цього робити не треба, оскільки цифри діленого вже вичерпано).

102


6. Ö³ëî÷èñëîâå ä³ëåííÿ

У поданому алгоритмі є дії, які доцільно реалізувати в допом% іжних підпрограмах: зсув цифр числа вліво, віднімання, по% рівняння з 0. Зсув цифр довгого числа a ліворуч на k розрядів та присвою% вання 0 молодшим розрядам опишемо такою процедурою. procedure shift(var A:long; k:word); var i:word; begin for i:=1 to max-k do A.number[i]:=A.number[i+k]; for i:=max-k+1 to max do A.number[i]:=0; end; Віднімання вже реалізовано процедурою minus_abs (див. параграф 4). Порівняння числа з нулем дає відповідь «так» або «ні», тому ре% алізуємо його функцією, що повертає булеве значення. Для при% пинення циклу пошуку ненульової цифри використовуємо метод бар’єрного елемента і тоді рівність довгого числа нулю буде в тому випадку, коли після закінчення роботи циклу відбудеться вихід за межі масиву, тобто i = max + 1. function comp_0(X:long):boolean; var i:word; begin i:=1; X.number[max+1]:=1; {бар’єрний елемент} while X.number[i]=0 do inc(i); comp_0:=(i=max+1); end; Нам знадобиться також функція обчислення довжини числа (див. вище). Після віднімання діленого від остачі довжина остачі невідома, тому її треба визначити. Окрім того, коли утворено по% чаткову остачу, вона може бути як більше діленого, так і менше. Залежно від цього перша цифра, яка записується в частку, є або не є значущою. Отже, кількість цифр остачі теж невідома, тому її теж треба визначити. Нарешті наведемо процедуру div_mod ділення з остачею. Ділене та дільник подані параметрами%значеннями A і B, частка та остача — параметрами%змінними Quot і Rest (від англійських

103


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

quotient — частка, rest — остача). Додамо лише, що частка є до% датною, якщо знаки діленого та дільника рівні, інакше від’ємною, а знак остачі збігається зі знаком діленого (саме так означено для операцій div і mod мови Turbo Pascal). procedure div_mod(A,B:long; var Quot,Rest:long); var i, p, nd : word; s:byte; begin {Частку та остачу покладемо рівними 0} for i:=1 to max do begin Quot.number[i]:=0; Rest.number[i]:=0; end; if comp_0(B) then writeln('Division by zero') else begin {Обчислення частки та остачі} if A.len<B.len then nd:=A.len else nd:=B.len; {Візьмемо з діленого nd початкових цифр} for i:=1 to nd do Rest.number[max-nd+i]:= A.number[max-A.len+i]; {Якщо ділене коротше дільника, то частка} {дорівнює 0, а остачею повинно бути ділене} if A.len>=B.len then for p:=max-A.len+B.len to max do begin {Обчислюємо цифру частки} s:=0; Rest.len:=longLen(Rest); while comp_abs(Rest,B)>=0 do begin minus_abs(Rest,B,Rest); inc(s); end; {Дописуємо цифру до частки} shift(Quot,1); Quot.number[max]:=s; if p<max then begin {«Зносимо» наступну цифру діленого} shift(Rest,1);

104


7. Ñêîðî÷åííÿ äðîáó

Rest.number[max]:=A.number[p+1]; end; end; Quot.len:=longLen(Quot); Rest.len:=longLen(Rest); end; if A.sign=B.sign then Quot.sign:='+' else Quot.sign:='-'; Rest.sign:=A.sign; end;

7. СКОРОЧЕННЯ ДРОБУ Щоб скоротити дріб, треба поділити чисельник і знаменник на їх найбільший спільний дільник (НСД). Отже, спочатку розг% лянемо процедуру обчислення НСД двох довгих чисел. За модифікованим алгоритмом Евкліда обчислення НСД тре% ба, поки обидва числа не рівні 0, ділити з остачею більше число на менше й заміняти ділене цією остачею. Останній ненульовий дільник і буде шуканим НСД. У наступній процедурі ім’я GCD є скороченням англійських слів greatest common divisor — найбіль% ший спільний дільник. procedure _GCD(A,B:Long; var GCD:Long); var Quot:Long; begin while not comp_0(A) and not comp_0(B) do if comp_abs(A,B)>0 then div_mod(A,B,Quot,A) else div_mod(B,A,Quot,B); if comp_0(A) then GCD:=B else GCD:=A; end; Знайшовши НСД чисельника та знаменника, скоротимо їх на цей НСД і отримаємо результат у вигляді пари довгих чисел, які представляють нескоротний дріб. У процедурі скорочення (reduce) початкові чисельник і знаменник представлено пара% метрами%значеннями A та B, скорочені — параметрами%змінни% ми Num і Den (ці імена є скороченнями від numerator — чисель% ник, denumerator — знаменник). Знаменник скороченого дробу

105


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

вважаємо додатним, а знак чисельника залежить від того, чи рівні знаки початкових чисел. procedure reduce(A,B:Long; var Num,Den:Long); var GCD,Rest:Long; {змінні для НСД та остачі} begin if comp_0(B) then writeln('Zero Denumerator') else begin _GCD(A,B,GCD); div_mod(A,GCD,Num,Rest); div_mod(B,GCD,Den,Rest); end; Den.sign:='+'; if A.sign=B.sign then Num.sign:='+' else Num.sign:='-'; end;

8. ДЕСЯТКОВЕ ДІЛЕННЯ Результатом десяткового ділення A на B є десятковий періо% дичний дріб. Цілу частину дробу знайдемо як частку від цілочис% лового ділення A на B. Остача Rest від цього ділення буде чи% сельником у дробовій частині. Якщо Rest дорівнює 0, дробова частина відсутня. Інакше відразу скоротимо Rest і B на їх НСД. Правильний десятковий дріб має частину до періоду й період у дужках (). Знайдемо частину дробу до періоду на основі тако% го факту: кількість розрядів до початку періоду є максимальною з кількостей множників 2 та 5 у дільнику. Кількості множників знайдемо за допомогою функції nMult , що повертає степінь, з яким число numb (від 2 до 9) входить як множник у довге число A. function nMult(A:Long; numb:byte):word; var i,k:word; S,Rest:Long; begin k:=0; for i:=1 to max do S.number[i]:=0; S.number[max]:=numb; S.sign:='+'; S.len:=1; repeat

106


8. Äåñÿòêîâå ä³ëåííÿ

div_mod(a,s,a,Rest); if comp_0(Rest) then inc(k); until not comp_0(Rest); nMult:=k; end; Цифри дробової частини обчислюються шляхом ділення пра% вильного дробу A/B у стовпчик. Обчислюється добуток 10A та ділиться націло на B. Частка є першою цифрою, а остача визна% чає чисельник правильного дробу зі знаменником B. Подальші множення цих чисельників на 10 та ділення на B дають наступні цифри. Один крок цього алгоритму (множення чисельника на 10, ділення на B та обчислення нового чисельника) реалізуємо фун% кцією, яка повертає цілу частку від ділення (число від 0 до 9). Ім’я entire означає «ціле». function entire(var A,B:Long):byte; var n : byte; begin n:=0; shift(A,1); A.len:=longLen(A); while comp_abs(A,B)>=0 do begin minus_abs(A,B,A); inc(n); end; entire:=n; end; Отже, виведемо дробову частину до періоду за допомогою k викликів функції entire(Rest,B), де k — довжина частини дробу до періоду, обчислена раніше. Остання остача, отримана при виведенні дробової частини до періоду, дасть першу цифру періоду. Запам’ятаємо її та продов% жимо виводити цифри періоду, поки ще раз не отримаємо цю саму остачу — це й буде кінцем періоду. Період візьмемо в круглі дужки. Окремий випадок — якщо остача рівна 0, то період виво% дити не треба. Проте довжина періоду може бути дуже великою, тому за пам’ятовувати цифри дробової частини не будемо. Обмежимо кількість цифр періоду, що виводяться, числом 2000, щоб вони уміщалися на екрані. Якщо після виведення 2000 цифр кінець періоду не досягнуто, виведемо '...' й на цьому закінчимо.

107


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

procedure div_dec(A,B:Long); var GCD_Rest_B, S, Quot, Rest : Long; i, k, k2, k5 : word; lPeriod : integer; begin k:=0; div_mod(A,B,Quot,Rest); {Виведення цілої частини} output(Quot); {Якщо є дробова частина, вона виводиться} if not comp_0(Rest) then begin write(','); _GCD(Rest,B,GCD_Rest_B); div_mod(Rest,GCD_Rest_B,Rest,S); div_mod(B,GCD_Rest_B,B,S); k2:=nMult(B,2); k5:=nMult(B,5); if k2<k5 then k:=k5 else k:=k2; if k<>0 then {Виведення цифр до періоду} for i:=1 to k do write(entire(Rest,B)); {Виведення періоду або його початку} if not comp_0(Rest) then begin write('('); lPeriod := 0; A:=Rest; repeat write(entire(Rest,B)); inc(lPeriod) until(compare(A,Rest)=0) or (lPeriod=2000); if compare(A,Rest)<>0 then write('...'); write(')'); end; end; end;

108


9. Ìîäóëü äëÿ ðîáîòè ç áàãàòîöèôðîâèìè ÷èñëàìè

Якщо викликати цю процедуру з аргументами, які представ% ляють числа 145 і 12, то відповіддю буде 12,08(3). За чисел 12 і 4 відповіддю буде -3 , за чисел 1 і 511997 період почнеть% ся цифрами 00000195, але його кінець не буде досягнуто, тому виведення закінчиться символами 540...).

9. МОДУЛЬ ДЛЯ РОБОТИ З БАГАТОЦИФРОВИМИ ЧИСЛАМИ Підпрограми обробки довгих чисел можуть знадобитися у різних програмах, і кожна така програма має містити чимало з наведених вище підпрограм. Проте цього дублювання коду можна позбутися. Система Turbo Pascal дозволяє зібрати всі засоби (ого% лошення та підпрограми), необхідні, наприклад, для обробки дов% гих чисел, в окрему програмну одиницю — модуль, або бібліоте ку. Якщо є цей «збірник оголошень та підпрограм», то у програмі треба лише вказати на його використання. Модуль у мові Турбо Паскаль має такий загальний вигляд. unit ім’я-модуля ; Інтерфейсний розділ; Розділ реалізації; Розділ ініціалізації end. Слово unit («одиниця») є службовим. Інтерфейсний розділ починається службовим словом Interface і містить оголошення імен, які можуть використовуватися за межами модуля. Замість підпрограм тут записуються тільки їх заголовки. Слово інтерфейс стосовно програми можна розуміти як «зовнішній вигляд її ви% конання», а модуля — як «те, що в ньому видно зовні». Розділ реалізації починається службовим словом I m p l e m e n t a t i o n («втілення», «реалізація») і містить усі підпрограми, вказані в інтерфейсному розділі, та, можливо, деякі інші. Інтерфейсні підпрограми тут можуть мати як повні заголов% ки, так і скорочені — за словом function або procedure за% писується тільки ім’я та крапка з комою. Розділ ініціалізації задає дії, які виконуються один раз на по% чатку виконання програми, що використовує оголошення модуля. Він має такий вигляд: begin послідовність операторів;

109


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

В операторах, як правило, присвоюються початкові значення змінним, оголошеним у модулі. Цей розділ не є обов’язковим. Розглянемо загальний вигляд модуля для роботи з довгими числами (розділу ініціалізації в ньому немає). unit longNums; Interface const max = 1000; {максимальна довжина чисел} type Long = ...; {тип чисел} {заголовки інтерфейсних підпрограм} procedure input(var X:Long); procedure output(X:Long); function comp_abs(A,B:Long):shortint; function compare(A,B:Long):shortint; procedure plus_abs(A,B:Long; var Res:Long); procedure minus_abs(A,B:Long; var Res:Long); procedure plus(A,B:Long; var Res:Long); procedure minus(A,B:Long; var Res:Long); procedure multiple(a,b:Long; var Res:long); procedure div_mod(A,B:long; var Quot,Rest:long); function comp_0(X:long):boolean; procedure reduce(A,B:Long; var Num,Den:Long); procedure _GCD(A,B:Long; var GCD:Long); procedure div_dec(A,B:Long); Implementation {допоміжні підпрограми} procedure shift(var a:long; k:byte); ... end; function longLen(a:long):byte; ... end; function entire(var A,B:Long):byte; ... end; {інтерфейсні підпрограми} procedure input(var X:Long); ... end; function compare; {скорочений заголовок} ... end; ... {решта підпрограм} end.

110


9. Ìîäóëü äëÿ ðîáîòè ç áàãàòîöèôðîâèìè ÷èñëàìè

Використання імен модуля у програмі або в іншому модулі на% зивається використанням модуля. Його вказують відразу після за% головка програми або слова interface у модулі за допомогою такого оголошення. uses ім’я-модуля ; Якщо програма або модуль використовує декілька інших мо% дулів, то їх імена записують через кому. uses ім’я1, ім’я2, ... ; Для прикладу розглянемо програму, яка, використовуючи за% соби модуля longNums , вводить два довгих числа, що задають чисельник і знаменник дробу, та виводить дріб у десятковому представленні. program Dec_Fraction; uses longNums; var A, B : Long; {чисельник і знаменник} begin input(A); input(B); div_dec(A,B); end. Як і Паскаль%програми, Паскаль%модулі записуються у файли з розширенням .pas. Модуль транслюється у файл з розширен% ням .tpu (tpu — скорочення від Turbo Pascal Unit). Компілю% вати програми й інші модулі, що використовують модуль, можна тільки після його трансляції. Наприклад, якщо модуль longNums записано у файл з іме% нем l o n g N u m s . p a s , то при трансляції буде створено файл longNums.tpu . Потім можна транслювати наведену вище про% граму Dec_Fraction. Тоді засоби з модуля longNums.tpu, що використовуються у цій програмі, буде додано до її машинного варіанту. У системах програмування, як правило, стандартні підпрогра% ми та ряд оголошень організуються в декілька модулів. Серед них, як правило, є «головний», який використовується практично всіма програмами. Він містить процедури введення та виведення, ма% тематичні функції та деякі інші підпрограми. Під час трансляції програми цей модуль підключається до неї неявно, тобто без по% силання uses. Використання всіх інших модулів, як написаних програмістом, так і системних, треба задавати явно.

111


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

У системі Турбо Паскаль є вісім стандартних модулів (бібліо% тек). П’ять із них (SYSTEM, DOS, CRT, PRINTER, OVERLAY) зібра% но в бібліотечний файл TURBO.TPL; модуль SYSTEM підключаєть% ся автоматично. Є й інші модулі в окремих TPU%файлах, проте в цій книжці вони не вивчаються. КОНТРОЛЬНІ ЗАПИТАННЯ 1. Що таке запис як тип даних? Яким є формат його опису? 2. Чому виникає необхідність використання «довгих» чисел? 3. Назвіть основні засоби представлення «довгих» чисел. 4. Складіть словесний опис порозрядного додавання, віднімання, множення, ділення та порівняння цілих чисел. 5. Що таке модуль (бібліотека)? Як його оформити мовою Turbo Pascal? 6. Що описується в розділі інтерфейсу модуля? 7. Що повинен містити розділ реалізації модуля? 8. У розділі реалізації модуля можна вказувати неповні заголов ки деяких підпрограм. Яких саме й чому? 9. Навіщо в модулі потрібен розділ ініціалізації? ЗАДАЧІ 1. Реалізувати модуль longNums . Написати програми, які, використовуючи ��одуль, вводять два довгих числа, застосовують до них одну з арифметичних операцій та виводять результат. 2. Модифікувати процедуру shift зсуву числа (див. пара% граф 6), щоб поле len числа змінювалося належним чином. Вра% хувати, що зсув може призвести до недопустимого збільшення довжини. 3.На вхід програми від клавіатури в окремих рядках подають% ся довгі цілі числа та знаки операцій +, -, *, / і % між числами (/ і % позначають div і mod). Результат застосування операції ви% водиться на екран і стає першим операндом наступної операції, якщо її буде задано. Ознакою закінчення є натискання <Ctrl+Z> замість наступного знака операції. Наприклад, після уведення 1 + 2 (в окремих рядках) виводиться 3, а після подальшого вве% дення * -4 виводиться -12 . 4. Написати процедуру піднесення довгого числа до степеня з показником типу byte .

112


9. Ìîäóëü äëÿ ðîáîòè ç áàãàòîöèôðîâèìè ÷èñëàìè

5. Написати процедуру, яка обчислює значення факторіала як довге число. Визначити найбільше число, факторіал якого мож% на представити у типі довгих чисел, який використано. 6. Написати процедуру, яка обчислює число Фібоначчі за його номером (як довге число). Визначити найбільший номер числа Фібоначчі, яке можна представити в типі Long . 7. Обчислити наближення до числа e. Скористатися формулою 1 1 + + ...  , накопичуючи доданки у вигляді дробу з довги% 1! 2! ми цілими чисельником і знаменником. 8. Обчислити наближення до числа π . Скористатися форму% e = 1+

π 1 1 1 1 = 1 − + − + −…, накопичуючи доданки у вигляді дро% 4 3 5 7 9 бу з довгими цілими чисельником і знаменником. 9. Кожна вершина трикутника має цілу додатну вагу. Середи% на кожної сторони трикутника стає вершиною другого трикутни% ка, а її вага дорівнює сумі ваг вершин, прилеглих до сторони. Ана% логічно утворюється третій трикутник, четвертий тощо. Вагою трикутника є сума ваг його вершин. Обчислити вагу n%го трикут% ника. На вхід подається номер n (n ≤ 10000) та три ваги вершин першого трикутника — цілі числа типу longint. Наприклад, за номером 3 й вагами 3 2 4 обчислюється 36. 10. Задано числа A і B типу Long. Знайти запис A у системі числення з основою B. Цифри запису видати в такому вигляді: або це десяткова цифра, або записаний у квадратних дужках де% сятковий запис числа, що має не менше двох цифр. Наприклад, за чисел 123 і 21 вихід має вигляд 5[18]. 11. Увести довге ціле число та вивести його розклад на прості множники. Між множниками вивести знак *. Якщо множник у розкладі має степінь більше 1, вивести показник степеня після множника та знака ^. Наприклад, за входу 12 виходом має бути 2^2*3 . 12. Обчислити кількість діагоналей опуклого n%кутника; n є числом типу Long , у якому не більше 10 цифр. 13. За числом n, у якому не більше 10 цифр, обчислити кількість пар діагоналей опуклого n%кутника, що перетинаються. лою

113


Àðèôìåòèêà áàãàòîöèôðîâèõ ÷èñåë

Наприклад, у чотирикутнику є одна пара діагоналей, у п’ятикут% нику їх п’ять, у шестикутнику — 15. 14. Шляхові робочі мають плитки для тротуарів 1× 1 та 1 × 2. Скількома різними способами вони можуть замостити доріжку розміром 2× n? Плитки 1 × 2 зроблено так, що вони лягають уз% довж доріжки тільки широкою стороною. Врахувати, що n ≤ 103 . 15. Чорнобильські орли мають довільну кількість голів. Ко% ефіцієнт інтелекту (IQ) орла рівний кількості його голів. Орли об’єднуються в групи. IQ групи дорівнює добутку IQ орлів групи. Наприклад, у групи з трьох орлів, у яких 5, 3 та 2 голови, IQ дор% івнює 30. За сумарною кількістю n голів орлів у групі визначити її максимально можливий IQ (n ≤ 3000). Наприклад, 5 голів дають IQ 6, 6 голів — 9.

114


ДОДАТОК ОКРЕМІ МОЖЛИВОСТІ СЕРЕДОВИЩА TURBO PASCAL ФАЙЛИ СЕРЕДОВИЩА TURBO PASCAL Систему програмування Turbo Pascal (далі називатимемо її сис% темою) встановлюють, як правило, у каталозі, ім’я якого TP, BP, PASCAL або схоже на них. Залежно від конкретної установки у цьо% му каталозі чи його підкаталозі \BIN ви знайдете файл TURBO.EXE (або BP.EXE). Ця програма є діалоговою системою програмування Turbo Pascal (відповідно, Borland Pascal, яка реалізує ту саму мову програмування) і містить текстовий редактор, транслятор, компо% нувальник, налагоджувач та завантажувач. Для роботи з нею необхідні файли TURBO.TPL та TURBO.HLP — головна бібліотека стандарт% них підпрограм та довідкова інформація про мову й систему про% грамування. Потрібний також файл конфігурації системи TURBO.TP, в якому зберігаються параметри її настроювання (пов’язані з вико% ристанням оперативної пам’яті, кольорами на екрані тощо). ОСНОВИ РОБОТИ В СЕРЕДОВИЩІ Система Турбо Паскаль забезпечує роботу з кількома прямокут% ними областями екрана — вікнами, меню та полями. Вікна містять тексти програмних одиниць, результати їх ком% піляції, інтерпретації та виконання, а також довідкові повідомлення й поля для уточнення команд. У будь%якому стані системи одне з вікон є активним; його виділено подвійною рамкою, решта — неак% тивні (в одинарній рамці). Після запуску системи активним є вікно редактора, відмічене іменем NONAME00.PAS (щоправда, можливе й інше). Для роботи з вікнами використовують клавіатуру або «мишу». Якщо в системі вікрито кілька вікон, їх можна активізувати по черзі за до% помогою <F6>. Ви можете також перемістити курсор миші на відкри% ту частину неактивного вікна й натиснути ліву кнопку. Вікно ство% рюють за допомогою <F3>, знищують — <Alt+F3>. Вікно може показувати не весь текст, з яким працює користу% вач, а лише його частину. Сам текст зберігається системою в ок% ремій частині оперативної пам’яті — буфері.

115


Äîäàòîê

Меню містять позначення інших меню (підменю) або команд, які система може виконати. Ці позначення називаються кодовими словами або опціями. Символи «…» після опції команди означа% ють, що команду буде виконано не відразу, а після подальших дій користувача. Праворуч від деяких опцій вказано функціональні клавіші чи сполучення клавіш, наприклад, <F3> або <Alt+X>, — за їх допомогою задають команду, не використовуючи систему меню. Потрібну опцію в меню виділяють за допомогою клавіш%стрілок, а вибирають, натискаючи <Enter> або ліву кнопку миші. Якщо пра% воруч від опції вказано «…», то при її виборі розгорнеться діалого% ве вікно з кількома полями, а якщо трикутний значок — додатко% ве меню. Поля в діалогових вікнах дозволяють задавати параметри коман% ди, наприклад, імена файлів або каталогів. При відкритті діалого% вого вікна одне з полів є активним; його виділено кольором. Для переходів між полями використовують клавішу <Tab>, а всередині поля — клавішу переміщення курсору. Діалогове вікно закривають за допомогою клавіші <Esc> (із утратою всіх установлених полів) або <Enter> (з виконанням команди або установок, заданих у по% лях). ПЕРШИЙ СЕАНС РОБОТИ Запустимо програму TURBO.EXE (BP.EXE) й побачимо її «го% ловний екран». Верхній рядок — це головне меню режимів роботи з системою (пункти меню задано словами File, Edit, Search тощо). Нижче розташовано велике порожнє поле в рамці — вікно тексто вого редактора. Нагорі записано NONAME00.PAS — це ім’я файлу, з яким ми будемо працювати (поточного файла). Цей файл буде за% писуватися у поточному каталозі, яким спочатку є каталог із систе% мою. Знизу розміщено ще один рядок («підказка»), в якому пере% лічено функціональні клавіші та їх функції (<F1> — Help, <F2> — Save, тобто «врятувати», тощо). Початок роботи з програмою. Відразу слід змінити поточний ката% лог, щоб ніяких файлів не додавати до каталогу із системою. Натис% немо функціональну клавішу <F10> — у головному меню буде підсвіче% но один із пунктів. За допомогою клавіш%стрілок (праворуч на клаві% атурі) або миші виберемо пункт File і натиснемо <Enter>. З’явить% ся вертикальне меню роботи з файлами (пункти New, Save, Open

116


Îêðåì³ ìîæëèâîñò³ ñåðåäîâèùà Turbo Pascal

тощо). За допомогою клавіш%стрілок виберемо пункт Change Dir (змінити каталог) і натиснемо <Enter>. У вікні, що з’явиться, вкаже% мо шлях до потрібного каталогу і натиснемо <Enter>. Поточний ка% талог установлено. Тепер задамо ім’я поточного файла. Знову виберемо пункт го% ловного меню File (файл) і в ньому пункт Save As (записати як …). У вікні, що з’явиться, вкажемо ім’я файла з розширенням .pas або без розширення (тоді система встановить його як .pas) і на% тиснемо <Enter>. Побачимо головне вікно редактора, де замість NONAME00.PAS ми задали ім’я. Якщо при роботі з меню ви зробили якусь помилку, натискан% ня на Esc (escape — утекти) поверне до вікна редактора. Набирання програми. Наберемо у вікні редактора текст програми. program First; begin writeln('Hello!'); end. Курсор у вікні редактора своєю формою вказує на режим робо% ти — вставку чи заміну. При вставці курсор є підкресленням, при заміні — прямокутником. Режими переключаються клавішами <Ins> або <Insert> праворуч. При натисканні <Enter> (у режимі вставки) курсор буде переведено до нового рядка, а до тексту додано неви% димий символ, що задає кінець рядка. Для виправлення помилок використовують клавіші <BackSpace> та <Delete>; <Ctrl+Y> задає видалення рядка, в якому перебуває курсор. Запис програми на диск. Набравши програму, запишемо її за до% помогою клавіші <F2> у поточний pas%файл поточного каталогу. Якби програма була більшою, цей запис слід було б повторювати, поступово додаючи текст у файлі. Компіляція та виконання програми. Для компіляції натисніть <F9>, для компіляції та виконання — <Ctrl+F9>. Якщо помилок при на% биранні не було, то у першому випадку з’явиться повідомлення п��о успішну компіляцію, а в другому щось «блимне й зникне». Натис% немо комбінацію клавіш <Alt+F5> й побачимо чорне вікно виве дення програми з рядком Hello!. Далі будь%яка клавіша поверне до вікна редактора. Можна змусити програму чекати, поки ми не натиснемо <Enter>, додавши перед рядком зі словом end рядок із словом readln.

117


Äîäàòîê

Помилки у програмі. Набираючи програму, можна помилитися. Тоді за спроби компіляції програми на її початку з’явиться напис, що починається словом Error, а курсор перебуватиме десь непо% далік від місця помилки (у дійсності він може бути як вище, так і нижче за текстом). Натисніть будь%яку клавішу, і це поверне вікну редактора звичний вигляд. Закінчення роботи з програмою. Закрити вікно редактора — <Alt+F3>. Закінчити роботу із системою — <Alt+X>. Поновлення роботи. Якщо треба починати роботу з новою програ% мою, можна відкрити нове вікно за допомогою <F3> (одночасно можна відкрити до десяти вікон). Для роботи з існуючою програмою в го% ловному меню оберемо пункт File, а в ньому — пункт Open (відкрити). У вікні, що відкриється, виберіть потрібний файл з програмою. Вікно виведення. Щоб постійно бачити вікно виведення, в го% ловному меню оберіть пункт Debug (наладка), а у вертикальному меню — пункт Output (виведення). Перехід між цими вікнами (і не лише цими) задається клавішею <F6>. При цьому активне вікно виділяється подвійною рамкою. Зміна розмірів вікон. Розміри вікон можна змінити та розташу% вати на екрані так, щоб їх було видно одночасно. Натиснемо <Ctrl+F5> та перейдемо до режиму зміни параметрів активного вікна. Його рамка дещо змінюється, клавіші%стрілки дозволяють зсувати вікно по екрану, ті ж стрілки при натиснутій <Shift> — змінювати форму вікна, <Enter> — закінчити зміни та вийти з цього режиму, а <Esc> — відмовитися від змін. У режимі зміни ці клавіші вказуються в нижньому рядку%підказці. Кожне вікно можна збільшити, щоб воно займало весь екран. Для цього натисніть <F5>. Та ж клавіша поверне вікно до попе% реднього розміру. Вікно налаштування. Наберемо програму з прикладу про темпе% ратуру (див. параграф 11 розділу 3 першої частини посібника). Пе% рейдемо до вікна редактора з текстом програми та натиснемо <Ctrl+F7> — з’явиться рамка з написом Add Watch (додати еле% мент перегляду), і у віконці рамки наберемо ім’я tСels, яке є у програмі. Ці дії повторимо з іменем tKelv. За допомогою F6 пе% реключимося на нове вікно з написом Watches і побачимо там (після виконання нашої програми) слова tCels і tKelv, а поруч з ними уведене число та суму його з 273.

118


Îêðåì³ ìîæëèâîñò³ ñåðåäîâèùà Turbo Pascal

Нарешті, змусимо систему показати покрокове виконання програ% ми. Натиснемо <Ctrl+F2> (програма буде виконуватися спочатку), а потім кілька разів натиснемо <F7> або <F8> (яку саме, поки що бай% дуже) й подивимося на зміни, що відбуваються у вікнах редактора, виведення та наладки. Зокрема, у вікні редактора виділяється кольо% ром рядок програми, який виконується на відповідному кроці, а у вікні наладки поруч із словами tCels і tKelv спочатку з’являються нулі, а потім уведене число та його сума з 273. • Покрокове виконання програми — це, по суті, її інтерпре тація. ГОЛОВНЕ МЕНЮ ТА ДЕЯКІ ЙОГО ПІДМЕНЮ Вхід у головне меню — <F10>, вихід — <Esc>. Розглянемо деякі з можливостей меню та підменю, які можуть виявитися корисни% ми на початку використання середовища. Підменю: File, Edit, Search, Run, Compile, Debug, Tools, Options, Window, Help. Вони позначають меню, які містять команди та підменю. МЕНЮ FILE (РОБОТА З ФАЙЛАМИ) New — створити нове вікно редактора. Open... (праворуч <F3>) — відкрити вікно редактора для робо% ти з існуючим файлом. Каталог і файл у ньому вказують у діалого% вому вікні. Save (<F2>) — зберегти буфер активного вікна редактора у файлі, з яким зв’язано вікно. Save as... — зберегти буфер активного вікна редактора у файлі, який буде вказано далі. Save All — зберегти буфери всіх активних вікон у файлах, по% в’язаних із вікнами. Change Dir... — установити поточний каталог. Print — надрукувати буфер активного вікна. Exit (<Alt+X>) — закінчити роботу із середовищем Турбо Паскаль. МЕНЮ EDIT (РОБОТА З РЕДАКТОРОМ) Undo (<Alt+BkSp>) — скасувати останню команду редагування. Redo — скасувати останню команду Undo. Cut (<Shift+Del>) — перемістити виділений текст з вікна ре% дактора до буфера обміну.

119


Äîäàòîê

Copy (<Ctrl+Ins>) — скопіювати виділений текст в буфер обміну. Paste (<Shift+Ins>) — уставити вміст буфера обміну у вікно ре% дактора. Clear (<Ctrl+Del>) — вилучити виділений текст. Show clipboard — відкрити вікно буфера обміну й зробити його активним. ÃÂÌ˛  Search  (ÔÓ ÛÍ  ≥  Á‡Ï≥̇  ÚÂÍÒÚÛ) Find... — знайти в тексті рядок, який задають далі в діалого% вому вікні. Replace... — знайти й замінити рядок. Search again — повторити останню операцію пошуку або за% міни. Go to line number... — перемістити курсор у вказаний ря% док. МЕНЮ RUN (ВИКОНАННЯ ТА ІНТЕРПРЕТАЦІЯ) Run (<Ctrl+F9>) — виконати програму в активному вікні. Step over (<F8>) — виконати оператори в наступному рядку тексту (підпрограми виконуються як єдине ціле, тобто за один крок інтерпретації). Trace into (<F7>) — виконати оператори в наступному ряд% ку тексту, включаючи оператори в підпрограмах. Go to cursor (<F4>) — виконати частину програми до ряд% ка з курсором. Program reset (<Ctrl+F2>) — завершити інтерпретацію та звільнити пам’ять, яку займає програма. МЕНЮ COMPILE (КОМПІЛЯЦІЯ ТА КОМПОНУВАННЯ ПРОГРАМ) Compile (<Alt+F9>) — компілювати програму в активному вікні. Destination (Memory або Disk) — указати, куди записуєть% ся відкомпільована програма чи модуль. Якщо праворуч вказано слово Memory (пам’ять), код завантажується й виконується, а якщо Disk — записується в exe%файл. Вибір цієї опції змінює напрямок коду. Information... — вивести інформацію про програму (розмір коду, даних, програмного стека, купи тощо).

120


Îêðåì³ ìîæëèâîñò³ ñåðåäîâèùà Turbo Pascal

МЕНЮ DEBUG (НАЛАДКА ПРОГРАМ) Call Stack (<Ctrl+F3>) — вивести список викликаних підпрограм (програму вказано внизу, активну підпрограму — згори). Watch — вивести вікно стану пам’яті зі значеннями змінних і виразів, указаних за допомогою опції Add watch (див. нижче). Output — зробити активним вікно виведення програми. User screen (<Alt+F5>) — зробити активним вікно виведен% ня програми, розгорнувши його на весь екран. Evaluate/Modify... (<Ctrl+F4>) — обчислити значення ви% разу або задати значення змінної. Цю опцію використовують, якщо програма перебуває в режимі наладки. Діалогове вікно містить три поля (ліворуч) і чотири кнопки (праворуч). У полі Expression можна ввести будь%який вираз, зокрема, ім’я змінної. Після натис% кання кнопки Evaluate або <Enter> у полі Result з’явиться зна% чення виразу при поточному стані пам’яті програми; якщо хоча б одне ім’я виразу не означено, ви побачите повідомлення Unknown identifier (невідомий ідентифікатор). Якщо в першому полі введено ім’я скалярної змінної, її зна% чення можна змінити, увівши його в поле New value і натиснув% ши <Enter>. При виклику цієї опції середовище аналізує текст, який указа% но курсором. Якщо це ім’я змінної або константи, воно з’являєть% ся у вікні Expression автоматично. Add watch... (<Ctrl+F7>) — додати змінні або вирази, зна% чення яких показуються у вікні стану пам’яті (опція Watch). Ці значення називаються контрольними. МЕНЮ OPTIONS (НАСТРОЮВАННЯ ІНТЕГРОВАНОГО СЕРЕДОВИЩА) Compiler... — установити режими роботи компілятора, які вико% ристовуються за узгодженням. Діалогове вікно має 17 полів — вони вказують режими, які вмикаються або вимикаються за допомогою клавіші пропуску або одиночного щиголя мишею. Докладну інфор% мацію про режими можна одержати за допомогою довідкової сис% теми середовища (при виділеному режимі натиснути <F1>). Memory sizes... — задати розмір програмного стека (не більше 65535 байт), мінімальний та максимальний розмір купи (не більше 655360 байт).

121


Äîäàòîê

Environment — установити режими роботи інтегрованого се% редовища, редактора й миші. Для цього використовують п’ять діа% логових вікон. У вікні Preferences установлюють, зокрема, кількість рядків текстового екрана (25, 43 або 50) та режими автоматичного збері% гання інформації перед запуском програми. Корисна опція збері% гання файлів Editor files, особливо при виконанні програми, яка може «зависнути». У вікні Editor установлюють режими роботи редактора сис% теми (перейменування попередньої версії вхідного файла в BAK% файл, вставка або заміна символів при введенні, виділення опера% торів відступами, використання символів і клавіші табуляції тощо). За допомогою вікна Mouse указують спосіб використання пра% вої кнопки миші, а вікна Startup — особливості роботи з графіч% ною інформацією, системною бібліотекою SYSTEM.TPU та опера% тивною пам’яттю редактора. Вікно Colors дозволяє встановити колірну палітру окремих елементів середовища (полів, меню й вікон). Open... — указати ім’я файла конфігурації, в якому зберігають% ся настроювання середовища. Save — зберегти поточне настроювання середовища у файлі кон% фігурації (за узгодженням це файл Turbo.tp у каталозі з систе% мою Turbo Pascal — файлом Turbo.exe). Save as... — указати каталог і файл, в якому буде збережено поточне настроювання. МЕНЮ WINDOW (КЕРУВАННЯ ВІКНАМИ) Tile — розташувати вікна так, щоб вони не накладалися й мали приблизно однакові розміри. Cascade — розташувати вікна так, щоб було видно рамки кож% ного з них (це зручно при використанні миші). Close all — закрити всі вікна, відкриті в середовищі. Refresh display — поновити екран (вилучити сліди виве% дення програми). Size/Move (<Ctrl+F5>) — змінити розташування й розмір а��% тивного вікна. Zoom (<F5>) — розгорнути активне вікно на весь екран. При по% вторному натисканні розміри вікна відновлюються. Next (<F6>) — активізувати наступне вікно.

122


Äåÿê³ ñëóæáîâ³ ñëîâà ìîâè Turbo Pascal

Previous (<Shift+F6>) — активізувати попереднє вікно. Close (<Alt+F3>) — закрити активне вікно. List... (<Alt+0>) — вивести список усіх відкритих вікон сере% довища. МЕНЮ HELP (ДІАЛОГОВА ДОВІДКОВА СИСТЕМА) У повідомленнях довідкової служби всі посилання на терміни, для яких існує окрема довідкова інформація, виділено кольором. Для одержання цієї інформації до посилання можна підвести кур% сор і натиснути <Enter> або клацнути на ньому лівою кнопкою миші. Contents — відобразити зміст довідкової системи. Index (<Shift+F1>) — вивести повний алфавітний список усіх посилань довідкової системи. Topic search (<Ctrl+F1>) — видати довідку з терміну, відміче% ного курсором (ключове слово, ім’я системної підпрограми тощо). Previous topic (<Alt+F1>) — вивести попереднє довідкове повідомлення. Compiler directives — видати довідку про директиви ком% пілятора. Reserved words — видати довідку про зарезервовані слова. Standard units — видати довідку про стандартні модулі. Turbo Pascal language — видати довідку про основні еле% менти мови (з подальшим їх уточненням). Error messages — видати довідку щодо повідомлень про по% милки (компіляції та часу виконання). About... — вивести інформацію про авторські права та версію середовища Турбо Паскаль.

ДЕЯКІ СЛУЖБОВІ СЛОВА МОВИ TURBO PASCAL Зарезервовані слова як імена не оголошуються. Стандартні ди% рективи та стандартні ідентифікатори оголошувати можна, але не ре% комендується.

123


Äîäàòîê

Зарезервовані слова and exports asm file array for begin function case goto const if constructor implementation destructor in div inherited do inline downto interface else label end library

mod nil not object of or packed procedure program record repeat set shl

shr string then to type unit until uses var while with xor

Деякі стандартні ідентифікатори Модулі системи Турбо Паскаль: Crt, Dos, Graph, Overlay, Printer, System, Graph3, Turbo3. Константи: false, true, maxInt, maxLongInt. Типи: boolean, byte, char, comp, double, extended, integer, longint, real, shortint, single, text, word. Файли: input, output. Деякі функції: abs, addr, arctan, chr, copy, cos, eof, eoln, exp, int, ln, length, odd, ord, pi, sqr, pos, pred, random, round, sin, sqrt, succ, trunc. Деякі процедури: assign, break, close, continue, dec, delete, dispose, erase, exit, freemem, getmem, halt, inc, insert, new, randomize, read, readln, release, reset, rewrite, seek, str, val, write, writeln.

ДИРЕКТИВИ КОМПІЛЯТОРА ТURBO PASCAL Список директив записують у дужках {} через кому, наприклад, {$A+,$P-,$R+}. Існує три види директив: перемикачі, умовні та параметричні. Директивиперемикачі впливають на режими, які вказано в діало% говому вікні OPTIONS/COMPILER. Їх задають однією буквою та зна% ком + або – (увімкнення відповідного режиму компіляції або його вимкнення). Деякі директиви%перемикачі є локальними; їх можна записати в будь%якому місці тексту програми (модуля), і вони діють 124


Äèðåêòèâè êîìï³ëÿòîðà Òurbo Pascal

до його кінця або до появи директив, протилежних їм. Глобальні ди% рективи записуються на початку тексту й діють на весь текст. Деякі директиви%перемикачі наведено в таблиці. Ліворуч записа% но стан директив за узгодженням. Знаком * відмічено локальні ди% рективи. У дужках вказано дію протилежної директиви (зі знаком +, якщо за узгодженням діє -, і навпаки). Директива Дія директиви {$A+} Вирівнювати дані на межу слова (байта). {$B-}* Застосовувати «ледачі» обчислення булевих виразів (обчислювати їх повністю) {$D+} Використовувати (не використовувати) вбудований налагоджувач. {$E+} Увімкнути (вимкнути) режим програмної емуляції співпроцесора 80x87 (якщо співпроцесор відсутній). {$F-} Використовувати ближню (дальню) модель виклику підпрограм. {$G-}* При генерації коду не використовувати (використо% вувати) повний набір інструкцій процесора 80286. {$I+}* Увімкнути (вимкнути) автоматичний контроль опе% рацій введення%виведення. {$L+} Включати (не включати) локальні символи в інфор% мацію для налагоджувача. {$N-} Операції з нефіксованою крапкою реалізувати про% грамно (використовувати числовий співпроцессор, завдяки якому будуть доступні додаткові дійсні типи). {$Q-} Заборонити (дозволити) перевірку цілочисельних операцій на переповнення. {$R-}* Заборонити (дозволити) контроль меж діапазонів. {$S+}* Генерувати (не генерувати) спеціальний код на по% чатку кожної підпрограми, який перевіряє можливе переповнення програмного стека. {$T-} Результат операції @ обробляти як нетипізований (ти% пізований) вказівник. {$V+}* Увімкнути (вимкнути) контроль довжини рядкових аргументів у викликах підпрограм. {$X+} Використовувати (не використовувати) розширений син% таксис, який дозволяє записувати виклики функцій як оператори. В умовних директивах використовуються умовні символи — імена, на які не може бути посилань у програмі. У директиві {$DEFINE 125


Äîäàòîê

умовний символ} задають установку умовного символу (він буде істинним), а в {$UNDEF умовний символ} — скасування установ% ки. У директиві вигляду {$IFDEF умовний символ} перевіряєть% ся, чи встановлено символ. Якщо встановлено, то весь фрагмент про% грами між цією директивою та найближчою директивою {$ELSE} або {$ENDIF} буде компілюватися, а якщо не встановлено — він не компілюється. Якщо нижче записано директиву {$ELSE}, то при невстановленому символі фрагмент програми між нею та {$ENDIF} буде компілюватися, а при встановленому — не буде. Директива {$IFNDEF умовний символ}, навпаки, задає компі% ляцію наступного за нею фрагмента, якщо символ не встановлено. Якщо нижче записано {$ELSE}, то при встановленому символі буде компілюватися фрагмент програми між нею та {$ENDIF}. Умовні директиви можна використовувати разом із операторами наладки, наприклад, у такий спосіб. {$IFDEF Debug} оператори наладки {$ENDIF} Якщо за допомогою директиви {$DEFINE} встановити умовний символ Debug, то оператори наладки буде відкомпільовано й вико% нано. По закінченні наладки установку Debug можна зняти й ще раз відкомпілювати програму — операторів наладки в машинній програмі не буде. Розглянемо також дві параметричні директиви. Глобальна дирек% тива вигляду {$M стек,низ,верх} задає розмір програмного стека в байтах, а також нижню та верхню межу адрес динамічної пам’яті, наприклад, {$M 16384,0,655360}. Між буквою M та першою кон% стантою записують хоча б один пропуск. Директива вигляду {$I ім'я файла} задає включення файла. Файл має містити фрагмент тексту Паскаль%програми. Якщо в дирек% тиві не задано розширення файла, то шукається файл із цим іменем і розширенням .pas. За цією директивою компілятор «перемикається» на компіляцію тексту у файлі, а по його закінченні продовжує компі% ляцію тексту після цієї директиви.

КОДУВАННЯ СИМВОЛІВ У першій таблиці представлено відповідність між номерами від 0 до 127 та символами, зафіксовану в стандарті ASCII (American Standard 126


Êîäóâàííÿ ñèìâîë³â

Code for Information Interchange — Американський стандартний код для обміну інформацією). Символи з номерами від 0 до 31 мають спеціальне призначення, яке в цій книжці не описано. Замість їх зображення вказано мнемонічне позначення. Символ з номером 32 — пропуск. Основна таблиця ASCII 0 NUL  1 SOX  2 STX  3 ETX  4 EQT  5 ENQ  6 BEL  7 ACK  8 BS  9 TAB  10 LF  11 VT  12 FF  13 CR  14 SO  15 SI 

16 DLE  17 DC1  18 DC2  19 DC3  20 DC4  21 NAC  22 SYN  23 ETB  24 CAN  25 EM  26 SUB  27 ESC  28 FS  29 GS  30 RS  31 US 

32    33 !  34 "  35 #  36 $  37 %  38 &  39 ë  40 (  41 )  42 *  43 +  44 ,  45 -  46 .  47 / 

48 0  49 1  50 2  51 3  52 4  53 5  54 6  55 7  56 8  57 9  58 :  59 ;  60 <  61 =  62 >  63 ? 

64 @  65 A  66 B  67 C  68 D  69 E  70 F  71 G  72 H  73 I  74 J  75 K  76 L  77 M  78 N  79 O 

80 P  81 Q  82 R  83 S  84 T  85 U  86 V  87 W  88 X  89 Y  90 Z  91 [  92 \  93 ]  94 ^  95 _ 

96 `  97 a  98 b  99 c  100 d  101 e  102 f  103 g  104 h  105 i  106 j  107 k  108 l  109 m  110 n  111 o 

112 p  113 q  114 r  115 s  116 t  117 u  118 v  119 w  120 x  121 y  122 z  123 {  124 |  125 }  126 ~  127   

Для номерів 128–255 існують різні відповідності (кодові таблиці). Нижче наведено одну з них. Варіант кодової таблиці

127


Äîäàòîê

Науково-виробниче видання Бібліотека «Шкільного світу» Ставровський Андрій, Скляр Ірина Програмуємо правильно Посібник Частина 2 Художній редактор О. Голик Літературний редактор Ю. Желєзна Коректор І. Бірюкович Комп’ютерна верстка К. Яскевич Набір Т. Корольова Підписано до друку 02.07.07. Формат 60х84/16. Папір офсетний № 1. Гарнітура Таймс. Друк офсетний. Умовн. друк. арк. 7,44. Обл.-вид. арк. 7,2. Тираж пр. Зам. № ТОВ Видавнцтво «Шкільний світ» 01014, м.Київ, вул. Тимірязєвська, 2 Свідоцтво про внесення суб’єкта видавничої справи до Державного реєстру видавців, виготівників і розповсюджувачів видавничої продукції серія ДК № 775 від 21.01.2002 р. Видрукувано з готових діапозитивів в ОП «Житомирська облдрукарня» 10014, Житомир, вул. Мала Бердичівська, 17 Свідоцтво про внесення суб’єкта видавничої справи до Державного реєстру видавців, виготівників і розповсюджувачів видавничої продукції серія ЖТ № 1 від 06.04.2001 р. 128


Програмуємо правильно. Скляр Ірина Віл'ївна, Ставровський Андрій.(2 частини)