- •Київський національний університет імені Тараса Шевченка
- •Гриф надано Міністерством освіти і науки України (лист № 1.4/18-г-1523 від 20.09.07)
- •Основи алгоритмізації
- •1.1. Поняття алгоритму
- •1.2. Класифікація внутрішніх структур алгоритмів
- •1. Ввести а,b.
- •3. Вивести значення s.
- •4. Кінець.
- •6. Закінчити роботу.
- •1. Ввести число n.
- •6. Перехід до п.3.
- •7. Друк к.
- •8. Кінець.
- •1.3. Складність алгоритмів
- •1.4. Складність задач
- •Завдання для самостійної роботи
- •Форма Бекуса – Наура
- •Завдання для самостійної роботи
- •Void main(){
- •3.2. Структура с-програми
- •3.3. Описувачі
- •3.4. Основні операції мови с
- •If(!inword)
- •Void main()
- •Int rozmir;
- •3.5. Оператори мови с
- •3.5.1. Прості оператори
- •3.5.2. Умовний оператор
- •3.5.3. Оператор циклу for
- •3.5.4. Оператори do-while, while
- •3.5.5. Оператор continue
- •3.5.6. Оператор-перемикач switch
- •3.5.7. Оператор break
- •3.5.8. Оператор goto
- •If(error(I,j,k)) goto exit;
- •3.5.9. Оператор return
- •3.6. Директиви препроцесору та вказівки компілятору
- •3.6.1. Директива препроцесору #define
- •1. Макровизначення:
- •3.6.3. Директива #include
- •3.6.4. Директиви умовної компіляції #if, #elif, #else, #endif
- •3.6.5. Директива #line
- •If(!cond)
- •3.7. Описувачі з модифікаторами
- •3.7.1. Моделі пам'яті
- •3.7.2. Модифікатори типу доступу в пам'яті
- •Int huge*near X;
- •3.7.3. Модифікатори const, volatile, cdecl, pascal, interrupt
- •Volatile int t;
- •Void interrupt timer()
- •Void wait(int interval)
- •Завдання для самостійної роботи
- •Принципи типізації даних
- •4.1. Прості типи даних
- •4.2. Похідні типи
- •4.3. Еквівалентність типів
- •4.4. Успадкування атрибутів
- •4.5. Перераховні типи
- •4.6. Логічні типи
- •4.7. Символьні типи
- •4.8. Числові типи
- •4.9. Структурні типи даних
- •4.9.1. Масиви
- •4.9.2. Структури
- •Int year;
- •4.10. Деякі особливості типів даних c
- •4.10.1. Базові типи даних
- •4.10.2. Перетворення типів
- •Int atoi(char s[]) /*char* s*/
- •4.10.3. Засіб typedef
- •Int curs;
- •4.10.4. Покажчики та масиви
- •Void * p;
- •Int array[12];
- •Void f(int a[])
- •Int f(char * s)
- •Наведемо деякі приклади розв'язання задач.
- •Int shift; /*відступ*/
- •Int count[n]; /*кількість монет даного типу (коефіцієнти ai)*/
- •Int coin;
- •Int sum; /*монета, яку міняємо*/
- •Int maxcoin; /*індекс по масиву cost[] монети максимальної вартості, допустимої при даному розміні.*/
- •If(count[I])
- •If(maxcoin)
- •Int* ctranspon (int *a,int n,int m)
- •Void dobutok(int* a, int* b, int** c, int n, int m)
- •Int n,m,I,size;
- •Int main()
- •4.10.5. Структури та об'єднання
- •Розглянемо деякі приклади розв'язання задач.
- •Int hashfunc(key); int eqkey(key, key);
- •Void freeval(val); void setval(val, val);
- •Void freekey(key); void setkey(key, key);
- •Int hashfunc(key key){
- •Val val; /*значення*/
- •Void set(key key, val val){
- •Void printcell(struct cell *ptr){
- •Void main(void)
- •Завдання для самостійної роботи
- •Зображення чисел у комп'ютері
- •Int main(void)
- •5.1. Системи числення
- •5.2. Правила переведення чисел з однієї системи числення в іншу
- •5.3. Правило визначення точності зображення
- •5.4. Двійкова арифметика
- •5.4.1. Додавання двійкових чисел
- •5.4.2. Зображення від'ємних чисел
- •XXXXXXXX 00000001 00000000.
- •5.4.3. Віднімання двійкових чисел
- •5.4.4. Множення двійкових чисел
- •5.4.5. Ділення двійкових чисел
- •5.5. Ознака переповнення розрядної сітки при арифметичних операціях
- •5.6. Зображення цілих чисел
- •5.7. Зображення дійсних чисел
- •5.8. Керування машинним зображенням чисел та особливості виконання арифметичних операцій
- •Завдання для самостійної роботи
- •Реалізація концепції структурного програмування
- •6.1. Оголошення та визначення функцій
- •Int d;} people;
- •6.2. Формальні та фактичні параметри
- •Void swap(int a,int b)
- •Void swap(int a,int*b)
- •6.3. Функції зі змінною кількістю параметрів
- •Void sum(char *msg,...)
- •6.5. Параметри функції main
- •6.6. Лiтернi покажчики та функцiї
- •Void strcpy(char*s,char*t)
- •Void f(void)
- •6.8. Класи пам'яті
- •Розглянемо деякі приклади розв'язання задач.
- •I, power(2,I),power(-3,I));
- •Void main() { choturukyt b; tochka *a; float s; long n,in; srand(time(null));
- •6.9. Введення–виведення с. Файли та потоки
- •6.9.1. Функції введення–виведення верхнього рівня
- •6.9.2. Функції введення–виведення консольного термінала та порту
- •Int main(void)
- •6.9.3. Функції введення–виведення нижнього рівня
- •Int main(void)
- •Int handle;
- •Розглянемо приклади розв'язання задач.
- •Void main(void)
- •Void main(argc,argv)
- •If(c& masks[I])
- •If (цей рядок довший за найдовший з попередніх)
- •Int max; /*максимальна довжина*/
- •Int len; /*довжиною цього рядка*/
- •Int nwords; /*кількість слів у рядку*/
- •If(!*s) /*рядок закінчився*/
- •Int ctr; /*кількість входжень слова*/
- •If(!strcmp(word,w[I].Wrd)){
- •If(alert){
- •Void main() { float X,y,z,t,s; int I,j,flag,n,k; m1: clrscr();
- •InitBase (void){
- •Int key, /*новий ключ*/
- •InitBase();
- •Завдання для самостійної роботи
- •7.1. Елементи концепції обєктно-орієнтованого програмування
- •Int year;
- •Int year;
- •7.3. Опис протоколу класу
- •7.4.1. Коментарі
- •7.4.2. Прототипи функцій
- •Void f();
- •7.4.5. Перевантаження функцій
- •Int Name (int first)
- •Int Name (unsigned first)
- •Int Name (int first,char*second)
- •7.4.6. Значення формальних параметрів за умовчанням
- •7.4.7. Посилання й покажчики
- •Void increment(int& X)
- •Int anotherint;
- •7.4.10. Покажчик на void
- •Void*void_ptr;
- •Void swap(void*&item1,void*&item2)
- •7.4.11. Зв'язування зі збереженням типів
- •7.4.12. Про структури та об'єднання
- •7.5. Функції-члени класу
- •X *this;
- •Int year;
- •7.6. Конструктори та деструктори
- •7.6.1. Поняття про конструктори
- •Int*data;
- •Int size;
- •7.6.3. Конструктор копіювання
- •Int data[large];
- •Inline Large1 Large1::fast(const Large1 & b)
- •7.7. Глобальні та локальні об'єкти
- •Void main(void)
- •7.8. Статична пам'ять і класи
- •Int statpol::I;
- •Vоid draw()
- •Int large;
- •Int bigwant;
- •Void f() {
- •Void g(int a)
- •7.9. Успадкування
- •7.9.1. Синтаксична реалізація успадкування
- •7.9.2. Правила доступу до полів даних
- •Void f(void)
- •Void g(void){}//...}
- •7.9.3. Конструктори та деструктори в похідних класах
- •7.9.4. Використання заміщуючих функцій-членів
- •Void Display (void); //замiщувальна функцiя
- •Void Region::Display(void)
- •Void Display(void);};
- •Void Population::Display(void)
- •7.9.5. Похідні класи й покажчики
- •7.9.6. Ієрархія типів
- •XyValue(int_x,int_y):X(_x),y(_y)
- •XyData(int_x,int_y)
- •7.9.7. Множинне успадкування
- •Void SetLoc(int_x,int_y);};
- •Int data;
- •7.10. Віртуальні функції та класи
- •7.10.1. Віртуальні функції
- •Int value;
- •Virtual int GetValue();
- •Int Value::GetValue(){return value;}
- •7.10.2. Чисті віртуальні функції. Абстрактні класи
- •Virtual void f1(void);
- •Virtual void f2(void);//...}
- •Int index;
- •7.10.3. Віртуальні деструктори
- •7.10.4. Посилання як засіб для реалізації поліморфізму
- •7.10.5. Дещо про механізм віртуальних функцій
- •Virtual int method1(float r);
- •Int data;
- •Void func(void){//тіло}};
- •Virtual public CocaCola {
- •Int size;
- •Void ShowValue(void)
- •Void ShowValues(void);};
- •Void Two::ShowValues(void)
- •7.11.2. Дружні функції
- •Void Show(One &c1,Two &c2)
- •Void Show(One &c1);
- •Void Two::Show(One &c1)
- •7.12. Перевантаження операцій
- •7.12.1. Загальний підхід
- •Void main()
- •7.12.2. Перетворення типів
- •X::operator т();
- •7.12.3. Перевантаження операції індексування масиву
- •Int znach;
- •7.12.4. Перевантаження операції виклику функції
- •Int operator()(void);
- •Int FuncClass::operator()(void)
- •Vidnosh*vec;
- •7.12.5. Перевантаження операції доступу до члена класу
- •7.12.6. Перевантаження операцій інкремента й декремента
- •Int index;
- •Void*operator new(size_t)
- •Void*operator new(size_t);
- •8.1. Функціональні шаблони
- •8.1.1. Визначення й використання шаблонів функцій
- •Void func(t t)
- •Int main(void)
- •8.1.2. Перевантаження шаблонів функції
- •Int main(void)
- •8.1.3. Cпецiалiзованi функцiї шаблона
- •Int main(void){
- •8.2. Шаблони класів
- •8.2.1. Визначення шаблонів класу
- •Void push(t t);
- •Int numitems;
- •8.2.2. Константи й типи як параметри шаблона
- •8.2.3. Використання шаблонних класів
- •Int main(void)
- •8.2.4. Спеціалізація шаблонів класу
- •Void add(t item);
- •Int main(void)
- •IArray.Add(i1);
- •Int main(void)
- •IList.Add(i1);
- •Завдання для самостійної роботи
- •Автоматна технологія програмування
- •If(!stop)printf("не входити");
- •Завдання для самостійної роботи
- •Список літератури
- •Передмова 3
1.3. Складність алгоритмів
Природно, що перед розробкою програми слід придумати, як розв'язати поставлену задачу. При розробці алгоритму необхвдно, перш за все, приділити увагу часу його роботи й обсягу пам'яті, яку необхідно витратити для зберігання й обробки даних. Дані поділяють на вхідну інформацію (вхідне слово), ті, що вимагають проміжного зберігання, і вихідну інформацію (вихідне слово). Не всі дані вимагають одночасного зберігання й використання, а тому можна планувати роботу з ефективного їх використання.
Для розв'язання задачі можна написати багато різних алгоритмів. Чим вони відрізняються, який краще використовувати? Як визначити поняття "краще"? Такі питання виникають (або мають виникати) у професійного програміста.
Для початку замінимо поняття "краще" на "ефективніше" й далі його використовуватимемо. Ефективність програми є дуже важливою її характеристикою. Вона має дві складові: розмір і час.
Розмір вимірюється обсягом пам'яті, що вимагається для виконання програми. Іноді обсяг необхідної пам'яті є домінуючим фактором в оцінці ефективності програми. Проте останніми роками ця складова ефективності поступово втрачає своє значення.
Тимчасова ефективність програми визначається часом, необхідним для її виконання. Вона залежить як від конкретної реалізації алгоритму, так і від власне вибраного алгоритму для розв'язання поставленої задачі.
Отже, складність обчислення, тобто роботи алгоритму на певному вхідному слові, визначається ресурсами, а ресурс, необхідний для конкретного обчислення, – це число. Знаходженням та оцінкою такого числа ми й займемося.
Основною властивістю алгоритму є виконання поставленої задачі за скінченну кількість кроків. Швидкість, з якою алгоритм виконується на конкретному пристрої, може істотно залежати від набору вхідних даних. При цьому швидкий у середньому алгоритм здатний давати збої в окремих "поганих" випадках. І, якщо задача має напевно розв'язуватися за певний час роботи процесора, то ми віддамо перевагу алгоритму, повільнішому в середньому, проте надійному в гірших ситуаціях. Саме уміння передбачати погані ситуації і відрізняє кваліфікованого алгоритміста від звичайного кодувальника.
Будь-який крок алгоритму реалізується деякою кількістю машинних операцій. Деталізація алгоритму має бути такою, щоб на кожному окремому кроці не потрібне було його подальше алгоритмічне опрацьовування. Тут можливі лише дві ситуації: або фіксований час виконання такого кроку визначено деяким набором простих, без циклів, команд мови програмування, або йдеться про ускладнений крок, для якого відповідний аналіз уже проводився і результати відомі.
Тепер перейдемо до головного питання, яке можна задати будь-якому програмісту: припинить його програма своє виконання або ж працюватиме до нескінченності? За теоремою, доведеною Тьюрінгом у 30-ті рр. XX ст., не існує алгоритму, що дає відповідь на це питання, тобто проблема зупинки нерозв'язна. Таким чином, навіть якщо ми маємо алгоритм, який розв'язує поставлену задачу, то ще не відомо, чи приведе він нас до відповіді. Тому нас цікавить не просто існування алгоритмів для нашої задачі, а й їх ефективність. Причому з усіх складових ефективності ми звертатимемо особливу увагу на час (швидкодію) роботи алгоритму. Справедливою, хоч і з деякими обмовками, є така формула:
(загальний час роботи)=(кількість операцій алгоритму)*(час на 1 операцію)
Поліпшення обох членів у правій частині формули здійснюється незалежно. Грубо кажучи, програми відповідають за перший співмножник, а процесори – за другий.
Розглянемо деякі приклади.
1. Алгоритм обміну значень двох змінних цілого типу – а і b – реалізується в загальному випадку за три кроки, незалежно від того, до якого типу простих даних він застосовується:
temp=а
а=b
b=temp
Якщо застосуємо для обміну значеннями алгоритм, що використовує арифметичні операції, то дістанемо таку послідовність дій:
a=a+b
b=a-b
a=a-b
При цьому отримаємо ті самі три кроки.
2. Знайти суму натуральних чисел від 1 до заданого n.
Якщо скористатися відомою формулою для суми арифметичної прогресії, то для обчислення також знадобляться лише три кроки: додавання, множення й ділення. Якщо ж реалізувати обчислювальний процес як циклічний (цикл із параметром), і керувальна змінна пробіжить значення від 1 до n, то доведеться виконати n кроків алгоритму.
Сумнівів, який з алгоритмів ефективніший, здається, не виникає.
Для алгоритмів, подібних до щойно розглянутого (n кроків для обробки n вхідних значень), кількість кроків є функцією від кількості елементів, що обробляються, – g(n). Не для кожного алгоритму таку функцію можна знайти й обчислити.
Для оцінки швидкості зростання досліджуваної функції порівняємо її з функцією, дія якої вже добре відома. При дослідженні поведінки функцій g(n) натурального n розглядатимемо такі функції g(n), що зростають не швидше f(n), тобто існує така пара додатних значень M і n0, що g(n) Mf(n0) для n n0. Або можна сказати, що функція g(n) має порядок f(n) для великих n. Таку залежність позначають у математиці символом О.
Укажемо кілька важливих властивостей O-оцінювання:
1. f(n) O(f(n)).
2. c·O(f (n)) O(f(n)), де с – деяка константа.
3. О(f(n)) O(f (n)) O(f (n)).
4. О(О(f (n))) O(f (n)).
5. О(f (n))·О(g(n)) O(f (n)·g(n)).
6. О(k*f ) O(f ), де k – деяка константа.
7. О(f g) дорівнює домінанті О(f ) і О(g).
Тут с, n позначають константи, а f і g – функції.
Можна сказати, що ефективність алгоритму безпосереднього підсумовування n елементів відповідає лінійній складності, оскільки його швидкодія, тобто кількість кроків, згідно із властивістю 1 становить О(n).
Розглянемо таку задачу: для набору з n попарно нерівних відрізків підрахувати кількість усіх трійок, з яких можна отримати невироджені трикутники.
Треба перевірити n(n – 1)(n – 2) варіантів, що відповідає кубічній складності О(n3).
У загальному випадку, якщо ефективність алгоритму визначається обчислювальною складністю обробки багаточлена порядку к, то часто задовольняються оцінкою О(nk), не зважаючи, згідно з властивістю 2, на старший коефіцієнт і решту членів полінома.
О-оцінювання дозволяє не використовувати конкретний обчислювальний пристрій для аналізу складності.
Нехай є деяка віртуальна машина М, яка обчислює функцію f, і визначені вхідні дані: х – деяке двійкове слово. Визначимо функцію Т(М, х) як кількість операцій, що потрібні машині М для роботи на вхідному слові х. Справедлива така теорема.
Теорема (Блюм, 1971). Існує така обчислювана функція f, що будь-яку машину М, яка її обчислює, можна прискорити так: існує інша машина що також обчислює f, для якої виконується нерівність для майже всіх n, де .
Теорема в наведеному формулюванні справедлива не для всіх функцій. За більш вільного формулювання скажімо так: неможливо придумати найкращий алгоритм, оскільки існує ще швидший.