- •Київський національний університет імені Тараса Шевченка
- •Гриф надано Міністерством освіти і науки України (лист № 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.4. Складність задач
Поняття складності алгоритму пов'язане з поняттям складності задачі. Із теореми Блюма можна зробити висновок, що визначати складність задачі через складність найкращого алгоритму для її розв'язання – не найвдаліший спосіб.
Нехай необхідно побудувати алгоритм для розв'язання такого класу задач: обчислити значення виразу у точці x b, де aiR, bR, а R – множина дійсних чисел.
Множина вхідних даних: деaiR, bR, – вектор зn 1 дійсних чисел, b – дійсне число.
Результат: , де.
Змінні: i – цілого типу; х, r – дійсного.
Константи: {aii 0,..., n}, п.
Наведемо алгоритм для даного класу задач:
Алгоритм:
Покласти i рівним n, s рівним 0, х рівним b.
Піднести х до степеня i.
Помножити на степінь.
Покласти s рівній сумі s і добутку на степінь.
Якщо i=0, то s – результат (стоп).
Інакше покласти i=i-1, перейти до кроку 2.
Послідовність обчислень за алгоритмом описує вираз
.
Існує також інший алгоритм для розв'язання задач цього класу.
Вхідні дані: ті самі, що й у попередній задачі.
Результат: такий самий.
Змінні: r, s, x – дійсного типу, i – цілого типу.
Константи: п.
Алгоритм:
Покласти i рівним п,x рівним b.
Покласти r рівним
Помножити r на x.
Покласти r рівним добутку.
Покласти i рівним i–1.
Покласти r рівним r+.
Якщо i=0, то r – результат, інакше перейти до кроку 3.
Послідовність обчислень за цим алгоритмом можна пояснити виразом
.
Такий метод обчислення значення полінома у точці називається схемою Горнера.
Виразимо складність дії піднесення до степеня i як (i – 1) операцій множення. Тоді складність другого алгоритму (за схемою Горнера) у вигляді кількості операцій додавання та множення дорівнюватиме 2п, а для прямого алгоритму становитиме
Таким чином, другий алгоритм ефективніший за перший.
Тепер можна дати визначення ефективному алгоритму.
Означення 1.7. Алгоритм, трудомісткість якого обмежена поліномом від характерного обсягу задачі, називається ефективним.
Під складністю задачі розумітимемо час, необхідний для обчислення функції, за допомогою якої знаходиться її розв'язок. Під складністю розумітимемо складність алгоритму у найгіршому випадку. Оскільки не можна побудувати для кожної функції найкращої машини, що її обчислює, то введемо поняття класу складності.
Означення 1.8. Клас складності містить функції f, для яких існує машина М, що обчислює f, така, що функція Т(М, х) за часом обмежена функцією t(n) із точністю до мультиплікативної константи, тобто
,
де t(n) – порядок класу.
Класом задач складності t(n) назвемо сукупність задач, які розв'язуються за час порядку t(n).
O-оцінки виражають відносну швидкість алгоритму залежно від початкових даних. Розглянемо класи O-складності алгоритмів. Нехай N – кількість оброблюваних даних (табл. 1.1).
Таблиця 1.1
Класи складності |
Опис програм |
О(1) |
Більшість операцій у програмі виконується або один раз, або фіксовану кількість разів. Будь-який алгоритм, що завжди вимагає (незалежно від обсягу даних) одного й того самого часу, має константну складність. |
О(N) |
Час роботи програми лінійно залежить від N. Властиво алгоритмам, які обробляють кожен елемент вхідних даних кількість разів, пропорційну лінійній функції. |
О(N2), О(N3), О(Nк) |
Поліноміальна складність. О(N2) – квадратична складність, О(N3) – кубічна складність. Час роботи програми пропорційний поліноміальній функції. |
О(Log(N)) |
Час роботи програми логарифмічний. Зустрічається зазвичай у програмах, які ділять велику задачу на маленькі й розв'язують їх окремо. |
О(N*log(N)) |
Такий час роботи мають алгоритми, які ділять велику задачу на маленькі, а потім, розв'язавши їх, сполучають розв'язки. |
О(2N) |
Експоненціальна складність. Такі алгоритми найчастіше виникають унаслідок підходу, що має назву "метод грубої сили". |
Розглянемо прийоми проведення аналізу алгоритмів і визначення їх складності. Тимчасова складність алгоритму може бути порахована, виходячи з аналізу його керувальних структур. Ми пам'ятаємо, що до керувальних структур належать лінійні та умовні вирази, цикли (табл. 1.2).
Таблиця 1.2
Керувальна структура |
Складність |
Простий вираз |
О(1) |
Лінійний вираз S1,…Sn |
Домінанта О(С1),…, О(Cn), де C1,…, Сn – складність обчислення виразів S1,…Sn |
Якщо <умова> то дія1 Інакше дія2 |
Домінанта О (С1), О(С2), О (С3), де С1, С2, С3 – складність обчислень дій та умови, відповідно |
Цикл із n повтореннями тіла S1 |
О(n*C1), де С1 – складність обчислення S1 |
Якщо немає рекурсії й циклів, то всі керувальні структури можуть бути зведені до структур константної складності. Отже, і весь алгоритм також характеризується константною складністю. Тому визначення складності алгоритму в основному зводиться до аналізу циклів і рекурсивних викликів.
Розглянемо алгоритм обробки елементів масиву.
Алгоритм:
Для i=1 до N виконувати
початок
{Простий вираз}
кінець;
Складність цього алгоритму становить О(N), оскільки тіло циклу виконується N разів, а складність тіла циклу дорівнює О(1).
Якщо один цикл вкладено в іншій та обидва мають однакову кількість повторень, то вся конструкція характеризується квадратичною складністю.
Розглянемо алгоритм.
Алгоритм:
Для i=1 до N виконувати
Для j=1 до N виконувати
початок
{Простий вираз}
кінець;
Його складність становить О(N2).
Оцінимо складність алгоритму, що має назву "Трійки Піфагора". Суть задачі: знайти всі трійки натуральних чисел (х, y, z), що задовольняють рівняння x 2 y 2 z 2. Піфагор знайшов формули, які в сучасному формалізмі можна записати як
x 2п 1, y 2п(п 1), z 2п2 2п 1,
де п – ціле число. Наприклад:
А |
B |
С |
3 |
4 |
5 |
5 |
12 |
13 |
8 |
15 |
17 |
7 |
24 |
25 |
20 |
21 |
29 |
12 |
35 |
37 |
9 |
40 |
41 |
28 |
45 |
53 |
11 |
60 |
61 |
Трикутники із такими сторонами є прямокутними. Розпочате Піфагором дослідження рівняння x 2 y 2 z 2 привело до складної проблеми сучасної теорії чисел – дослідження у цілих числах рівняння xn yn zn. Перед аналізом складності програми, яка знаходить трійки Піфагора, зазначимо, що існують два способи аналізу складності: висхідний (від внутрішніх керувальних структур до зовнішніх) і низхідний (від зовнішніх – до внутрішніх). Ми користуватимемося висхідним. Розглянемо алгоритм.
Алгоритм:
D |
А |
|
Вхід: n 1. small=1 | |||
E |
|
2. Поки small<n виконувати Початок | ||||
B |
|
3. next=small | ||||
G |
|
4. Поки next<=n виконувати Початок | ||||
С |
|
5. last=next | ||||
I |
|
6. Поки last<n виконувати Початок | ||||
F |
|
7. Якщо last<=2*small і next<=2*small і last*last=small*small+next*next | ||||
H |
друк(next) друк(last) | |||||
J |
|
| ||||
К |
|
| ||||
|
| |||||
|
Вихід: роздруковані значення small,next,last |
Підрахуємо його складність:
О(H) O(1) O(1) O(1) O(1);
О(I) O(N)*(О(F) O(J)) O(N)*O (домінанти умови) О(N);
О(G) O(N)*(О(C) O(I) O(K)) O(N)*(О(1) O(N) O(1)) O(N2);
О(E) O(N)*(О(B) O(G) O(L)) O(N)*О(N2) О(N3);
О(D) O(A) O(E) O(1) О(N3) О(N3).
Таким чином, складність алгоритму становить О(N3).
Проаналізувавши програму, можна дійти висновку, що основну оцінку складності забезпечують вкладені цикли. Тому для зменшення складності, перш за все, можна спробувати зменшити глибину вкладеності циклів. Далі слід розглянути можливість скорочення кількості операторів у циклах з найбільшою глибиною вкладеності. Однак ці рекомендації не завжди можна виконати.
Оцінимо алгоритм бінарного пошуку ключа в упорядкованому масиві. Суть алгоритму: розглядаємо серединний елемент масиву й перевіряємо відповідність ключа до його значення. Якщо не вдається знайти відповідність, то порівнюємо ключ і значення серединного елемента, а потім переміщаємося в нижню половину списку елементів масиву, якщо ключ менше, і верхню – якщо більше. В обраній половині знову шукаємо середину та знову порівнюємо із ключем. Якщо не виходить, то знову ділимо надвоє поточний інтервал.
Розглянемо детальніше алгоритм. Вхід: low, high – змінні, що задають нижню й верхню межі масиву, key – шукане число.
початок
поки low<=high виконувати
початок
mid=(low+high)/2;
data=a[mid];
якщо key=data
тоді початок search=mid; закінчити роботу кінець
інакше якщо key<data
то high=mid-1
інакше low=mid+1;
кінець;
search=-1;
кінець;
Перша ітерація циклу має справу з усім списком елементів. Кожна подальша ітерація ділить список навпіл. Наприклад, розмірами списків для послідовних кроків алгоритму є n n/21 n/22 n/23 n/24... n/2m. Урешті-решт знайдеться таке ціле m, що n/2m 2 або n 2m+1. Оскільки m – перше ціле, для якого n/2m 2, то має бути вірним n/2m –1 2 або 2m n. Із цього випливає, що 2m n 2m 1.
Візьмемо логарифм кожної частини нерівності й дістанемо m log2(n) x m 1. Значення m – найбільше ціле, що менше або дорівнює х. Отже, складність дорівнює О(log2n).
Знайдемо вираз для часу, що необхідний програмі для обробки масиву з N елементами як функцію від N. Зазвичай нас цікавить середній випадок – очікуваний час роботи програми на типових вхідних даних, і гірший – очікуваний час роботи програми на найгірших вхідних даних.
Через труднощі, пов'язані з проведенням аналізу часової складності алгоритму в середньому, часто доводиться задовольнятися оцінками для гіршого й кращого випадків. Ці оцінки, по суті, визначають нижню й верхню межі складності в середньому. У такому разі оцінка, одержана для найгіршого випадку, може служити хорошою апроксимацією складності в середньому.
Основним недоліком O-оцінювання є його надмірна грубість. Якщо алгоритм А виконує деяку задачу за 0.001*N, тоді як для її розв'язання за допомогою алгоритму В потрібно 1000*Nз, то В у мільйон разів швидше за А. Однак А та В мають одну й ту саму складність О(N).
Можна умовно розділити задачі на прості, які мають поліноміальний час розв'язання (розв'язання системи лінійних алгебраїчних рівнянь у раціональних числах); складні (важкі, що не мають розв'язку) – задачі, які не розв'язуються за поліноміальний час, або алгоритм розв'язання за поліноміальний час не знайдений. Крім цього, існують принципово нерозв'язні задачі. Дане твердження доведене А. Тьюрінгом.
Завершимо розгляд основ теорії складності тезами двох великих учених.
Теза Колмогорова. Проблеми, які не можуть бути розв'язані без повного перебирання, залишаться за межами можливостей машини на скільки завгодно високому рівні техніки й культури.
На думку А. Тьюрінга, усе те, що природно визнали б обчислюваним, могло б, принаймні у принципі, бути обчислене у природі. Тому справедлива
Теза Тьюрінга. Можливо побудувати генератор віртуальної реальності з репертуаром, що включає всі середовища, існування яких не суперечить законам фізики. Це означає наявність:
відповідної програми;
необхідних обчислювальних ресурсів (швидкодії, обсягу пам'яті).