Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Бичков - Основи сучасного програмування.doc
Скачиваний:
69
Добавлен:
07.03.2016
Размер:
2.67 Mб
Скачать

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, де .

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