VB, MS Access, VC++, Delphi, Builder З++ принципы (технология) , алгоритми программирования

Тип работы:
Реферат
Предмет:
Информатика, программирование


Узнать стоимость

Детальная информация о работе

Выдержка из работы

Запровадження 8

Цільова аудиторія 10

Глава 1. Основні поняття 15

Що таке алгоритми? 15

Аналіз швидкості виконання алгоритмів 16

Простір — час 17

Оцінка з точністю до порядку 17

Пошук складних частин алгоритму 19

Складність рекурсивних алгоритмів 20

Багатократна рекурсія 21

Непряма рекурсія 22

Вимоги рекурсивних алгоритмів обсягу пам’яті 22

Найгірший і усереднений випадок 23

Часто які функції оцінки порядку складності 24

Логарифми 25

Реальні умови — як швидко? 25

Звернення до файлу підкачування 26

Псевдоуказатели, посилання об'єкти й колекцію 27

Резюме 29

Глава 2. Списки 30

Знайомство зі списками 31

Прості списки 31

Колекції 32

Список змінного розміру 33

Клас SimpleList 36

Невпорядковані списки 37

Зв’язкові списки 41

Додавання елементів до зв’язков списку 43

Видалення елементів з зв’язкового списку 44

Знищення зв’язкового списку 44

Сигнальні мітки 45

Інкапсуляція зв’язкових списків 46

Доступ до осередків 47

Різновиду зв’язкових списків 49

Циклічні зв’язкові списки 49

Проблема циклічних посилань 50

Двусвязные списки 50

Потоки 53

Інші зв’язкові структури 56

Псевдоуказатели 56

Резюме 59

Глава 3. Стеки і беззмістовності черги 60

Стеки 60

Численні стеки 62

Черги 63

Циклічні черги 65

Черги з урахуванням зв’язкових списків 69

Застосування колекцій як черг 70

Пріоритетні черги 70

Многопоточные черги 72

Резюме 74

Глава 4. Масиви 75

Трикутні масиви 75

Діагональні елементи 77

Нерегулярні масиви 78

Пряма зірка 78

Нерегулярні зв’язкові списки 79

Зріджені масиви 80

Індексування масиву 82

Дуже зріджені масиви 85

Резюме 86

Глава 5. Рекурсія 86

Що таке рекурсія? 87

Рекурсивне обчислення факториалов 88

Аналіз часу виконання програми 89

Рекурсивне обчислення найбільшого загального дільника 90

Аналіз часу виконання програми 91

Рекурсивне обчислення чисел Фібоначчі 92

Аналіз часу виконання програми 93

Рекурсивне побудова кривих Гільберта 94

Аналіз часу виконання програми 96

Рекурсивне побудова кривих Серпинского 98

Аналіз часу виконання програми 100

Небезпеки рекурсії 101

Безкінечна рекурсія 101

Втрати пам’яті 102

Необгрунтоване застосування рекурсії 103

Коли потрібно використовувати рекурсию 104

Хвостова рекурсія 105

Нерекурсивное обчислення чисел Фібоначчі 107

Усунення рекурсії у випадку 110

Нерекурсивное побудова кривих Гільберта 114

Нерекурсивное побудова кривих Серпинского 117

Резюме 121

Глава 6. Дерева 121

Визначення 122

Уявлення дерев 123

Повні вузли 123

Списки нащадків 124

Уявлення нумерацією зв’язків 126

Повні дерева 129

Обхід дерева 130

Впорядковані дерева 135

Додавання елементів 135

Видалення елементів 136

Обхід упорядкованих дерев 139

Дерева із посиланням 141

Фундаментальна обізнаність із деревами із посиланням 144

Квадродеревья 145

Зміна MAX_PER_NODE 151

Використання псевдоуказателей в квадродеревьях 151

Восьмеричні дерева 152

Резюме 152

Глава 7. Збалансовані дерева 153

Збалансованість дерева 153

АВЛ-деревья 154

Видалення вузла з АВЛ-дерева 161

Б-деревья 166

Продуктивність Б-деревьев 167

Вставка елементів в Б-дерево 167

Видалення елементів з Б-дерева 168

Різновиду Б-деревьев 169

Поліпшення продуктивності Б-деревьев 171

Балансування усунення розбивки блоків 171

Питання, пов’язані зі зверненням до диску 173

База даних з урахуванням Б+дерева 176

Резюме 179

Глава 8. Дерева рішень 179

Пошук в деревах гри 180

Минимаксный пошук 181

Поліпшення пошуку дереві гри 185

Пошук за іншими деревах рішень 187

Метод гілок і національних кордонів 187

Евристики 191

Інші складні завдання 207

Завдання про здійсненності 207

Завдання про розбивці 208

Завдання пошуку Гамильтонова шляху 209

Завдання комівояжера 210

Завдання про пожежних депо 211

Коротка характеристика складних завдань 212

Резюме 212

Глава 9. Сортування 213

Загальні міркування 213

Таблиці покажчиків 213

Об'єднання і стиснення ключів 215

Приклади програм 217

Сортування вибором 219

Рандомизация 220

Сортування вставкою 221

Вставка в зв’язкових списках 222

Пузырьковая сортування 224

Швидка сортування 227

Сортування злиттям 232

Пірамідальна сортування 234

Піраміди 235

Пріоритетні черги 237

Алгоритм пірамідальній сортування 240

Сортування підрахунком 241

Блокова сортування 242

Блокова сортування із застосуванням зв’язкового списку 243

Блокова сортування з урахуванням масиву 245

Резюме 248

Глава 10. Пошук 248

Приклади програм 249

Пошук методом повного перебору 249

Пошук в упорядкованих списках 250

Пошук в зв’язкових списках 251

Двоїчний пошук 253

Интерполяционный пошук 255

Строковые дані 259

Стежить пошук 260

Интерполяционный стежить пошук 261

Резюме 262

Глава 11. Змішування 263

Зв’язування 265

Переваги й недоліки зв’язування 266

Блоки 268

Збереження хеш-таблиц на диску 270

Зв’язування блоків 274

Видалення елементів 275

Переваги й недоліки застосування блоків 277

Відкрита адресація 277

Лінійна перевірка 278

Квадратична перевірка 284

Псевдослучайная перевірка 286

Видалення елементів 289

Резюме 291

Глава 12. Мережні алгоритми 292

Визначення 292

Уявлення мережі 293

Оперування вузлами і зв’язками 295

Обходи мережі 296

Найменші остовные дерева 298

Найкоротший маршрут 302

Установка міток 304

Корекція міток 308

Інші завдання пошуку найкоротшого маршруту 311

Застосування методу пошуку найкоротшого маршруту 316

Максимальний потік 319

Додатка максимального потоку 325

Резюме 327

Глава 13. Об'єктно-орієнтовані методи 327

Переваги ОВП 328

Інкапсуляція 328

Поліморфізм 330

Успадкування і повторне використання 333

Парадигми ОВП 335

Управляючі об'єкти 335

Контролююча об'єкт 336

Итератор 337

Дружній клас 338

Інтерфейс 340

Фасад 340

Який Породжує об'єкт 340

Єдиний об'єкт 341

Перетворення в послідовну форму 341

Парадигма Модель/Вид/Контроллер. 344

Резюме 346

Вимоги до апаратному забезпечення 346

Виконання програм прикладів 346

programmer@newmail. ru

Далее слід «текст», який будь-який пристойний програміст повинен прочитати хоча колись. (Це наша суб'єктивне мнение)

Программирование під Windows завжди був нелегкої завданням. Інтерфейс прикладного програмування (Application Programming Interface) Windows надає у розпорядження програміста набір потужних, але завжди безпечних інструментів і розробити додатків. Можна порівняти його з бульдозером, з якого вдається домогтися разючих результатів, але не матимуть відповідних навичок та обережність, швидше за все, справа вичерпається лише руйнуваннями та збитками. Ця картина змінилася з приходом Visual Basic. Використовуючи візуальний інтерфейс, Visual Basic дозволяє швидко і легко розробляти закінчені докладання. З допомогою Visual Basic можна робити і тестувати складні докладання без прямого використання функцій АПІ. Позбавляючи програміста від проблем АПІ, Visual Basic дозволяє сконцентруватися на деталях докладання. Хоча Visual Basic і полегшує розробку користувальницького інтерфейсу, завдання написання коду для реакцію вхідні впливу, обробки їх, і уявлення результатів лягає на його плечі програміста. Тут починається застосування алгоритмів. Алгоритми є формальні інструкції до виконання складних завдань за комп’ютером. Наприклад, алгоритм сортування може визначати, як знайти конкретну запис у базі з десяти мільйонів записів. Залежно від класу використовуваних алгоритмів шукані дані вдасться знайти за секунди, годинник чи взагалі знайдено. У цьому вся матеріалі обговорюються алгоритми на Visual Basic міститься велике число потужних алгоритмів, повністю написаних цією мовою. У ньому також аналізуються методи поводження з структурами даних, такі як списки, стеки, черзі й дерева, і алгоритми до виконання типових завдань, таких як сортування, пошук і освоєння хэширование. Щоб успішно застосовувати ці алгоритми, недостатньо їх просто скопіювати до своєї програми. Необхідно крім цього розуміти, як різні алгоритми поводяться у різних ситуаціях, що в результаті вони й визначатиме вибір найбільш гідного алгоритму. У цьому вся матеріалі поведінка алгоритмів в типовому і найгіршому випадках описано доступним мовою. Це дозволить зрозуміти, чого ви вправі розраховувати від тієї чи іншої алгоритму і розпізнати, за яких зустрічається найгірший випадок, і згідно з цим переписати чи поміняти алгоритм. Навіть найкращий алгоритм недопоможе у вирішенні завдання, якщо застосовувати його неправильно.

=============xi

Все алгоритми також як вихідних текстів на Visual Basic, що ви можете використовувати у своїх програмах без жодних змін. Вони демонструють використання алгоритмів програми, і навіть важливі характерні риси роботи самих алгоритмов.

Что дають вам ці знання Після ознайомлення з цим матеріалом і прикладами ви отримаєте: 1. Поняття про алгоритми. Після прочитання цього матеріалу і виконання прикладів програм, ви зможете застосовувати складні алгоритми у проектах на Visual Basic та критично оцінювати інші алгоритми, написані вами чи будь-ким ще. 2. Велику добірку вихідних текстів, що ви зможете легко додати вашим програмам. Використовуючи код, який міститься у прикладах, ви зможете легко додавати потужні алгоритми до вашим додатків. 3. Готові приклади програм дадуть можливість протестувати алгоритмы.

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

Целевая аудитория

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

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

=================xii

Некоторые нові поняття, такі як посилання об'єкти, класи й колекцію, хто був вперше уведено підрозділи до 4-й версії Visual Basic, полегшують розуміння, розробку й налагодження деяких алгоритмів. Класи можуть укладати деякі алгоритми на добре продуманих модулях, які легко вставити у програмі. Хоча у тому, щоб застосовувати ці алгоритми, необов’язково розумітися на нових поняттях мови, нові можливості надають дуже великі переваги, щоб ними можна було знехтувати. Тому приклади алгоритмів у цьому матеріалі написані від використання на чотири- і і 5-ї версіях Visual. Якщо вже ви відкриєте в 5-ї версії Visual Basic, середовище розробки запропонує вам зберегти в форматі 5-ї версії, але ніяких змін у код вносити вийде. Усі алгоритми були протестовані на обох версіях. Ці програми демонструють використання алгоритмів не залучаючи объектно-ориентированного підходу. Посилання та приватні колекції полегшують програмування, та їх застосування може спричинить деякому уповільнення роботи програм порівняно з старими версіями. Проте, ігнорування класів, об'єктів і колекцій призвело б до недогляду багатьох справжньою потужних властивостей. Їх використання дозволяє досягти нового рівня модульности, розробки та використання коду. Їх, безумовно, необхідно пам’ятати, по крайнього заходу, на на початкових етапах розробки. Надалі, якщо виникнуть проблеми з продуктивністю, ви зможете модифікувати код, використовуючи більш швидкі низькорівневі методи. Мови програмування найчастіше розвиваються убік ускладнення, але рідко у напрямі. Чудовим прикладом є наявність оператора goto у мові З. Це незручний оператор, потенційний джерело помилок, що майже немає більшістю програмістів на З, але залишається в синтаксисі мови з 1970 року. Навіть було включено в З++ і пізніше у Java, хоча створення нової мови було хорошим приводом позбутися цієї зброї. Ось і нові версії Visual Basic продовжуватимуть вводити нових властивостей в мову, але малоймовірно, що із них виключені будівельні блоки, використані при застосуванні алгоритмів, описаних у Пророчих даному матеріалі. Незалежно від цього, що додано в 6-ї, 7-й чи 8-ї версії Visual Basic, класи, масиви і зумовлені користувачем типи даних залишаться у мові. Більшість, і може і всі алгоритми з наведених нижче, будуть виконуватися змін у протягом ще багато лет.

Обзор глав У 1 главі розглядаються поняття, що ви повинні розуміти доти, як розпочати аналізу складних алгоритмів. У ньому викладено методи, які знадобляться для теоретичного аналізу обчислювальної складності алгоритмів. Деякі алгоритми із високим теоретичної продуктивністю практично дають невідь що хороші результати, у цієї главі також зачіпаються практичні міркування, наприклад звернення до файлу підкачування і порівнюється використання колекцій і масивів. У 2 главі показано, як утворюються різні види списків із використанням масивів, об'єктів, і псевдоуказателей. Ці структури даних з успіхом застосовувати у багатьох програмах, і їх використовують у таких розділах У три главі описані дві особливі типу списків: стеки і першої черги. Ці структури даних використовують у багатьох алгоритми, включаючи деякі алгоритми, достойні наступних розділах. Наприкінці глави приведено модель черги на реєстрацію у аеропорту. У 5 главі обговорюється потужний інструмент — рекурсія. Рекурсія то, можливо також заплутаною й призводити до проблем. У 5 главі пояснюється, у яких випадках варто застосовувати рекурсию і, якомога далі від неї позбутися, якщо це потрібно. О 6-й главі використовуються частина з раніше описаних прийомів, такі як рекурсія і зв’язкові списки, з вивчення складнішою теми — дерев. Ця глава також охоплює різні уявлення дерев, такі як дерева з повними вузлами (fat node) і помилкове уявлення як нумерацією зв’язків (forward star). У ньому також описані деякі важливі алгоритми роботи з деревами, таки як обхід вершин дерева. О 7-й главі порушена складніша тема. Збалансовані дерева мають особливі властивості, що дозволяють їм залишатися врівноваженими і ефективними. Алгоритми збалансованих дерев дивовижно просто описуються, та їх досить важко реалізувати програмно. У цьому главі використовується одне з найпотужніших структур такого типу — Б+дерево (B+Tree) до створення складної бази даних. У 8 главі обговорюються завдання, які можна описати як пошук відповідей в дереві рішень. Навіть для невеликих завдань, ці дерева може бути гігантськими, тож треба здійснювати пошук у яких максимально ефективно. У цьому главі порівнюються деякі різні методи, які дозволяють виконати такий поиск.

Глава 9 присвячена, мабуть, найбільш досліджуваної області теорії алгоритмів — сортування. Алгоритми сортування цікаві з кількох причин. По-перше, сортування — часто яка трапляється завдання. По-друге, різні алгоритми сортировок мають своїми сильними і слабкими сторонами, тому існує одного алгоритму, які свідчили б найкращі результати у різноманітних ситуаціях. І, нарешті, алгоритми сортування демонструють широкий, спектр важливих алгоритмічних методів, як-от рекурсія, піраміди, і навіть використання генератора випадкових чисел для зменшення ймовірності випадання найгіршого випадку. У розділі 10 розглядається близька до сортування тема. По виконанні сортування списку, програмі може знадобитися знайти елементи у ньому. У цієї главі порівнюється кілька найефективніших методів пошуку елементів в сортированных списках.

=========xiv

В главі 11 обговорюються методи збереження та пошуку елементів, працюючі навіть швидше, чому це можливе за використання дерев, сортування чи пошуку. У цьому главі також описані деякі методи хэширования, включаючи використання блоків і зв’язкових списків, і кілька варіантів відкритої адресації. У розділі 12 описана інша категорія алгоритмів — мережні алгоритми. Деякі з цих алгоритмів, такі як обчислення найкоротшого шляху, безпосередньо застосовні фізичних мереж. Ці алгоритми також можуть побічно застосовуватися на вирішення інші завдання, котрі з погляд не здаються пов’язані з мережами. Наприклад, алгоритми пошуку найкоротшого відстані можуть розбивати мережу на райони чи визначати критичні завдання у розкладі проекту. У розділі 13 пояснюються методи, застосування яких уможливлено завдяки запровадження класів в 4-й версії Visual Basic. Ці методи використовують объектно- орієнтований підхід для реалізації нетипового традиційних алгоритмів поведения.

===================xv

Апаратні вимоги Робота прикладах вам знадобиться комп’ютер, конфігурація якого відповідає вимогам до роботи програмної середовища Visual Basic. Ці вимоги виконуються майже всіх комп’ютерів, у яких може працювати операційна система Windows. На комп’ютерах різною конфігурації алгоритми виконуються з різноманітною швидкістю. Комп’ютер з процесором Pentium Pro з тактовою частотою 2000 МГц і 64 Мбайт оперативної пам’яті працюватиме значно швидше, ніж машина з 386 процесором і лише 4 Мбайт пам’яті. Ви швидко дізнаєтеся, потім здатне ваше апаратне обеспечение.

Изменения у друге видання Найбільше зміна у новій версії Visual Basic — це поява класів. Класи дозволяють розглянути деякі завдання з іншого боку, дозволяючи використовувати простіший і природний підхід до розуміння і застосуванню багатьох алгоритмів. Зміни у коді програм, у цьому викладі використовують переваги, надані класами. Їх може бути розбитий втричі категорії: 1. Заміна псевдоуказателей класами. Хоча усі алгоритми, написані для старих версій VB, досі працюють, ті, хто був написані із застосуванням псевдоуказателей (описаних у 2 главі), набагато простіше зрозуміти, використовуючи класи. 2. Інкапсуляція. Класи дозволяють укласти алгоритм в компактний модуль, який легко залучити до програмі. Наприклад, з допомогою класів можна створити кілька зв’язкових списків і писати у своїй додатковий код керувати кожним списком окремо. 3. Об'єктно-орієнтовані технології. Використання класів дає підстави легше зрозуміти деякі об'єктно-орієнтовані алгоритми. У розділі 13 описуються методи, які важко реалізувати без використання классов.

Как користуватимуться цим матеріалом У розділі 1 даються загальні поняття, що використовуються протягом усього викладу, тому вам слід почати читання з цим глави. Ви можете ознайомитися з цим тематикою, навіть коли ви не хочете відразу ж потрапити досягти глибокого розуміння алгоритмів. О 6-й главі обговорюються поняття, які використовуються у 7, 8 та дванадцяти розділах, тому вам слід прочитати 6 главу доти, як починати них. Інші глави можна читати у кожному порядке.

=============xvi

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

Останній план дає порядок з вивчення всього матеріалу повністю. Хоча 7 і побачили 8-го глави логічно випливають із 6 глави, вони складніше вивчення, ніж такі глави, тому вони вивчаються кілька позже.

Почему саме Visual Basic? Найчастіше зустрічаються скарги на повільне виконання програм, написаних на Visual Basic. Багато інші компілятори, такі як Delphi, Visual З++ дають швидший і гнучкий код, надають програмісту потужніші кошти, ніж Visual Basic. Тому цілком логічно запитати — «Чому маю використати Visual Basic для написання складних алгоритмів? Ненайкраща було б послуговуватись Delphi чи З++ чи, по крайньої мері, написати алгоритми одному з мов і підключати їх до програмам на Visual Basic з допомогою бібліотек?» Написання алгоритмів на Visual Basic можна буде з кількох причин. По-перше, розробка докладання на Visual З++ набагато складніший і проблематичніше, ніж Visual Basic. Некоректна реалізація у програмі всіх деталей програмування під Windows можуть призвести до збоїв у вашій додатку, середовищі розробки, чи самої операційній системі Windows. По-друге, розробка бібліотеки мовою З++ від використання в програмах на Visual Basic включає у собі багато потенційних небезпек, характерних й у додатків Windows, написаних на З++. Якщо бібліотека буде неправильно взаємодіяти з програмою на Visual Basic, вона також призведе до збоїв у програмі, а можлива й у середовищі розробки та системі. По-третє, багато алгоритми досить ефективні показують непогану продуктивність навіть за застосуванні невідь що швидких компіляторів, як-от Visual Basic. Наприклад, алгоритм сортування подсчетом,

@Таблица 1. Плани занятий

===============xvii

описываемый о 9-й главі, сортує мільйон цілих чисел менш як по 2 секунди за комп’ютером з процесором Pentium з тактовою частотою 233 МГц. Використовуючи бібліотеку З++, можна було б зробити алгоритм трохи спритнішим, але швидкості версії на Visual Basic й дуже вистачає більшість додатків. Скомпільовані з допомогою 5-ї версією Visual Basic виконувані файли зводять відставання за швидкістю до мінімуму. У кінцевому підсумку, розробка алгоритмів якою мовою програмування дозволяє більше дізнатися про алгоритми взагалі. Принаймні вивчення алгоритмів, ви освоїте методи, які зможете запровадити у інших частинах своїх програм. Потому, як ви вже опануєте досконало алгоритмами на Visual Basic, вам буде набагато легше дати раду на Delphi чи З++, якщо це буде необходимо.

=============xviii

Глава 1. Основні понятия

В цієї главі містяться загальні поняття, потрібно засвоїти до початку серйозного вивчення алгоритмів. Починається вона із питання «Що таке алгоритми?». Перш ніж вкопатися під деталі програмування алгоритмів, стоїть витратити небагато часу, щоб зрозуміти тому, що це таке. Потім у цій главі дається введення у формальну теорію складності алгоритмів (complexity theory). Із цієї теорії можна оцінити теоретичну обчислювальну складність алгоритмів. Такий підхід дозволяє порівнювати різні алгоритми і пророкувати їх продуктивність у різних умовах. У розділі наводиться кілька прикладів застосування теорії складності до невеликим завданням. Деякі алгоритми із високим теоретичної продуктивністю дуже добре працюють практично, у даної главі також обговорюються деякі реальні передумови до створення програм. Занадто часте звернення до файлу підкачування чи поганий використання посилань на об'єкти і колекції може значно знизити продуктивність прекрасного в інших відносинах докладання. Після знайомства з основними поняттями, ви зможете застосовувати їх до алгоритмам, викладених у наступних розділах, і навіть для аналізу власних програм з оцінки їх продуктивності і зможете передбачати можливі проблеми доти, як вони обернуться катастрофой.

Что таке алгоритмы?

Алгоритм — це послідовність інструкцій до виконання будь-якого завдання. Коли ж ви даєте комусь інструкції у тому, як відремонтувати газонокосарку, спекти торт, ви цим ставите алгоритм дій. Звісно, подібні побутові алгоритми описуються неформально, наприклад, так:

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

==========1

У цьому за умовчанням передбачається, що людина, який слідувати цим інструкціям, зможе самостійно виконати безліч дрібних операцій, наприклад, відімкнути і двері, сісти за кермо, пристебнути ремінь, знайти ручний гальмо тощо. Якщо ж складається алгоритм виспівати комп’ютером, ви можете покладатися те що, що зрозуміє щось, якщо це описано заздалегідь. Словник комп’ютера (мову програмування) дуже обмежений і всі інструкції для комп’ютера повинні прагнути бути сформульовані цією мовою. Тому для написання комп’ютерних алгоритмів використовується формалізований стиль. Цікаво спробувати написати формалізований алгоритм для звичайних щоденних завдань. Наприклад, алгоритм водіння машини міг би виглядати приблизно так:

Если двері закрыта:

Вставити ключ в замок

Повернути ключ

Якщо двері залишається закритою, то:

Повернути ключ у бік Повернути ручку двері І т.д.

Этот фрагмент «коду» відповідає за відкривання двері; у своїй навіть перевіряється, яка двері відкривається. Якщо двері заїло чи машині встановлено противоугонная система, то алгоритм відкривання дверей то, можливо досить складною. Формалізацією алгоритмів займаються вже тисячі років. За 300 років е. Евклид написав алгоритми розподілу кутів навпіл, перевірки рівності трикутників і вирішення інших геометричних завдань. Він з невеликого словника аксіом, як-от «паралельні лінії не перетинаються» та побудував з їхньої основі алгоритми на вирішення складних завдань. Формалізовані алгоритми подібного типу добре підходять для завдань математики, де повинна бути доведена істинність будь-яким положенням чи можливість виконання якихось дій, швидкість ж виконання алгоритму байдужа. На виконання реальних завдань, пов’язаних із виконанням інструкцій, наприклад завдання сортування за комп’ютером записів про мільйоні покупців, ефективність виконання стає важливою частиною цьогорічного постановки задачи.

Анализ швидкості виконання алгоритмов

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

Пространство — время

Многие алгоритми надають вибір між швидкістю виконання і використовуваними програмою ресурсами. Завдання може виконуватися швидше, використовуючи більше пам’яті, навпаки, повільніше, зайнявши менший обсяг памяти.

===========2

Хорошим прикладом у разі може бути алгоритм перебування найкоротшого шляху. Задавши карту вулиць міста, у вигляді мережі, написати алгоритм, вычисляющий найкоротший відстань між будь-якими двома точками в цієї мережі. Замість щоразу наново перераховувати найкоротший відстань між двома заданими точками, можна заздалегідь прорахувати її всіх пар крапок і зберегти результати в таблиці. Тоді, щоб знайти найкоротший відстань обох заданих точок, достатньо просто взяти готове значення з таблиці. А отримаємо результату практично миттєво, але це потребує великого об'єму пам’яті. Карта вулиць для великого міста, такого як Бостон чи Денвер, може містити сотні тисяч точок. Для такий мережі таблиця найкоротших відстаней містила би більш 10 мільярдів записів. У цьому вся разі вибір між часом виконання й обсягом необхідної пам’яті очевидний: поставивши додаткові 10 гігабайтів оперативної пам’яті, можна змусити програму виконуватися набагато швидше. З цьому разі випливає ідея просторово-часової складності алгоритмів. У цьому підході складність алгоритму становить термінах часу й простору, і залишається компроміс з-поміж них. У цьому вся матеріалі основну увагу приділяється тимчасової складності, але коли ми також постаралися звернути увага фахівців і на особливі вимоги обсягу пам’яті декому алгоритмів. Наприклад, сортування злиттям (mergesort), обговорювана о 9-й главі, вимагає більше тимчасової пам’яті. Інші алгоритми, наприклад пірамідальна сортування (heapsort), що також обговорюється о 9-й главі, вимагає звичайного обсягу памяти.

Оценка з точністю до порядка

При порівнянні різних алгоритмів важливо усвідомлювати, як складність алгоритму співвідноситься складності розв’язуваної завдання. При розрахунках за одним алгоритму сортування тисячі чисел триватиме 1 секунду, а сортування мільйона — 10 секунд, тоді як розрахунки з іншому алгоритму можуть зажадати 2 і п’яти секунд відповідно. І тут не можна однозначно сказати, яка з двох програм краще — це від вихідних даних. Різниця продуктивності алгоритмів на завданнях різною обчислювальної складності часто важливіше, ніж просто швидкість алгоритму. У вищенаведеному разі, перший алгоритм швидше сортує короткі списки, а другий — довгі. Продуктивність алгоритму можна оцінити усе своєю чергою величини. Алгоритм має складність порядку O (f (N)) (вимовляється «Про велике від F від N»), якщо час виконання алгоритму зростає пропорційно функції f (N) з збільшенням розмірності вихідних даних N. Наприклад, розглянемо фрагмент коду, сортирующий позитивні числа:

For I = 1 To N

" Пошук найбільшого елемента у списке.

MaxValue = 0

For J = 1 to N

If Value (J) > MaxValue Then

MaxValue = Value (J)

MaxJ = J

End If

Next J

" Висновок найбільшого елемента на печать.

Print Format$(MaxJ) & «: «& Str$(MaxValue)

" Обнуління елемента щоб уникнути його з подальшого поиска.

Value (MaxJ) = 0 Next I

===============3

В цьому алгоритмі змінна циклу I послідовно приймає значення від 1 до N. До кожного збільшення I змінна J своєю чергою також приймає значення від 1 до N. Отже, у кожному зовнішньому циклі виконується ще N внутрішніх циклів. У результаті внутрішній цикл виконується N*N чи N2 разів, і, отже, складність алгоритму порядку O (N2). Оцінюючи порядку складності алгоритмів використовується лише найшвидше зросла частина рівняння алгоритму. Припустимо, час виконання алгоритму пропорційно N3+N. Тоді складність алгоритму дорівнюватиме O (N3). Відкидання повільно зростаючих частин рівняння дозволяє оцінити поведінка алгоритму зі збільшенням розмірності даних завдання N. При великих N внесок другій частині в рівняння N3+N стає менш помітним. При N=100, різницю N3+N=1. 000. 100 і N3 дорівнює усього сто, чи менш як 0,01 відсотка. Але це правильно лише великих N. При N=2, різницю між N3+N =10 і N3=8 дорівнює 2, але це вже 20 відсотків. Постійні множники у відсотковому співвідношенні також ігноруються. Це дозволяє легко оцінити зміни у обчислювальної складності завдання. Алгоритм, час виконання якого пропорційно 3*N2, матиме порядок O (N2). Якщо збільшити N вдвічі, той час виконання завдання зросте приблизно 22, тобто у 4 разу. Ігнорування постійних множників дозволяє також спростити підрахунок числа кроків алгоритму. У минулому прикладі внутрішній цикл виконується N2 раз, у своїй всередині циклу виконується кілька інструкцій. Можна просто підрахувати число інструкцій If, можна визначити також інструкції, що їх всередині циклу, або, ще, що й інструкції в зовнішньому циклі, наприклад оператори Print. Обчислювальна складність алгоритму цьому буде пропорційна N2, 3*N2 чи 3*N2+N. Оцінка складності алгоритму усе своєю чергою величини дасть один і той ж значення O (N3) і відпаде необхідність у точному підрахунку кількості операторов.

Поиск складних частин алгоритма

Обычно найскладнішим є виконання циклів і викликів процедур. У попередньому прикладі, весь алгоритм полягає у двох циклах.

============4

Если процедура викликає іншу процедуру, необхідно враховувати складність спричиненої процедури. Якщо ній виконується фіксований число інструкцій, наприклад, здійснюється висновок на печатку, то, при оцінці порядку складності яку можна нехтувати. З іншого боку, тоді як спричиненої процедурі виконується O (N) кроків, вони можуть вносити значний внесок у складність алгоритму. Якщо виклик процедури здійснюється всередині циклу, цей внесок може й більше. Наведемо за приклад програму, що містить повільну процедуру Slow складності порядку O (N3) і швидку процедуру Fast складності порядку O (N2). Складність всієї програми залежатиме від співвідношень між цими двома процедурами. Якщо процедура Slow викликається у кожному циклі процедури Fast, порядки складності процедур перемножуються. І тут складність алгоритму дорівнює твору O (N2) і O (N3) чи O (N3*N2)=O (N5). Наведемо який ілюструє цей випадок фрагмент кода:

Sub Slow () Dim I As Integer Dim J As Integer Dim K As Integer

For I = 1 To N

For J = 1 To N

For K = 1 To N

" Виконати будь-які действия.

Next K

Next J

Next I End Sub

Sub Fast () Dim I As Integer Dim J As Integer Dim K As Integer

For I = 1 To N

For J = 1 To N

Slow «Виклик процедури Slow.

Next J

Next I End Sub

Sub MainProgram ()

Fast End Sub

С з іншого боку, якщо процедури незалежно викликаються з основний програми, їх обчислювальна складність підсумовується. І тут повна складність дорівнюватиме O (N3)+O (N2)=O (N3). Таку складність, наприклад, буде мати наступний фрагмент кода:

Sub Slow () Dim I As Integer Dim J As Integer Dim K As Integer

For I = 1 To N

For J = 1 To N

For K = 1 To N

" Виконати будь-які действия.

Next K

Next J

Next I End Sub

Sub Fast () Dim I As Integer Dim J As Integer

For I = 1 To N

For J = 1 To N

" Виконати будь-які действия.

Next J

Next I End Sub

Sub MainProgram ()

Slow

Fast End Sub

==============5

Складність рекурсивних алгоритмов

Рекурсивными процедурами (recursive procedure) називаються процедури, викликають самі себе. Багато рекурсивних алгоритми саме ступінь вкладеності рекурсії визначає складність алгоритму, у своїй який завжди легко оцінити порядок складності. Рекурсивна процедура може бути простий, та заодно вносити великий внесок у складність програми, багаторазово викликаючи себе. Наступний фрагмент коду містить підпрограму лише з двох операторів. Тим щонайменше, для заданого N підпрограма виконується N раз, в такий спосіб, обчислювальна складність фрагмента порядку O (N).

Sub CountDown (N As Integer)

If N 20 Then

ArraySize = ArraySize -10

ReDim Preserve List (1 To ArraySize)

End If End Sub

=============20

Для великих масивів це рішення може також виявитися не самим найкращим. Якщо до вас потрібен список, у якому 1000 елементів, якого зазвичай додається по 100 елементів, то ми все ще занадто чимало часу буде витрачатися зміна розміру масиву. Очевидною стратегією у разі було б збільшення збільшення розміру масиву з десятьма до 100 чи більше осередків. Тоді можна було б додавати по 100 елементів одночасно без частого зміни розміру списку. Більше гнучким рішенням буде зміна збільшення залежно від розміру масиву. Для невеликих списків це прирощення було також невеликим. Хоча зміни розміру масиву відбувалися б частіше, вони зажадали відносно небагато часу для невеликих масивів. Для великих списків, прирощення розміру буде більше, тому їх розмір змінюватися рідше. Наступна програма намагається підтримувати приблизно 10 відсотків списку вільним. Коли масив заповнюється, її розмір поповнюється 10 відсотків. Якщо вільне простір становить понад 20 відсотків від розміру масиву, програма зменшує його. При збільшенні розміру масиву, додається незгірш від 10 елементів, навіть якщо 10 відсотків від розміру масиву становлять меншу величину. Це зменшує число необхідних змін розміру масиву, якщо список дуже мал.

Const WANT_FREE_PERCENT = .1 ' 10% вільного місця. Const MIN_FREE = 10 ' Мінімальна число порожніх осередків. Global List () As String ' Масив елементів списку. Global ArraySize As Integer ' Розмір масиву. Global NumItems As Integer ' Кількість елементів у списку. Global ShrinkWhen As Integer ' Зменшити розмір, якщо NumItems < ShrinkWhen.

' Якщо масив заповнений, збільшити її розмір. ' Потім додати новий елемент насамкінець списку. Sub Add (value As String)

NumItems = NumItems + 1

If NumItems > ArraySize Then ResizeList

List (NumItems) = value End Sub

' Видалити останній елемент зі списку. ' Якщо масиві багато порожніх осередків, зменшити її розмір. Sub RemoveLast ()

NumItems = NumItems — 1

If NumItems < ShrinkWhen Then ResizeList End Sub

' Збільшити розмір масиву, щоб 10% осередків мали свободу. Sub ResizeList () Dim want_free As Integer want_free = WANT_FREE_PERCENT * NumItems

If want_free < MIN_FREE Then want_free = MIN_FREE

ArraySize = NumItems + want_free

ReDim Preserve List (1 To ArraySize)

' Зменшити розмір масиву, якщо NumItems < ShrinkWhen.

ShrinkWhen = NumItems — want_free End Sub

===============21

Клас SimpleList

Чтобы використовувати ця проста підхід, програмі треба зазначити все параметри списку, цьому треба ознайомитися з розміром масиву, числом використовуваних елементів, тощо. Якщо потрібно створити довше списку, знадобиться безліч копій змінних і код, управляючий різними списками, буде дублюватися. Класи Visual Basic можуть сильно полегшити виконання це завдання. Клас SimpleList инкапсулирует цей дивний організм списку, спрощуючи управління списками. У цьому вся класі присутні методи Add і Remove від використання в основний програмі. У ньому також є процедури вилучення властивостей NumItems і ArraySize, з допомогою яких програма може число елементів в списку і обсяг яку він обіймав пам’яті. Процедура ResizeList оголошено як всередині класу SimpleList. Це приховує зміна розміру списку основної програми, оскільки це код повинен застосовуватися лише всередині класу. Використовуючи клас SimpleList, легко створення у додатку кілька списків. Щоб створити новий об'єкт кожному за списку, просто використовується оператор New. Кожен із об'єктів має перемінні, тому кожен із них може керувати окремим списком:

Dim List1 As New SimpleList Dim List2 As New SimpleList

Когда об'єкт SimpleList збільшує масив, виводить вікно повідомлення, що показує розмір масиву, кількість невикористовуваних елементів у ньому, і значення перемінної ShrinkWhen. Коли число використаних осередків у масиві дедалі менше, ніж значення ShrinkWhen, програма зменшує розмір масиву. Зауважимо, що коли і масив практично порожній, змінна ShrinkWhen іноді стає рівної нулю чи негативною. І тут розмір масиву нічого очікувати зменшуватися, навіть коли ви приберіть все елементи з списка.

=============22

Программа SimList додає до масиву ще 50 відсотків порожніх осередків, якщо слід збільшити її розмір, і завжди залишає у своїй щонайменше 1 порожній осередки. Ці значення був обрані для зручності роботи з програмою. У реальному додатку, відсоток вільної пам’яті може бути менше, а число вільних осередків більше. Більше розумним у разі було обрати значення порядку 10 відсотків від того плинного розміру списку та мінімум 10 вільних ячеек.

Неупорядоченные списки

В деяких додатках може знадобитися видаляти елементи із середини списку, додаючи у своїй елементи насамкінець списку. І тут порядок розташування елементів може бути важливий, та заодно то, можливо необхідно видаляти певні елементи зі списку. Списки подібного типу називаються неупорядоченными списками (unordered lists). Вони також інколи називаються «безліччю элементов».

Неупорядоченный список повинен підтримувати такі операції: 1. додавання елемента до списку; 2. видалення елемента із списку; 3. визначення наявності елемента у списку; 4. виконання будь-яких операцій (наприклад, виведення дисплей чи принтер) всім елементів списку. Просту структуру, подану у минулому параграфі, можна легко змінити у тому, щоб обробляти такі списки. Коли видаляється елемент із середини списку, інші елементи зсуваються однією позицію, заповнюючи зчинений проміжок. Це показано на рис. 2. 1, у якому другий елемент видаляється зі списку, та третій, четвертий, і п’ятий елементи зсуваються вліво, заповнюючи вільний ділянку. Видалення з масиву елемента за такого підходу триватиме досить чимало часу, якщо видаляється елемент на початку списку. Щоб видалити перший елемент з масиву з 1000 елементів, знадобиться зрушити вліво однією позицію 999 елементів. Набагато швидше видаляти елементи можна з допомогою простої схеми чистки пам’яті (garbage collection). Замість видалення елементів зі списку, позначте їх як невикористовувані. Якщо елементи списку — дані простих типів, наприклад цілі, можна помічати елементи, використовуючи певне, зване «сміттєве» значення (garbage value).

@Малюнок 2.1 Видалення елемента із середини массива

===========23

Для цілих чисел можна використовуватиме цього значення -32. 767. Для перемінної типу Variant можна використовувати значення NULL. Це значення присвоюється кожному неиспользуемому елементу. Наступний фрагмент коду демонструє видалення елемента із подібного целочисленного списка:

Const GARBAGE_VALUE = -32 767

' Позначити елемент як невикористовуваний. Sub RemoveFromList (position As Long)

List (position) = GARBAGE_VALUE End Sub

Если елементи списку — це структури, певні оператором Type, ви можете додати такий структурі нове полі IsGarbage. Коли елемент видаляється зі списку, значення поля IsGarbage встановлюється в True.

Type MyData

Name As Sring ' Данные.

IsGarbage As Integer ' Цей елемент немає? End Type

' Позначити елемент, як і який використовується. Sub RemoveFromList (position As Long)

List (position). IsGarbage = True End Sub

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

' Печатка елементів списку. Sub PrintItems () Dim I As Long

For I = 1 To ArraySize

If Not IsNull (List (I)) Then ' Якщо елемент не помечен

Print Str$(List (I)) ' надрукувати его.

End If

Next I End Sub

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

=============24

Для уникнення цього, можна періодично запускати процедуру очищення пам’яті (garbage collection routine). Цю процедуру переміщає все непомеченные запис у початок масиву. Після цього можна додати їх до вільним елементам наприкінці масиву. Коли знадобиться додати масиву додаткові елементи, їхнє співчуття також можна використовувати без зміни розміру масиву. Після додавання помічених елементів решти вільним осередків масиву, повний обсяг вільного простору може досить великим, й у цьому випадку можна зменшити розмір масиву, звільняючи память:

Private Sub CollectGarbage () Dim і As Long Dim good As Long

good = 1 ' Перший використовуваний элемент.

For і = 1 To m_NumItems

' Якщо не помечен, перемістити його за нове место.

If Not IsNull (m_List (i)) Then m_List (good) = m_list (i) good = good + 1

End If

Next i

' Останній використовуваний елемент. m_NumItems (good) = good — 1

' Чи потрібне зменшувати розмір списка?

If m_NumItems < m_ShrinkWhen Then ResizeList End Sub

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

===========25

Во-вторых, якщо список починає заповнюватися непотрібними даними, процедури, які використовують, можуть бути надзвичайно неефективними. Якщо масиві з 30. 000 елементів 25. 000 не використовуються, підпрограма типу описаної вище PrintItems, може виконуватися жахливо повільно. І, нарешті, чистка пам’яті для дуже великої масиву вимагатиме значного часу, особливо, якщо обході елементів масиву програмі доводиться звертатися до сторінкам, вивантаженим на диск. Це може спричинить «подвисанию» вашої програми сталася на кілька секунд під час чистки пам’яті. Щоб розв’язати цю проблему, можна створити нову зміну GarbageCount, в якої перебувати число непотрібних елементів у списку. Коли значної частини пам’яті, займаній списком, містить непотрібні елементи, ви може, розпочати процедуру «складання мусора».

Dim GarbageCount As Long ' Кількість непотрібних елементів. Dim MaxGarbage As Long ' Це значення визначається ResizeList.

' Позначити елемент як непотрібний. ' Якщо «сміття» занадто багато, розпочати чистку пам’яті. Public Sub Remove (position As Long) m_List (position) = Null m_GarbageCount = m_GarbageCount + 1

' Якщо «сміття» занадто багато, розпочати чистку памяти.

If m_GarbageCount > m_MaxGarbage Then CollectGarbage End Sub

Программа Garbage демонструє його чистки пам’яті. Вона пише поруч із неиспользуемыми елементами списку слово «unused», а поруч із позначеними як непотрібні - слово «garbage». Програма використовує клас GarbageList приблизно як і, як програма SimList використовувала клас SimpleList, та заодно вона здійснює «складання сміття». Щоб додати елемент до списку, введіть його значення і натиснімо на кнопку Add (Додати). Для видалення елемента виділіть його, та був натисніть на кнопку Remove (Видалити). Якщо список містить занадто багато «сміття», програма почне виконувати чистку пам’яті. При кожному зміну величини списку об'єкта GarbageList, програма виводить вікно повідомлення, у якому наводиться число які і вільних елементів у списку, і навіть значення змінних MaxGarbage і ShrinkWhen. Якщо видалити достатньо елементів, отже більше, ніж MaxGarbage елементів будуть позначені як непотрібні, програма почне виконувати чистку пам’яті. Після цього її закінчення, програма зменшує розмір масиву, коли він містить менше, ніж ShrinkWhen зайнятих елементів. Якщо розмір масиву має збільшитися, програма Garbage додає до масиву ще 50 відсотків порожніх осередків, і завжди залишає хоча б одну порожню осередок незалежно від зміну величини масиву. Ці значення були обрані спрощення роботи користувача з переліком. У реальної програмі відсоток вільної пам’яті може бути менше, а число вільних осередків — більше. Оптимальними виглядають значення порядку 10 відсотків і десяти вільних ячеек.

==========26

Зв’язкові списки

Другая стратегія використовується при управлінні пов’язаними списками. Пов’язаний список зберігає елементи в структурах даних чи об'єктах, які називаються осередками (cells). Кожна осередок містить покажчик для наступної осередок в списку. Бо єдиний тип покажчиків, які підтримують Visual Basic — це посилання об'єкти, то осередки в зв’язковому списку мали бути зацікавленими об'єктами. У класі, задающем осередок, має визначитися змінна NextCell, яка визначає для наступної осередок у списку. У ньому також мають бути визначено перемінні, містять дані, з яким працювати програма. Ці перемінні можуть бути оголошені як відкриті (public) всередині класу, чи клас може містити процедури для читання і запис значень цих змінних. Наприклад, в зв’язковому списку з записами працівників, в цих полях можуть бути ім'я співробітника, номер соціального страхування, назва посади, тощо. Визначення для класу EmpCell вигляд матимуть приблизно так:

Public EmpName As String Public SSN As String Public JobTitle As String Public NextCell As EmpCell

Программа відкриває нові осередки з допомогою оператора New, задає їх значення і з'єднує їх, використовуючи зміну NextCell. Програма завжди повинна зберігати посилання вершину списку. А, щоб визначити, де закінчується список, програма має встановити значення NextCell останньому елемента списку рівним Nothing (нічого). Наприклад, наступний фрагмент коду створює список, що становить трьох сотрудников:

Dim top_cell As EmpCell Dim cell1 As EmpCell Dim cell2 As EmpCell Dim cell3 As EmpCell

' Створення ячеек.

Set cell1 = New EmpCell cell1. EmpName = «Стівенс» cell1. SSN = «123−45−6789 «cell1. JobTitle = «Автор «

Set cell2 = New EmpCell cell2. EmpName = «Кэтс» cell2. SSN = «123−45−6789 «cell2. JobTitle = «Юрист «

Set cell3 = New EmpCell cell3. EmpName = «Тулі» cell3. SSN = «123−45−6789 «cell3. JobTitle = «Менеджер «

' Поєднати осередки, створюючи зв’язний список.

Set cell1. NextCell = cell2

Set cell2. NextCell = cell3

Set cell3. NextCell = Nothing

' Зберегти посилання вершину списка.

Set top_cell = cell1

===============27

На рис. 2.2 показано схематичне представлення цього проекту зв’язкового списку. Прямокутники представляють осередки, а стрілки — посилання об'єкти. Маленький перекреслений прямокутник представляє значення Nothing, позначає кінець списку. Майте у вигляді, що top_cell, cell1 і cell2 — це справжні об'єкти, лише посилання, які свідчить про них. Наступний код використовує зв’язний список, побудований з допомогою попереднього прикладу до друку імен працівників із списку. Змінна ptr використовується як покажчика на елементи списку. Вона спочатку свідчить про вершину списку. У коді використовується цикл Do для переміщення ptr за списком до того часу, поки покажчик не сягне кінця списку. Під час кожного циклу, процедура друкує полі EmpName осередки, яку вказує ptr. Потім вона збільшує ptr, нагадуючи про таку осередок у списку. Наприкінці кінців, ptr сягає кінця списку та отримує значення Nothing, та циклу Do останавливается.

Dim ptr As EmpCell

Set ptr = top_cell ' Почати з вершини списка.

Do While Not (ptr Is Nothing)

' Вивести полі EmpName цієї ячейки.

Debug. Print ptr. Empname

' Перейти до наступній осередку в списке.

Set ptr = ptr. NextCell

Loop

После виконання коду ви отримаєте наступний результат:

Стивенс Кэтс Туле

@Рис. 2.2. Зв’язний список

=======28

Использование покажчика в інший об'єкт називається непрямої адресацією (indirection), оскільки ви використовуєте покажчик для непрямого маніпулювання даними. Непряма адресація може дуже заплутаною. Навіть для простого розташування елементів, такого, як зв’язний список, інколи важко запам’ятати, який об'єкт вказує кожна посилання. У складних структурах даних, покажчик може посилатись на об'єкт, у якому інший покажчик. Якщо кілька покажчиків і кілька рівнів непрямої адресації, ви легко можете заплутатися у яких А, щоб полегшити розуміння, у викладі використовуються ілюстрації, такі як рис. 2. 2,(для мережевий версії виключені, т.к. вони багаторазово збільшують розмір загружаемого файла) аби допомогти вам наочно уявити ситуацію там, де може бути. Чимало з подібних алгоритмів, які використовують покажчики, можна легко проілюструвати подібними рисунками.

Добавление елементів до зв’язков списку

Простой зв’язний список, показаний на рис. 2. 2, має кількома важливими властивостями. По-перше, можна дуже просто додати нову осередок в початок списку. Встановимо покажчик нової осередки NextCell на поточну вершину списку. Потім встановимо покажчик top_cell нові осередок. Рис. 2.3 відповідає цієї операції. Код мовою Visual Basic з цією операції дуже прост:

Set new_cell. NextCell = top_cell Set top_cell = new_cell

@Рис. 2.3. Додавання елемента у початок зв’язкового списка

Сравните розмір цього коду і коду, який довелося б написати для додавання нового елемента у початок списку, заснованого на масиві, в якому потрібно було перемістити все елементи масиву однією позицію, щоб звільнити місце для створення нового елемента. Ця операція складності порядку O (N) вимагатиме чимало часу, якщо список досить довгий. Використовуючи зв’язний список, моно додати новий елемент на початок списку всього кілька шагов.

======29

Так ж легко додати новий елемент й у середину зв’язкового списку. Припустимо, ви мені хочете вставити новий елемент після осередки, яку вказує змінна after_me. Встановимо значення NextCell нової осередки рівним after_me. NextCell. Тепер встановимо покажчик after_me. NextCell на нову осередок. Ця операція показано на рис. 2.4. Код на Visual Basic знову дуже прост:

Set new_cell. NextCell = after_me. NextCell Set after_me. NextCell = new_cell

Видалення елементів з зв’язкового списка

Удалить елемент з вершини зв’язкового списку як і просто, як і додати його. Просто встановіть покажчик top_cell для наступної осередок у списку. Рис. 2.5 відповідає цієї операції. Вихідний код з цією операції ще простіше, ніж код для додавання элемента.

Set top_cell = top_cell. NextCell

Когда покажчик top_cell переміщається другого елемент у списку, в програмі большє нє залишиться змінних, вказують перший об'єкт. У цьому випадку, лічильник посилань цей об'єкт стане нульовий, і системи автоматично знищить його. Також просто видалити елемент із середини списку. Припустимо, ви бажаєте видалити елемент, стоїть після осередки after_me. Просто встановіть покажчик NextCell цієї осередки для наступної осередок. Ця операція показано на рис. 2.6. Код на Visual Basic простий і понятен:

after_me. NextCell = after_me. NextCell. NextCell

@Рис. 2.4. Додавання елемента у середину зв’язкового списка

=======30

@Рис. 2.5. Видалення елемента із початку зв’язкового списка

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

Уничтожение зв’язкового списка

Можно припустити, що з знищення зв’язкового списку необхідно обійти весь список, встановлюючи значення NextCell всім осередків рівним Nothing. Насправді процес набагато простіше: лише top_cell приймає значення Nothing. Коли програма встановлює значення top_cell рівним Nothing, лічильник посилань перша осередки стає рівним нулю, і Visual Basic знищує цей осередок. Під час знищення осередки, система визначає, що у полі NextCell цієї осередки міститься посилання іншу осередок. Оскільки перший об'єкт знищується, то число посилань другого об'єкт зменшується. У цьому лічильник посилань другого об'єкт списку стає рівним нулю, тому система знищує та її. Під час знищення другого об'єкта, система зменшує число посилань на третій об'єкт, тощо до того часу, доки всі об'єкти у списку ні знищені. Коли програмою вже нічого очікувати посилань на об'єкти списку, можна знищити й усе список з допомогою єдиного оператора Set top_cell = Nothing.

@Рис. 2.6. Видалення елемента із середини зв’язкового списка

========31

Сигнальні метки

Для додавання чи видалення елементів з початку чи середини списку використовуються різні процедури. Можна звести обидва цих випадку одного і позбутися надлишкового коду, якщо запровадити спеціальну сигнальну мітку (sentinel) від початку списку. Сигнальну мітку не можна видалити. Вона не містить даних, і використовується лише позначення початку списку. Нині замість здобуття права обробляти особливий випадок додавання елемента у початок списку, можна поміщати елемент після мітки. Так само, замість особливого випадку видалення першого елемента із списку, просто видаляється елемент, наступний за міткою. Використання сигнальних міток доки вносить особливих відмінностей. Сигнальні мітки відіграють істотне значення значно складніших алгоритми. Вони дозволяють обробляти особливі випадки, такі як початок списку, як звичайні. У цьому потрібно написати, і налагодити менше коду, і алгоритми стають більш узгодженими і більше простими розуміння. У табл. 2.1 порівнюється складність виконання деяких типових операцій із використанням списків з урахуванням масивів зі «складанням сміття» чи зв’язкових списків. Списки з урахуванням масивів мають ще одна перевага: вони використовують менше пам’яті. Для зв’язкових списків треба додати полі NextCell до кожного елементу даних. Кожна посилання об'єкт займає чотири додаткових байта пам’яті. Для великих масивів це вимагатиме великих витрат пам’яті. Програма LnkList1 демонструє простий зв’язний список з сигнальною міткою. Запровадьте значення в текстове полі введення, і натисніть на елемент в списку чи мітку. Потім натиснімо на кнопку Add After (Додати після), і програма додасть новий елемент після зазначеного. Для видалення елемента із списку, натисніть на елемент і далі на кнопку Remove After (Видалити после).

@Таблица 2.1. Порівняння списків з урахуванням масивів і зв’язкових списков

=========32

Інкапсуляція зв’язкових списков

Программа LnkList1 управляє списком явно. Наприклад, наступний код показує, як програма видаляє елемент зі списку. Коли підпрограма розпочинає свою роботу, глобальна змінна SelectedIndex дає становище елемента, попереднього удаляемому елементу у списку. Змінна Sentinel містить посилання сигнальну мітку списка.

Private Sub CmdRemoveAfter_Click () Dim ptr As ListCell Dim position As Integer

If SelectedIndex < 0 Then Exit Sub

' Знайти элемент.

Set ptr = Sentinel position = SelectedIndex

Do While position > 0 position = position — 1

Set ptr = ptr. nextCell

Loop

' Видалити следуюший элемент.

Set ptr. NextCell = ptr. NextCell. NextCell

NumItems = NumItems — 1

SelectItem SelectedIndex ' Знову вибрати элемент.

DisplayList

NewItem. SetFocus End Sub

Чтобы спростити використання зв’язкового списку, можна инкапсулировать його функції у п’ятому класі. Це реалізовано програмі LnkList2. Вона аналогічна програмі LnkList1, але використовує керувати списком клас LinkedList. Клас LinekedList управляє внутрішньої організацією зв’язкового списку. У ньому перебувають процедури для додавання і видалення елементів, повернення значення елемента з його індексу, числа елементів у списку, і очищення списку. Цей клас дозволяє звертатися зі зв’язковим списком майже з масивом. Це значно спрощує основну програму. Наприклад, наступний код показує, як програма LnkList2 видаляє елемент зі списку. Одна тільки рядок у програмі насправді відпо-відає видалення елемента. Інші відбивають новий список. Порівняйте цей код з попереднім процедурой:

Private sub CmdRemoveAfter_Click ()

Llist. RemoveAfter SelectedIndex

SelectedItem SelectedList ' Знову вибрати элемент.

DisplayList

NewItem. SetFocus

CmdClearList. Enabled End Sub

=====33

Доступ до ячейкам

Класс LinkedList, використовуваний програмою LnkLst2, дозволяє основний програмі використовувати список майже масив. Наприклад, підпрограма Item, приведений у наступному коді, повертає значення елемента з його положению:

Function Item (ByVal position As Long) As Variant Dim ptr As ListCell

If position < 1 Or position > m_NumItems Then

' Вихід поза межі. Повернути NULL.

Item = Null

Exit Function

End If

' Знайти элемент.

Set ptr = m_Sentinel

Do While position > 0 position = position — 1

Set ptr = ptr. NextCell

Loop

Item = ptr. Value End Function

Эта процедура є простою, але він не використовує переваги зв’язковою структури списку. Наприклад, припустимо, що програмі потрібно послідовно перебрати всі об'єкти у списку. Вона могла використовувати підпрограму Item для почергового доступу до них, як показано наступного коде:

Dim і As Integer

For і = 1 To LList. NumItems

' Виконати будь-які дії з LList. Item (i).

:

Next i

При кожному виклик процедури Item, вона переглядає список у пошуках наступного елемента. Щоб знайти елемент I, програма повинна пропустити I-1 елементів. Щоб перевірити все елементи у списку з N елементів, процедура пропустить 0+1+2+3+…+N-1 =N*(N-1)/2 елемента. При великих N програма втратить чимало часу на перепустку елементів. Клас LinkedList може прискорити цю операцію, використовуючи інший метод доступу. Можна також використовувати приватну зміну m_CurrentCell для відстежування поточної позиції з списку. Для повернення значення поточного становища використовується підпрограма CurrentItem. Процедури MoveFirst, MoveNext і EndOfList дозволяють основну програму управляти поточної позицією в списке.

=======34

Например, наступний код містить підпрограму MoveNext:

Public Sub MoveNext ()

' Якщо поточна осередок не обрано, щось делать.

If Not (m_CurrentCell Is Nothing) Then _

Set m_CurrentCell = m_CurrentCell. NextCell End Sub

При допомоги цих процедур, основна програма може звернутися всім елементам списку, використовуючи наступний код. Ця версія трохи складніше, ніж попередня, але набагато ефективніше. Замість пропускати N*(N-1)/2 елементів і опитувати почергово все N елементів списку, вона пропускає жодного. Якщо список складається з 1000 елементів, це заощаджує майже півмільйона шагов.

LList. MoveFirst

Do While Not LList. EndOfList

' Виконати будь-які дії над елементом LList. Item (i).

:

LList. MoveNext Loop

Программа LnkList3 використовує нові методи керувати зв’язковим списком. Вона аналогічна програмі LnkList2, а більш ефективно звертається до елементам. Для невеликих списків, які у програмі, ця різниця непомітна. Для програми, яка звертається всім елементам великого списку, цю версію класу LinkedList більш эффективна.

Разновидности зв’язкових списков

Связные списки відіграють істотне значення у багатьох алгоритми, і що ви чи з лицарями у протязі всього матеріалу. У наступних розділах обговорюються кілька спеціальних різновидів зв’язкових списков.

Циклические зв’язкові списки

Вместо здобуття права встановлювати покажчик NextCell рівним Nothing, можна з’ясувати час його перший елемент списку, створюючи циклічний список (circular list), як показано на рис. 2.7. Циклічні списки корисні, коли потрібно обходити ряд елементів в нескінченному циклі. При кожен крок циклу, програма просто переміщає покажчик на таку осередок у списку. Припустимо, є циклічний список елементів, у якому назви днів тижня. Тоді програма міг би перераховувати дні місяці, використовуючи наступний код:

===========35

@Рис. 2.7. Циклічний зв’язний список

' Тут є код до створення і настрою списку та т.д.

: ' Надрукувати календар озер місяцем. ' ' first_day — це індекс структури, що містить день тижня для ' першого дні місяця. Наприклад, місяць може починатися ' у понеділок. ' ' num_days — число днів, у місяці. Private Sub ListMonth (first_day As Integer, num_days As Integer) Dim ptr As ListCell Dim і As Integer

Set ptr = top_cell

For і = 1 to num_days

Print Format$(i) & «: «& ptr. Value

Set ptr = ptr. NextCell

Next I End Sub

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

Private Sub PrintList (start_cell As Integer) Dim ptr As Integer

Set ptr = start_cell

Do

Print ptr. Value

Set ptr = ptr. NextCell

Loop While Not (ptr Is start_cell) End Sub

========36

Проблема циклічних ссылок

Уничтожение циклічного списку вимагає значно більше уваги, ніж видалення звичайного списку. Якщо вже ви просто встановіть значення перемінної top_cell рівним Nothing, то програма зможе більше звернутися до списку. Проте, оскільки лічильник посилань першої осередки не нульовий, вона буде знищено. Кожна елемент списку вказує якась інша елемент, тому них нічого очікувати знищено. Це циклічних посилань (circular referencing problem). Оскільки осередки називають інші осередки, жодна їх нічого очікувати знищена. Програма неспроможна отримання доступу до жодної їх, тому зайнята ними пам’ять витрачатиметься даремно до роботи програми. Проблема циклічних посилань може зустрітися у цьому випадку. Багато мережі містять циклічні посилання — навіть одиночна осередок, полі NextCell якої свідчить про самі цю осередок, може викликати цієї проблеми. Рішення її у тому, щоб розбити ланцюг посилань. Наприклад, ви можете використовувати у своїй програмі наступний код знищення циклічного зв’язкового списка:

Set top_cell. NextCell = Nothing Set top_cell = Nothing

Первая рядок розбиває цикл посилань. Саме тоді другу осередок списку не вказує жодна змінна, тому система зменшує лічильник посилань осередки нанівець і нищить його. Це лічильник посилань втретє елемент нанівець, і, він також знищується. Цей процес відбувається триває до того часу, коли будуть знищено всі елементи списку, крім першого. Установка значення top_cell елемента у Nothing зменшує його лічильник посилань нанівець, і осередок також уничтожается.

Двусвязные списки

Во час обговорення зв’язкових списків ви могли помітити, більшість операцій визначалося в термінах виконання чогось після певної осередки у списку. Якщо задана певна осередок, легко додати чи видалити осередок після неї чи перерахувати що йдуть з ним осередки. Видалити саму осередок, вставити нову осередок перед ній чи перерахувати що йдуть перед ній осередки вже не легко. Проте, невеличке зміна дозволить полегшити й інші операції. Додамо нове полі покажчика до кожної осередку, яке свідчить про попередню осередок у списку. Використовуючи це нове полі, можна легко створити двусвязный список (doubly linked list), що дозволяє переміщатися уперед і тому за списку. Нині можна легко видалити осередок, вставити її перед інший осередком і перерахувати осередки у кожному направлении.

@Рис. 2.8. Двусвязный список

============37

Класс DoubleListCell, що використовується для таких типів списків, може оголошувати перемінні так:

Public Value As Variant Public NextCell As DoubleListCell Public PrevCell As DoubleListCell

Часто буває корисно зберігати покажчики і початок, і кінець двусвязного списку. Тоді ви зможете легко додавати елементи до будь-кого з кінців списку. Іноді також буває корисно розміщувати сигнальні мітки й у початку, і наприкінці списку. Тоді мері роботи з списком ви повинні стане піклуватися про тому, чи з початком, з серединою чи з кінцем списку. На рис. 2.9 показаний двусвязный список з сигнальними знаками. У цьому малюнку невикористовувані покажчики міток NextCell і PrevCell прописані у Nothing. Оскільки програму пізнає кінці списку, порівнюючи значення покажчиків осередків з сигнальними знаками, і перевіряє, рівні чи значення Nothing, установка цих значень рівними Nothing перестав бути абсолютно необхідної. Проте, це — ознака хорошого стилю. Код для вставки і видалення елементів з двусвязного списку подібний до наведеній раніше коду для односвязного списку. Процедури потребують лише незначних змінах до роботи з покажчиками PrevCell.

@Рис. 2.9. Двусвязный список з сигнальними метками

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

Public Sub RemoveItem (ByVal target As DoubleListCell) Dim after_target As DoubleListCell Dim before_target As DoubleListCell

Set after_target = target. NextCell

Set before_target = target. PrevCell

Set after_target. NextCell = after_target

Set after_target. PrevCell = before_target End Sub

Sub AddAfter (new_Cell As DoubleListCell, after_me As DoubleListCell) Dim before_me As DoubleListCell

Set before_me = after_me. NextCell

Set after_me. NextCell = new_cell

Set new_cell. NextCell = before_me

Set before_me. PrevCell = new_cell

Set new_cell. PrevCell = after_me End Sub

Sub AddBefore (new_cell As DoubleListCell, before_me As DoubleListCell) Dim after_me As DoubleListCell

Set after_me = before_me. PrevCell

Set after_me. NextCell = new_cell

Set new_cell. NextCell = before_me

Set before_me. PrevCell = new_cell

Set new_cell. PrevCell = after_me End Sub

===========39

Если знову подивитись рис. 2. 9, ви не побачите, кожна пара сусідніх осередків утворює циклічну заслання. Це знищення двусвязного списку трохи більше складним завданням, ніж знищення односвязных чи циклічних списків. Наступний код наводить одне із способів очищення двусвязного списку. Спочатку покажчики PrevCell всіх осередків встановлюються рівними Nothing, щоб розірвати циклічні посилання. Це з суті, перетворює список в односвязный. Коли посилання сигнальних міток встановлюються в Nothing, все елементи звільняються автоматично, як і як й у односвязном списке.

Dim ptr As DoubleListCell

" Очистити покажчики PrevCell, щоб розірвати циклічні ссылки.

Set ptr = TopSentinel. NextCell

Do While Not (ptr Is BottomSentinel)

Set ptr. PrevCell = Nothing

Set ptr = ptr. NextCell

Loop

Set TopSentinel. NextCell = Nothing

Set BottomSentinel. PrevCell = Nothing

Если створити клас, инкапсулирующий двусвязный список, його оброблювач події Terminate зможе знищувати список. Коли основна програма встановить значення посилання список рівним Nothing, список автоматично звільнить зайняту пам’ять. Програма DblLink працює із двусвязным списком. Вона дозволяє додавати елементи до чи ж після обраного елемента, і навіть видаляти обраний элемент.

=============39

Потоки

В деяких додатках буває зручно обходити зв’язний список у одному порядку. У різних частинах докладання вам може знадобитися виводити список співробітників з їхньої прізвищ, заробітної плати, идентификационному номера системи соціального страхування, чи фаху. Звичайний зв’язний список дозволяє переглядати елементи тільки одного порядку. Використовуючи покажчик PrevCell, можна створити двусвязный список, що дозволить переміщатися за списком уперед і тому. Такий підхід можна розвинути також далі, додавши більше покажчиків на структуру даних, дозволяючи виводити список у порядку. Набір посилань, який задає будь-якої порядок перегляду, називається потоком (thread), а сам отриманий список — многопоточным списком (threaded list). Не плутайте ці потоки з потоками, що надає система Windows NT. Список може містити скільки завгодно потоків, хоча, починаючи з певного моменту, гра годі свічок. Застосування потоку, упорядочивающего список співробітників на прізвище, буде обгрунтоване, якщо ваше додаток часто використовує цей порядок, на відміну розташування по батькові, яке навряд коли використовуватиметься. Деякі розташування годі організовувати як потоків. Наприклад, потік, упорядочивающий співробітників підлогою, навряд чи доцільний тому, що таке впорядкування легко отримати й ж без нього. А, щоб скласти список співробітників підлогою, не так важко обійти список з кожного іншому потоку, друкуючи прізвища жінок, та був повторити обхід вкотре, друкуючи прізвища чоловіків. Для отримання такого розташування достатньо лише двох проходів списку. Порівняйте на цей випадок про те, як ви хочете впорядкувати список співробітників на прізвище. Якщо список не включає потік прізвищ, вам доведеться знайти прізвище, яка першим у списку, потім наступну і т.д. Це процес складності порядку O (N2), набагато менш ефективний, ніж сортування підлогою складності порядку O (N). У випадку, завдання потоку то, можливо доцільно, якщо його необхідно часто використовувати, і якщо необхідності отримати хоча б порядок дуже складно. Потік непотрібен, якщо його завжди легко створити наново. Програма Treads демонструє простий многопоточный список співробітників. Заповніть поля прізвища, спеціальності, статі та номери соціального страхування для створення нового співробітника. Потім натисніть на кнопку Add (Додати), щоб додати співробітника до списку. Програма містить потоки, які упорядковують список на прізвище по алфавіту й у зворотному напрямку, за безплатним номером соціального страхування і спеціальності у прямому й зворотному напрямку. Можете використовувати додаткові кнопки для вибору потоку, гаразд якого програма виводить список. На рис. 2. 10 показано вікно програми Threads з переліком співробітників, упорядкованим на прізвище. Клас ThreadedCell, використовуваний програмою Threads, визначає такі переменные:

Public LastName As String Public FirstName As String Public SSN As String Public Sex As String Public JobClass As Integer Public NextName As TreadedCell ' На прізвище у прямому порядку. Public PrevName As TreadedCell ' На прізвище у порядку. Public NextSSN As TreadedCell ' За номером у прямому порядку. Public NextJobClass As TreadedCell ' За фахом у прямому порядку. Public PrevJobClass As TreadedCell ' За фахом у протилежному порядке.

Класс ThreadedList инкапсулирует многопоточный список. Коли програма викликає метод AddItem, список оновлює свої потоки. До кожного потоку програма повинна вставити елемент в правильному порядку. Наприклад, у тому, щоб вставити запис із прізвищем «Сміт», програма обходить список, використовуючи потік NextName, до того часу, доки знайде елемент із прізвищем, які мають слідувати за «Сміт». Потім вона вставляє в потік NextName нову запис які були елементом. При визначенні місцеположення нових записів серед важливе значення мають сигнальні мітки. Оброблювач подій Class_Initialize класу ThreadedList створює сигнальні мітки на вершині та наприкінці списку та инициализирует їх покажчики те щоб вони вказували друг на друга. Потім значення мітки в початку списку встановлюється в такий спосіб, щоб він завжди перебувало до будь-якого значення реальних даних всім потоків. Наприклад, змінна LastName може містити строковые значення. Порожня рядок «» йде з алфавіту перед будь-якими дійсними значеннями рядків, тому програма встановлює значення сигнальною мітки LastName на початку списку рівним порожній рядку. Так само Class_Initialize встановлює значення даних для мітки в кінці списку, перевершували будь-які реальні значення переважають у всіх потоках. Оскільки «~ «йде з алфавіту після всіх видимих символів ASCII, програма встановлює значення поля LastName для мітки наприкінці списку рівним «~ «. Привласнюючи полю LastName сигнальних міток значення «» і «~ «, програма позбувається необхідності перевіряти особливі випадки, коли не потрібно вставити новий елемент на початок чи кінець списку. Будь-які нові справжні значення перебуватимуть між значеннями LastValue сигнальних міток, тому програма зможе визначити правильне становище для створення нового елемента, не переймаючись тим, ніж зайти за концевую мітку і вийти за кордону списка.

@Рис. 2. 10. Програма Threads

=====41

Следующий код показує, як клас ThreadedList вставляє новий елемент в потоки NextName і PrevName. Оскільки ці потоки використовують і той ж ключ — прізвища, програма може оновлювати їх одновременно.

Dim ptr As ThreadedCell Dim nxt As ThreadedCell Dim new_cell As New ThreadedCell Dim new_name As String Dim next_name As String

" Записати значення нової ячейки.

With new_cell

. LastName = LastName

. FirstName = FirstName

. SSN = SSN

•Sex = Sex

. JobClass = JobClass

End With

" Визначити місце нової осередки серед NextThread. new_name = LastName & «, «& FirstName

Set ptr = m_TopSentinel

Do

Set nxt = ptr. NextName next_name = nxt. LastName & «, «& nxt. FirstName

If next_name >= new_name Then Exit Do

Set ptr = nxt

Loop

" Вставити нову осередок в потоки NextName і prevName.

Set new_cell. NextName = nxt

Set new_cell. PrevName = ptr

Set ptr. NextName = new_cell

Set nxt. PrevName = new_cell

Чтобы такий працював, програма має гарантувати, що значення нової осередки лежать між значеннями міток. Наприклад, якщо користувач запровадить у ролі прізвища «~~ «, цикл вийде за мітку кінця списку, т.к. «~~ «йде після «~ «. Потім програма аварійно завершить роботу під час спроби доступу до значенням nxt. LastName, якщо nxt було встановлено рівним Nothing.

========42

Інші зв’язкові структуры

Используя покажчики, можна побудувати багато інших корисних різновидів зв’язкових структур, як-от дерева, нерегулярні масиви, зріджені масиви, графи і мережі. Осередок може містити будь-яке число покажчиків інші осередки. Наприклад, до створення двоичного дерева можна використовувати осередок, що містить два покажчика, одного лівого нащадка, і другий — на правого. Клас BinaryCell може складатися зі наступних определений:

Public LeftChild As BinaryCell Public RightChild As BinaryCell

На рис. 2. 11 показано дерево, створена із осередків подібного типу. О 6-й главі дерева обговорюються докладніше. Осередок навіть утримувати колекцію чи зв’язний список з покажчиками на інші осередки. Це дозволяє програмі зв’язати осередок із кожним числом інших об'єктів. На рис. 2. 12 наведено приклади інших зв’язкових структур даних. Ви також зустрінете схожі структури далі, особливо у 12 главе.

Псевдоуказатели

При допомоги посилань в Visual Basic можна легко створювати зв’язкові структури, такі як списки, дерева і мережі, але посилання вимагають додаткових ресурсів. Лічильники заслань та проблеми із розподілом пам’яті уповільнюють роботу структур даних, побудованих за використанням посилань. Інший стратегією, що найчастіше забезпечує кращу продуктивність, є застосування псевдоуказателей (fake pointers). У цьому програма створює масив структур даних. Замість використання посилань для зв’язування структур, програма використовує індекси масиву. Перебування елемента у масиві ввозяться Visual Basic швидше, ніж вибірка його за засланні на об'єкт. Це дає кращу продуктивність при застосуванні псевдоуказателей проти відповідними методами посилань на об'єкти. З іншого боку, застосування псевдоуказателей менш інтуїтивно, як застосування посилань. Це ускладнити розробку й налагодження складних алгоритмів, як-от алгоритми мереж чи збалансованих деревьев.

@Рис. 2. 11. Двоичное дерево

========43

@Рис. 2. 12. Зв’язкові структуры

Программа FakeList управляє зв’язковим списком, використовуючи псевдоуказатели. Вона створює масив простих структур даних для зберігання осередків списку. Програма аналогічна програмі LnkList1, але використовує псевдоуказатели. Наступний код демонструє, як програма FakeList створює масив клітинних структур:

" Структура даних осередки. Type FakeCell

Value As String

NextCell As Integer End Type

" Масив осередків зв’язкового списку. Global Cells (0 To 100) As FakeCell

" Сигнальна мітка списку. Global Sentinel As Integer

Поскольку псевдоуказатели — це посилання, а й просто цілі числа, програма неспроможна використовувати значення Nothing для маркування кінця списку. Програма FakeList використовує постійну END_OF_LIST, значення одно -32. 767 для позначення порожнього покажчика. Для полегшення виявлення невикористовуваних осередків, програма FakeList також використовує спеціальний «сміттєвий» список, у якому невикористовувані осередки. Наступний код демонструє ініціалізацію порожнього зв’язкового списку. У ньому сигнальна мітка NextCell приймає значення END_OF_LIST. Потім вона поміщає невикористовувані осередки в «сміттєвий» список.

========44

" Зв’язний список невикористовуваних осередків. Global TopGarbage As Integer

Public Sub InitializeList () Dim і As Integer

Sentinel = 0

Cells (Sentinel). NextCell = END_OF_LIST

" Помістити й інші осередки в «сміттєвий» список.

For і = 1 To UBound (Cells) — 1

Cells (i). NextCell = і + 1

Next i

Cells (UBound (Cells)). NextCell = END_OF_LIST

TopGarbage = 1 End Sub

При додаванні елемента до зв’язков списку, програма використовує першу доступну осередок з «сміттєвого» списку, инициализирует полі осередки Value і вставляє осередок до списку. Наступний код показує, як програма додає елемент після выбранного:

Private Sub CmdAddAfter_Click () Dim ptr As Integer Dim position As Integer Dim new_cell As Integer

" Знайти місце вставки. ptr = Sentinel position = Selectedlndex

Do While position > 0 position = position — 1 ptr = Cells (ptr). NextCell

Loop

" Вибрати нову осередок з «сміттєвого» списку. new_cell = TopGarbage

TopGarbage = Cells (TopGarbage). NextCell

" Вставити элемент.

Cells (new_cell). Value = NewItem. Text

Cells (new_cell). NextCell = Cells (ptr). NextCell

Cells (ptr). NextCell = new_cell

NumItems = NumItems + 1

DisplayList

SelectItem SelectedIndex + 1 «Вибрати новий элемент.

NewItem. Text = ««

NewItem. SetFocus

CmdClearList. Enabled = True End Sub

После видалення осередки зі списку, програма FakeList поміщає найвіддаленіші осередок в «сміттєвий» список, щоб їх потім можна було легко использовать:

Private Sub CmdRemoveAfter_Click () Dim ptr As Integer Dim target As Integer Dim position As Integer

If SelectedIndex < 0 Then Exit Sub

" Знайти елемент. ptr = Sentinel position = SelectedIndex

Do While position > 0 position = position — 1 ptr = Cells (ptr). NextCell

Loop

" Пропустити наступний елемент. target = Cells (ptr). NextCell

Cells (ptr). NextCell = Cells (target). NextCell

NumItems = NumItems — 1

" Додати найвіддаленіші осередок в «сміттєвий» список.

Cells (target). NextCell = TopGarbage

TopGarbage = target

SelectItem Selectedlndex «Знову вибрати элемент.

DisplayList

CmdClearList. Enabled = NumItems > 0

NewItem. SetFocus End Sub

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

=======45−46

Резюме

Используя посилання об'єкти, ви можете створювати гнучкі структури даних, такі як зв’язкові списки, циклічні зв’язкові списки і двусвязные списки. Ці списки дозволяють легко додавати і видаляти елементи із будь-якої місця списку. Додаючи додаткові посилання до класу осередків, можна перетворити двусвязный список в многопоточный. Розвиваючи і далі цих ідей, можна екзотичні структури даних, включаючи зріджені масиви, дерева, хэш- таблиці і мережі. Вони докладно описуються у таких главах.

========47

Глава 3. Стеки і очереди

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

Стеки

Стек (stack) — це упорядкований список, у якому додавання і видалення елементів завжди відбувається з одного боку списку. Можна уявити стік як стопку предметів на підлозі. Можете додавати елементи на її вершину і видаляти їх звідти, але з можете додавати чи видаляти елементи із середини стоси. Стеки часто називають списками типу перший ввійшов — останній вийшов (Last-In- First-Out list). За історичними причин, додавання елемента у стік називається проштовхуванням (pushing) елемента у стік, а видалення елемента із стека — виштовхуванням (popping) елемента із стека. Перша реалізація простого списку з урахуванням масиву, описана на початку 2 глави, є стеком. Для відстежування вершини списку використовується лічильник. Далі ця лічильник використовується для вставки чи видалення елемента з вершини списку. Невеликий зміна — це нова процедура Pop, яка видаляє елемент зі списку, одночасно повертаючи його значення. У цьому інші процедури можуть видобувати елемент і видаляти його зі списку за крок. Крім цього зміни, наступний код збігаються з кодом, які під 2 главе.

Dim Stack () As Variant Dim StackSize As Variant

Sub Push (value As Variant)

StackSize = StackSize + 1

ReDim Preserve Stack (1 To StackSize)

Stack (StackSize) = value End Sub

Sub Pop (value As Variant) value = Stack (StackSize)

StackSize = StackSize — 1

ReDim Preserve Stack (1 To StackSize) End Sub

=====49

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

Dim List () As Variant Dim NumItems As Integer

" Ініціалізація массива.

:

" Проштовхнути елементи в стік. For I = 1 To NumItems

Push List (I) Next I

" Виштовхнути елементи з стека знову на масив. For I = 1 To NumItems

Pop List (I) Next I

В цьому прикладі, довжина стека може багаторазово змінюватися доти, як, в результаті розширення зрештою, він спорожніє. Якщо відомо заздалегідь, наскільки великим повинен бути масив, можна відразу створити досить великий стік. Замість зміни розміру стека тоді, як він росте і зменшується, можна відвести під нього пам’ять на початку праці та знищити після її завершення. Наступний код дозволяє створити стік, якщо наперед відомий його максимальна величина. Процедура Pop не змінює розмір масиву. Коли програма завершує роботу зі стеком, вона повинна переважно викликати процедуру EmptyStack для звільнення зайнятою під стік памяти.

======50

Const WANT_FREE_PERCENT = .1 «10% вільного простору. Const MIN_FREE = 10 «Мінімальний розмір. Global Stack () As Integer «Стековый масив. Global StackSize As Integer «Розмір стекового масиву. Global Lastltem As Integer «Індекс останнього элемента.

Sub PreallocateStack (entries As Integer)

StackSize = entries

ReDim Stack (1 To StackSize) End Sub

Sub EmptyStack ()

StackSize = 0

LastItem = 0

Erase Stack «Звільнити пам’ять, зайняту масивом. End Sub

Sub Push (value As Integer)

LastItem = LastItem + 1

If LastItem > StackSize Then ResizeStack

Stack (LastItem) = value End Sub

Sub Pop (value As Integer) value = Stack (LastItem)

LastItem = LastItem — 1 End Sub

Sub ResizeStack () Dim want_free As Integer

want_free = WANT_FREE_PERCENT * LastItem

If want_free < MIN_FREE Then want_free = MIN_FREE

StackSize = LastItem + want_free

ReDim Preserve Stack (1 To StackSize) End Sub

Этот вид реалізації стеков досить ефективним у Visual Basic. Стік не витрачає даремно пам’ять, не надто часто змінює свій розмір, якщо відразу відомо, наскільки великим він має быть.

=======51

Численні стеки

В одному масиві можна створити два стека, помістивши як початку масиву, а інший — наприкінці. Для двох стеков використовуються окремі лічильники довжини стека Top, і стеки ростуть назустріч одна одній, як показано на рис. 3.1. Цей метод дозволяє двом стекам зростати, займаючи те ж область пам’яті, до того часу, поки вони зіштовхнуться, коли масив заповниться. На жаль, змінювати розмір цих стеков непросто. При збільшенні масиву необхідно зрушувати все елементи у верхній стеці, либонь для виділення пам’ять під нові елементи у середині. За зменшення масиву, необхідно спочатку зрушити елементи верхнього стека, до того, як змінювати розмір масиву. Цей метод також важко масштабувати для оперування більш як двома стеками. Зв’язкові списки надають гнучкіший метод побудови багатьох стеков. Для проштовхування елемента у стік, він міститься у початок зв’язкового списку. Для виштовхування елемента із стека, видаляється перший елемент з зв’язкового списку. Оскільки елементи додаються і видаляються лише на початку списку, для реалізації стеков подібного типу непотрібен застосування сигнальних міток чи двусвязных списків. Основна хиба застосування стеков з урахуванням зв’язкових списків полягає у тому, що вони вимагають додаткової пам’яті для зберігання покажчиків NextCell. Для стека з урахуванням масиву, що містить N елементів, потрібно всього 2*N байт пам’яті (по 2 байта на ціла кількість). Той-таки стік, реалізований з урахуванням зв’язкового списку, зажадає додатково 4*N байт пам’яті для покажчиків NextCell, збільшуючи розмір необхідної пам’яті втричі. Програма Stack використовує кілька стеков, реалізованих у вигляді зв’язкових списків. Використовуючи програму, можна вставляти і виштовхувати елементи з кожного з цих списків. Програма Stack2 аналогічна програмі, але він використовує клас LinkedListStack до роботи зі стеками.

Очереди

Упорядоченный список, у якому елементи додаються одного кінцю списку, а видаляються з іншого боку, називається чергою (queue). Група людей, очікують обслуговування у книгарні, утворює чергу. Знову прибулі підходять ззаду. Коли покупець сягає початку черги, касир його обслуговує. Через їх природи, черги іноді називають списками типу перший ввійшов — перший вийшов (First-In-First-Out list).

@Рис. 3.1. Два стека щодо одного массиве

=======52

Можно реалізувати черги, у Visual Basic, використовуючи методи типу використаних в організацію простих стеков. Створимо масив, і за допомоги лічильників визначатимемо становище початку й кінця черги. Значення перемінної QueueFront дає індекс елемента у початку черги. Змінна QueueBack визначає, куди може бути додано черговий елемент черги. Принаймні того як нові елементи додаються у чергу і залишають її, розмір масиву, що містить чергу, змінюється тож він зростає з одного боку і зменшується на другом.

Global Queue () As String «Масив черги. Global QueuePront As Integer «Початок черги. Global QueueBack As Integer «Кінець очереди.

Sub EnterQueue (value As String)

ReDim Preserve Queue (QueueFront To QueueBack)

Queue (QueueBack) = value

QueueBack = QueueBack + 1 End Sub

Sub LeaveQueue (value As String) value = Queue (QueueFront)

QueueFront = QueueFront + 1

ReDim Preserve Queue (QueueFront To QueueBack — 1) End Sub

К жалю, Visual Basic Демшевського не дозволяє використовувати ключовим словом Preserve в операторі ReDim, якщо змінюється нижню межу масиву. Навіть якби Visual Basic дозволяв виконання цієї операції, чергу у своїй «рухалася» би за пам’яті. При кожному додаванні чи видаленні елемента із черги, кордону масиву збільшувалися б. Після пропускання досить великої кількості елементів через чергу, її межі міг би у кінцевому результаті стати дуже великі. Тому, коли потрібно збільшити розмір масиву, спочатку необхідно перемістити дані на початок масиву. У цьому може утворитися достатньо вільних осередків у кінці масиву, отже збільшення розміру масиву може не знадобитися. Інакше, можна скористатися оператором ReDim збільшення чи зменшення розміру масиву. Як і випадку з списками, можна підвищити продуктивність, додаючи відразу кількох елементів зі збільшенням розміру масиву. Можна заощадити час, зменшуючи розмір масиву, тільки він містить занадто багато невикористовуваних осередків. Що стосується простого списку чи стека, елементи додаються і видаляються на одному його кінці. Якщо розмір списку залишається майже постійним, їх доведеться змінювати занадто часто. З іншого боку, оскільки елементи додаються з одного боку черги, а видаляються з протилежного кінця, може знадобитися раз у раз переупорядочивать чергу, навіть коли його розмір залишається неизменным.

=====53

Const WANT_FREE_PERCENT = .1 «10% вільного простору. Const MIN_FREE = 10 «Мінімум вільних осередків. Global Queue () As String «Масив черги. Global QueueMax As Integer «Найбільший індекс масиву. Global QueueFront As Integer «Початок черги. Global QueueBack As Integer «Кінець черги. Global ResizeWhen As Integer «Коли збільшити розмір массива.

" При ініціалізації програма має встановити QueueMax = -1 «показуючи, під масив ще виділено память.

Sub EnterQueue (value As String)

If QueueBack > QueueMax Then ResizeQueue

Queue (QueueBack) = value

QueueBack = QueueBack + 1 End Sub

Sub LeaveQueue (value As String) value = Queue (QueueFront)

QueueFront = QueueFront + 1

If QueueFront > ResizeWhen Then ResizeOueue End Sub

Sub ResizeQueue () Dim want_free As Integer Dim і As Integer

" Перемістити запис у початок массива.

For і = QueueFront To QueueBack — 1

Queue (i — QueueFront) = Queue (i)

Next i

QueueBack = QueueBack — QueuePront

QueueFront = 0

" Змінити розмір масиву. want_free = WANT_FREE_PERCENT * (QueueBack — QueueFront)

If want_free < MIN_FREE Then want_free = MIN_FREE

Max = QueueBack + want_free — 1

ReDim Preserve Queue (0 To Max)

" Якщо QueueFront > ResizeWhen, змінити розмір массива.

ResizeWhen = want_free End Sub

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

=======54

Программа ArrayQ2 аналогічна програмі ArrayQ, але він використовує для управління чергою клас ArrayQueue.

Циклические очереди

Очереди, достойні попередньому розділі, потрібно переупорядочивать час від часу, навіть якщо розмір черги майже змінюється. Навіть якби кількаразовому додаванні і видаленні одного елемента знадобиться переупорядочивать чергу. Якщо наперед відомо, наскільки великий то, можливо чергу, цього можна уникнути, створивши циклічну чергу (circular queue). Ідея залежить від тому, щоб розглядати масив черги начебто він загортається, створюючи коло. У цьому останній елемент масиву хіба що йде перед першим. На рис. 3.2 зображено циклічна чергу. Програма може зберігати в перемінної QueueFront індекс елемента, який найбільше перебуває у черги. Змінна QueueBack може містити кінець черги, куди додається новий елемент. На відміну від попередньої реалізації, при відновленні значень змінних QueueFront і QueueBack, необхідно використовувати оператор Mod у тому, щоб індекси залишалися у межах масиву. Наприклад, наступний код додає елемент до очереди:

Queue (QueueBack) = value QueueBack = (QueueBack + 1) Mod QueueSize

На рис. 3.3 показаний процес додавання нового елемента до циклічною черги, яка може містити чотири записи. Елемент З додається в кінець черги. Потім кінець черги зсувається, нагадуючи про таку запис в масиві. Так само, коли програма видаляє елемент з черги, необхідно оновлювати покажчик початку черги з допомогою наступного кода:

value = Queue (QueueFront) QueueFront = (QueueFront + 1) Mod QueueSize

@Рис. 3.2. Циклічна очередь

=======55

@Рис. 3.3. Додавання елемента до циклічною очереди

На рис. 3.4 показаний процес видалення елемента із циклічною черги. Перший елемент, у разі елемент A, видаляється з початку черги, і покажчик початку черги оновлюється, нагадуючи про наступний елемент масиву. Для циклічних черг трапляється складно відрізнити порожню чергу від повної. У обох випадках значення змінних QueueBottom і QueueTop будуть рівні. На рис. 3.5 показані дві циклічні черги, порожня і повна. Простий варіант вирішення даної цієї проблеми — зберігати число елементів у черзі в окремої перемінної NumInQueue. З цього лічильника можна почути, залишилися у ще елементи, залишилося у черги місце нових элементов.

@Рис. 3.4. Видалення елемента із циклічною очереди

@Рис. 3.5 Цілковита й порожня циклічна очереди

=========56

Следующий код використовує всі ці методи керувати циклічною очередью:

Queue () As String «Масив черги. QueueSize As Integer «Найбільший індекс у черзі. QueueFront As Integer «Початок черги. QueueBack As Integer «Кінець черги. NumInQueue As Integer «Кількість елементів в очереди.

Sub NewCircularQueue (num_items As Integer)

QueueSize = num_items

ReDim Queue (0 To QueueSize — 1) End Sub

Sub EnterQueue (value As String)

" Якщо чергу заповнена, вийти з процедуры.

" У цьому додатку знадобиться більш як складний код.

If NumInQueue >= QueueSize Then Exit Sub

Queue (QueueBack) = value

QueueBack = (QueueBack + 1) Mod QueueSize

NumInQueue = NumInQueue + 1 End Sub

Sub LeaveQueue (value As String)

" Якщо чергу порожня, вийти з процедуры.

" У цьому додатку буде потрібно понад складний код.

If NumInQueue = QueueSize Then ResizeQueue

Queue (QueueBack) = value

QueueBack = (QueueBack + 1) Mod QueueSize

NumInQueue = NumInQueue + 1 End Sub

Private Sub LeaveQueue (value As String)

If NumInQueue new_priority cell = nxt nxt = cell. NextCell

Loop

" Вставити елемент після осередки в списке.

:

Для видалення зі списку елемента із найвищим пріоритетом, просто видаляється елемент після сигнальною мітки початку. Оскільки список відсортований в порядку пріоритетів, перший елемент має найвищий пріоритет. Додавання нового елемента у цю чергу посідає у середньому N/2 кроків. Іноді новий елемент здійснюватиметься на початку списку, іноді ближчі один до кінцю, але у середньому він опинятися десь посередині. Проста чергу з урахуванням списку вимагала O (1) кроків для додавання нового елемента і O (N) кроків видалення елементів із найвищим пріоритетом з черги. Версія на основі впорядкованого зв’язкового списку вимагає O (N) кроків для додавання елемента і O (1) кроків видалення верхнього елемента. Обом версіям вимагає O (N) кроків до котроїсь із операцій, але не тоді упорядкованого зв’язкового списку на середньому потрібно лише (N/2) кроків. Програма PriList використовує упорядкований зв’язний список до роботи з пріоритетною чергою. Можете поставити пріоритет, і значення елемента даних, і натиснути кнопку Enter для додавання їх у пріоритетну чергу. Натиснімо на кнопку Leave видалення з черги елемента із найвищим пріоритетом. Програма PriList2 аналогічна програмі PriList, але він використовує для управління чергою клас LinkedPriorityQueue.

========63

Затратив ще небагато зусиль, можна побудувати пріоритетну чергу, в якої додавання і видалення елемента зажадають порядку O (log (N)) кроків. Для великих черг, прискорення роботи може коштувати цих зусиль. Цей тип пріоритетних черг використовує структури даних як піраміди, які теж застосовують у алгоритмі пірамідальній сортування. Піраміди і пріоритетні черги з їхньої основі обговорюються докладніше о 9-й главе.

Многопоточные очереди

Интересной різновидом черг є многопоточные черги (multi- headed queues). Елементи, звісно ж, додаються насамкінець черги, але чергу, має кілька потоків (front end) чи голів (heads). Програма може видаляти елементи із будь-якої потоку. Прикладом многопоточной черги, у звичайного життя є чергу клієнтів в банку. Усі клієнти перебувають у одній черзі, та їх обслуговує кілька службовців. Звільнений банківський працівник обслуговує клієнта, який перебуває у черги першим. Такий порядок обслуговування здається справедливим, оскільки клієнти обслуговуються гаразд прибуття. Він також ефективний, бо всі службовці залишаються зайнятими до того часу, поки клієнти чекають у черзі. Порівняйте цей тип черги з кількома однопоточными чергами в супермаркеті, у яких покупці необов’язково обслуговуються гаразд прибуття. Покупець в повільно що просувалася черги, може прочекати довше, чому він, який підійшов пізніше, але у черзі, яка просувається швидше. Касири також може бути який завжди зайняті, оскільки якась чергу, може виявитися порожній, тоді в інших ще довго будуть перебувати покупці. У випадку, многопоточные черзі більш як ефективні, ніж декілька однопоточных черг. Останній варіант використовують у супермаркетах оскільки візки для покупок займають велике місце. З використанням многопоточной черги всім покупцям довелося б побудуватися до однієї чергу. Коли касир звільниться, покупцю довелося б переміститися з громіздкою візком до касира. З іншого боку, у банку відвідувачам не потрібно рухати великі візки для покупок, тому вони охоче можуть розміститися лише у черги. Черги на реєстрацію у аеропорту іноді є комбінацію цих двох ситуацій. Хоча пасажири мають із собою дуже багато багажу, на літовищі все-таки використовуються многопоточные черги, у своїй доводиться відводити додаткове місце, щоб пасажири могли вишикуватися гаразд черги. Многопоточную чергу просто побудувати, використовуючи звичайну однопоточную чергу. Елементи, які мають клієнтів, зберігаються у звичайній однопоточной черги. Коли агент (касир, банківський службовець тощо.) звільняється, перший елемент на початку черги видаляється і передається цьому агенту.

Модель очереди

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

=====63

1. реєстрація кожного пасажира займає від двох до п’ятьох хвилин; 2. під час використання кількох однопоточных черг, які прибувають пасажири стають в коротку чергу; 3. швидкість надходження пасажирів приблизно незмінна. Програма HeadedQ моделює цю ситуацію. Можете змінювати деякі параметри моделі, включаючи такі: 4. число прибуваючих протягом години пасажирів; 5. мінімальне і забезпечити максимальне затрачуване час; 6. число вільних службовців; 7. паузу між кроками програми в миллисекундах. За виконання програми, модель показує час, середнє і максимальне час очікування пасажирами обслуговування, і відсоток часу, в протягом якого службовці зайняті. При експериментуванні з різними значеннями параметрів, ви помітите кілька цікавих моментів. По-перше, для многопоточной черги середнє і забезпечити максимальне час очікування буде набагато меншою. У цьому, службовці також виявляються трохи більше завантажені, ніж у випадку однопоточной черги. Для обох типів черги є поріг, у якому час очікування пасажирів значно зростає. Припустимо, що у обслуговування одного пасажира потрібно від 2 до 10 хвилин, чи середньому 6 хвилин. Якщо потік пасажирів становить 60 чоловік на годину, тоді персонал витратить близько 6*60=360 хвилин, у годину обслуговування всіх пасажирів. Розділивши це значення на 60 хвилин, у годині, одержимо, що з обслуговування клієнтів у разі знадобиться 6 клерків. Якщо запустити програму HeadedQ з тими параметрами, ви не побачите, що черги рухаються досить швидко. Для многопоточной черги час очікування становитиме лише кілька хвилин. Якщо додати чергового службовця, щоб всього було 7 службовців, середнє і забезпечити максимальне час очікування значно зменшаться. Середнє час очікування впаде приблизно до однією десятою хвилини. З іншого боку, якщо зменшити кількість службовців до 5, це сприятиме великому збільшення середнього та максимального часу очікування. Ці показники також зростатимуть згодом. І чим довше працюватиме програма, тим довше будуть задержки.

@Таблица 3.1. Час очікування в хвилинах для одне- і многопоточных очередей

======64

@Рис. 3.9. Програма HeadedQ

В табл. 3.1 наведено середнє і забезпечити максимальне час очікування для 2 різних типів черг. Програма моделює роботу у протягом 3 годинників та передбачає, що прибуває 60 пасажирів на годину і обслуговування кожного їх йде від 2 до 10 хвилин. Многопоточная чергу, також здається справедливішою, оскільки пасажири обслуговуються гаразд прибуття. На рис. 3.9 показано програма HeadedQ після моделювання трохи більше, як двох годин роботи термінала. У многопоточной черги першим стоїть пасажир з номером 104. Усі пасажири, прибулі перед ним, вже обслужені чи обслуговуються зараз. У однопоточной черги, обслуговується пасажир з номером 106. Пасажири з номерами 100, 102, 103 і 105 досі чекають своєї черги, хоча які й прибутку раніше, ніж пасажир з номером 106.

Резюме

Разные реалізації стеков і черг мають різні властивості. Стеки і циклічні черги з урахуванням масивів прості й ефективні, особливо, якщо наперед відомо наскільки великим може бути їх розмір. Зв’язкові списки забезпечують більшої гнучкості, якщо розмір списку часто змінюється. Стеки і беззмістовності черги з урахуванням колекцій Visual Basic негаразд ефективні, як реалізації з урахуванням масивів, але дуже прості. Колекції можуть підійти для невеликих структур даних, якщо продуктивність не критична. Після тестування докладання, можна переписати код для стека або черги, якщо колекції виявляться занадто медленными.

Глава 4. Массивы

В цієї главі описані структури даних як масивів. З допомогою Visual Basic ви можете легко створювати масиви даних стандартних чи певних користувачем типів. Якщо визначити масив без кордонів, потім можна змінювати її розмір з допомогою оператора ReDim. Ці властивості роблять застосування масивів в Visual Basic дуже корисним. Деякі програми використовують особливі типи масивів, які підтримуються Visual Basic безпосередньо. До цих типу ставляться трикутні масиви, нерегулярні масиви і зріджені масиви. У цьому главі пояснюється, як і використовувати гнучкі структури масивів, що потенційно можуть значно знизити обсяг займаній памяти.

Треугольные массивы

Некоторым програмам потрібно лише половина елементів в двовимірному масиві. Припустимо, що ми маємо картою, де 10 міст є такі цифрами від 0 до 9. Можна також використовувати масив до створення матриці суміжності (adjacency matrix), яка б показала наявність автостради між парами міст. Елемент A (I, J) дорівнює True, якщо розрив між містами I і J є автострада. І тут, значення половині матриці будуть дублювати значення інший її половині, оскільки A (I, J)=A (J, I). Також елемент A (I, I) немає сенсу, оскільки безглуздо будувати автостраду із міста I той самий самий місто. Насправді знадобляться тільки елементи A (I, J) з верхнього лівого кута, котрим I > J. Натомість можна також ознайомитися використовувати елементи з верхнього правого кута. Оскільки ці елементи утворюють трикутник, цей тип масивів називається трикутним масивом (triangular array). На рис. 4.1 показаний трикутний масив. Елементи зі значущими даними є такі буквою X, осередки, відповідні дублирующимся елементам, залишені порожніми. Незначні елементи A (I, I) є такі тирі. Для невеликих масивів втрати пам’яті під час використання звичайних двовимірні масивів для зберігання таких даних дуже істотні. Якщо ж карті багато міст, втрати пам’яті може бути великі. Для N міст ці втрати становитимуть N*(N-1)/2 дублирующихся елементів і N незначних діагональних елементів A (I, I). Якщо карта містить 1000 міст, в масиві буде більш півмільйона непотрібних элементов.

====67

@Рис. 4.1. Трикутний массив

Избежать втрат пам’яті можна, створивши одновимірний масив B і упакувавши до нього значущі елементи з масиву A. Розмістимо елементи в масиві B по рядкам, як показано на рис. 4.2. Зауважте, що індекси масивів розпочинаються з нуля. Це спрощує наступні рівняння. А, щоб спростити використання цього подання трикутного масиву, написати функції для перетворення індексів масивів A і B. Рівняння для перетворення індексу A (I, J) в B (X) виглядає так:

X = I * (I — 1) / 2 + J «Для I > J.

Например, для I=2 і J=1 одержимо X = 2 * (2 — 1) / 2 + 1 = 2. Це означає, що A (2,1) відображається на 2 позицію у масиві B, як показано на рис. 4.2. Пам’ятаєте, що масиви нумеруються від початку. Рівняння залишається справедливим лише I > J. Значення інші елементи масиву A не зберігаються в масиві B, оскільки є надмірними чи незначними. Якщо до вас потрібно одержати значення A (I, J) при I < J, замість цього треба обраховувати значення A (J, I). Рівняння протилежного перетворення B (X) в A (I, J) виглядає так:

I = Int ((1 + Sqr (1 + 8 * X)) / 2) J = X — I * (I — 1) / 2

@Рис. 4.2. Упаковка трикутного масиву в одномірному массиве

=====68

Подстановка у ці рівняння X=4 дає I = Int ((1 + Sqr (1 + 8 * 4)) / 2) = 3 і J = 4 — 3 * (3 — 1) / 2 = 1. Це означає, що елемент B (4) відображається на позицію A (3,1). І це відповідає рис. 4.2. Ці обчислення дуже прості. Вони потребують кількох умножений і ділень, і навіть обчислення квадратного кореня. Якщо програмі доведеться виконувати цих функцій часто-густо, це внесе певну затримку швидкості виконання. Це приклад компромісу між простором і часом. Упаковка трикутного масиву в одновимірний масив заощаджує пам’ять, зберігання даних в двовимірному масиві вимагає більше пам’яті, але заощаджує час. Використовуючи ці рівняння, написати процедури Visual Basic для перетворення координат між двома массивами:

Private Sub AtoB (ByVal I As Integer, ByVal J As Integer, X As Integer) Dim tmp As Integer

If I = J Then «Невартісний элемент.

X = -1

Exit Sub

ElseIf I < J Then «Замінити місцями I і J. tmp = I

I = J

J = tmp

End If

X = I * (I — 1) / 2 + J End Sub

Private Sub BtoA (ByVal X As Integer, I As Integer, J As Integer) I = Int ((1 + Sqr (1 + 8 * X)) / 2) J = X — I * (I — 1) /2 End Sub

Программа Triang використовує ці підпрограми до роботи з трикутними масивами. Якщо вже ви натиснете на кнопку A to B (З A в B), програма позначить елементи в масиві A і скопіює ці мітки на відповідні елементи масиву B. Якщо ви хоч натиснете на кнопку B to A (З B в A), програма позначить елементи в масиві B, і далі скопіює мітки в масив A. Програма Triangc використовує клас TriangularArray до роботи з трикутним масивом. При старті програми, вона записує в об'єкт TriangularArray рядки, які становлять елементи масиву. Потім вона дістає і виводить на екран елементи массива.

Диагональные элементы

Некоторые програми використовують трикутні масиви, куди входять діагональні елементи A (I, I). І тут необхідно ухвалити лише три зміни у процедури перетворення індексів. Процедура перетворення AtoB має пропускати випадки з I=J, і має додавати до I одиницю при підрахунку індексу масиву B.

=====69

Private Sub AtoB (ByVal I As Integer, ByVal J As Integer, X As Integer) Dim tmp As Integer

If I < J Then «Поміняти місцями I і J. tmp = I

I = J

J = tmp

End If

I = I + 1

X = I * (I — 1) / 2 + J End Sub

Процедура перетворення BtoA повинна вичитати з I одиницю перед поверненням значения.

Private Sub BtoA (ByVal X As Integer, I As Integer, J As Integer)

I = Int ((1 + Sqr (1 + 8 * X)) / 2)

J = X — I * (I — 1) / 2

I = J — 1 End Sub

Программа Triang2 аналогічна програмі Triang, але він використовує до роботи з діагональними елементами в масиві A нові функції. Програма TriangC2 аналогічна програмі TriangC, але використовує клас TriangularArray, що включає діагональні элементы.

Нерегулярные массивы

В деяких програмах потрібні масиви нестандартного розміру й форми. Двомірний масив може містити шість елементів у першому ряду, три — у другому, чотири — у третій, тощо. Це може знадобитися, наприклад, для збереження низки многоугольников, кожен із яких тільки з різного числа точок. Масив буде цьому виглядати, як у рис. 4.3. Масиви в Visual Basic що неспроможні мати такі нерівні краю. Можна було б використовувати масив, досить великий у тому, щоб у ній могли поміститися все рядки, та заодно у тому масиві було б безліч невикористовуваних осередків. Наприклад, масив на рис. 4.3 міг бути оголошено при допомоги оператора Dim Polygons (1 To 3, 1 To 6), і навіть чотири осередки залишаться невикористаними. Є кілька способів уявлення нерегулярних массивов.

@Рис. 4.3. Нерегулярний массив

=====70

Пряма звезда

Один із засобів уникнути втрат пам’яті у тому, щоб упакувати дані в одномірному масиві B. На відміну від трикутних масивів, для нерегулярних масивів не можна записати формули визначення відповідності елементів у різних масивах. Щоб упоратися з цим завданням, можна створити іще одна масив A зі зміщеннями кожної рядки у одномірному масиві B. Для спрощення визначення в масиві B становища точок, відповідних кожному рядку, насамкінець масиву A можна додати сигнальну мітку, яка свідчить про точку відразу за останнім елементом в масиві B. Тоді точки, що утворюють багатокутник I, займають у масиві B позиції з A (I) до A (I+1) — 1. Наприклад, програма може перерахувати елементи, що утворюють рядок I, використовуючи наступний код:

For J = A (I) To A (I + 1) — 1

' Внести до списку елемент I.

: Next J

Этот метод називається прямий зіркою (forward star). На рис. 4.4 показано уявлення нерегулярного масиву з рис. 4.3 як прямий зірки. Сигнальна мітка зафарбована сірим кольором. Цей метод можна легко узагальнити до створення багатомірних нерегулярних масивів. Для зберігання набору малюнків, кожен із яких тільки з різного числа многоугольников, можна використовувати тривимірну пряму зірку. На рис. 4.5 схематично представлена тривимірна структура даних як прямий зірки. Дві сигнальних мітки зафарбовані сірим кольором. Вони вказують однією позицію позаду значущих даних в масиві. Це уявлення як прямий зірки вимагає дуже невеликих витрат пам’яті. Тільки пам’ять, зайнята сигнальними знаками, витрачається «даремно». З використанням структури даних прямий зірки легко і швидко можна перерахувати точки, що утворюють багатокутник. Також просто зберігати такі дані на диску і завантажувати їх на згадку про. З іншого боку, оновлювати масиви, записані форматі прямий зірки, дуже складно. Припустимо, собі хочете додати новий кут до першого многоугольнику на рис. 4.4. І тому знадобиться зрушити все елементи праворуч від нової точки однією позицію, щоб звільнити місце для створення нового елемента. По-друге, треба додати по одиниці всім елементам масиву A, які йдуть після першого, щоб врахувати зрушення, викликаний додаванням точки. І, нарешті, треба вставити новий елемент. Схожі проблеми виникають під час видалення точки з першого многоугольника.

@Рис. 4.4. Уявлення нерегулярного масиву як прямий звезды

=====71

@Рис. 4.5. Тривимірна пряма звезда

На рис. 4.6 показано подання до вигляді прямий зірки з рис. 4.4 після додавання однієї точки до першого многоугольнику. Елементи, хто був змінені, зафарбовані сірим кольором. Як очевидно з малюнка, майже всі елементи в обох масивах були изменены.

Нерегулярные зв’язкові списки

Другим методом створення нерегулярних масивів є використання зв’язкових списків. Кожна осередок містить покажчик для наступної осередок на тому рівні ієрархії, і покажчик на список осередків нижчому рівні ієрархії. Наприклад, осередок багатокутника може містити покажчик на наступний багатокутник і покажчик на осередок, що містить координати першої точки. Наступний код наводить визначення змінних для класів, які можна використовуватиме створення зв’язкового списку малюнків. Кожен із малюнків містить зв’язний список многоугольников, кожен із яких містить зв’язний список точок. У класі PictureCell:

Dim NextPicture As PictureCell «Наступний малюнок. Dim FirstPolygon As PolyfonCell «Перший багатокутник у цьому рисунке.

В класі PolygonCell:

Dim NextPolygon As PolygonCell «Наступний багатокутник. Dim FirstPoint As PointCell «Перша точка у тому многоугольнике.

В класі PointCell:

@Рис. 4.6. Додавання точки до прямий звезде

======72

Dim NextPoint As PointCell «Наступна точка у тому многоугольнике. Dim X As Single «Координати точки. Dim Y As Single

Используя ці методи, можна легко додавати і видаляти малюнки, багатокутники чи крапки будь-де структури даних. Програма Poly на диску містить зв’язний список многоугольников. Кожен багатокутник містить зв’язний список точок. Коли ж ви закриваєте форму, посилання список многоугольников з форми знищується. Це лічильник посилань на верхню осередок многоугольников нанівець. Вона знищується, тому її посилання наступний багатокутник та її першу точку також знищуються. Лічильники посилань для цієї осередки також зменшуються нанівець, і вони також знищуються. Знищення кожної осередки багатокутника чи крапки призводить до знищення наступній осередки. Цей процес відбувається триває до тих пір, доки всі багатокутники і точки ні уничтожены.

Разреженные массивы

Во багатьох додатках потрібні великі масиви, які містять лише мало ненульових елементів. Матриця суміжності для авіаліній, наприклад, може містити 1 в позиції A (I, J) є рейс між містами I і J. Багато авіалінії обслуговують сотні міст, але число існуючих рейсів значно менше, ніж N2 можливих комбінацій. На рис. 4.8 показано невеличка карта рейсів авіалінії, де зображені лише 11 існуючих рейсів зі ста можливих пар поєднань городов.

@Рис. 4.7. Програма Poly

====73

@Рис. 4.8. Карта рейсів авиалинии

Можно побудувати матрицю суміжності при цьому прикладу з допомогою масиву 10 на 10 елементів, але цей масив буде по більшу частину порожнім. Можна уникнути втрат пам’яті, використовуючи до створення розрідженого масиву покажчики. Кожна осередок містить покажчики наступного року елемент в рядку і стовпці масиву. Це дозволяє програмі визначити положення будь-якого елемента у масиві і обходити елементи в рядку чи стовпці. Залежно від докладання, може у пригоді також додати зворотні покажчики. На рис. 4.9 показано розріджена матриця суміжності, відповідна карті рейсів з рис. 4.8. Щоб побудувати розріджене масив в Visual Basic, створіть клас для уявлення елементів масиву. І тут, кожна осередок представляє наявність рейсів між двома містами. Для уявлення зв’язку, клас повинен утримувати перемінні з індексами міст, пов’язані між собою. Ці індекси, по суті, дають номери рядків і шпальт осередки. Кожна осередок також має утримувати покажчики для наступної осередок в рядку і стовпці. Наступний код показує оголошення змінних у п’ятому класі ConnectionCell:

Public FromCity As Integer «Рядок осередки. Public ToCity As Integer «Стовпець осередки. Public NextInRow As ConnectionCell Public NextInCol As ConnectionCell

Строки і стовпчики у тому масиві сутнісно є зв’язкові списки. Як часто трапляється зі зв’язковими списками, із нею простіше працювати, якщо вони містять сигнальні мітки. Наприклад, змінна RowHead (I) повинна містити сигнальну мітку для рядки I. Для обходу рядки I в масиві можна використовувати наступний код:

Private Sub PrintRow (I As Integer) Dim cell As ConnectionCell

Set Cell = RowHead (I). Next «Перший елемент данных.

Do While Not (cell Is Nothing)

Print Format$(cell. FromCity) & «-> «& Format$(cell. ToCity)

Set cell = cell. NextInRow

Loop End Sub

====74

@Рис. 4.9. Розріджена матриця смежности

Індексування массива

Нормальное індексування масиву типу A (I, J) нічого очікувати працювати з цими структурами. Можна полегшити індексування, написавши процедури, які витягають і встановлюють значення елементів масиву. Якщо масив представляє матрицю, можуть також знадобитися процедури для складання, множення, та інших матричних операцій. Спеціальне значення NoValue представляє порожній елемент масиву. Процедура, яка дістає елементи масиву, повинна повертати значення NoValue під час спроби отримати значення елемента, не що міститься в масиві. Аналогічно, процедура, що встановлює значення елементів, повинна видаляти осередок з масиву, коли його значення встановлено в NoValue. Значення NoValue має вибиратися залежно від природи даних докладання. Для матриці суміжності авіалінії порожні осередки може мати значення False. У цьому значення A (I, J) може визначатися рівним True, якщо є рейс між містами I і J. Клас SparseArray визначає процедуру get для властивості Value для повернення значення елемента у масиві. Процедура розпочинає переговори з першої осередки у зазначеній рядку і далі переміщається по зв’язков списку осередків рядки. Щойно знайдеться осередок із потрібною номером шпальти, це і буде бажана осередок. Оскільки осередки у списку рядки розташовані усе своєю чергою, процедура може зупинитися, якщо знайдеться осередок, номер шпальти якої більший від искомого.

=====75

Property Get Value (t As Integer, з As Integer) As Variant Dim cell As SparseArrayCell

Value = NoValue «Припустимо, що ми знайдемо элемент.

If r < 1 Or з < 1 Or _ r > NumRows Or з > NumCols _

Then Exit Property

Set cell = RowHead®. NextInRow «Пропустити метку.

Do

If cell Is Nothing Then Exit Property «Не найден.

If cell. Col > з Then Exit Property «Не найден.

If cell. Col = з Then Exit Do «Найден.

Set cell = cell. NextInRow Loop

Value = cell. Data End Property

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

Property Let Value (r As Integer, з As Integer, new_value As Variant) Dim і As Integer Dim found_it As Boolean Dim cell As SparseArrayCell Dim nxt As SparseArrayCell Dim new_cell As SparseArrayCell

" Якщо value = MoValue, видалити елемент з массива.

If new_value = NoValue Then

RemoveEntry r, c

Exit Property

End If

" Якщо потрібне, додати строки.

If r > NumRows Then

ReDim Preserve RowHead (1 To r)

" Форматувати мітку кожної нової строки.

For і = NumRows + 1 To r

Set RowHead (i) = New SparseArrayCell

Next i

End If

" Якщо потрібне, додати столбцы.

If з > NumCols Then

ReDim Preserve ColHead (1 To c)

" Форматувати мітку кожної нової строки.

For і = NumCols + 1 To c

Set ColHead (i) = New SparseArrayCell

Next i

NumCols = c

End If

" Спроба знайти элемент.

Set cell = RowHead®

Set nxt = cell. NextInRow

Do

If nxt Is Nothing Then Exit Do

If nxt. Col >= з Then Exit Do

Set cell = nxt

Set nxt = cell. NextInRow

Loop

" Перевірка, знайдено чи элемент.

If nxt Is Nothing Then found_it = False

Else found_it = (nxt. Col = c)

End If

" Якщо елемент не знайдено, створити его.

If Not found_it Then

Set new_cell = New SparseArrayCell

" Помістити елемент до списку строки.

Set new_cell. NextInRow = nxt

Set cell. NextInRow = new_cell

" Помістити елемент до списку столбца.

Set cell = ColHead (c)

Set nxt = cell. NextInCol

Do

If nxt Is Nothing Then Exit Do

If nxt. Col >= з Then Exit Do

Set cell = nxt

Set nxt = cell. NextInRow

Loop

Set new_cell. NextInCol = nxt

Set cell. NextInCol = new_cell new_cell. Row = r new_cell. Col = c

" Помістимо значення в елемент nxt.

Set nxt = new_cell End If

" Встановимо значення. nxt. Data = new_value End Property

Программа Sparse, показана на рис. 4. 10, використовує класи SparseArray і SparseArrayCell до роботи з розрідженим масивом. Використовуючи програму, можна встановлювати і видобувати елементи масиву. У цьому програмі значення NoValue одно нулю, тож коли ви встановіть значення елемента рівним нулю, програма віддалить цей елемент з массива.

Очень зріджені массивы

Некоторые масиви містять такі малі непорожніх елементів, що чимало рядки — і стовпчики повністю порожні. І тут, краще зберігати заголовки рядків і шпальт в зв’язкових списках, а чи не в масивах. Це дозволяє програмі повністю пропускати порожні рядки — і стовпчики. Заголовки рядки — і шпальт свідчить про зв’язкові списки елементів рядків і шпальт. На рис. 4. 11 показаний масив 100 на 100, який містить всього 7 непорожніх элементов.

@Рис. 4. 10. Програма Sparse

=====76−78

@Рис. 4. 11. Дуже розріджене массив

Для роботи з масивами цього можна досить просто доопрацювати попередній код. Більшість коду залишається незмінною, й у елементів масиву можна використовувати той самий клас SparseArray. Тем щонайменше, замість зберігання міток рядків і шпальт в масивах, вони записуються в зв’язкових списках. Об'єкти класу HeaderCell представляють зв’язкові списки рядків і шпальт. У цьому класі визначаються перемінні, містять число рядків і шпальт, що він представляє, сигнальна мітка на початку зв’язкового списку елементів рядків чи шпальт, і той HeaderCell, що становить наступний заголовок рядки чи столбца.

Public Number As Integer «Номер рядки чи шпальти. Public Sentinel As SparseArrayCell «Мітка для рядки или

" шпальти. Public NextHeader As HeaderCell «Наступна рядок или

" столбец.

Например, щоб звернутися до рядка I, потрібно спочатку переглянути зв’язний список заголовків HeaderCells рядків, доки знайдеться заголовок, відповідний рядку I. Потім триває робота з рядком I.

Private Sub PrintRow (r As Integer) Dim row As HeaderCell Dim cell As SparseArrayCell

" Знайти правильний заголовок строки.

Set row = RowHead. NextHeader «Список першої строки.

Do

If row Is Nothing Then Exit Sub «Такий рядки нет.

If row. Number > r Then Exit Sub «Такий рядки нет.

If row. Number = r Then Exit Do «Рядок найдена.

Set row = row. NextHeader Loop

" Вивести елементи в строке.

Set cell = row. Sentinel. NextInRow «Перший елемент в строке.

Do While Not (cell Is Nothing)

Print Format$(cell. FromCity) & «-> «& Format$(cell. ToCity)

Set cell = cell. NextInRow

Loop End Sub

Резюме

Некоторые програми використовують масиви, містять лише мало значущих елементів. Використання звичайних масивів Visual Basic привела би до до великих втрат пам’яті. Використовуючи трикутні, нерегулярні, зріджені і дуже зріджені масиви, ви можете створювати потужні уявлення масивів, які прагнуть набагато менших обсягів памяти.

=========80

Глава 5. Рекурсия

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

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

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

Потім, у розділі розглядається кілька прикладів, у яких застосування рекурсії доречніше. Алгоритми побудови кривих Гільберта і Серпинского використовують рекурсию правильно і эффективно.

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

Что таке рекурсия?

Рекурсия відбувається, якщо функція чи підпрограма викликає сама себе. Пряма рекурсія (direct recursion) виглядає так:

Function Factorial (num As Long) As Long

Factorial = num * Factorial (num — 1) End Function

Що стосується непрямої рекурсії (indirect recursion) рекурсивна процедура викликає іншу процедуру, яка, своєю чергою, викликає первую:

Private Sub Ping (num As Integer)

Pong (num — 1) End Sub

Private Sub Pong (num As Integer)

Ping (num / 2) End Sub

===========81

Рекурсія корисна під час вирішення завдань, які природним чином розбиваються сталася на кілька подзадач, кожна з яких є простим випадком вихідної завдання. Можна уявити дерево на рис. 5.1 як «стовбура», у якому перебувають два дерева менших розмірів. Тоді можна написати рекурсивную процедуру для малювання деревьев:

Private Sub DrawTree ()

Намалювати «стовбур «

Намалювати дерево меншого розміру, повернене на -45 градусов

Намалювати дерево меншого розміру, повернене на 45 градусів End Sub

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

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

Рекурсивное обчислення факториалов

Факториал числа N записується як N! (вимовляється «ен факторіал»). По визначенню, 0! одно 1. Інші значення визначаються формулой:

N! = N * (N — 1) * (N — 2) * … * 2 * 1

Як не згадувалось у 1 главі, цю функцію з надзвичайною швидкістю зростає зі збільшенням N. У табл. 5.1 наведено 10 перших значень функції факториала.

Можна ще визначити функцію факториала рекурсивно:

0! = 1 N! = N * (N — 1)! для N > 0.

@Рис. 5.1. Дерево, складене з цих двох дерев меншого размера

===========82

@Таблиця 5.1. Значення функції факториала

Легко написати з урахуванням цього визначення рекурсивную функцию:

Public Function Factorial (num As Integer) As Integer

If num = 2 * Fib (N) — 1

С точністю до порядку на це знадобиться O (Fib (N)). Цікаво, що цю функцію як рекурсивна, але вона також використовується з оцінки її виконання. Щоб допомогти вам уявити швидкість зростання функції Фібоначчі, можна показати, що Fib (M)> (M-2 де (- константа, яка дорівнює 1,6. Це означає, що час виконання незгірш від, ніж значення експоненційною функції O ((M). Як і інші экспоненциальные функції, цю функцію зростає швидше, ніж полиномиальные функції, але повільніше, ніж функція факториала. Оскільки час виконання зростає нас дуже швидко, цей алгоритм досить повільно виконується для великих вхідних значень. Фактично, настільки повільно, що у практиці практично неможливо обчислити значення функції Fib (N) для N, що набагато більше 30. У табл. 5.4 показано час виконання при цьому алгоритму за комп’ютером з процесором Pentium з тактовою частотою 90 МГц в різних вхідних значеннях. Програма Fibo використовує цей рекурсивний алгоритм для обчислення чисел Фібоначчі. Запровадьте ціла кількість і натиснімо на кнопку Go для обчислення чисел Фібоначчі. Розпочніть з невеликих чисел, доки оцініть, як швидко ваш комп’ютер може виконувати ці вычисления.

Рекурсивное побудова кривих Гильберта

Кривые Гільберта (Hilbert curves) — це самоподібні (self-similar) криві, які зазвичай визначаються з допомогою рекурсії. На рис. 5.2. показані криві Гільберта із першого, 2 чи 3 порядка.

@Таблица 5.4. Час виконання програми Fibonacci

=====88

@Рис. 5.2. Криві Гильберта

Кривая Гільберта, як будь-який інший самоподобная крива, створюється розбивкою великий кривою на менші частини. Потім ви можете використовувати цю криву, після зміни розміру й повороту, для побудови цих частин. Ці частини може бути розбитий більш дрібні частини, тощо, поки процес не досягне потрібної глибини рекурсії. Порядок кривою визначається як максимальна глибина рекурсії, яких становить процедура. Процедура Hilbert управляє глибиною рекурсії, використовуючи відповідний параметр. При кожному рекурсивном виклик, процедура зменшує параметр глибини рекурсії на одиницю. Якщо процедура викликається з глибиною рекурсії, рівної 1, вона малює просту криву 1 порядку, показану на рис. 5.2 зліва і завершує роботу. Це умова зупинки рекурсії. Наприклад, крива Гільберта 2 порядку складається з чотирьох кривих Гільберта 1 порядку. Аналогічно, крива Гільберта 3 порядку складається з чотирьох кривих 2 порядку, кожна з яких складається з чотирьох кривих 1 порядку. На рис. 5.3 показані криві Гільберта 2 і трьох порядку. Менші криві, у тому числі побудовано криві більшого розміру, виділено полужирными лініями. Наступний код будує криву Гільберта 1 порядка:

Line -Step (Length, 0) Line -Step (0, Length) Line -Step (-Length, 0)

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

Private Sub Hilbert (Depth As Integer)

If Depth = 1 Then

Намалювати криву Гільберта 1 порядка

Else

Намалювати і з'єднати 4 криві порядку (Depth — 1)

End If End Sub

====89

@Рис. 5.3. Криві Гільберта, освічені меншими кривыми

Этот метод вимагає невеликого ускладнення визначення напрями малювання кривих. Це потрібно здобуття права вибрати тип використовуваних кривих Гільберта. Цю інформацію можна передати процедурі з допомогою параметрів Dx і Dy для визначення напрями виведення першої лінії в кривою. Для кривою 1 порядку, процедура малює першу лінію з допомогою функції Line-Step (Dx, Dy). Якщо крива має як високий порядок, процедура з'єднує два подкривых, використовуючи функцію Line-Step (Dx, Dy). У кожному разі, процедура може використовувати параметри Dx і Dy для вибору напрямку, у якому вона повинна малювати лінії, що утворюють криву. Код мовою Visual Basic для малювання кривих Гільберта короткий, але складний. Вам може знадобитися кілька разів пройти їх у отладчике для кривих 1 і 2 порядку, аби побачити, як змінюються параметри Dx і Dy, при побудові різних частин кривой.

Private Sub Hilbert (depth As Integer, Dx As Single, Dy As Single)

If depth > 1 Then Hilbert depth — 1, Dy, Dx

HilbertPicture. Line -Step (Dx, Dy)

If depth > 1 Then Hilbert depth — 1, Dx, Dy

HilbertPicture. Line -Step (Dy, Dx)

If depth > 1 Then Hilbert depth — 1, Dx, Dy

HilbertPicture. Line -Step (-Dx, -Dy)

If depth > 1 Then Hilbert depth — 1, -Dy, -Dx End Sub

Аналіз часу виконання программы

Чтобы проаналізувати час виконання цієї процедури, ви можете визначити число викликів процедури Hilbert. При кожної рекурсії вона викликає себе в чотири рази. Якщо T (N) — їх кількість викликів процедури, коли вона викликається з глибиною рекурсії N, то:

T (1) = 1 T (N) = 1 + 4 * T (N — 1) для N > 1.

Если розкрити визначення T (N), получим:

T (N) = 1 + 4 * T (N — 1)

= 1 + 4 *(1 + 4 * T (N — 2))

= 1 + 4 + 16 * T (N — 2)

= 1 + 4 + 16 * (1 + 4 * T (N — 3))

= 1 + 4 + 16 + 64 * T (N — 3)

= …

= 40 + 41 + 42 + 43 + … + 4K * T (N — K)

Раскрыв це рівняння до того часу, поки що не виконано умова зупинки рекурсії T (1)=1, получим:

T (N) = 40 + 41 + 42 + 43 + … + 4N-1

Это рівняння можна спростити, скориставшись соотношением:

X0 + X1 + X2 + X3 + … + XM = (XM+1 — 1) / (X — 1)

После перетворення, рівняння наводиться до виду:

T (N) = (4(N-1)+1 — 1) / (4 — 1)

= (4N — 1) / 3

=====90

С точністю до постійних, цю процедуру виконується під час порядку O (4N). У табл. 5.5 наведено кілька перших значень функції часу виконання. Якщо ви і уважно подивіться для цієї числа, то побачите, що вони відповідають рекурсивному визначенню. Цей алгоритм є типовим прикладом рекурсивного алгоритму, який виконується під час порядку O (CN), де З — деяка стала. При кожному виклик підпрограми Hilbert, вона збільшує розмірність завдання у 4 разу. У випадку, якщо кожному виконанні певної кількості кроків алгоритму розмір завдання збільшується щонайменше, ніж у З раз, той час виконання алгоритму порядку O (CN). Це поведінка протилежно поведінці алгоритму пошуку найбільшого загального дільника. Процедура GCD зменшує розмірність завдання у 2 разу при кожному другому своєму виклик, і тому час виконання порядку O (log (N)). Процедура побудови кривих Гільберта збільшує розмір завдання у 4 разу при кожному своєму виклик, тому час виконання порядку O (4N). Функція (4N-1)/3 — це экспоненциальная функція, яка зростає дуже швидко. Фактично, вона зростає так швидко, що ви можете припустити, що це занадто ефективний алгоритм. Насправді робота цього алгоритму займає багато місця часу, але є дві причини, по якою це непогані й погано. По-перше, жоден алгоритм для побудови кривих Гільберта може бути значно швидше. Криві Гільберта містять безліч відрізків ліній, і будь-який який малює їх алгоритм вимагатиме досить чимало часу. При кожному виклик процедури Hilbert, вона малює три лінії. Нехай L (N) — сумарна кількість ліній, із яких складається крива Гільберта порядку N. Тоді L (N) = 3 * T (N) = 4N — 1, тому L (N) також порядку O (4N). Будь-який алгоритм, який малює криві Гільберта, повинен вивести O (4N) ліній, виконавши у своїй O (4N) кроків. Існують інші алгоритми побудови кривих Гільберта, але вони займають не менше часу, як і цей алгоритм.

@Таблица 5.5. Кількість рекурсивних викликів підпрограми Hilbert

=====91

Второй факт, що свідчить про, що це алгоритм непогані поганий, у тому, що криві Гільберта 9 порядку містять дуже багато ліній, що екран більшості комп’ютерних моніторів у своїй виявляється повністю закрашенным. Це не дивно, оскільки ця крива містить 262. 143 відрізків ліній. Це означає, що вам мабуть будь-коли знадобиться виводити на екран криві Гільберта 9 або як високих порядків. У якому- то порядку ви зіштовхнетеся з обмеженнями мови Visual Basic і рішення вашого комп’ютера, але, швидше за все, ще раніше будете обмежені максимальним дозволом екрана. Програма Hilbert, показана на рис. 5. 4, використовує цей рекурсивний алгоритм для малювання кривих Гільберта. За виконання програми не ставте надто велику глибину рекурсії (більше 6) до того часу, поки ви не визначте, як швидко виконується цю програму вашому компьютере.

Рекурсивное побудова кривих Серпинского

Как і криві Гільберта, криві Серпинского (Sierpinski curves) — це самоподібні криві, які зазвичай визначаються рекурсивно. На рис. 5.5 показані криві Серпинского 1, 2 і трьох порядку. Алгоритм побудови кривих Гільберта використовує всього одну підпрограму для малювання кривих. Криві Серпинского простіше малювати, використовуючи чотири окремих процедури, які працюють спільно. Ці процедури називаються SierpA, SierpB, SierpC і SierpD. Це процедури з непрямої рекурсією — кожна процедура викликає інші, які потім викликають початкову процедуру. Вони малюють верхню, ліву, нижню праву частини кривою Серпинского, відповідно. На рис. 5.6 показано, щоб ці процедури працюють спільно, створюючи криву Серпинского 1 порядку. Подкривые зображені стрілками, щоб показати напрям, де вони малюються. Відтинки, що з'єднують чотири подкривые, намальовані пунктирними линиями.

@Рис. 5.4. Програма Hilbert

=====92

@Рис. 5.5. Криві Серпинского

Каждая з чотирьох основних кривих складається з діагонального відрізка, потім вертикального чи горизонтального відрізка, і чергового діагонального відрізка. Якщо глибина рекурсії більше одиниці, кожна з цих кривих розбивається на менші частини. Це здійснюється розбивкою кожного з цих двох діагональних відрізків на дві подкривые. Наприклад, для розбивки кривою типу A, перший діагональний відрізок розбивається на криву типу A, яку слід крива типу B. Потім малюється не змінювалась горизонтальний відрізок з вихідної кривою типу A. Нарешті, другий діагональний відрізок розбивається на криву типу D, за яка повинна крива типу A. На рис. 5.7 показано, як крива типу A другого порядку утворюється з кількох кривих 1 порядку. Подкривые зображені жирними лініями. На рис. 5.8 показано, як повна крива Серпинского 2 порядку утворюється з 4 подкривых 1 порядку. Кожна з подкривых обведена контурній лінією. Можна також використовувати стрілки (і (для позначення типу ліній, що з'єднують подкривые (тонкі лінії на рис. 5. 8), можна буде буде зобразити рекурсивні відносини між чотирма типами кривих оскільки показано на рис. 5.9.

@Рис. 5.6. Частини кривою Серпинского

=====93

@Рис. 5.7. Розбивка кривою типу A

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

Private Sub SierpA (Depth As Integer, Dist As Single)

If Depth = 1 Then

Line -Step (-Dist, Dist)

Line -Step (-Dist, 0)

Line -Step (-Dist, -Dist)

Else

SierpA Depth — 1, Dist

Line -Step (-Dist, Dist)

SierpB Depth — 1, Dist

Line -Step (-Dist, 0)

SierpD Depth — 1, Dist

Line -Step (-Dist, -Dist)

SierpA Depth — 1, Dist

End If End Sub

@Рис. 5.8. Криві Серпинского, освічені з менших кривих Серпинского

=====94

@Рис. 5.9. Рекурсивні співвідношень між кривими Серпинского

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

Sub Sierpinski (Depth As Integer, Dist As Single)

SierpB Depth, Dist

Line -Step (Dist, Dist)

SierpC Depth, Dist

Line -Step (Dist, -Dist)

SierpD Depth, Dist

Line -Step (-Dist, -Dist)

SierpA Depth, Dist

Line -Step (-Dist, Dist) End Sub

Аналіз часу виконання программы

Чтобы проаналізувати час виконання цієї алгоритму, необхідно визначити число викликів кожної з чотирьох процедур малювання кривих. Нехай T (N) — число викликів будь-якої з чотирьох основних підпрограм основний процедури Sierpinski при побудові кривою порядку N. Якщо порядок кривою дорівнює 1, крива кожного типу малюється лише одне раз. Збільшивши сюди основну процедуру, одержимо T (1) = 5. При кожному рекурсивном виклик, процедура викликає себе й інші процедури в чотири рази. Оскільки ці процедури практично однакові, то T (N) буде однаковим, незалежно від цього, яка процедура викликається першої. Це пов’язано з тим, що криві Серпинского симетричні мають один і той самого числа кривих різних типів. Рекурсивні рівняння для T (N) виглядають так:

T (1) = 5 T (N) = 1 + 4 * T (N-1) для N > 1.

Эти рівняння майже збігаються з рівняннями, що були для оцінки часу виконання алгоритму, яка малює криві Гільберта. Єдина відмінність у тому, що з кривих Гільберта T (1) = 1. Порівняння значень цих рівнянь показує, що TSierpinski (N) = THilbert (N+1). Наприкінці попереднього розділу засвідчили, що THilbert (N) = (4N — 1) / 3, тому TSierpinski (N) = (4N+1 — 1) / 3, що також становить O (4N).

=====95

Так ж, як і алгоритм побудови кривих Гільберта, цей алгоритм виконується під час порядку O (4N), але ці непогані й погано. Крива Серпинского складається з O (4N) ліній, тому один алгоритм неспроможна намалювати криву Серпинского швидше, як час порядку O (4N). Криві Серпинского також цілком заповнюють екран більшості комп’ютерів при порядку кривою, більшому чи рівному 9. При якомусь порядку, більшому 9, ви зіштовхнетеся з обмеженнями мови Visual Basic і можливостей вашого комп’ютера, але, швидше за все, ще раніше будете обмежені граничним дозволом екрана. Програма Sierp, показана на рис. 5. 10, використовує цей рекурсивний алгоритм для малювання кривих Серпинского. За виконання програми, ставте спочатку невелику глибину рекурсії (менше 6), до того часу, поки ви визначте, як швидко виконується цю програму вашому компьютере.

Опасности рекурсии

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

Бесконечная рекурсия

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

@Рис. 5. 10 Програма Sierp

=====96

Private Function BadFactorial (num As Integer) As Integer

BadFactorial = num * BadFactorial (num — 1) End Function

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

Private Function BadFactorial2(num As Double) As Double

If num = 0 Then

BadFactorial2 = 1

Else

BadFactorial2 = num * BadFactorial2(num-1)

End If End Function

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

Private Function BadFib (num As Double) As Double

If num = 0 Then

BadFib = 0

Else

BadFib = BadPib (num — 1) + BadFib (num — 2)

End If End Function

И остання проблема, що з безкінечною рекурсією, у тому, що «нескінченна» насправді означає «до того часу, поки що не вичерпана стековое простір». Навіть коректно написані рекурсивні процедури будуть іноді спричинить переповненню стека і аварійному завершення роботи. Наступна функція, яка обчислює суму N + (N — 1) + … + 2 +1, призводить до вичерпаності стекового простору на великих значеннях N. Найбільше можливе значення N, у якому програма ще працюватиме, залежить від конфігурації вашого компьютера.

Private Function BigAdd (N As Double) As Double

If N 1 Then Hilbert depth — 1, Dy, Dx 2 HilbertPicture. Line -Step (Dx, Dy)

If depth > 1 Then Hilbert depth — 1, Dx, Dy 3 HilbertPicture. Line -Step (Dy, Dx)

If depth > 1 Then Hilbert depth — 1, Dx, Dy 4 HilbertPicture. Line -Step (-Dx, -Dy)

If depth > 1 Then Hilbert depth — 1, -Dy, -Dx

End Sub

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

====109

Const STACK_SIZE =20 Dim DepthStack (0 To STACK_SIZE) Dim DxStack (0 To STACK_SIZE) Dim DyStack (0 To STACK_SIZE) Dim PCStack (0 To STACK_SIZE) Dim TopOfStack As Integer

Private Sub SaveValues (Depth As Integer, Dx As Single, _

Dy As Single, pc As Integer)

TopOfStack = TopOfStack + 1

DepthStack (TopOfStack) = Depth

DxStack (TopOfStack) = Dx

DyStack (TopOfStack) = Dy

PCStack (TopOfStack) = pc End Sub

Private Sub RestoreValues (Depth As Integer, Dx As Single, _

Dy As Single, pc As Integer)

Depth = DepthStack (TopOfStack)

Dx = DxStack (TopOfStack)

Dy = DyStack (TopOfStack) pc = PCStack (TopOfStack)

TopOfStack = TopOfStack — 1

End Sub

Следующий код демонструє нерекурсивную версію підпрограми Hilbert.

Private Sub Hilbert (Depth As Integer, Dx As Single, Dy As Single) Dim pc As Integer Dim tmp As Single

pc = 1

Do

Select Case pc

Case 1

If Depth > 1 Then «Рекурсия.

" Зберегти поточні значения.

SaveValues Depth, Dx, Dy, 2

" Підготуватися до рекурсии.

Depth = Depth — 1 tmp = Dx

Dx = Dy

Dy = tmp pc = 1 «Перейти на початок рекурсивного вызова.

Else «Умова остановки.

" Досить глибокий рівень рекурсии.

" Продовжити зі 2 блоком коду. pc = 2

End If

Case 2

HilbertPicture. Line -Step (Dx, Dy)

If Depth > 1 Then «Рекурсия.

" Зберегти поточні значения.

SaveValues Depth, Dx, Dy, 3

" Підготуватися до рекурсии.

Depth = Depth — 1

" Dx і Dy залишаються не змінювалась. pc = 1 Перейти на початок рекурсивного вызова.

Else «Умова остановки.

" Досить глибокий рівень рекурсии.

" Продовжити з 3 блоком коду. pc = 3

End If

Case 3

HilbertPicture. Line -Step (Dy, Dx)

If Depth > 1 Then «Рекурсия.

" Зберегти поточні значения.

SaveValues Depth, Dx, Dy, 4

" Підготуватися до рекурсии.

Depth = Depth — 1

" Dx і Dy залишаються не змінювалась. pc = 1 Перейти на початок рекурсивного вызова.

Else «Умова остановки.

" Досить глибокий рівень рекурсии.

" Продовжити із чотирьох блоком коду. pc = 4

End If

Case 4

HilbertPicture. Line -Step (-Dx, -Dy)

If Depth > 1 Then «Рекурсия.

" Зберегти поточні значения.

SaveValues Depth, Dx, Dy, 0

" Підготуватися до рекурсии.

Depth = Depth — 1 tmp = Dx

Dx = -Dy

Dy = -tmp pc = 1 Перейти на початок рекурсивного вызова.

Else «Умова остановки.

" Досить глибокий рівень рекурсии.

" Кінець цього рекурсивного виклику. pc = 0

End If

Case 0 «Повернення з рекурсии.

If TopOfStack > 0 Then

RestoreValues Depth, Dx, Dy, pc

Else

" Стік порожній. Выход.

Exit Do

End If

End Select

Loop End Sub

======111

Время виконання цієї алгоритму то, можливо нелегко оцінити безпосередньо. Оскільки методи перетворення рекурсивних процедур в нерекурсивні не змінюють час виконання алгоритму, цю процедуру як і, як і попередня версія, має час виконання порядку O (N4). Програма Hilbert2 демонструє нерекурсивный алгоритм побудови кривих Гільберта. Давайте спочатку побудова нескладних кривих (менше 6 порядку), доки дізнаєтеся, як швидко виконуватиметься цю програму вашому компьютере.

Нерекурсивное побудова кривих Серпинского

Приведенный раніше алгоритм побудови кривих Серпинского включає у собі непряму і множинну рекурсию. Оскільки алгоритм складається з чотирьох підпрограм, що викликають одне одного, не можна просто пронумерувати важливі рядки, як це можна було зробити на разі алгоритму побудови кривих Гільберта. З цього проблемою можна впоратися, злегка змінивши алгоритм. Рекурсивна версія цього алгоритму складається з чотирьох підпрограм SierpA, SierpB, SierpC і SierpD. Підпрограма SierpA виглядає так:

Private Sub SierpA (Depth As Integer, Dist As Single)

If Depth = 1 Then

Line -Step (-Dist, Dist)

Line -Step (-Dist, 0)

Line -Step (-Dist, -Dist)

Else

SierpA Depth — 1, Dist

Line -Step (-Dist, Dist)

SierpB Depth — 1, Dist

Line -Step (-Dist, 0)

SierpD Depth — 1, Dist

Line -Step (-Dist, -Dist)

SierpA Depth — 1, Dist

End If End Sub

Три інші процедури аналогічні. Нескладно об'єднати ці чотири процедури в одну подпрограмму.

Private Sub SierpAll (Depth As Integer, Dist As Single, Func As Integer)

Select Case Punc

Case 1 «SierpA

Case 2 «SierpB

Case 3 «SierpC

Case 4 «SierpD

End Select End Sub

======112

Параметр Func повідомляє подпрограмме, який блок коду виконувати. Виклики підпрограм вживають виклики процедури SierpAll з певним значенням Func. Наприклад, виклик підпрограми SierpA замінюється на виклик процедури SierpAll з параметром Func, рівним 1. Так само замінюються виклики підпрограм SierpB, SierpC і SierpD. Отримана процедура рекурсивно викликає себе у 16 різних точках. Ця процедура набагато складніше, ніж процедура Hilbert, але у інші стосунки вона не має ті ж самі структуру і тому до неї можна застосувати тих самих методів усунення рекурсії. Можна також використовувати першу цифру міток pc, визначення номери блоку коду, що повинен виконуватися. Перенумеруем рядки у коді SierpA числами 11, 12, 13 тощо. Перенумеруем рядки у коді SierpB числами 21, 22, 23 і т.д. Нині можна пронумерувати ключові рядки коду усередині кожного з блоків. Для коду підпрограми SierpA ключовими рядками будут:

" Код SierpA. 11 If Depth = 1 Then

Line -Step (-Dist, Dist)

Line -Step (-Dist, 0)

Line -Step (-Dist, -Dist)

Else

SierpA Depth — 1, Dist 12 Line -Step (-Dist, Dist)

SierpB Depth — 1, Dist 13 Line -Step (-Dist, 0)

SierpD Depth — 1, Dist 14 Line -Step (-Dist, -Dist)

SierpA Depth — 1, Dist

End If

Типичная «рекурсія» з коду підпрограми SierpA в код підпрограми SierpB виглядає так:

SaveValues Depth, 13 «Продовжити з кроку 13 після завершення. Depth = Depth — 1 pc = 21 «Передати управління початку коду SierpB.

======113

Метка 0 зарезервована для позначення виходу з «рекурсії». Наступний код демонструє нерекурсивную версію процедури SierpAll. Код для підпрограм SierpB, SierpC, і SierpD аналогічний коду для SierpA, й тому він опущен.

Private Sub SierpAll (Depth As Integer, pc As Integer)

Do

Select Case pc

" **********

" * SierpA *

" **********

Case 11

If Depth 0 Then ReDim Preserve ToNode (0 To NumLinks — 1) End Sub

Sub RemoveNode (node As Integer) Dim і As Integer

" Зрушити елементи масиву FirstLink, щоб заполнить

" порожню ячейку.

For і = node + 1 To NumNodes

FirstLink (i — 1) = FirstLink (i)

Next i

" Зрушити елементи масиву NodeCaption.

For і = node + 1 To NumNodes — 1

NodeCaption (i — 1) = NodeCaption (i)

Next i

" Обновити записи масиву ToNode.

For і = 0 To NumLinks — 1

If ToNode (i) >= node Then ToNode (i) = ToNode (i) — 1

Next i

" Видалити зайву запис масиву FirstLink.

NumNodes = NumNodes — 1

ReDim Preserve FirstLink (0 To NumNodes)

ReDim Preserve NodeCaption (0 To NumNodes — 1)

Unload FStarForm. NodeLabel (NumNodes) End Sub

Это набагато складніше, ніж відповідний код у програмі NAry:

Public Function DeleteDescendant (target As NAryNode) As Boolean Dim і As Integer Dim child As NAryNode

" Чи є вузол дочірнім узлом.

For і = 1 To Children. Count

If Children. Item (i) Is target Then

Children. Remove i

DeleteDescendant = True

Exit Function

End If

Next i

" Якщо це дочірній вузол, рекурсивно

" перевірити інших потомков.

For Each child In Children

If child. DeleteDescendant (target) Then

DeleteDescendant = True

Exit Function

End If

Next child End Function

=======125−126

Повні деревья

Полное дерево (complete tree) містить максимально можливу кількість вузлів на кожному рівні, крім нижнього. Усі вузли на нижньому рівні зсуваються вліво. Наприклад, кожен рівень троичного дерева містить у точності три дочірніх вузла, крім листя, і, можливо, одного вузла врівень вище листя. На рис. 6.9 показані повні двоичное і троичное дерева. Повні дерева мають також низку важливих властивостей. По-перше, це найкоротші дерева, які можуть містити заданий число вузлів. Наприклад, двоичное дерево на рис. 6.9 — одне з коротких двійкових дерев з 6 вузлами. Існують інші двоичные дерева з 6 вузлами, але й одна з них має висоту менше 3. По-друге, якщо повне дерево порядку D складається з N вузлів, матиме висоту порядку O (logD (N)) і O (N) листя. Ці факти яких багато важать, оскільки багато алгоритми обходять дерева згори донизу чи протилежному напрямі. Час виконання алгоритму, виконує одне з цих дій, порядку O (N). Надзвичайно корисне властивість повних дерев у тому, що вони можуть бути дуже компактно записані в масивах. Якщо пронумерувати вузли в «природному» порядку, згори донизу й зліва направо, можна помістити елементи дерева в масив у порядку. На рис. 6. 10 показано, як і записати повне дерево в масиві. Корінь дерева перебуває у нульової позиції. Дочірні вузли вузла I перебувають на позиціях 2 * I + 1 і 2 * I + 2. Наприклад, на рис. 6. 10, нащадки вузла в позиції 1 (вузла B), перебувають у позиціях 3 і 4 (вузли D і E). Легко узагальнити цей спектакль на повні дерева вищого порядку D. Корінь дерева також перебувати у позиції 0. Нащадки вузла I займають позиції від D * I + 1 до D * I +(I — 1). Наприклад, в троичном дереві, нащадки вузла в позиції 2, посідатимуть позиції 7, 8 і 9-те. На рис. 6. 11 показано повне троичное дерево та її подання до вигляді массива.

@Рис. 6.9. Повні деревья

=========127

@Рис. 6. 10. Запис повного двоичного дерева в массиве

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

Обход дерева

Последовательное звернення всім вузлам називається обходом (traversing) дерева. Є кілька послідовностей обходу вузлів двоичного дерева. Три найпростіших їх — прямий (preorder), симетричний (inorder), і зворотний (postorder)обход, описуються простими рекурсивними алгоритмами. До кожного заданого вузла алгоритми виконують такі дії: Прямий обхід: 1. Звернення до вузлу. 2. Рекурсивний прямий обхід лівого поддерева. 3. Рекурсивний прямий обхід правого поддерева. Симетричний обхід: 1. Рекурсивний симетричний обхід лівого поддерева. 2. Звернення до вузлу. 3. Рекурсивний симетричний обхід лівого поддерева. Зворотний обхід: 1. Рекурсивний зворотний обхід лівого поддерева. 2. Рекурсивний зворотний обхід правого поддерева. 3. Звернення до узлу.

@Рис. 6. 11. Запис повного троичного дерева в массиве

=======128

Все три порядку обходу є взірцями обходу завглибшки (depth-first traversal). Обхід починається з проходу всередину дерева до того часу, поки алгоритм не досягне листя. За повернення з рекурсивного виклику підпрограми, алгоритм переміщається з дерева у напрямі, переглядаючи шляху, що він пропустив під час руху вниз. Обхід завглибшки зручно залучити до алгоритми, які мають спочатку обійти листя. Наприклад, метод гілок і національних кордонів, описаний у вісім главі, як би якнайшвидше намагається досягти листя. Він використовує результати, отримані лише на рівні листя зменшення часу пошуку що залишилася частини дерева. Четвертий метод перебору вузлів дерева — це обхід завширшки (breadth-first traversal). Цей метод звертається всім вузлам на заданому рівні дерева, до того, як можливість перейти до глибшим рівням. Алгоритми, які проводять повний пошук з дерева, часто використовують обхід завширшки. Алгоритм пошуку найкоротшого маршруту із установкою міток, описаний о 12-й главі, є обхід завширшки, дерева найкоротшого шляху до мережі. На рис. 6. 12 показано невеличке дерево і Порядок, у якому перебираються вузли під час прямого, симетричного й протилежного обходу, і навіть обходу в ширину.

@Рис. 6. 12. Обходи дерева

======129

Для дерев більше, ніж 2 порядку, досі можна буде визначати прямий, зворотний обхід, і обхід завширшки. Симетричний обхід визначається неоднозначно, оскільки звернення до кожного вузлу може статися після звернення одного, двом, чи трьом його нащадкам. Наприклад, в троичном дереві, звернення до вузлу може статися після звернення для її першому нащадку чи влітку після звернення до другого нащадку. Деталі реалізації обходу залежить від того, як записано дерево. Для обходу дерева з урахуванням колекцій дочірніх вузлів, програма повинна використовувати трохи інший алгоритм, ніж для обходу дерева, записаного з допомогою нумерації зв’язків. Особливо просто обходити повні дерева, записані масиві. Алгоритм обходу завширшки, який вимагає додаткових зусиль у інших уявленнях дерев, для уявлень з урахуванням масиву тривіальний, так як вузли записані у такому порядку. Наступний код демонструє алгоритми обходу повного двоичного дерева:

Dim NodeLabel () As String «Запис міток вузлів. Dim NumNodes As Integer

" Ініціалізація дерева.

: Private Sub Preorder (node As Integer)

Print NodeLabel (node) «Узел.

" Перший потомок.

If node * 2 + 1 FOX. Так само можна закодувати рядки 6 заголовних літер на вигляді числа в форматі long і рядки 10 літер — і кількість в форматі double. Дві такі процедури конвертують рядки у вересня форматі double і обратно:

Const STRING_BASE = 27 Const ASC_A = 65 ' ASCII код для символу «A «. ' Перетворення рядки з число в форматі double. ' ' full_len — повна довжина, що має мати рядок. ' Потрібна, якщо рядок занадто коротка (наприклад «AX «- ' це рядок із трьох символів). Function StringToDbl (txt As String, full_len As Integer) As Double Dim strlen As Integer Dim і As Integer Dim value As Double Dim ch As String * 1

strlen = Len (txt)

If strlen > full_len Then strlen = full_len

value = 0#

For і = 1 To strlen ch = Mid$(txt, і, 1) value = value * STRING_BASE + Asc (ch) — ASC_A + 1

Next i

For і = strlen + 1 To full_len value = value * STRING_BASE

Next і End Function

' Протилежне декодування рядки формату double. Function DblToString (ByVal value As Double) As String Dim strlen As Integer Dim і As Integer Dim txt As String Dim Power As Integer Dim ch As Integer Dim new_value As Double

txt = ««

Do While value > 0 new_value = Int (value / STRING_BASE) ch = value — new_value * STRING_BASE

If ch 0 Then txt = Chr$(ch + ASC_A — 1) + txt value = new_value

Loop

DblToString = txt End Function

===========228

В табл. 9.1 наведено час виконання програмою Encode сортування 2000 рядків різної довжини за комп’ютером з процесором Pentium і тактовою частотою 90 МГц. Зауважимо, що результати схожі кожному за типу кодування. Сортування 2000 чисел в форматі double займає приблизно однаковий час незалежно від цього, представляють вони рядки 3 чи 10 символов.

========229

@Таблиця 9.1. Час сортування 2000 рядків з різних кодувань в секундах

Можно також кодувати рядки, котрі перебувають з заголовних літер. Строку з заголовних літер і цифр можна закодувати по підставі 37 замість 27. Код літери A дорівнюватиме 1, B — 2, …, Z — 26, код 0 буде 27, …, і 9-те — 36. Рядок AH7 буде кодуватимуть як 372 * 1 + 37 * 8 + 35 = 1700. Звісно, під час використання більшого підстави, довжина рядки, що можна закодувати числом типу integer, long чи double буде відповідно коротше. При підставі рівному 37, можна закодувати рядок із 2 символів в числі формату integer, з 5 символів серед формату long, і десяти символів в числі формату double.

Примеры программ

Чтобы полегшити порівняння різних алгоритмів сортування, програма Sort демонструє більшість алгоритмів, добре описані у цієї главі. Сортування дозволяє поставити число сортируемых елементів, їх максимальне значення, і порядок розташування елементів — прямий, зворотний чи розташування в випадковому порядку. Програма створює список випадково розташованих чисел в форматі long і сортує його, використовуючи обраний алгоритм. Спочатку сортируйте короткі списки, доки визначте, як швидко ваш комп’ютер може виконувати операції сортування. Особливо важливо для повільних алгоритмів сортування вставкою, сортування вставкою з використанням зв’язкового списку, сортування вибором, і пузырьковой сортування. Деякі алгоритми переміщають великі блоки пам’яті. Наприклад, алгоритм сортування вставкою переміщає елементи списку у тому, щоб було вставити новий елемент до середини списку. Для переміщення елементів програмі, написаної на Visual Basic, доводиться використовувати цикл For. Наступний код показує, як сортування вставкою переміщає елементи з List (j) до List (max_sorted) у тому, щоб звільнити місце під новий елемент в позиції List (j):

For k = max_sorted To j Step -1

List (k + 1) = List (k) Next k List (j) = next_num

==========230

Интерфейс прикладного програмування системи Windows включає дві функції, що дозволяють значно швидше виконувати переміщення блоків пам’яті. Програми, скомпільовані 16-битной версією компілятора Visual Basic 4, може використати функцію hmemcopy. Програми, скомпільовані 32-битными компиляторами Visual Basic 4 і п’яти, може використати функцію RtlMoveMemory. Обидві функції беруть у ролі параметрів кінцевий і вихідний адреси — й число байт, що має бути скопійовано. Наступний код показує, що й оголошувати цих функцій в модулі. BAS:

#if Win16 Then

Declare Sub MemCopy Lib «Kernel «Alias _

" hmemcpy «(dest As Any, src As Any, _

ByVal numbytes As Long) #Else

Declare Sub MemCopy Lib «Kernel32 «Alias _

" RtlMoveMemory «(dest As Any, src As Any, _

ByVal numbytes As Long) #EndIf

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

If max_sorted >= j Then _

MemCopy List (j + 1), List (j), _

Len (next_num) * (max_sorted — j + 1) List (j) = next_num

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

Сортировка выбором

Сортировка вибором (selectionsort) — простий алгоритм зі складність порядку O (N2). Ідея полягає у пошуку найменшого елемента у списку, і потім змінюється місцями з елементом на вершині списку. Потім перебуває найменший елемент які залишилися, і змінюється місцями з іншим елементом. Процес триває до того часу, доки всі елементи не займуть своє кінцеве положение.

Public Sub Selectionsort (List () As Long, min As Long, max As Long) Dim і As Long Dim j As Long Dim best_value As Long Dim best_j As Long

For і = min To max — 1

' Знайти найменший елемент які залишилися. best_value = List (i) best_j = i

For j = і + 1 To max

If List (j) < best_value Then best_value = List (j) best_j = j

End If

Next j

' Помістити елемент на место.

List (best_j) = List (i)

List (i) = best_value

Next і End Sub

========231

При пошуку I-го найменшого елемента, алгоритму доводиться перебрати N-I елементів, котрі зайняли своє кінцеве становище. Час виконання алгоритму пропорційно N + (N — 1) + (N — 2) + … + 1, чи порядку O (N2). Сортування вибором непогано працює зі списками, елементи у яких розташовані випадково, чи у прямому порядку, але трохи гірше, якщо список спочатку відсортований у порядку. Для пошуку найменшого елемента у списку сортування вибором виконує наступний код:

If list (j) < best_value Then best_value = list (j) best_j = j End If

Если спочатку список відсортований у порядку, умова list (j) < best_value виконується багато часу. Наприклад, з першого проході він буде істинно всім елементів, оскільки з елемент менше попереднього. Алгоритм буде багаторазово виконувати рядки з оператором If, що сприятиме деякому уповільнення роботи алгоритму. Не найшвидший алгоритм у складі добре описані у главі, але надзвичайно простий. Не лише полегшує розробку і налагодження, а й робить сортування вибором досить швидкої для невеликих завдань. Багато інші алгоритми настільки складні, що вони сортують дуже маленькі списки медленнее.

Рандомизация

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

Public Sub Unsort (List () As Long, min As Long, max As Long) Dim і As Long Dim Pos As Long Dim tmp As Long

For і - min To max — 1 pos = Int ((max — і + 1) * Rnd + і) tmp = List (pos)

List (pos) = List (i)

List (i) = tmp

Next і End Sub

==============232

Т.к. алгоритм заповнює кожну позицію лише одне разів його складність порядку O (N). Нескладно показати, що можливість, що елемент виявиться на будь-якої позиції, дорівнює 1/N. Оскільки елемент може у будь-якому положенні з рівної ймовірністю, цей алгоритм справді призводить до випадковому розміщення елементів. Результат залежить від цього, наскільки хорошим є генератор випадкових чисел. Функція Rnd в Visual Basic дає прийнятний результат більшість випадків. Слід переконатися, що ваша програма використовує оператор Randomize для ініціалізації функції Rnd, інакше при кожному запуску програми функція Rnd видаватиме те ж послідовність «випадкових» значень. Зауважимо, що з алгоритму не важливий початковий порядок розташування елементів. Якщо до вас необхідно неодноразово рандомизировать список елементів, немає потреби його попередньо сортувати. Програма Unsort показує використання цього алгоритму для рандомизации відсортованої списку. Запровадьте число елементів, що ви хочете рандомизировать, і натиснімо кнопку Go (Почати). Програма показує вихідний відсортований список чисел і результати рандомизации.

Сортировка вставкой

Сортировка вставкою (insertionsort) — іще одна алгоритм складності порядку O (N2). Ідея у тому, щоб зробити новий сортированный список, переглядаючи по черзі все елементи в вихідному списку. У цьому, обираючи черговий елемент, алгоритм переглядає зростаючий відсортований список, знаходить необхідну становище елемента у ньому, й поміщає елемент на своє місце у новий список.

Public Sub Insertionsort (List () As Long, min As Long, max As Long) Dim і As Long Dim j As Long Dim k As Long Dim max_sorted As Long Dim next_num As Long

max_sorted = min -1

For і = min To max

' Це вставляемое число.

Next_num = List (i)

' Пошук його у списке.

For j = min To max_sorted

If List (j) >= next_num Then Exit For

Next j

' Перемістити великі елементи вниз, чтобы

' звільнити місце для створення нового числа.

For k = max_sorted To j Step -1

List (k + 1) = List (k)

Next k

' Помістити новий элемент.

List (j) = next_num

' Збільшити лічильник відсортованих елементів. max_sorted = max_sorted + 1

Next і End Sub

=======233

Может виявитися, що кожного з елементів в вихідному списку, алгоритму доведеться перевіряти вже всі відсортовані елементи. Це відбувається, наприклад, тоді як вихідному списку елементи були вже відсортовані. У цьому вся разі, алгоритм поміщає кожен новий елемент насамкінець зростаючого відсортованої списку. Повне число кроків, які знадобиться виконати, становить 1 + 2 + 3 + … + (N — 1), тобто O (N2). Не занадто ефективно, якщо з теоретичним межею O (N * log (N)) для алгоритмів з урахуванням операцій порівняння. Фактично, цей алгоритм дуже швидкий навіть у порівнянні з іншими алгоритмами порядку O (N2), такі як сортування вибором. Чимало часу алгоритм сортування вставкою витрачає на переміщення елементів у тому, щоб вставити новий елемент до середини відсортованої списку. Використання при цьому функції АПІ MemCopy збільшує швидкість роботи алгоритму майже вдвічі більше. Чимало часу витрачається і пошук правильного становища для нового елемента. У 10-му главі описано кілька алгоритмів пошуку відсортованих списках. Застосування алгоритму интерполяционного пошуку набагато прискорює виконання алгоритму сортування вставкою. Интерполяционный пошук докладно описується удесятеро главі, тому не будемо нині ньому зупинятися. Програма FastSort використовує обидва цих методу підвищення продуктивності сортування вставкою. З використанням функції MemCopy і интерполяционного пошуку, цю версію алгоритму більш ніж 15 раз швидше, ніж исходная.

Вставка в зв’язкових списках

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

=========234

Public Sub LinkInsertionSort (ListTop As ListCell) Dim new_top As New ListCell Dim old_top As ListCell Dim cell As ListCell Dim after_me As ListCell Dim nxt As ListCell

Set old_top = ListTop. NextCell

Do While Not (old_top Is Nothing)

Set cell = old_top

Set old_top = old_top. NextCell

' Знайти, куди треба помістити элемент.

Set after_me = new_top

Do

Set nxt = after_me. NextCell

If nxt Is Nothing Then Exit Do

If nxt. Value >= cell. Value Then Exit Do

Set after_me = nxt

Loop

' Вставити елемент після позиції after_me.

Set after_me. NextCll = cell

Set cell. NextCell = nx

Loop

Set ListTop. NextCell = new_top. NextCell End Sub

Т.к. цей алгоритм перебирає все елементи, може знадобитися порівняння кожного елемента з усіма елементами в відсортованому списку. У цьому вся найгіршому разі обчислювальна складність алгоритму порядку O (N2). Найкращий випадок при цьому алгоритму досягається, коли вихідний список спочатку відсортований у порядку. У цьому кожну наступну елемент менше, ніж попередня, тому алгоритм поміщає їх у початок відсортованої списку. У цьому потрібно виконати одну операцію порівняння елементів, й у найкращому разі час виконання алгоритму буде порядку O (N). У усередненому разі, алгоритму доведеться здійснити пошук приблизно на половині відсортованої списку у тому, щоб знайти місце розташування елемента. У цьому алгоритм виконується приблизно за 1 + 1 + 2 + 2 + … + N/2, чи порядку O (N2) кроків. Поліпшена процедура сортування вставкою, яка використовує интерполяционный пошук і освоєння функцію MemCopy, працює значно швидше, ніж версія зі зв’язковим списком, тому останню процедуру краще використовувати, якщо програма вже зберігає елементи в зв’язковому списку. Перевага використання зв’язкових списків для вставки у цьому, що заодно переміщаються лише покажчики, а чи не самі записи даних. Передача покажчиків то, можливо швидше, ніж копіювання записів повністю, якщо елементи є великі структури данных.

=======235

Пузырьковая сортировка

Пузырьковая сортування (bubblesort) — це алгоритм, готовий до сортування списків, у яких перебувають у майже упорядкованому стані. Коли на початку процедури список повністю відсортований, алгоритм виконується нас дуже швидко під час порядку O (N). Якщо частина елементів перебувають не так на своїх місцях, алгоритм виконується повільніше. Якщо спочатку елементи перебувають у випадковому порядку, алгоритм виконується під час порядку O (N2). Тому перед застосуванням пузырьковой сортування важливо переконатися, що елементи переважно розташовані усе своєю чергою. При пузырьковой сортування список проглядається до того часу, доки знайдуться дві сусідні елемента, розташованих за порядку. Але вони змінюються місцями, і процедуру триває далі. Алгоритм повторює цей процес до того часу, доки всі елементи не займуть місця. На рис. 9.2 показано, як алгоритм спочатку виявляє, що елементи 6 і трьох розташовані за ладу і тому змінює їх місцями. Під час наступного проходу, змінюються місцями елементи 5 і трьох, наступного — 4 і трьох. Після ще одного проходу алгоритм виявляє, що це елементи розташовані по ладу і завершує роботу. Можна простежити за переміщеннями елемента, який первісно був розташований нижче, ніж після сортування, наприклад елемента 3 на рис. 9.2. У час кожного проходу елемент переміщається однією позицію ближчі один до своєму кінцевому становищу. Він йде до вершині списку подібно пухирцю газу, що спливає до у склянці води. Цей ефект і дав назву алгоритму пузырьковой сортування. Можна доповнити алгоритм кілька поліпшень. По-перше, якщо елемент лежить у списку вище, аніж має бути, ви не побачите картину, відрізняється від тієї, яка приведено на рис. 9.2. На рис. 9.3 показано, що алгоритм спочатку виявляє, що елементи 6 і трьох перебувають у неправильному порядку, і змінює їх місцями. Потім алгоритм продовжує переглядати масив й зауважує, що тепер неправильно розташовані елементи 6 і 4, і також змінює їх місцями. Потім змінюються місцями елементи 6 і п’яти, і елемент 6 займає своє место.

@Рис. 9.2. «Спливання» элемента

========236

@Рис. 9.3. «Занурення» элемента

При перегляді масиву згори донизу, елементи, які переміщаються вгору, зсуваються всього однією позицію. Ті ж елементи, які переміщаються вниз, зсуваються сталася на кілька позицій за прохід. Це можна використовуватиме прискорення роботи алгоритму пузырьковой сортування. Якщо чергувати перегляд масиву згори донизу і знизу вгору, то переміщення елементів у прямому й зворотному напрямах буде однаково швидким. Під час проходів згори донизу, найбільший елемент списку переміщається на місце, тоді як у час проходів знизу вгору — найменший. Якщо M елементів списку розташовані не так на своїх місцях, алгоритму знадобиться трохи більше M проходів у тому, аби схилити елементи усе своєю чергою. Якщо у списку N елементів, алгоритму знадобиться N кроків кожному за проходу. Таким чином, повне час виконання при цьому алгоритму порядку O (M * N). Якщо спочатку список організований випадково, більшість елементів перебуватиме не так на своїх місцях. У прикладі, наведеному на рис. 9. 3, елемент 6 тричі змінюється місцями з іншими елементами. Замість виконання трьох окремих перестановок, можна зберегти значення 6 у тимчасової перемінної до того часу, поки що не знайдено кінцеве становище елемента. Це може заощадити велика кількість кроків алгоритму, якщо елементи переміщаються великі відстані всередині масиву. Останнє поліпшення — обмеження проходів масиву. Після перегляду масиву, останні переставлені елементи позначають частина масиву, що містить невпорядковані елементи. При проході згори донизу, наприклад, найбільший елемент переміщається в кінцеве становище. Оскільки немає великих елементів, які було б помістити його, можна розпочати черговий прохід знизу вгору з цим крапки й у ньому ж закінчувати такі проходи згори вниз.

========237

Таким самим чином, після проходу знизу вгору, можна зрушити позицію, з якої розпочнеться черговий прохід згори донизу, і буде закінчуватися наступні проходи знизу вгору. Реалізація алгоритму пузырьковой сортування мовою Visual Basic використовує перемінні min і max для позначення першого заступника й останнього елементів списку, які у своїх місцях. Принаймні того, як алгоритму повторює проходи за списком, ці перемінні оновлюються, вказуючи становище останньої перестановки.

Public Sub Bubblesort (List () As Long, ByVal min As Long, ByVal max As Long) Dim last_swap As Long Dim і As Long Dim j As Long Dim tmp As Long

' Повторювати до завершения.

Do While min < max

' «Спливання». last_swap = min — 1

' Тобто For і = min + 1 To max. і = min + 1

Do While і List (i) Then

' Знайти, де його помістити. tmp = List (i — 1) j = i

Do

List (j — 1) = List (j) j = j + 1

If j > max Then Exit Do

Loop While List (j) < tmp

List (j — 1) = tmp last_swap = j — 1 і = j + 1

Else і = і + 1

End If

Loop

' Обновити зміну max. max = last_swap — 1

' «Занурення». last_swap = max + 1

' Тобто For і = max -1 To min Step -1 і = max — 1

Do While і >= min

' Знайти «пузырек».

If List (i + 1) < List (i) Then

' Знайти, де його помістити. tmp = List (i + 1) j = i

Do

List (j + 1) = List (j) j = j — 1

If j < min Then Exit Do

Loop While List (j) > tmp

List (j + 1) = tmp last_swap = j + 1 і = j — 1

Else і = і - 1

End If

Loop

' Обновити зміну min.

Min = last_swap + 1

Loop End Sub

==========238

Для здобуття права протестувати алгоритм пузырьковой сортування з допомогою програми Sort, поставте галочку на полі Sorted (Відсортовані) у сфері Initial Ordering (Початковий порядок). Запровадьте число елементів на полі #Unsorted (Кількість несортированных). Після натискання на кнопку Go (Почати), програма створює наразі і сортує список, та був переставляє випадково обрані пари елементів. Наприклад, коли ви введете число 10 на полі #Unsorted, програма переставить 5 пар чисел, тобто 10 елементів виявляться не так на своїх місцях. Для другого варіанта початкового алгоритму, програма зберігає елемент в тимчасовій перемінної при переміщенні сталася на кілька кроків. Цей відбувається рішуче, якщо використовувати функцію АПІ MemCopy. Алгоритм пузырьковой сортування у програмі FastSort, використовуючи функцію MemCopy, сортує елементи в 50 чи 75 раз швидше, ніж початковий версія, реалізована у програмі Sort. У табл. 9.2 наведено час виконання пузырьковой сортування 2000 елементів за комп’ютером з процесором Pentium з тактовою частотою 90 МГц в залежність від ступеня початкової упорядкованості списку. З таблиці видно, що алгоритм пузырьковой сортування забезпечує хорошу продуктивність, лише коли список від початку майже відсортований. Алгоритм швидкої сортування, який описується далі у цій главі, здатний відсортувати хоча б список з 2000 елементів приблизно за 0,12 сек, незалежно від початкового порядку розташування елементів у списку. Пузырьковая сортування може перевершити цей результат, лише коли приблизно 97 відсотків списку було упорядковано на початок сортировки.

=====239

@Таблица 9.2. Час пузырьковой сортування 2. 000 элементов

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

Быстрая сортировка

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

Public Sub QuickSort (List () As Long, ByVal min as Integer, _

ByVal max As Integer) Dim med_value As Long Dim hi As Integer Dim lo As Integer

' Якщо залишилося менше 1 елемента, подсписок отсортирован.

If min >= max Then Exit Sub

' Вибрати значення для розподілу списку. med_value = list (min) lo = min hi = max

Do

Перегляд від hi до значення < med_value.

Do While list (hi) >= med_value hi = hi — 1

If hi = hi Then Exit Do

Loop

If lo >= hi Then lo = hi list (hi) = med_value

Exit Do

End If

' Поміняти місцями значення lo і hi. list (hi) = list (lo)

Loop

' Рекурсивна сортування двох подлистов.

QuickSort list (), min, lo — 1

QuickSort list (), lo + 1, max End Sub

=========240

Есть кілька важливими моментами у цій версії алгоритму, які варто згадати. По-перше, значення med_value для розподілу списку не входить ні з один подсписок. Це означає, що у двох подсписках міститься лише на елемент менше, ніж у вихідному списку. Т.к. число аналізованих елементів зменшується, то остаточному підсумку алгоритм завершить роботу. Ця версія алгоритму використовують у ролі роздільника перший елемент в списку. У ідеалі, це значення мала б перебувати десь посередині списку, те щоб два подсписка були приблизно рівного розміру. Проте він менш, якщо елементи спочатку майже відсортовані, то перший елемент — найменший у списку. У цьому алгоритм не розмістить жодного елемента у перший подсписок, і всі елементи на другий. Послідовність дій алгоритму приблизно такий, як показано на рис. 9.4. І тут кожен виклик підпрограми вимагає порядку O (N) кроків для переміщення всіх елементів на другий подсписок. Т.к. алгоритм рекурсивно викликає себе N — 1 раз, його виконання порядку O (N2), що ні краще, ніж в раніше розглянутих алгоритмів. Ситуацію ще більше погіршує те, що рівень вкладеності рекурсії алгоритму N — 1. Для великих списків величезна глибина рекурсії призведе до переповненню стека і збою у роботі программы.

=========242

@Рис. 9.4. Швидка сортування упорядкованого списка

Существует багато стратегій вибору розподільного елемента. Можна використовувати елемент із середини списку. Це може бути непоганим вибором, тим щонайменше, може й але це виявиться найменший чи найбільший елемент списку. У цьому один подсписок буде набагато більше, чим інший, що сприятиме зниження продуктивності до порядку O (N2) і глибокому рівню рекурсії. Інша стратегія може полягати у тому, щоб переглянути весь список, обчислити середнє арифметичне всіх значень, і використовувати його на ролі розподільного значення. Такий підхід даватиме непогані результати, але зажадає додаткових зусиль. Додатковий прохід зі складністю порядку O (N) не змінить теоретичне час виконання алгоритму, але знизить загальну продуктивність. Третя стратегія — вибрати середній з елементів на початку, кінці і середині списку. Перевага цього підходу в швидкості, оскільки знадобиться вибрати всього три елемента. У цьому гарантується, що це елемент не найбільший чи найменшим у списку, і, мабуть виявиться разів у середині списку. І, нарешті, остання стратегія, що використовується у програмі Sort, залежить від випадковому виборі елемента із списку. Можливо, це завжди буде непоганим вибором. Навіть якщо негаразд, можливо ось на чому кроці алгоритм, можливо, зробить найкращий вибір. Можливість постійного випадання найгіршого випадку дуже мала. Цікаво, що це метод перетворює ситуацію «невеличка можливість, що завжди буде погана продуктивність» у ситуацію «завжди невеличка ймовірність поганий продуктивності». Це дуже заплутана твердження пояснюється наступних абзацах. З використанням інших методів вибору точки розділу, існує невеличка можливість, що з певної організації списку час сортування порядку O (N2), Хоча малоймовірно, що така організація списку на початку сортування зустрінеться насправді, тим щонайменше, час виконання цьому буде точно порядку O (N2), неважливо чому. Це те, які можна назвати «невеличкий ймовірністю те, що завжди буде погана производительность».

===========242

При випадковому виборі точки розділу початкове розташування елементів не впливає продуктивність алгоритму. Існує невеличка ймовірність невдалого вибору елемента, але можливість, що відбуватиметься постійно, надзвичайно мала. Це можна як «завжди невеличка ймовірність поганий продуктивності». Незалежно від початкового організації списку, дуже малоймовірно, що продуктивність алгоритму порядку O (N2). Проте, залишається ситуація, яка може викликати проблеми при використанні будь-якої з цих методів. Якщо у списку обмаль різних значень у списку, алгоритм заносить безліч однакових значень в подсписок при кожному виклик. Наприклад, якщо кожне елемент у списку має значення 1, послідовність виконання буде таке, як показано на рис. 9.5. Це спричиняє великому рівню вкладеності рекурсії і дає продуктивність порядку O (N2). Схоже поведінка відбувається також за наявності значної частини повторюваних значень. Якщо список складається з 10. 000 елементів зі значеннями від 1 до 10, алгоритм досить швидко розділить список на подсписки, кожен із яких містить лише одна значення. Найпростіший вихід — ігнорувати цієї проблеми. Якщо ви знаєте, що дані немає такого розподілу, то проблеми немає. Якщо є мають невеличкий діапазон значень, то, вам стоїть розглянути інший алгоритм сортування. Описувані далі у цій главі алгоритми сортування підрахунком і блокової сортування нас дуже швидко сортують списки, даних у яких перебувають у вузькому діапазоні. Можна внести ще одне невеличке поліпшення в алгоритм швидкої сортування. Подібно багатьох іншим складнішим алгоритмам, описаним далі у цій главі, швидка сортування — не найкращий вибір для сортування невеликих списків. Завдяки простотою, сортування вибором швидше при сортування приблизно десятка записей.

@Рис. 9.5. Швидка сортування списку з единиц

==========243

@Таблица 9.3. Час швидкої сортування 20. 000 элементов

Можно поліпшити продуктивність швидкої сортування, якщо припинити рекурсию доти, як подсписки зменшаться нанівець, і використовуватиме роботи сортування вибором. У табл. 9.3 наведено час, яке займає виконання швидкої сортування 20. 000 елементів за комп’ютером з процесором Pentium з тактовою частотою 90 МГц, якщо зупиняти сортування під час досягнення подсписками певного розміру. У цьому вся тесті оптимальне значення цієї параметра було одно 15. Наступний код демонструє доопрацьований алгоритм:

Public Sub QuickSort*List () As Long, ByVal min As Long, ByVal max As Long) Dim med_value As Long Dim hi As Long Dim lo As Long Dim і As Long

' Якщо у списку більше, ніж CutOff элементов,

' завершити його сортування процедурою SelectionSort.

If max — min < cutOff Then

SelectionSort List (), min, max

Exit Sub

End If

' Вибрати те що розмежовує значення. і = Int ((max — min + 1) * Rnd + min) med_value = List (i)

' Перемістити його вперед.

List (i) = List (min)

lo = min hi = max

Do

' Перегляд згори донизу від hi до значення < med_value.

Do While List (hi) >= med_value hi = hi — 1

If hi = hi Then Exit Do

Loop

If lo >= hi Then lo = hi

List (hi) = med_value

Exit Do

End If

' Замінити місцями значення lo і hi.

List (hi) = List (lo)

Loop

' Сортувати два подсписка.

QuickSort List (), min, lo — 1

QuickSort List (), lo + 1, max End Sub

=======244

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

Сортировка слиянием

Как і швидка сортування, сортування злиттям (mergesort) — це рекурсивний алгоритм. Він також поділяє список на два подсписка, і рекурсивно сортує подсписки. Сортування злиттям ділить список навпіл, формуючи два подсписка однакового розміру. Потім подсписки рекурсивно сортуються, і відсортовані подсписки зливаються, створюючи повністю відсортований список. Хоча етап злиття легко зрозуміти, це найбільш цікава частина алгоритму. Подсписки зливаються тимчасово масив, і результати копіюється в початковий список. Створення тимчасового масиву то, можливо недоліком, якщо розмір елементів великий. Якщо розмір тимчасового розміру дуже великий, може спричинить звернення до файлу підкачування і знижувати продуктивність. Фундаментальна обізнаність із тимчасовим масивом також призводить до з того що більшість часу забирають копіювання елементів між масивами. Також, як у разі зі швидкою сортуванням, можна прискорити виконання сортування злиттям, зупинивши рекурсию, коли подсписки досягають певного мінімальної відстані. Потім можна використовувати сортування вибором завершення работы.

=========245

Public Sub Mergesort (List () As Long, Scratch () As Long, _

ByVal min As Long, ByVal max As Long) Dim middle As Long Dim i1 As Long Dim i2 As Long Dim i3 As Long

' Якщо у списку більше, ніж CutOff элементов,

' завершити його сортування процедурою SelectionSort.

If max — min < CutOff Then

Selectionsort List (), min, max

Exit Sub

End If

' Рекурсивна сортування подсписков. middle = max 2 + min 2

Mergesort List (), Scratch (), min, middle

Mergesort List (), Scratch (), middle + 1, max

' Злити відсортовані списки. i1 = min ' Індекс списку 1. i2 = middle + 1 ' Індекс списку 2. i3 = min ' Індекс об'єднаного списка.

Do While i1 NumItems Then

Search = 0 «Елемент не найден.

Else

Search = і «Елемент найден.

End If End Function

Так як алгоритм перевіряє елементи послідовно, він знаходить елементи на початку списку швидше, ніж елементи, які працюють у кінці. Найгірший випадок при цьому алгоритму виникає, якщо елемент перебуває у кінці списку чи узагалі відсутній у ньому. У таких випадках, алгоритм перевіряє все елементи у списку, тому його виконання складність в найгіршому разі порядку O (N).

@Рис. 10.1. Програма Search

========266

Если елемент перебуває у списку, то середньому алгоритм перевіряє N/2 елементів доти, як знайде шуканий. Отже, в усередненому разі час виконання алгоритму також порядку O (N). Хоча алгоритми, які виконуються під час порядку O (N), є дуже швидкими, цей алгоритм досить простий, щоб показати практично непоганих результатів. Для невеликих списків цей алгоритм має прийнятну производительность.

Поиск в упорядкованих списках

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

Public Function LinearSearch (target As Long) As Long Dim і As Long

NumSearches = 0

For і = 1 To NumItems

NumSearches = NumSearches + 1

If List (i) >= target Then Exit For

Next i

If і > NumItems Then

LinearSearch = 0 «Елемент не найден.

ElseIf List (i) target Then

LinearSearch = 0 «Елемент не найден.

Else

LinearSearch = і «Елемент найден.

End If End Function

Эта модифікація зменшує час виконання алгоритму, якщо елемент відсутні у списку. Попередньої версії пошуку вимагалося перевірити весь список остаточно, якщо шуканого елемента у ньому було. Нову версію зупиниться, щойно знайде елемент більший, ніж шуканий. Якщо шуканий елемент розташований випадково між найбільшим і найменшим елементами у списку, то середньому алгоритму знадобиться порядку O (N) кроків, щоб визначити, що цей елемент відсутні у списку. Час виконання у своїй має хоча б порядок, але практично його продуктивність буде трішки вище. Програма Search використовує цю версію алгоритма.

======267

Пошук в зв’язкових списках

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

Public Function LListSearch (target As Long) As SearchCell Dim cell As SearchCell

NumSearches = 0

Set cell = ListTop. NextCell

Do While Not (cell Is Nothing)

NumSearches = NumSearches + 1

If cell. Value >= target Then Exit Do

Set cell = cell. NextCell

Loop

If Not (cell Is Nothing) Then

If cell. Value = target Then

Set LListSearch = cell «Елемент найден.

End If

End If End Function

Программа Search використовує цей алгоритм на допомогу пошуку елементів в зв’язковому списку. Цей алгоритм виконується трохи повільніше, ніж алгоритм повного перебору в масиві через додаткових накладних витрат, пов’язані з міським управлінням покажчиками на об'єкти. Зауважте, що ваша програма Search будує зв’язкові списки, лише коли список містить трохи більше 10. 000 елементів. Щоб алгоритм виконувався трохи спритнішим, до нього можна зробити ще одне зміна. Коли покажчик наприкінці списку, можна додати в кінець списку осередок, що не міститиме шуканий елемент. Цей елемент називається сигнальною міткою (sentinel), і є тим ж цілей, як і сигнальні мітки, описані у 2 главі. Це дозволяє обробляти особливий випадок кінця списку як і, як й інші. І тут, додавання мітки насамкінець списку гарантує, що наприкінці кінців шуканий елемент буде знайдено. У цьому програма неспроможна вийти за кінець списку, і немає необхідності перевіряти умова Not (cell Is Nothing) в кожному циклі While.

Public Function SentinelSearch (target As Long) As SearchCell Dim cell As SearchCell Dim sentinel As New SearchCell

NumSearches = 0

" Встановити сигнальну мітку. sentinel. Value = target

Set ListBottom. NextCell = sentinel

" Знайти шуканий элемент.

Set cell = ListTop. NextCell

Do While cell. Value < target

NumSearches = NumSearches + 1

Set cell = cell. NextCell

Loop

" Визначити знайдено чи шуканий элемент.

If Not ((cell Is sentinel) Or _

(cell. Value target)) _

Then

Set SentinelSearch = cell «Елемент найден.

End If

" Видалити сигнальну метку.

Set ListBottom. NextCell = Nothing End Function

Хотя може бути, що це й зміна незначно, перевірка Not (cell Is Nothing) виконується в циклі, який викликається часто-густо. Для великих списків цей цикл викликається безліч разів, і виграш часу підсумовується. У Visual Basic, цей версія алгоритму пошуку зв’язкових списках виконується на 20 відсотків швидше, ніж попередня версія. У конкурсній програмі Search наведено обидві версії цього алгоритму, і це можете порівняти їх. Деякі алгоритми використовують потоки з метою прискорення пошуку зв’язкових списках. Наприклад, з допомогою покажчиків в осередках списку можна організувати список як двоичного дерева. Пошук елемента з використанням цього дерева займе час порядку O (log (N)), якщо дерево збалансовано. Такі структури даних не є просто списками, тому ми обговорюємо в цієї главі. Щоб більше дізнатися про деревах, зверніться до 6 і аналогічних сім главам

Двоичный поиск

Как згадувалося у роки розділах, пошук повним перебором виконується нас дуже швидко для невеликих списків, але для великих списків значно швидше виконується двоїчний пошук. Алгоритм двоичного пошуку (binary search) порівнює елемент у середині списку з потрібним. Якщо шуканий елемент менше, ніж знайдений, то алгоритм продовжує пошук в першій половині списку, якщо більше — у правій половині. На рис. 10.2 цей процес зображений графічно. Хоча з своїй — природі цей алгоритм є рекурсивним, його досить просто записати і застосування рекурсії. Тому що це алгоритм простий для розуміння у будь-якому варіанті (з рекурсією чи ні), ми наводимо тут його нерекурсивную версію, що містить менше викликів функцій. Основна ув’язнена у цьому алгоритмі ідея проста, але деталі його реалізації досить складні. Програмі доводиться акуратно відстежувати частина масиву, яка може містити шуканий елемент, інакше вони можуть його пропустити. Алгоритм використовує дві перемінні, min і max, у яких містяться мінімальний і максимальний індекси осередків масиву, які можуть містити шуканий елемент. Під час виконання алгоритму, індекс шуканої осередки завжди лежатиме між min і max. Інакше кажучи, min = NumEntries Then

LocateItem = HASH_NOT_FOUND pos = -1

Exit Function

End If

pos = (pos + 1) Mod NumEntries probes = probes + 1

Loop End Function

Программа Linear демонструє відкриту адресацію з лінійної перевіркою. Заповнивши полі Table Size (Розмір таблиці) і натиснувши на кнопку Create table (Створити таблицю), можна хеш-таблицы різних розмірів. Потім можна запровадити значення елемента й тицьнути на на кнопку Add (Додати) чи Find (Знайти), щоб вставити чи знайти елемент в таблиці. Щоб додати в таблицю відразу кількох випадкових значень, введіть число елементів, що ви хочете й ті максимальне значення, що вони може мати у сфері Random Items (Випадкові елементи), і далі натисніть на кнопку Create Items (Створити елементи). Після закінчення програмою будь-якої операції вона виводить статус операції (успішне чи безуспішне завершення) і довжину тестової послідовності. Вона також виводить середню довжину успішної та безуспішною тестової послідовностей. Програма обчислює середню довжину тестової послідовності, виконуючи пошук всіх значень від 1 до максимального значення таблиці. У табл. 11.1 приведено середня довжина успішних і безуспішних тестових послідовностей, здобутих у програмі Linear для таблиці зі 100 осередками, елементи у яких містяться буде в діапазоні від 1 до 999. З таблиці видно, що продуктивність алгоритму падає у міру заповнення таблиці. Чи є продуктивність прийнятною, залежить від цього, як використовується таблиця. Якщо програма витрачає багато часу до пошуку значень, що є у таблиці, то продуктивність то, можливо непоганий, навіть якщо таблиця практично заповнена. Якщо ж програма часто шукає значення, яких у таблиці, то продуктивність то, можливо дуже низької, якщо таблиця переповнена. Зазвичай, змішування забезпечує прийнятну продуктивність, не витрачаючи у своїй занадто багато пам’яті, якщо заповнене від 50 до 75 відсотків таблиці. Якщо таблиця заповнена більше, ніж 75 відсотків, то продуктивність падає. Якщо таблиця заповнена менше, ніж 50 відсотків, вона забирає понад пам’яті, ніж потрібно. Це відкриту адресацію хорошим прикладом просторово-тимчасового компромісу. Збільшуючи хеш-таблицу, можна зменшити час, необхідне вставки чи пошуку элементов.

=======297

@Таблица 11.1. Довжина успішної та безуспішною тестових последовательностей

Первинна кластеризация

Линейная перевірка має одну неприємне властивість, що називається первинної кластеризацией (primary clustering). Після додавання великого числа елементів в таблицю, виникає конфлікт між новими елементами вже наявними кластерами, у своїй для вставки нового елемента слід оминути кластер, щоб знайти порожню осередок. Щоб побачити, як утворюються кластери, припустимо, що спочатку є порожня хеш-таблица, яка може містити N елементів. Якщо вибрати випадкове число і вставити їх у таблицю, то можливість, що елемент займе будь-яку задану позицію P в таблиці, дорівнює 1/N. При уставці другого випадково обраного елемента, може відбитися на таку ж позицію із ймовірністю 1/N. Через конфлікт у разі він міститься у позицію P + 1. Існує також ймовірність 1/N, що елемент і повинен розташовуватися в позиції P + 1, і можливість 1/N, що він має перебувати у позиції P — 1. В усіх цих три випадки новий елемент розташовується поруч із попереднім. Отже, загалом існує ймовірність 3/N те, що 2 елемента виявляться розташованими поблизу друг від друга, створюючи невеличкий кластер. В міру зростання кластера можливість, що такі елементи будуть розташовуватися поблизу кластера, зростає. Якщо кластері перебуває два елемента, то можливість, що черговий елемент приєднається до кластеру, дорівнює 4/N, тоді як кластері чотири елемента, ця ймовірність дорівнює 6/N, тощо. Що ще гірше, якщо кластер починає зростати, його зростання триває до тих пір, що він не зіштовхнеться з сусіднім кластерів. Два кластера зливаються, створюючи кластер ще більшого розміру, що росте рішуче, зливається коїться з іншими кластерами і утворить ще більші кластеры.

======298

В ідеальному разі хеш-таблица мусить бути наполовину порожня, і елементи в ній мають чергуватися з порожніми осередками. Тоді з імовірністю 50 відсотків алгоритм відразу ж потрапляє знайде порожню осередок для створення нового який додається елемента. Існує також 50-відсоткова можливість, що він знайде порожню осередок після перевірки лише двох позицій в таблиці. Середня довжина тестової послідовності дорівнює 0,5 * 1 + 0,5 * 2 = 1,5. У найгіршому цьому випадку всі елементи в таблиці будуть згруповані до одного гігантський кластер. У цьому усе є 50-відсоткова можливість, що алгоритм відразу знайде порожню осередок, у якому можна помістити новий елемент. Проте, якщо алгоритм не знайде порожню осередок першою кроці, то пошук вільної осередки зажадає значно більше часу. Якщо елемент повинен бути на першої позиції кластера, то алгоритму доведеться перевірити все елементи в кластері, щоб знайти вільну осередок. У середньому для вставки елемента за такого розподілу знадобиться вулицю значно більше часу, ніж коли елементи рівномірно розподілені за таблицею. Насправді, ступінь кластеризації знаходитиметься між цими двома крайніми випадками. Можете використовувати програму Linear на дослідження ефекту кластеризації. Запустіть програму і створіть хеш-таблицу зі 100 осередками, та був додайте 50 випадкових елементів зі значеннями до 999. Ви знайдете, що утворюються кілька кластерів. У одному із тестів 38 з 50 елементів стали частиною кластерів. Якщо додати ще 25 елементів до таблиці, то більшість елементів входитимуть в кластери. У другому тесті 70 з 75 елементів були згруповані в кластеры.

Упорядоченная лінійна проверка

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

Public Function LocateItem (Value As Long, pos As Integer, _ probes As Integer) As Integer Dim new_value As Long

probes = 1 pos = (Value Mod m_NumEntries)

Do new_value = m_HashTable (pos)

" Елемента в таблиці нет.

If new_value = UNUSED Or probes > NumEntries Then

LocateItem = HASH_NOT_FOUND pos = -1

Exit Function

End If

" Елемент знайдено або немає в таблице.

If new_value >= Value Then Exit Do

pos = (pos + 1) Mod NumEntries probes = probes + 1

Loop

If Value = new_value Then

LocateItem = HASH_FOUND

Else

LocateItem = HASH_NOT_FOUND

End If End Function

Для здобуття права його працював, необхідно організувати елементи в хеш- таблиці те щоб і під час тестової послідовності вони зустрічались у зростаючу котячу порядку. Існує досить простий метод вставки елементів, що гарантує таке розташування елементів. Коли таблицю вставляється новий елемент, йому виконується тестова послідовність. Якщо знайдеться вільна осередок, то елемент вставляється у цю позицію і процедуру завершено. Якщо зустрічається елемент, значення якого більше значення нового елемента, всі вони змінюються місцями й триває виконання тестової послідовності для більшого елемента. У цьому може зустрітися елемент з поглибленим значенням. Тоді елементи знову змінюються місцями, і виконується пошук нового місцеположення при цьому елемента. Цей процес відбувається триває до того часу, поки, зрештою, не знайдеться вільна осередок, у своїй можливо кілька елементів змінюються местами.

========299−300

Public Function InsertItem (ByVal Value As Long, pos As Integer,_ probes As Integer) As Integer Dim new_value As Long Dim status As Integer

" Перевірити, заповнена чи таблица.

If m_NumUnused < 1 Then

" Пошук елемента. status = LocateItem (Value, pos, probes)

If status = HASH_FOUND Then

InsertItem = HASH_FOUND

Else

InsertItem = HASH_TABLE_FULL pos = -1

End If

Exit Function

End If

probes = 1 pos = (Value Mod m_NumEntries)

Do new_value = m_HashTable (pos)

" Якщо значення знайдено, пошук завершен.

If new_value = Value Then

InsertItem = HASH_FOUND

Exit Function

End If

" Якщо осередок вільна, елемент має перебувати у ней.

If new_value = UNUSED Then m_HashTable (pos) = Value

HashForm. TableControl (pos). Caption = Format$(Value)

InsertItem = HASH_INSERTED m_NumUnused = m_NumUnused — 1

Exit Function

End If

" Якщо значення в осередку таблиці більше значения

" елемента, поміняти їх місцями, що продолжить.

If new_value > Value Then m_HashTable (pos) = Value

Value = new_value

End If

pos = (pos + 1) Mod NumEntries probes = probes + 1

Loop End Function

Программа Ordered демонструє відкриту адресацію з упорядкованою лінійної перевіркою. Вона ідентична програмі Linear, але використовує впорядковану хеш- таблицю. У табл. 11.2 приведено середня довжина успішної та безуспішною тестових послідовностей під час використання лінійної і упорядкованим лінійної перевірок. Середня довжина успішної перевірки обох методів майже однакова, але не тоді неуспіху упорядкована лінійна перевірка виконується значно швидше. Розбіжність у особливості помітна, якщо хеш-таблица заповнена більш, ніж 70 процентов.

=========301

@Таблица 11.2. Довжина пошуку під час використання лінійної і упорядкованим лінійної проверки

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

Квадратичная проверка

Один із засобів зменшити первинну кластеризацию у тому, щоб використовувати хеш-функцию наступного вида:

Hash (K, P) = (K + P2) Mod N де P = 0, 1, 2, …

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

=======302

На рис. 11.8 показано хеш-таблица, що містить великий кластер елементів. На ньому теж показано тестові послідовності, які виникають при спробі вставити два різних елемента у позиції, займані кластерів. Обидві ці тестові послідовності закінчуються у точці, яка прилягає до кластеру, тому після вставки цих елементів розмір кластера не збільшується. Наступний код демонструє пошук елемента з допомогою квадратичной перевірки (quadratic probing):

Public Function LocateItem (Value As Long, pos As Integer, probes As Integer) As Integer Dim new_value As Long

probes = 1 pos = (Value Mod m_NumEntries)

Do new_value = m_HashTable (pos)

" Елемент найден.

If new_value = Value Then

LocateItem = HASH_FOUND

Exit Function

End If

" Елемента немає у таблице.

If new_value = UNUSED Or probes > NumEntries Then

LocateItem = HASH_NOT_FOUND pos = -1

Exit Function

End If

pos = (Value + probes * probes) Mod NumEntries probes = probes + 1

Loop End Function

Программа Quad демонструє відкриту адресацію з допомогою квадратичной перевірки. Вона аналогічна програмі Linear, але використовує квадратичную, а чи не лінійну перевірку. У табл. 11.3 приведено середня довжина тестових послідовностей, отримані програмах Linear і Quad для хеш-таблицы зі 100 осередками, значення елементів у якій перебувають у діапазоні від 1 до 999. Квадратична перевірка зазвичай дає кращі результаты.

@Рис. 11.8. Квадратична проверка

======303

@Таблица 11.3. Довжина пошуку під час використання лінійної і квадратичной проверки

Квадратичная перевірка також має й певні недоліки. Через способу формування тестової послідовності, не можна гарантувати, що вона обійде все осередки в таблиці, що означає, іноді в таблицю не можна буде вставити елемент, навіть якщо вона заповнена остаточно. Наприклад, розглянемо невелику хеш-таблицу, що складається з 6 осередків. Тестова послідовність для числа 3 буде следующей:

3 3 + 12 = 4 = 4 (Mod 6) 3 + 22 = 7 = 1 (Mod 6) 3 + 32 = 12 = 0 (Mod 6) 3 + 42 = 19 = 1 (Mod 6) 3 + 52 = 28 = 4 (Mod 6) 3 + 62 = 39 = 3 (Mod 6) 3 + 72 = 52 = 4 (Mod 6) 3 + 82 = 67 = 1 (Mod 6) 3 + 92 = 84 = 0 (Mod 6) 3 + 102 = 103 = 1 (Mod 6) й дуже далее.

Эта тестова послідовність звертається наші позиції 1 і 4 двічі перед тим, як звернутися позиції 3, і не потрапляє у позиції 2 і п’яти. Щоб підгледіти цей ефект, створіть у програмі Quad хеш-таблицу з шістьма осередками, та був вставте елементи 1, 3, 4, 6 і 9-те. Програма визначить, що таблиця заповнена повністю, хоча дві чарунки й залишилися невикористаними. Тестова послідовність для елемента 9 не звертається до елементам 2 і п’яти, тому програма неспроможна вставити в таблицю новий элемент.

=======304

Можно показати, що квадратична тестова послідовність буде звертатися, по меншою мірою, до N/2 осередків таблиці, якщо розмір таблиці N — просте число. Хоча заодно гарантується певний рівень продуктивності, однаково виникатимуть проблеми, якщо таблиця майже заповнена. Оскільки продуктивність для майже заповненою таблиці у кожному разі сильно падає, то можливо краще буде просто збільшити розмір хеш- таблиці, а чи не тривожитися, зможе тестова послідовність знайти вільну осередок. Не настільки очевидна проблема, що виникає при застосуванні квадратичной перевірки, у тому, хоча вона усуває первинну кластеризацию, під час неї може постати схожа проблема, яка називається вторинної кластеризацией (secondary clustering). Якщо два елемента відбиваються до однієї осередок, них буде виконуватися сама й так ж тестова послідовність. Якщо безліч елементів відбиваються на жодну з осередків таблиці, вони утворюють вторинний кластер, який розподілено по хеш-таблице. Якщо з’являється новий елемент з тим самим самим початковим значенням, йому доводиться виконувати тривалу тестову послідовність, перш ніж обійде елементи у вторинному кластері. На рис. 11.9 показано хеш-таблица, яка може містити 10 осередків. У таблиці перебувають елементи 2, 12, 22 і 32, котрі всі спочатку відбиваються в позицію 2. Якщо вставити в таблицю елемент 42, то потрібно буде виконати тривалу тестову послідовність, яка обійде всі ці елементи, як знайде вільну ячейку.

Псевдослучайная проверка

Степень кластеризації зростає, тоді як кластер додаються елементи, які відбиваються цього разу вже зайняті кластерів осередки. Вторинна кластеризація виникає, коли для елементів, які спочатку обійматимуть одну й саму осередок, виконується сама й той самий тестова послідовність, і утворюється вторинний кластер, розподілений по хеш-таблице. Можна усунути обидві ці ефекту, якщо хочете зробити те щоб до різних елементів виконувалися різні тестові послідовності, навіть якщо елементи спочатку повинні були займати те ж осередок. Одне з способів зробити це залежить від використанні в тестової послідовності генератора псевдослучайных чисел. Для обчислення тестової послідовності для елемента, його значення використовується для ініціалізації генератора випадкових чисел. Потім для побудови тестової послідовності використовуються послідовні випадкові числа, одержувані не вдома генератора. Це називається псевдослучайной перевіркою (pseudo-random probing). Коли пізніше потрібно знайти елемент в хеш-таблице, генератор випадкових чисел знову инициализируется значенням елемента, у своїй не вдома генератора ми матимемо те саме послідовність чисел, яка використовувалася для вставки елемента у таблицю. Використовуючи ці числа, можна відтворити вихідну тестову послідовність і знайти элемент.

@Рис. 11.9. Вторинна кластеризация

==========305

Если використовується якісний генератор випадкових чисел, то різні значення елементів даватимуть різні випадкові числа і різні тестові послідовності. Навіть якщо взяти два значення спочатку відбиваються однією й саму осередок, тож позиції з тестової послідовності будуть вже різними. І тут в хеш-таблице не виникатиме первинна чи вторинна кластеризація. Можна проинициализировать генератор випадкових чисел Visual Basic, використовуючи початкова число, з двох рядків кода:

Rnd -1 Randomize seed_value

Оператор Rnd дає те ж послідовність чисел після ініціалізації у тому ж початковим числом. Наступний коду показує, як і виконувати пошук елемента з допомогою псевдослучайной проверки:

Public Function LocateItem (Value As Long, pos As Integer, _ probes As Integer) As Integer Dim new_value As Long

" Проинициализировать генератор випадкових чисел.

Rnd -1

Randomize Value

probes = 1 pos = Int (Rnd * m_NumEntries)

Do new_value = m_HashTable (pos)

" Елемент найден.

If new_value = Value Then

LocateItem = HASH_FOUND

Exit Function

End If

" Елемента немає у таблице.

If new_value = UNUSED Or probes > NumEntries Then

LocateItem = HASH_NOT_FOUND pos = -1

Exit Function

End If

pos = Int (Rnd * m_NumEntries) probes = probes + 1

Loop End Function

=======306

Программа Rand демонструє відкриту адресацію з псевдослучайной перевіркою. Вона аналогічна програмам Linear і Quad, але використовує псевдослучайную, а чи не лінійну чи квадратичную перевірку. У табл. 11.4 приведено приблизна середня довжина тестової послідовності, отриманого програмах Quad чи Rand для хеш-таблицы зі 100 осередками і елементами, значення яких у діапазоні від 1 до 999. Зазвичай псевдослучайная перевірка дає найкращі результати, хоча відмінність між псевдослучайной і квадратичной перевірками негаразд велика, як між лінійної і квадратичной. Псевдослучайная перевірка також має свої вади. Оскільки тестова послідовність вибирається псевдослучайно, не можна точно передбачити, як швидко алгоритм обійде все елементи в таблиці. Якщо таблиця менше, ніж число можливих псевдослучайных значень, що існує можливість, що тестова послідовність повернеться одному значенням кілька разів доти, як вибере інших значень в таблиці. Можливе також, що тестова послідовність буде пропускати якусь осередок в таблиці і зможе вставити новий елемент, навіть якщо таблиця не заповнена остаточно. Також, як у разі квадратичной перевірки, ці ефекти можуть викликати труднощі, лише коли таблиця майже заповнена. І тут збільшення таблиці дає набагато більший приріст продуктивності, ніж пошук невикористовуваних осередків таблицы.

@Рис. 11.4. Довжина пошуку під час використання квадратичной і псевдослучайной проверки

=======307

Видалення элементов

Удаление елементів з хеш-таблицы, у якій використовується відкрита адресація, виконується непросто, як видалення їх із таблиці, використовує зв’язкові списки чи блоки. Просто видалити елемент з таблиці не можна, оскільки він може знайтися в тестової послідовності іншого елемента. Припустимо, що елемент A перебуває у тестової послідовності елемента B. Якщо видалити з таблиці елемент A, знайти елемент B неможливо. У час пошуку елемента B зустрінеться порожня осередок, що залишилася після видалення елемента A, тому буде зроблено неправильний висновок у тому, що елемент B немає у таблиці. Замість видалення елемента із хеш-таблицы можна просто позначити його як віддалений. Можна також використовувати цей осередок пізніше, якщо вона зустрінеться у час виконання вставки нового елемента у таблицю. Якщо позначений елемент зустрічається під час пошуку іншого елемента, він ігнорується і тестова послідовність продовжиться. Потому, як велика кількість елементів буде позначений як віддалені, в хеш- таблиці може бути безліч невикористовуваних осередків, і за пошуку елементів досить чимало часу йтиме на перепустку віддалених елементів. Зрештою, може знадобитися рехеширование таблиці для звільнення невикористовуваної памяти.

Рехеширование

Чтобы звільнити віддалені елементи з хеш-таблицы, можна провести її рехеширование (rehashing) дома. Щоб ця алгоритм міг працювати, потрібно мати певний спосіб визначення, було виконано рехеширование елемента. Найпростіший спосіб зробити це — визначити елементи як структур даних, містять полі Rehashed.

Type ItemType

Value As Long

Rehashed As Boolean End Type

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

======308

Зміна розміру хеш-таблиц

Если хеш-таблица стає майже заповненою, продуктивність значно падає. І тут може знадобитися підвищення розміру таблиці, щоб у ньому було більше для елементів. І навпаки, тоді як таблиці замало осередків, може знадобитися зменшити її, щоб звільнити зайняту пам’ять. Використовуючи методи, схожі тих, які використовувалися при рехешировании таблиці дома, можна збільшувати і зменшувати розмір хеш-таблицы. Щоб збільшити хеш-таблицу, спочатку розмір масиву, у якому вона перебуває, збільшується з допомогою оператора Dim Preserve. Потім виконується рехеширование таблиці, у своїй елементи можуть тривати осередки у створеній вільної області наприкінці таблиці. Після закінчення рехеширования таблиця побудують для використання. Щоб зменшити розмір таблиці, спочатку визначимо, скільки елементів має утримуватися в масиві таблиці після зменшення. Потім виконуємо рехеширование таблиці, причому елементи поміщаються лише у зменшену частина таблиці. Після закінчення рехеширования всіх елементів, розмір масиву зменшується з допомогою оператора ReDim Preserve. Наступний код демонструє рехеширование таблиці з допомогою лінійної перевірки. Код для рехеширования таблиці з допомогою квадратичной чи псевдослучайной перевірки виглядає схоже же:

Public Sub Rehash () Dim і As Integer Dim pos As Integer Dim probes As Integer Dim Value As Long Dim new_value As Long

" Позначити все елементи, як нерехешированные.

For і = 0 To NumEntries — 1 m_HashTable (i). Rehashed = False

Next i

" Пошук нерехешированных элементов.

For і = 0 To NumEntries — 1

If Not m_HashTable (i). Rehashed Then

Value = m_HashTable (i). Value m_HashTable (i). Value = UNUSED

If Value DELETED And Value UNUSED Then

" Виконати тестову последовательность

" при цьому елемента, доки знайдеться свободная,

" віддалена чи нерехешированная осередок. probes = 0

Do pos = (Value + probes) Mod NumEntries new_value = m_HashTable (pos). Value

" Якщо осередок вільна чи позначена как

" віддалена, помістити елемент в нее.

If new_value = UNUSED Or _ new_value = DELETED _

Then m_HashTable (pos). Value = Value m_HashTable (pos). Rehashed = True

Exit Do

End If

" Якщо осередок не позначена як рехешированная,

" поміняти їх місцями й продолжить.

If Not m_HashTable (pos). Rehashed Then m_HashTable (pos). Value = Value m_HashTable (pos). Rehashed = True

Value = new_value probes = 0

Else probes = probes + 1

End If

Loop

End If

End If

Next і End Sub

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

Резюме

Различные типи хеш-таблиц, достойні цієї главі, мають переваги й недоліки. Для хеш-таблиц, що використовують зв’язкові списки чи блоки можна легко змінювати розмір таблиці і видаляти з її елементи. Використання блоків дає підстави легко працювати з таблицями на диску, дозволяючи вважати за звернення до диска відразу безліч елементів даних. Проте, обидва ці методу є як повільними, ніж відкрита адресація. Лінійна перевірка проста і дозволяє досить швидко вставляти і видаляти елементи з таблиці. Застосування упорядкованим лінійної перевірки дозволяє швидше, ніж у випадку неупорядкованою лінійної перевірки, встановити, що елемент немає у таблиці. З іншого боку, вставку елементів в таблицю у своїй виконати складніше. Квадратична перевірка дозволяє уникнути кластеризації, яке характерне для лінійної перевірки, і тому забезпечує вищу продуктивність. Псевдослучайная перевірка забезпечує ще більше високу продуктивність, бо за цьому вдається позбутися як від, і від вторинної кластеризації. У табл. 11.5 наведено переваги та недоліки різних методів хеширования.

======310

@Таблица 11.5. Переваги й недоліки різних методів хеширования

Выбор найкращого методу хеширования для даного докладання залежить від даних завдання й способів їх використання. При застосуванні різних схем досягаються різні компроміси між займаній пам’яттю, швидкістю і простотою змін. Табл. 11.5 може допомогти вам вибрати найкращий алгоритм до вашої приложения.

=======311

Глава 12. Мережні алгоритмы

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

Определения

Как і у визначенні дерев, мережею (network) чи графом (graph) називається набір вузлів (nodes), з'єднаних ребрами (edges) чи зв’язками (links). Для графа, на відміну дерева, не містять поняття батьківського чи дочірнього вузла. З ребрами мережі пов’язана відповідне напрям, тоді цьому разі мережу називається орієнтованої мережею (directed network). Для кожної зв’язку можна також ознайомитися визначити її ціну (cost). Для мережі доріг, наприклад, ціна може дорівнювати часу, яке займе проїзд по відтинку дороги, поданому руба мережі. У телефонної мережі ціна може дорівнювати коефіцієнта електричних втрат надходжень у кабелі, представленому зв’язком. На рис. 12.1 показано невеличка орієнтована мережу, у якій числа поруч із ребрами відповідають ціні ребра. Шляхом (path) між вузлами A і B називається послідовність ребер, яка пов’язує дві ці вузла між собою. Якщо між будь-якими двома вузлами мережі не довше ребра, шлях можна однозначно описати, перерахувавши що входять до нього вузли. Оскільки таке опис простіше уявити наочно, то шляху наскільки можна описуються в такий спосіб. На рис. 12.1 шлях, проходить через вузли B, E, F, G, E і D, з'єднує вузли B і D. Циклом (cycle) називається шлях який пов’язує вузол з нею самим. Шлях E, F, G, E на рис. 12.1 є циклом. Шлях називається простим (simple), якщо він містить циклів. Шлях B, E, F, G, E, D перестав бути простим, так як і містить цикл E, F, G, E. Якщо існує будь-якої шлях між двома вузлами, маєш існувати та простий шлях з-поміж них. Цей шлях можна знайти, якщо видалити все цикли з вихідного шляху. Наприклад, якщо замінити цикл E, F, G, E їсти дорогою B, E, F, G, E, D на вузол E, вийде простіший шлях B, E, D, зв’язуючий вузли B і D.

=======313

@Рис. 12.1. Орієнтована мережу з ціною ребер

Сеть називається зв’язковою (connected), якщо розрив між будь-якими двома вузлами існує хоча б тільки шлях. У орієнтованої мережі який завжди очевидно, чи є мережу зв’язковою. На рис. 12.2 мережу зліва є зв’язковою. Мережа справа перестав бути зв’язковою, бо існує шляхи виходу з вузла E в вузол C.

Представления сети

В 6 главі було описане кілька уявлень дерев. Більшість їх застосовно також і роботи з мережами. Наприклад, уявлення повними вузлами, списком нащадків (списком сусідів для мереж) чи нумерацією зв’язків також можна використовувати для зберігання мереж. За описом цих уявлень зверніться до 6 главе.

@Рис. 12.2. Зв’язкова (зліва) і несвязная (справа) сети

======314

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

Public Id As Integer «Номер вузла. Public Links As Collection «Зв'язки, які ведуть сусіднім узлам.

Можно використовувати таке визначення класу связей:

Public ToNode As NetworkNode «Вузол іншому кінці зв’язку. Public Cost As Integer «Ціна связи.

Используя ці визначення, програма може знайти зв’язку з найменшої ціною, використовуючи наступний код:

Dim link As NetworkLink Dim best_link As NetworkLink Dim best_cost As Integer

best_cost = 32 767

For Each link In node. Links

If link. cost < best_cost Then

Set best_link = link best_cost = link. cost

End If

Next link

Классы node і link часто розширюються для зручності роботи з конкретними алгоритмами. Наприклад, до класу node часто додається прапор Marked. Якщо програма звертається до вузлу, вона встановлює значення поля Marked рівним true, аби знати, що вузол вже було перевірений. Програма, управляюча неориентированной мережею, може використовувати трохи інше уявлення. Клас node залишається тим самим, як раніше, але клас link включає посилання обидва вузла на кінцях связи.

Public Node1 As NetwokNode «Одне з вузлів на кінці зв’язку. Public Node2 As NetwokNode «Інший вузол. Public Cost As Integer «Ціна связи.

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

=======315

Используя цей спектакль, програма NetEdit дозволяє оперувати неориентированными мережами з ціною зв’язків. Меню File (Файл) дозволяє навантажувати й зберігати мережі в файлах. Команди в меню Edit (Правка) дозволяють вам вставляти і видаляти вузли та зв’язку. На рис. 12.3 показано вікно програми NetEdit. Директорія OldSrcCh12 містить програми, що використовують уявлення нумерацією зв’язків. Ці програми трохи складніше зрозуміти, проте працюють швидше. Не описані у тексті, але використані у яких методи нагадують ті, що застосовувалися програми, написаних для 4 версії Visual Basic. Наприклад, обидві програми SrcCh12Paths і OldSrcCh12Paths знаходять найкоротший маршрут, використовуючи описаний нижче алгоритм установки міток. Основне різниця між ними у тому, що як перша програма використовує колекції представлена і класи, а друга — псевдоуказатели і помилкове уявлення нумерацією связей.

Оперирование вузлами і связями

Корень дерева — це єдиний вузол, яка має батька. Можна познаходити будь-який вузол у мережі, почавши від кореня і слідуючи по покажчикам на дочірні вузли. Отже, вузол представляє підставу дерева. Якщо запровадити зміну, що не міститиме покажчик на кореневої вузол, то згодом можна буде одержати доступ всім вузлам в дереві. Мережі який завжди містять вузол, який займає таке особливе становище. У незв’язною мережі може існувати способу обійти всі вузли зв’язків, почавши з однієї вузла. Тому програми, хто з мережами, зазвичай містять повний перелік всіх вузлів у мережі. Програма він може зберігати повний перелік всіх зв’язків. При допомоги цих списків можна легко виконати будь-які дії з усіх вузлами чи зв’язками у мережі. Наприклад, якщо програма зберігає покажчики на вузли та зв’язку в колекціях Nodes і Links, вони можуть вивести мережу на екран з допомогою наступного метода:

@Рис. 12.3. Програма NetEdit

=======316

Dim node As NetworkNode dim link As NetworkLink

For Each link in links

" Намалювати связь.

:

Next link

For Each node in nodes

" Намалювати узел.

:

Next node

Программа NetEdit використовує колекції Nodes і Links висновку мереж на экран.

Обходы сети

Обход мережі виконується аналогічно обходу дерева. Можна обходити мережу, використовуючи або обхід завглибшки, або обхід завширшки. Обхід завширшки зазвичай нагадує прямий обхід дерева, хоча до мереж можна визначити також зворотний і навіть симетричний обхід. Алгоритм до виконання прямого обходу двоичного дерева, описаний в 6 главі, формулюється так:

Звернутися до узлу.

Виконати рекурсивний прямий обхід лівого поддерева.

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

Позначити узел.

Звернутися до вузлу. 3. Виконати рекурсивний обхід не помічених сусідніх узлов.

========317

В Visual Basic можна додати прапор Marked до класу NetworkNode.

Public Id As Long Public Marked As Boolean Public Links As Collection

Класс NetworkNode може охоплювати відкриту процедуру для обходу мережі, починаючи від цього вузла. Процедура вузла PreorderPrint звертається всім непомеченным вузлам, доступними з цього вузла. Якщо мережу є зв’язковою, то, при такому обході станеться звернення всім вузлам сети.

Public Sub PreorderPrint () Dim link As NoworkLink Dim node As NetworkNode

" Позначити узел.

Marked = True

" Звернутися до непомеченным узлам.

For Each link In Links

" Знайти сусідній узел.

If link. Node1 Is Me Then

Set node = link. Node2

Else

Set node = link. Node1

End If

" Визначити, потрібно чи звернення до сусіднього узлу.

If Not node. Marked Then node. PreorderPrint

Next link End Sub

Так як процедура не звертається до жодного вузлу двічі, то колекція обходимых зв’язків зовсім позбавлений циклів і утворить дерево. Якщо мережу є зв’язковою, то дерево обминатиме все вузли мережі. Оскільки це дерево охоплює все вузли мережі, воно називається остовным деревом (spanning tree). На рис. 12.4 показано невеличка мережу з остовным деревом з коренем в вузлі A, яке зображено жирними лініями. Можна також використовувати схожий підхід із позначкою вузлів для перетворення обходу дерева завширшки в мережевий алгоритм. Алгоритм обходу дерева починається з помешкання кореневого вузла у чергу. Потім перший вузол видаляється з черги, відбувається звернення до вузлу, і у кінці черги поміщаються його дочірні вузли. Далі ця процес повторюється до того часу, поки черга опустеет.

======318

@Рис. 12.4. Остовное дерево

В алгоритмі обходу мережі потрібно спочатку переконатися, що вузол не перевірявся раніше чи що вона не перебуває у черги. І тому ми помечаем кожен вузол, який міститься у чергу. Мережевий версія цього алгоритму виглядає так: 1. Позначити перший вузол (який коренем остовного дерева) і додати їх у кінець черги. 2. Повторювати такі кроки до того часу, поки черга спорожніє: a) Видалити з черги перший вузол і звернутися щодо нього. b) До кожного з непомічених сусідніх вузлів, позначити його й додати насамкінець черги. Наступна процедура друкує список вузлів мережі гаразд обходу в ширину:

Public Sub BreadthFirstPrint (root As NetworkNode) Dim queue As New Collection Dim node As NetworkNode Dim neighbor As NetworkNode Dim link As NetworkLink

" Помістити корінь у чергу. root. Marked = True queue. Add root

" Багаторазово поміщати верхній елемент в очередь

" поки черга опустеет.

Do While queue. Count > 0

" Вибрати наступний вузол з очереди.

Set node = queue. Item (1) queue. Remove 1

" Звернутися до узлу.

Print node. Id

" Додати у чергу все непомеченные сусідні узлы.

For Each link In node. Links

" Знайти сусідній узел.

If link. Node1 Is Me Then

Set neighbor = link. Node2

Else

Set neighbor = link. Node1

End If

" Перевірити, чи потрібно звернення до сусіднього узлу.

If Not neighbor. Marked Then queue. Add neighbor

Next link

Loop End Sub

Найменші остовные деревья

Если задана мережу з ціною зв’язків, то найменшим остовным деревом (minimal spanning tree) називається остовное дерево, у якому сумарна ціна всіх зв’язків в дереві буде найменшої. Найкоротший остовное дерево можна використовувати, щоб пов’язати все вузли у мережі шляхом з найменшої ціною. Наприклад, припустимо, що потрібно розробити телефонну мережу, яка повинна з'єднати шість міст. Можна прокласти магістральний кабель між усіма парами міст, але це буде невиправдано дорого. Меншу вартість матиме рішення, у якому міста буде з'єднано зв’язками, які зберігають у найменшому остовном дереві. На рис. 12.5 показані шість міст, щодва у тому числі з'єднані магістральним кабелем. Жирними лініями намальовано найменше остовное дерево. Зауважте, що мережа може мати кілька найменших остовных дерев. На рис. 12.6 показані два зображення мережі з різноманітними найменшими остовными деревами, які намальовані жирними лініями. Повна ціна обох дерев дорівнює 32.

@Рис. 12.5. Магістральні телефонні кабелі, котрі пов’язують шість городов

========320

@Рис. 12.6. Два різних найменших остовных дерева одній сети

Существует простий алгоритм пошуку найменшого остовного дерева для мережі. Спочатку помістимо в остовное дерево будь-який вузол. Потім знайдемо зв’язку з найменшої ціною, яка з'єднує вузол в дереві з вузлом, який ще поміщений у дерево. Додамо цей зв’язок і відповідні вузол в дерево. Потім цю процедуру повторюється до того часу, доки всі вузли не будуть у дереві. Цей алгоритм нагадує евристику сходження на пагорб, описану у вісім главі. На кожен крок обидва алгоритму змінюють рішення, намагаючись максимально поліпшити. Алгоритм остовного дерева кожному кроці вибирає зв’язку з найменшої ціною, яка додає новий вузол в дерево. На відміну від евристики сходження на пагорб, яка завжди знаходить найкраще рішення, цей алгоритм гарантовано знаходить найменше остовное дерево. Такі алгоритми, які знаходять глобальний оптимум, з допомогою серії локально оптимальних наближень називаються поглинаючими алгоритмами (greedy algorithms). Можна уявляти собі які поглинають алгоритми як алгоритми типу сходження на пагорб, які є у своїй эвристиками — вони гарантовано знаходять найкраще можливе рішення. Алгоритм найменшого остовного дерева використовує колекцію для зберігання списку зв’язків, які можна додано до остовному дереву. Спочатку алгоритм поміщає до цього списку зв’язку кореневого вузла. Потім проводиться пошук через відкликання найменшої вартістю цьому. Щоб максимально прискорити пошук, програма може використовувати пріоритетну чергу типу описаної о 9-й главі. Чи, навпаки, щоб спростити реалізацію, програма може використовуватиме зберігання списку можливих зв’язків колекцію. Якщо вузол іншому кінці зв’язку ще перебуває у остовном дереві, то програма додає його й відповідну зв’язок в дерево. Потім вона додає зв’язку, що виходять із нового вузла, до списку можливих вузлів. Алгоритм використовує прапор Used у п’ятому класі link, щоб визначити, потрапляла чи цей зв’язок до цього часу список можливих зв’язків. Якщо можна, вона вільний від заношуваності в цей перелік знову. Може бути, що список можливих зв’язків спорожніє доти, й усе вузли додасться в остовное дерево. І тут мережу є незв’язною, і немає шлях, який пов’язує кореневої вузол із іншими вузлами сети.

=========321

Private Sub FindSpanningTree (root As SpanNode) Dim candidates As New Collection Dim to_node As SpanNode Dim link As SpanLink Dim і As Integer Dim best_i As Integer Dim best_cost As Integer Dim best_to_node As SpanNode

If root Is Nothing Then Exit Sub

" Скинути прапор Marked всім вузлів і флаги

" Used і InSpanningTree всім связей.

ResetSpanningTree

" Почати з кореня остовного дерева. root. Marked = True

Set best_to_node = root

Do

" Додати зв’язку останнього вузла в список

" можливих связей.

For Each link In best_to_node. Links

If Not link. Used Then candidates. Add link link. Used = True

End If

Next link

" Знайти саму коротку зв’язок у списку возможных

" зв’язків, веде до вузлу, якої ще нет

" в дереві. best_i = 0 best_cost = INFINITY і = 1

Do While і 0

" Знайти найближчий до корені узел-кандидат. best_dist = INFINITY

For і = 1 To candidates. Count new_dist = candidates (i). Dist

If new_dist < best_dist Then best_i = і best_dist = new_dist

End If

Next i

" Додати вузол до дерева найкоротшого маршрута.

Set node = candidates (best_i) candidates. Remove best_i node. NodeStatus = WAS_IN_LIST

" Перевірити сусідні узлы.

For Each link In node. Links

If node Is link. Node1 Then

Set to_node = link. Node2

Else

Set to_node = link. Node1

End If

If to_node. NodeStatus = NOT_IN_LIST Then

" Вузол раніше ні у списку возможных

" вузлів. Додати його до списку. candidates. Add to_node to_node. NodeStatus = NOW_IN_LIST to_node. Dist = best_dist + link. Cost

Set to_node. InLink = link

ElseIf to_node. NodeStatus = NOW_IN_LIST Then

" Вузол перебуває у списку можливих узлов.

" Обновити значення його полів Dist і inlink,

" якщо це потрібно. new_dist = best_dist + link. Cost

If new_dist < to_node. Dist Then to_node. Dist = new_dist

Set to_node. InLink = link

End If

End If

Next link

Loop

GotPathTree = True

" Позначити вхідні вузли, щоб їх було легше вивести на экран.

For Each node In Nodes

If Not (node. InLink Is Nothing) Then _ node. InLink. InPathTree = True

Next node

" Перемалювати сеть.

DrawNetwork End Sub

Важно, щоб алгоритм оновлював поля InLink і Dist лише вузлів, в яких полі NodeStatus одно NOW_IN_LIST. Більшість мереж не можна отримати найкоротший шлях, додаючи вузли, котрі перебувають у списку можливих вузлів. Проте, якщо мережу містить цикл, повна довжина якого негативною, алгоритм може знайти, які можна зменшити відстань до деяких вузлів, у яких перебувають у дереві найкоротшого маршруту, у своїй дві галузі дерева найкоротшого маршруту виявляться пов’язаними друг з одним, отже воно не буде деревом. На рис. 12. 10 показано мережу з циклом негативною ціни, і «дерево» найкоротшого маршруту, яке б, якби алгоритм оновлював ціну вузлів, у яких перебувають у дереве.

=======329

@Рис. 12. 10. Неправильне «дерево» найкоротшого маршруту для мережі з циклом негативною цены

Программа PathS використовує цей алгоритм установки міток для обчислення найкоротшого маршруту. Вона аналогічна програмам NetEdit і Span. Якщо ви і не вставляєте або видаляєте вузол чи зв’язок, можна вибрати вузол з допомогою миші і яскрава програма у своїй знайде і виведе на екран дерево найкоротшого маршруту з коренем у тому вузлі. На рис. 12. 11 показано вікно програми PathS з деревом найкоротшого маршруту з коренем в вузлі 3.

@Рис. 12. 11. Дерево найкоротшого маршруту з коренем в вузлі 3

=======330

Варіанти методу установки меток

Узкое місце цього алгоритму залежить від пошуку вузла з найменшою значенням поля Dist у списку можливих вузлів. Деякі варіанти цього алгоритму використовують інші структури даних для зберігання списку можливих вузлів. Наприклад, можна було б послуговуватись упорядкований зв’язний список. З використанням цього знадобиться лише одне крок у тому, щоб знайти наступний вузол, який додано до дерева найкоротшого маршруту. Цей перелік завжди буде упорядкованим, тому вузол на вершині списку завжди буде потрібним вузлом. Це полегшить пошук потрібного вузла у списку, але ускладнить додавання вузла в нього. Замість просто поміщати вузол на початок списку, його доведеться розмістити у потрібну позицію. Іноді також потрібна переміщати вузли у списку. Якщо результаті додавання вузла в дерево найкоротшого маршруту зменшилося найкоротший відстань до іншого вузла, що був у списку, потрібно перемістити цей елемент ближчі один до вершині списку. Попередній алгоритм і його новий варіант є два крайніх випадку управління списком можливих вузлів. Перший алгоритм не впорядковує список і витрачає досить чимало часу до пошуку вузлів в мережі. Другий витрачає чимало часу для підтримки упорядкованості списку, але може нас дуже швидко вибирати потім із нього вузли. Інші варіанти використовують проміжні стратегії. Наприклад, можна використовуватиме зберігання списку можливих вузлів пріоритетну чергу з урахуванням пірамід, можна буде буде просто вибрати наступний вузол з вершини піраміди. Вставка нового вузла до піраміди і його переупорядочение виконуватиметься швидше, ніж аналогічні операції для упорядкованого зв’язкового списку. Інші стратегії використовують складні схеми організації блоків у тому, щоб спростити пошук можливих вузлів. Деякі з цих варіантів досить складні. Через цієї можливості їхнього складності ці алгоритми для невеликих мереж часто виконуються повільніше, що більш прості алгоритми. Проте, для великих мереж чи мереж, в яких кожний вузол має дуже великий число зв’язків, виграш від використання цих алгоритмів може коштувати додаткового усложнения.

Коррекция меток

Как і алгоритм установки міток, цей алгоритм розпочинає переговори з скасування значення поля Dist кореневого вузла й поміщає кореневої вузол до списку можливих вузлів. У цьому значення полів Dist інших вузлів встановлюються рівними нескінченності. Потім для вставки в дерево найкоротшого маршруту вибирається перший вузол у списку можливих вузлів. Після цього алгоритм перевіряє вузли, сусідні поруч з обраним, з’ясовуючи, буде чи відстань від кореня до обраного вузла плюс ціна зв’язку менше, ніж поточне значення поля Dist сусіднього вузла. Якщо це, то поля Dist і InLink сусіднього вузла оновлюються те щоб найкоротший маршрут до сусіднього вузлу проходив через обраний вузол. Якщо сусідній вузол у своїй не був у списку можливих вузлів, то алгоритм також додає його до списку. Зауважте, що алгоритм не перевіряє, потрапляв цей вузол до списку раніше. Якщо шлях від кореня до сусіднього вузла стає коротше, вузол завжди додається до списку можливих вузлів. Алгоритм продовжує видаляти вузли зі списку можливих вузлів, перевіряючи сусідні поруч з ними вузли і додаючи сусідні вузли до списку до того часу, поки список не спорожніє. Якщо уважно порівняти алгоритми установки міток і корекції міток, то видно, що схожі. Єдина відмінність у тому, як і кожен їх вибирає елементи зі списку можливих вузлів для вставки в дерево найкоротшого маршрута.

=====331

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

Private Sub FindPathTree (root As PathCNode) Dim candidates As New Collection Dim node_dist As Integer Dim new_dist As Integer Dim node As PathCNode Dim to_node As PathCNode Dim link As PathCLink

If root Is Nothing Then Exit Sub

" Скинути поля Marked і NodeStatus всім узлов,

" і прапори Used і InPathTree всім связей.

ResetPathTree

" Почати з деревокореня найкоротшого маршруту. root. Dist = 0

Set root. InLink = Nothing root. NodeStatus = NOW_IN_LIST candidates. Add root

Do While candidates. Count > 0

" Додати вузол в дерево найкоротшого маршрута.

Set node = candidates (1) candidates. Remove 1 node_dist = node. Dist node. NodeStatus = NOT_IN_LIST

" Перевірити сусідні узлы.

For Each link In node. Links

If node Is link. Node1 Then

Set to_node = link. Node2

Else

Set to_node = link. Node1

End If

" Перевірити, може бути більш короткий

" шлях через цей вузол. new_dist = node_dist + link. Cost

If to_node. Dist > new_dist Then

" Шлях краще. Обновити значення Dist і InLink.

Set to_node. InLink = link to_node. Dist = new_dist

" Додати вузол до списку можливих узлов,

" якщо його ще нет.

If to_node. NodeStatus = NOT_IN_LIST Then candidates. Add to_node to_node. NodeStatus = NOW_IN_LIST

End If

End If

Next link

Loop

" Позначити вхідні зв’язку, щоб їх було легше вывести.

For Each node In Nodes

If Not (node. InLink Is Nothing) Then _ node. InLink. InPathTree = True

Next node

" Перемалювати сеть.

DrawNetwork End Sub

В на відміну від алгоритму установки міток, цей алгоритм неспроможна працювати з мережами, які містять цикли із від'ємною ціною. Якщо зустрічається такий цикл, то алгоритм нескінченно переміщається зв’язків усередині нього. При кожному обході циклу відстань до назв вузлів зменшується, при цьому алгоритм знову поміщає вузли до списку можливих вузлів, і знову може перевіряти їх надалі. При наступній перевірці цих вузлів, відстань до них також зменшиться, тощо. Цей процес відбувається триватиме до тих пір, поки відстань до цих вузлів не досягне нижнього межового значення -32. 768, якщо довжина шляху задана цілим числом. Якщо відомо, що у мережі є цикли із від'ємною ціною, то найпростіше просто використовуватиме роботи із нею метод установки, а чи не корекції міток. Програма PathC використовує цей алгоритм корекції міток для обчислення найкоротшого маршруту. Вона аналогічна програмі PathS, але використовує метод корекції, а чи не установки меток.

=======333

Варіанти методу корекції меток

Алгоритм корекції міток дозволяє нас дуже швидко вибрати вузол зі списку можливих вузлів. Він він може вставити вузол до списку лише за чи два кроку. Недолік цього алгоритму у тому, що він вибирає вузол зі списку можливих вузлів, може зробити занадто хороший вибір. Якщо алгоритм вибирає вузол доти, як він поля Dist і InLink отримають свої кінцевий значення, він має пізніше скоригувати значення цих полів і знову помістити вузол до списку можливих вузлів. Чим частіше алгоритм поміщає вузли у список можливих вузлів, тим більше коштів часу це займає. Варіанти цього алгоритму намагаються підвищити якість вибору вузлів без великого ускладнення алгоритму. Одне з методів, який непогано дбає про практиці, у тому, щоб додавати вузли одночасно у початок і поклала край списку можливих вузлів. Якщо вузол раніше не потрапляв до списку можливих вузлів, алгоритм, звісно ж, додає їх у кінець списку. Якщо вузол вже колись був у списку можливих вузлів, та його немає, алгоритм вставляє їх у початок списку. У цьому повторне звернення до вузлу виконується відразу, можливо, за першому зверненні до списку. Ідея, ув’язнена у тому підході, у тому, аби змінити якщо алгоритм робить помилку, вона виправлялася якнайшвидше. Якщо помилка нічого очікувати виправлено протягом досить довгого часу, алгоритм може використовувати неправильне інформацію для побудови довгих хибних шляхів, які потім доведеться виправляти. Завдяки швидкому виправленню помилок, алгоритм може зменшити кількість невірних шляхів, що їх доведеться перебудувати. У найкращому разі, коли всі сусідні вузли усе ще перебувають в списку можливих вузлів, повторна перевірка цього вузла до перевірки сусідів відверне побудова невірних путей.

Другие завдання пошуку найкоротшого маршрута

Описанные вище алгоритми пошуку найкоротшого маршруту вираховували все найкоротші шляхи виходу з кореневого вузла до решти вузлів у мережі. Існує багато інших типів завдання перебування найкоротшого маршруту. У цьому розділі обговорюються троє фахівців з них: двоточковим найкоротший маршрут (point- to-point shortest path), найкоротший маршрут всім пар (all pairs shortest path) і найкоротший маршрут зі штрафами за повороты.

Двухточечный найкоротший маршрут

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

=======334

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

" Знайти найближчий до корені вузол у списку можливих узлов.

:

" Перевірити, чи ринковий цей вузол потрібним. If node = destination Then Exit Do

" Додати цей вузол в дерево найкоротшого маршрута.

:

На практиці, якщо дві точки у мережі розташовані далеко друг від друга, то цей алгоритм зазвичай виконуватиметься довше, ніж займе обчислення повного дерева найкоротшого маршруту. Алгоритм виконується повільніше через те, що у кожному циклі виконання алгоритму перевіряється, досягнуть чи шуканий вузол. З іншого боку, якщо вузли розташовані поруч, то виконання цього алгоритму вимагатиме значно менше часу, ніж спорудження повного дерева найкоротшого маршруту. Для деяких мереж, як-от мережу вулиць, можна оцінити, наскільки близько розташовані дві крапки й потім вирішити, яку версію алгоритму вибрати. Якщо мережу містить все вулиці південної Каліфорнії, і ще дві точки розташовані на півметровій відстані 10 миль, варто використовувати версію, яка зупиняється по тому, як знайде кінцевий вузол. Якщо ж точки віддалені друг від друга на 100 миль, можливо, менше займе обчислення повного дерева найкоротшого маршрута.

Вычисление найкоротшого маршруту всім пар

В деяких додатках може знадобитися швидко знайти найкоротший маршрут поміж усіма парами вузлів у мережі. Якщо потрібне обчислити більшу частину з N2 можливих шляхів, то, можливо швидше обчислити всіх можливих шляху замість здобуття права знаходити ті, які потрібні. Можна записати найкоротші маршрути, використовуючи два двовимірні масиву, Dist і InLinks. У осередку Dist (I, J) перебуває найкоротший маршрут з вузла I в вузол J, а осередку InLinks (I, J) — зв’язок, веде до вузлу J в найкоротшому шляхи виходу з вузла I в вузол J. Ці значення аналогічні значенням Dist і InLink в класі вузла у минулому алгоритмі. Одне з способів знайти найкоротші маршрути у тому, щоб побудувати дерева найкоротшого маршруту з коренем у кожному з вузлів мережі при допомоги однієї з попередніх алгоритмів, і далі зберегти результати в масивах Dists і InLinks.

========335

Другой метод обчислення всіх найкоротших маршрутів послідовно будує шляху, використовуючи дедалі більше вузлів. Спочатку алгоритм знаходить усе найкоротші маршрути, що використовують лише перший вузол і вузли на кінцях шляху. Інакше кажучи, для вузлів J і K алгоритм знаходить найкоротший маршрут між тими вузлами, який використовує лише вузол з номером 1 і вузли J і K, якщо цей шлях існує Потім алгоритм знаходить усе найкоротші маршрути, що використовують лише два перших вузла. Потім він шляху, використовуючи перші три вузла, перші чотири вузла, тощо до того часу, коли будуть збудовано всі найкоротші маршрути, використовуючи все вузли. Саме тоді, оскільки найкоротші маршрути може використати будь-який вузол, алгоритм знайде все найкоротші маршрути в мережі. Зауважте, що найкоротший маршрут між вузлами J і K, використовує лише перші I вузлів, включає вузол I, лише коли Dist (J, K) > Dist (J, I) + Dist (I, K). Інакше найкоротшим маршрутом буде попередній найкоротший маршрут, що використовував лише I — 1 вузлів. Це означає, що коли і алгоритм розглядає вузол I, потрібно лише перевірити виконання умови Dist (J, K) > Dist (J, I) + Dist (I, K). Якщо ця умова виконується, алгоритм оновлює найкоротший маршрут з вузла J в вузол K. Інакше старий найкоротший маршрут між двома вузлами був би таковым.

Штрафы за повороты

В деяких мережах, особливо мережах вулиць, буває корисно додати штраф і заборони на повороти (turn penalties) У «тенета вулиць автомобіль повинен уповільнити рух до того, як виконати поворот. Поворот наліво може займати більше часу, ніж поворот направо чи рух прямо. Деякі повороти можуть бути заборонені чи неможливі через наявність розділової смуги. Ці аспекти можна врахувати, вводячи у мережу штрафи за повороты.

Небольшое число штрафів за повороты

Часто важливі лише окремі штрафи за повороти. Може знадобитися запобігти виконання заборонених чи неможливих поворотів і привласнити штрафи за повороти тільки кількох ключових перехрестях, не визначаючи штрафи всім перехресть у мережі. І тут може бути розбитий кожен вузол, котрій задано штрафи, сталася на кілька вузлів, які неявно враховувати штрафи. Припустимо, що потрібно додати один штраф за поворот на перехресті ліворуч кермо і інший штраф за поворот направо. На рис. 12. 12 показаний перехрестя, у якому потрібно застосувати ці штрафи. Кількість поруч із кожної зв’язком відповідає її ціні. Потрібна застосувати штрафи за вхід в вузол A зв’язку L1, і далі вихід із без нього в зв’язкам L2 чи L3. Для застосування штрафів до вузлу A, розіб'ємо цей вузол на два вузла, за одним кожної з які покидають його зв’язків. У цьому прикладі, з вузла A виходять дві зв’язку, тому вузол A розбивається на два вузла A1 і A2, та зв’язку, що виходять із вузла A, замінюються відповідними зв’язками, що виходять з отриманих вузлів. Можна уявити, що з двох які утворилися вузлів відповідає входу в вузол A і повороту убік відповідної связи.

======336

@Рис. 12. 12. Перекресток

Затем зв’язок L1, що входить у вузол A, замінюється на дві зв’язку, що входять до кожен із двох вузлів A1 і A2. Ціна цих зв’язків дорівнює ціні вихідної зв’язку L1 плюс штрафу за поворот у відповідній напрямі. На рис. 12. 13 показаний перехрестя, у якому запроваджені штрафи за поворот. У цьому малюнку штраф за поворот наліво з вузла A дорівнює 5, а й за поворот направо -2. Помістивши інформацію про штрафи у конфігурацію мережі, ми уникаємо необхідності модифікувати алгоритми пошуку найкоротшого маршруту. Ці алгоритми знаходитимуть правильні найкоротші маршрути з урахуванням штрафів за повороти. У цьому доведеться-таки злегка змінити програми, щоб врахувати розбивка вузлів сталася на кілька частин. Припустимо, що потрібно знайти найкоротший маршрут між вузлами I і J, але вузол I виявився розбитий сталася на кілька вузлів. Вважаючи, які можна залишити вузол І за будь-який зв’язку, можна створити помилковий вузол і використовувати його на ролі деревокореня найкоротшого маршруту. З'єднаємо цей вузол зв’язками з травня нульової ціною з кожним із вузлів, одержані після розбивки вузла I. Тоді, якщо побудувати дерево найкоротшого маршруту з коренем в фальшивому вузлі, то знайдуть все найкоротші маршрути, містять будь-якій із цих вузлів. На рис. 12. 14 показаний перехрестя з рис. 12. 13, пов’язані з хибним кореневим узлом.

@Рис. 12. 13. Перехрестя зі штрафами за повороты

=======337

@Рис. 12. 14. Перехрестя, пов’язані з хибним корнем

Обрабатывать випадок пошуку шляху до вузлу, який з’явився сталася на кілька вузлів, простіше. Якщо потрібно знайти найкоротший маршрут між вузлами I і J, і вузол J з’явився сталася на кілька вузлів, то спочатку, звісно ж, потрібно знайти дерево найкоротшого маршруту з коренем в вузлі I. Потім перевіряються все вузли, куди з’явився вузол J і залишається найближчий із них корені дерева. Шлях до цього вузлу це і є найкоротший маршрут до вихідному вузлу J.

Большое число штрафів за повороты

Предыдущий метод буде дуже ефективним, якщо хочете запровадити штрафи за повороти більшість вузлів у мережі. Краще буде створити цілком нову мережу, що включатиме інформацію про штрафи. Для кожної зв’язок між вузлами A і B в вихідної мережі у новій мережі створюється вузол AB;. Якщо вихідної мережі відповідні зв’язку з'єднали, то отримані вузли також з'єднуються між собою. Наприклад, припустимо, що у вихідної мережі одна зв’язок з'єднувала вузли A і B, іншу — вузли B і C.

Тоді, у нової мережі треба створити зв’язок, з'єднуючу вузол AB з вузлом BC;. Ціна нової зв’язку складається від ціни другий зв’язку в вихідної сіті й штрафу за поворот. У цьому вся прикладі ціна зв’язок між вузлом AB і вузлом BC дорівнюватиме ціні зв’язку, що з'єднує вузли B і З в вихідної мережі плюс штрафу за поворот під час руху з вузла A в вузол B і у вузол З. На рис. 12. 15 зображено невеличка мережу й гарантована відповідна нова мережу, що становить штрафи за повороти. Штраф за поворот наліво дорівнює 3, за поворот направо — 2, а й за «поворот» прямо — нулю. Наприклад, оскільки поворот з вузла B в вузол E — це лівий поворот в вихідної мережі, штраф для зв’язку між вузлами BE і EF у новій мережі дорівнює 3. Ціна зв’язку, що з'єднує вузли E і F в вихідної мережі, дорівнює 3, тому повна ціна нової зв’язку дорівнює 3 + 3 = 6.

=======338

@Рис. 12. 15. Мережа й гарантована відповідна їй мережу з штрафами за повороты

Предположим тепер, що потрібно знайти для вихідної мережі дерево найкоротшого маршруту з коренем в вузлі D. Аби зробити це, створимо у новій мережі помилковий кореневої вузол, потім побудуємо зв’язку, що з'єднують цей вузол зі усіма зв’язками, які залишають вузол D в вихідної мережі. Дамо цим зв’язкам таку ціну, яку мають відповідні зв’язку в вихідної мережі. На рис. 12. 16 показано нова мережу з рис. 12. 15 з хибним кореневим вузлом, відповідним вузлу D. Дерево найкоротшого маршруту у цій мережі намальовано жирною лінією. Щоб знайти найкоротший маршрут з вузла D в вузол З, необхідно перевірити все вузли у новій мережі, які відповідають зв’язкам, заканчивающимся в вузлі З. У цьому вся прикладі це вузли BC і FC. Найближчий до брехливому корені вузол відповідає щонайкоротшого маршруту до вузлу З в вихідної мережі. Вузли в найкоротшому маршруті у новій мережі відповідають зв’язкам в найкоротшому маршруті в вихідної сети.

@Рис. 12. 16. Дерево найкоротшого маршруту у мережі зі штрафами за повороты

========339

На рис. 12. 16 найкоротший маршрут починається з помилкового кореня, іде у вузол DE, потім вузли EF і FC і має ціну 16. Цей шлях відповідає шляху D, E, F, З в вихідної мережі. Збільшивши один штраф за лівий поворот E, F, З, одержимо, що ціна цього шляху до вихідної мережі також дорівнює 16. Зауважте, що ви знайшли б цей нелегкий шлях, якби побудували дерево найкоротшого маршруту в вихідної мережі. Без обліку штрафів за повороти, найкоротшим маршрутом з вузла D в вузол З було б шлях D, E, B, З з повним ціною 12. З урахуванням штрафів ціна цього шляху дорівнює 17.

Применения методу пошуку найкоротшого маршрута

Вычисления найкоротшого маршруту використовують у багатьох додатках. Очевидним прикладом є пошук найкоротшого маршруту між двома точками в вуличної мережі. Багато інші докладання використовують метод пошуку найкоротшого маршруту менш очевидними способами. Наступні розділи описують дехто з тих приложений.

Разбиение на районы

Предположим, що є карта міста, яку завдані все пожежні депо. Може знадобитися визначити кожної точки міста найближче до ній депо. На погляд це здається складним завданням. Можна спробувати розрахувати дерево найкоротшого маршруту з коренем у кожному вузлі мережі, щоб знайти, яке депо розміщено найближче кожному з вузлів. Чи можна побудувати дерево найкоротшого маршруту з коренем у кожному з пожежних депо і записати відстань від кожної з вузлів до кожного з депо. Але є набагато більше швидкий метод. Створимо помилковий кореневої вузол і з'єднаємо його з кожним із пожежних депо зв’язками з травня нульової ціною. Потім знайдемо дерево найкоротшого маршруту з коренем у тому фальшивому вузлі. Для кожної точки у мережі найкоротший маршрут з помилкового кореневого вузла до цієї точці пройде через найближче до цієї точці пожежне депо. Щоб знайти найближче до точки пожежне депо, треба тільки проїхати по щонайкоротшого маршруту від цього точки до корені, перебувають у шляху не зустрінеться одна з депо. Побудувавши лише одне дерево найкоротшого маршруту, можна знайти найближчі пожежні депо кожної точки у мережі. Програма District використовує цей алгоритм для розбивки мережі на райони. Також, як і яскрава програма PathC та інші програми, достойні цієї главі, вона дозволяє завантажувати, редагувати і зберігати на диску орієнтовані мережі з ціною зв’язків. Якщо ви і не додаєте і видаляєте вузли або зв’язки, ви можете вибрати депо потреби ділити на райони. Додайте вузли до списку пожежних депо клацанням лівої кнопки миші, потім клацніть правої кнопкою в місці форми, і яскрава програма розіб'є мережу на райони. На рис. 12. 17 показано вікно програми, у якому зображено мережу із трьома депо. Депо в вузлах 3, 18 і 20 обведені жирними кружечками. Разбивающие мережу на райони дерева найкоротшого маршруту зображені жирними линиями.

=====340

@Рис. 12. 17. Програма District

Упорядкування плану робіт з допомогою методу критичного пути

Во багатьох завданнях, зокрема у великих програмних проектах, певні дії мають бути виконані раніше від інших. Наприклад, для будівництва вдома до установки фундаменту потрібно викопати котлован, фундамент повинен завмерти доти, як розпочнеться спорудження стін, каркас вдома може бути зібрано колись, чим можна виконуватиме проводку електрики, водогону і покрівельні праці та таке інше. Деякі з з завдань можуть виконуватися одночасно, інші повинні виконуватися послідовно. Наприклад, можна одночасно проводити електрика і прокладати водогін. Критичним шляхом (critical path) називається один із найбільш довгих послідовностей завдань, що має бути виконано завершення проекту. Важливість завдань, лежачих на критичному шляху, залежить від того, що зрушення термінів виконання з завдань призведе зміну часу завершення проекту на цілому. Якщо закласти фундамент тиждень пізніше, те й будинок буде завершено тиждень пізніше. Для визначення завдань, які перебувають критичному шляху, можна використовувати модифікований алгоритм пошуку найкоротшого маршруту. Спочатку створимо мережу, що представляє тимчасові співвідношень між завданнями проекту. Нехай кожного завдання відповідає вузол. Намалюємо зв’язок між завданням I і завданням J, коли завдання I має бути виконане на початок завдання J, і дамо цьому разі ціну, рівну часу виконання завдання I. Після цього створимо два хибних вузла, одна з яких відповідатиме початку проекту, а інший — його завершення. З'єднаємо початковий вузол зв’язками з травня нульової ціною з усіма вузлами у проекті, у яких не входить жодна інша зв’язок. Ці вузли відповідають завданням, виконання яких можна починати негайно, без вичікування завершення інші завдання. Потім створимо хибні зв’язку нульової довжини, що з'єднують все вузли, у тому числі теж не виходить не однієї зв’язку, з кінцевим вузлом. Ці вузли представляють завдання, які гальмують виконання інші завдання. Потому, як усі ці завдання обіцяє, проект буде завершено. Знайшовши найдовший маршрут між початковим і кінцевим вузлами мережі, ми одержимо критичний шлях проекту. Вхідні до нього завдання будуть критичними до виконання проекта.

========341

@Таблица 12.1. Етапи складання дощувальної установки

Рассмотрим, наприклад, спрощений проект складання дощувальної установки, що з п’яти завдань. У табл. 12.1 наведено завдання й тимчасові співвідношень між ними. Мережа при цьому проекту показано на рис. 12. 18. У цьому вся простому прикладі легко побачити, що довгий маршрут у мережі виконує послідовність завдань: викопати канави, змонтувати труби, закопати їх. Це критичні завдання, і тоді як виконанні будь-якої їх настане затримка, виконання також затримається. Довжина цього критичного шляху дорівнює очікуваному часу закінчення. У разі, коли всі завдання обіцяє вчасно, виконання проекту займе п’ять днів. Передбачається також, що якщо це можливо, кілька завдань робитиметься одночасно. Наприклад, один то вона може копати канави, поки інший закуповуватиме труби. У значному проекті, такому як будівництво хмарочоса чи зйомка фільму, можуть утримуватися тисячі завдань, і критичні шляху у своїй можуть бути не очевидны.

Планирование колективної работы

Предположим, що потрібно набрати кілька тисяч працівників для відповіді телефонні дзвінки, у своїй кожен із новачків буде зайнятий не весь день. При цьому слід, щоб сумарна зарплата була найменшої, і найнятий колектив співробітників відповідав на дзвінки з 9 ранку до 5 вечора. У табл. 12.2 наведено робочий день співробітників, та його погодинна оплата.

@Рис. 12. 18. Мережа завдань складання дощувальної установки

======342

@Таблица 12.2. Робітники годинник співробітників та його погодинна оплата

Для побудови відповідної мережі, створимо один вузол кожному за робочого години. З'єднаємо ці вузли зв’язками, кожна з яких відповідає робочим годинах будь-якого співробітника. Якщо працівник може працювати з 9 до 11, намалюємо зв’язок між вузлом 9: 00 і вузлом 11: 00, і дамо цьому разі ціну, рівну зарплаті, одержуваної даним співробітником за відповідне час. Якщо працівник отримує 6,5 доларів за годину, і час становить дві години, то ціна зв’язку дорівнює 13 доларам. На рис. 12. 19 показано мережу, відповідна даним з табл. 12.2. Найкоротший маршрут з першого вузла за останній дозволяє набрати колектив працівників із найменшої сумарною зарплатою. Кожна зв’язок їсти дорогою відповідає роботі співробітника у визначений проміжок часу. У цьому разі найкоротший маршрут з вузла 9: 00 в вузол 5: 00 проходить через вузли 11: 00, 12: 00 і 3: 00. Цьому відповідає наступний графік роботи: співробітник A працює із 9: 00 до 11: 00, співробітник D працює із 11: 00 до 12: 00, потім співробітник A знову працює із 12: 00 до 3: 00 і співробітник E працює із 3: 00 до 5: 00. Повна зарплата всіх співробітників в такому графіці становить 52,15 доллара.

@Рис. 12. 19. Мережа графіка роботи коллектива

======343

Максимальний поток

Во багатьох мережах зв’язку мають, окрім ціни, що й пропускну спроможність (capacity). Через кожен вузол мережі може проходити потік (flow), який перевищує її пропускну здатність. Наприклад, вулицями може проїхати лише певної число машин. Мережа із наперед заданими пропускними здібностями її зв’язків називається навантаженої мережею (capacitated network). Якщо задана навантажена мережу, завдання про максимальному потоці залежить від визначенні найбільшого можливого потоку через мережа з заданого джерела (source) в поставлене стік (sink). На рис. 12. 20 показано невеличка навантажена мережу. Числа поруч із зв’язками в цієї мережі - це ціна зв’язку, та її пропускну здатність. У цьому вся прикладі максимальний потік, рівний 4, виходить, якщо дві одиниці потоку направляються шляхом A, B, E, F і ще — шляхом A, З, D, F. Описаний тут алгоритм починається сіло, що потік переважають у всіх зв’язках дорівнює нулю і далі алгоритм поступово збільшує потік, намагаючись поліпшити знайдене рішення. Алгоритм завершує роботу, коли можна поліпшити те що рішення. Для пошуку шляхів засобів збільшення повного потоку, алгоритм перевіряє залишкову пропускну спроможність (residual capacity) зв’язків. Залишкова пропускну здатність зв’язок між вузлами I і J дорівнює максимальному додатковому потоку, що можна направити з вузла I в вузол J, використовуючи зв’язок між I і J і зв’язок між J і I. Цей сумарний потік може включати додатковий потік зв’язку I-J, тоді як цьому разі є резерв пропускну здатність, або вилучати частина потоку з зв’язку J-I, коли з цьому разі йде потік. Наприклад, припустимо, що у мережі, що з'єднує вузли A і З на рис. 12. 20, існує потік, рівний 2. Оскільки пропускну здатність цьому разі дорівнює 3, чи до цьому разі можна додати одиницю потоку, тому залишкова пропускну здатність цьому разі дорівнює 1. Хоча мережу, показана на рис. 12. 20 немає зв’язку C-A, з цією зв’язку існує залишкова пропускна здатність. У цьому прикладі, оскільки за зв’язку A-C йде потік, рівний 2, можна видалити до двох одиниць цього потоку. У цьому сумарний потік з вузла З в вузол A збільшився на 2, тому залишкова пропускна здатність зв’язку C-A дорівнює 2.

@Рис. 12. 20. Навантажена сеть

========344

@Рис. 12. 21. Потоки в сети

Сеть, що складається з всіх зв’язку з позитивної залишкової пропускної здатністю, називається залишкової мережею (residual network). На рис. 12. 21 показано мережу з рис. 12. 20, кожної зв’язку у якій присвоєно потік. Для кожної зв’язку, перше число одно потоку через зв’язок, а друге — її пропускну здатність. Напис «½», наприклад, означає, що потік через зв’язок дорівнює 1, і його пропускну здатність дорівнює 2. Зв’язки, потік через що більше нуля, намальовані жирними лініями. На рис. 12. 22 показано залишкова мережу, відповідна потокам на рис. 12. 21. Намальовані тільки зв’язки, які таки мати залишкову пропускну спроможність. Наприклад, між вузлами A і D не намальовано жодної зв’язку. Вихідна мережу зовсім позбавлений зв’язку A-D чи D-A, тому ті зв’язку завжди матимуть нульову залишкову пропускну здатність. Один із властивостей залишкових мереж у тому, що кожен шлях, використовує через відкликання залишкової пропускною спроможністю більше нуля, який пов’язує джерело зі стоком, дає спосіб збільшення потоку у мережі. Тому що це шлях дає спосіб підвищення або розширення потоку у мережі, він називається які розширюють шляхом (augmenting path). На рис. 12. 23 показано залишкова мережу з рис. 12. 22 з які розширюють шляхом, намальованим жирною лінією. Щоб оновити рішення, використовуючи яким розширено шлях, знайдемо найменшу залишкову пропускну спроможність їсти дорогою. Потім скоригуємо потоки їсти дорогою відповідно до цим значенням. Наприклад, на рис. 12. 23 найменша залишкова пропускну здатність мереж в расширяющем шляху дорівнює 2. Щоб оновити потоки у мережі, до будь-якої зв’язку I-J по дорозі додається потік 2, та якщо з всіх зворотних їм зв’язків J-I віднімається потік 2.

@Рис. 12. 22. Залишкова сеть

========345

@Рис. 12. 23. Яким Розширено шлях через залишкову сеть

Вместо здобуття права коригувати потоки, і далі перебудовувати залишкову мережу, простіше просто скоригувати залишкову мережу. Потім після завершення роботи алгоритму можна використовувати результат для обчислення потоків для зв’язків в вихідної мережі. Щоб скоригувати залишкову мережу цьому прикладі, проїдемо по расширяющему шляху. Віднімемо 2 з залишкової пропускну здатність всіх зв’язків I-J вздовж шляху, і додамо 2 до залишкової пропускну здатність відповідної зв’язку J-I. На рис. 12. 24 показано скоригована залишкова мережу при цьому прикладу. Якщо більше можна віднайти жодного котрий розширює шляху, можна використовувати залишкову мережу для обчислення потоків в вихідної мережі. Для кожної зв’язку між вузлами I і J, якщо залишковий потік між вузлами I і J менше, ніж пропускну здатність зв’язку, то потік має дорівнювати пропускної здібності мінус залишковий потік. Інакше потік може бути нульовий. Наприклад, на рис. 12. 24 залишковий потік з вузла A в вузол З дорівнює 1 і пропускну здатність зв’язку A-C дорівнює 3. Оскільки 1 менше 3, то потік через вузол дорівнюватиме 3 — 1 = 2. На рис. 12. 25 показані потоки у мережі, відповідні залишкової мережі на рис. 12. 24.

@Рис. 12. 24. Скоригована залишкова сеть

========346

@Рис. 12. 25. Максимальні потоки

Полученный алгоритм ще містить методу на допомогу пошуку які розширюють шляхів в залишкової мережі. Одна з імовірних методів аналогічний методу корекції міток для алгоритму найкоротшого маршруту. Спочатку помістимо узел-источник в список можливих вузлів. Потім, якщо список можливих вузлів не порожній, будемо видаляти потім із нього за одним вузлу. Перевіримо все сусідні вузли, з'єднані з обраним вузлом зв’язку, залишкова пропускну здатність якої більший від нуля. Якщо сусідній вузол ще було поміщений у список можливих вузлів, додати його до списку. Продовжити той процес до того часу, поки список можливих вузлів не спорожніє. Цей метод має дві відмінності між методу пошуку найкоротшого маршруту корекцією міток. По-перше, його не простежує зв’язки Польщі з нульової залишкової пропускною спроможністю. Алгоритм ж найкоротшого маршруту перевіряє всі дороги, незалежно від ціни. По-друге, цей алгоритм перевіряє все вузли максимум один раз. Алгоритм пошуку найкоротшого маршруту корекцією міток, буде оновлювати вузли і поміщати їх знову у список можливих вузлів, коли він пізніше знайде більш найкоротший шлях від кореня до цього вузлу. Під час пошуку котрий розширює шляху немає необхідності перевіряти його довжину, тому потрібно щороку оновлювати шляху й поміщати вузли у список можливих вузлів. Наступний код демонструє, як і вираховуватимуть максимальні потоки в програмі на Visual Basic. Цей код призначений до роботи з неориентированными мережами, схожими тих, що були за іншими програмах прикладів, описаних у Пророчих цієї главі. Після закінчення роботи алгоритму він привласнює зв’язку ціну, рівну потоку неї, взятому зі знаком мінус, якщо потік тече до напрямку. Інакше кажучи, якщо мережу містить об'єкт, що становить зв’язок I-J, а алгоритм визначає, що потік повинен текти у бік зв’язку J-I, то потоку через зв’язок I-J присвоюється значення, однакову потоку, що був б текти через зв’язок J-I, взятому зі знаком мінус. Це дозволяє програмі визначати напрям потоку, використовуючи існуючу структуру узлов.

=======347

Private Sub FindMaxFlows () Dim candidates As Collection

Dim Residual () As Integer Dim num_nodes As Integer Dim id1 As Integer Dim id2 As Integer Dim node As FlowNode Dim to_node As FlowNode Dim from_node As FlowNode Dim link As FlowLink Dim min_residual As Integer

If SourceNode Is Nothing Or SinkNode Is Nothing _

Then Exit Sub

" Поставити розмір масиву залишкової пропускну здатність. num_nodes = Nodes. Count

ReDim Residual (1 To num_nodes, 1 To num_nodes)

" Спочатку значення залишкової пропускної способности

" рівні значенням пропускної способности.

For Each node In Nodes id1 = node. Id

For Each link In node. Links

If link. Node1 Is node Then

Set to_node = link. Node2

Else

Set to_node = link. Node1

End If id2 = to_node. Id

Residual (id1, id2) = link. Capacity

Next link

Next node

" Повторювати до того часу, поки больше

" бракуватиме які розширюють путей.

Do

" Знайти яким розширено шлях у залишкової сети.

" Скинути значення NodeStatus і InLink всіх узлов.

For Each node In Nodes node. NodeStatus = NOT_IN_LIST

Set node. InLink = Nothing

Next node

" Почати з порожнього списку можливих узлов.

Set candidates = New Collection

" Помістити джерело до списку можливих вузлів. candidates. Add SourceNode

SourceNode. NodeStatus = NOW_IN_LIST

" Продовжувати, поки список можливих вузлів не опустеет.

Do While candidates. Count > 0

Set node = candidates (1) candidates. Remove 1 node. NodeStatus = WAS_IN_LIST id1 = node. Id

" Перевірити що виходять із вузла связи.

For Each link In node. Links

If link. Node1 Is node Then

Set to_node = link. Node2

Else

Set to_node = link. Node1

End If id2 = to_node. Id

" Перевірити, що residual > 0, і це узел

" ніколи було в списке.

If Residual (id1, id2) > 0 And _ to_node. NodeStatus = NOT_IN_LIST _

Then

" Додати вузол до списку. candidates. Add to_node to_node. NodeStatus = NOW_IN_LIST

Set to_node. InLink = link

End If

Next link

" Зупинитися, якщо помечен узел-сток.

If Not (SinkNode. InLink Is Nothing) Then _

Exit Do

Loop

" Зупинитися, якщо яким розширено шлях не найден.

If SinkNode. InLink Is Nothing Then Exit Do

" Знайти найменшу залишкову пропускну способность

" вздовж котрий розширює шляху. min_residual = INFINITY

Set node = SinkNode

Do

If node Is SourceNode Then Exit Do id2 = node. Id

Set link = node. InLink

If link. Node1 Is node Then

Set from_node = link. Node2

Else

Set from_node = link. Node1

End If id1 = from_node. Id

If min_residual > Residual (id1, id2) Then _ min_residual = Residual (id1, id2)

Set node = from_node

Loop

" Обновити залишкові пропускні способности,

" використовуючи яким розширено путь.

Set node = SinkNode

Do

If node Is SourceNode Then Exit Do id2 = node. Id

Set link = node. InLink

If link. Node1 Is node Then

Set from_node = link. Node2

Else

Set from_node = link. Node1

End If id1 = from_node. Id

Residual (id1, id2) = Residual (id1, id2) _

— min_residual

Residual (id2, id1) = Residual (id2, id1) _

+ min_residual

Set node = from_node

Loop

Loop «Повторювати, більше не залишиться які розширюють путей.

" Обчислити потоки в залишкової сети.

For Each link In Links id1 = link. Node1. Id id2 = link. Node2. Id

If link. Capacity > Residual (id1, id2) Then link. Flow = link. Capacity — Residual (id1, id2)

Else

" Негативні значення соответствуют

" зворотному напрямку руху. link. Flow = Residual (id2, id1) — link. Capacity

End If

Next link

" Знайти повний поток.

TotalFlow = 0

For Each link In SourceNode. Links

TotalFlow = TotalFlow + Abs (link. Flow)

Next link End Sub

=======348−350

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

Приложения максимального потока

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

Непересекающиеся пути

Большие мережі зв’язку повинні мати надмірністю (redundancy). Для заданої мережі, наприклад такий, як у рис. 12. 27, може знадобитися знайти число непересічних шляхів із джерела до стоку. У цьому, якщо розрив між двома вузлами мережі є чимало непересічних шляхів, всі системи зв’язку у яких різні, то з'єднання між тими вузлами залишиться, навіть якщо дещо зв’язків у мережі буде розірвано. Можна визначити число різних шляхів, використовуючи метод обчислення максимального потоку. Створимо мережу з вузлами і зв’язками, відповідними вузлам і зв’язкам в комунікаційної мережі. Дамо кожної зв’язку одиничну пропускну способность.

@Рис. 12. 26. Програма Flow

=====351

@Рис. 12. 27. Мережа коммуникаций

Затем обчислимо максимальний потік у мережі. Максимальний потік дорівнюватиме числу різних шляхів джерела до стоку. Оскільки кожна зв’язок може нести одиничний потік, то одне із шляхів, використаних при обчисленні максимального потоку, неспроможна мати загальної зв’язку. За більш суворому визначенні надмірності можна зажадати, щоб різні шляху або не мали ні загальних зв’язків, ні загальних вузлів. Трохи змінивши попередню мережу, можна використовувати обчислення максимального потоку для рішення і це завдання. Розділимо кожен вузол крім джерела і стоку на два вузла, з'єднаних зв’язком одиничної пропускну здатність. З'єднаємо перший із отриманих вузлів з усіма зв’язками, які входять у вихідний вузол. Усі зв’язку, що виходять із вихідного вузла, приєднаємо до другого одержаному після розбивки вузлу. На рис. 12. 28 показано мережу з рис. 12. 27, вузли де розбиті в такий спосіб. Тепер знайдемо максимальний потік з цією мережі. Якщо шлях, використаний для обчислення максимального потоку, проходить через вузол, він може використовувати зв’язок, яка з'єднує два одержані після розбивки вузла. Оскільки цей зв’язок має одиничну пропускну спроможність, ніякі двома способами, отримані при обчисленні максимального потоку, що неспроможні подолати на цьому разі між вузлами, у вихідної мережі ніякі двома способами що неспроможні використовувати і той ж узел.

@Рис. 12. 28. Комунікаційна мережу після преобразования

======352

@Рис. 12. 29. Мережа розподілу работы

Розподіл работы

Предположим, що є групу працівників, кожен із яких має певними навичками. Припустимо також, що існує низка завдань, які прагнуть залучення співробітника, який володіє заданим набором навичок. Завдання розподілу роботи (work assignment) у тому, щоб розподілити роботу співробітників те щоб кожне завдання виконував співробітник, має відповідні навички. Щоб звести це завдання до вирахування максимального потоку, створимо мережу з двома стовпчиками вузлів. Кожен вузол у лівій стовпці представляє одного співробітника. Кожен вузол у правому стовпці представляє одне завдання. Потім порівняємо навички кожного працівника з навичками, необхідні виконання кожного із завдань. Створимо зв’язок між кожним співробітником і кожним завданням, що він здатний виконати, і дамо всім зв’язкам одиничну пропускну спроможність. Створимо узел-источник і з'єднаємо його з кожним із співробітників зв’язком одиничної пропускну здатність. Потім створимо узел-сток і з'єднаємо з нею кожне завдання, знову з допомогою зв’язку з одиничної пропускної здатністю. На рис. 12. 29 показано відповідна мережу для завдання розподілу роботи з чотирма працівниками та чотирма завданнями. Тепер знайдемо максимальний потік із джерела в стік. Кожна одиниця потоку повинна пройти через один вузол працівника можна й один вузол завдання. Цей потік представляє розподіл роботи з цього сотрудника.

@Рис. 12. 30. Програма Work

=======353

Если співробітники мають відповідними навичками до виконання всіх завдань, то обчислення максимального потоку розподілять їх усіх. Якщо неможливо виконати все завдання, то процесі обчислення максимального потоку робота буде розподілено те щоб було реалізоване максимально можливу кількість завдань. Програма Work використовує цей алгоритм задля розподілення роботи між співробітниками. Запровадьте прізвища співробітників та його навички в текстовому полі зліва, а завдання, які слід виконати і потрібні їм навички в текстовому полі посередині. Потому, як ви вже натиснете на кнопку Go (Почати), програма розподілить роботу співробітників, використовуючи для цього мережу максимального потоку. На рис. 12. 30 показано вікно програми з отриманим розподілом работы.

Резюме

Некоторые мережні алгоритми можна застосувати безпосередньо до сетеподобным об'єктах. Наприклад, можна використовувати алгоритм пошуку найкоротшого маршруту перебування найкращого шляху до вуличної мережі. Для визначення найменшої вартості побудови мережі зв’язку чи сполуки міст залізницями можна використовувати мінімальне остовное дерево. Багато інші мережні алгоритм знаходять менш очевидні застосування. Наприклад, можна використовувати алгоритми пошуку найкоротшого маршруту для розбивки на райони, складання плану робіт методом найкоротшого шляху, чи графіка колективної роботи. Алгоритми обчислення максимального потоку можна використовуватиме розподілу роботи. Ці менш очевидні застосування мережевих алгоритмів зазвичай є цікавішими і перспективными.

======354

Глава 13. Об'єктно-орієнтовані методы

Использование функцій і підпрограм дозволяє програмісту розбити код великий програми на частини. Масиви і певні користувачем типи даних дозволяють згрупувати елементи даних те щоб впросити роботи з ними. Класи, які вперше з’явилися торік у 4-й версії Visual Basic, дозволяють програмісту по-новому згрупувати дані і логіку роботи програми. Клас дозволяє поєднати у одному об'єкті дані та методи роботи із нею. Цей новий підхід до управління складністю програм дозволяє подивитись алгоритми під іншим кутом зору. У цьому главі розглядаються питання объектно-ориентированного програмування, які під час застосуванні класів Visual Basic. У ньому описані переваги объектно-ориентированного програмування (ОВП) і показано, яку вигоду можна отримати їх застосування програми на мові Visual Basic. Потім у главі розглядається набір корисних объектно- орієнтованих прикладів, що ви можете використовуватиме управління складністю ваших приложений.

Преимущества ООП

К традиційним переваг объектно-ориентированного програмування ставляться інкапсуляція чи приховання (encapsulation), поліморфізм (polymorphism) і повторне використання (reuse). Реалізація в класах Visual Basic трохи відрізняється від того, як вони реалізовані за іншими объектно-ориентированных мовами. У наступних розділах розглядаються ці переваги ОВП і те, як і ними скористатися програми на Visual Basic.

Инкапсуляция

Объект, певний з допомогою класу, укладає у собі дані, що він містить. Інші частини програми може використати об'єкт для оперування його даними, не знаючи, як зберігаються чи змінюються значення даних. Об'єкт надає відкриті (public) процедури, функції, і складні процедури зміни властивостей, що дозволяють програмі побічно маніпулювати чи переглядати дані. Бо за цьому такі є абстрактними з місця зору програми, це теж називається абстракцією даних (data abstraction). Інкапсуляція дозволяє програмі використовувати об'єкти як «чорних ящиків». Програма може використовувати відкриті методи об'єкта для перевірки і зміни значень без необхідності розумітися на тому, що відбувається всередині чорного ящика.

=========355

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

Обеспечение инкапсуляции

Для забезпечення инкапсуляции клас повинен запобігати безпосередній доступом до своїм даним. Якщо змінна у п’ятому класі оголошено як відкрита, то інші частини програми зможуть безпосередньо змінювати і зчитувати дані з її. Якщо пізніше уявлення даних зміниться, будь-які частини програми, які безпосередньо взаємодіють із даними, також має будуть змінитися. У цьому втрачається перевагу инкапсуляции. Щоб якось забезпечити доступом до даним, клас повинен використовувати процедури для роботи з властивостями. Наприклад, такі процедури дозволяють іншим частинам програми переглядати змінювати значення DegreesF об'єкта Temperature.

Private m_DegreesF As Single «Градуси Фаренгейта.

Public Property Get DegreesF () As Single

DegreesF = m_DegreesF End Property

Public Property Let DegreesF (new_DegreesF As Single) m_DegreesF = new_DegreesF End Property

Различия між тими процедурами і визначенням m_DegreesF як відкритої перемінної поки що невеликі. Проте, використання цих процедур дозволяє легко змінювати клас, у подальшому. Наприклад, припустимо, що ви вирішите вимірювати температуру в градусах Кельвіна, а чи не за Фаренгейтом. У цьому можна змінити клас, не чіпаючи інших частин програми, у яких використовуються процедури властивості DegreesF. Можна ще додати код для перевірки помилок, аби переконатися, що ваша програма не спробує передати об'єкту неприпустимі значения.

Private m_DegreesK As Single «Градуси Кельвина.

Public Property Get DegreesF () As Single

DegreesF = (m_DegreesK — 273. 15) * 1.8 End Property

Public Property Let DegreesF (ByVal new_DegreesF As Single) Dim new_value As Single

new_value = (new_DegreesF / 1. 8) + 273. 15

If new_value < 0 Then

" Повідомити про помилку — неприпустиме значении.

Error. Raise 380, «Temperature », _

" Температура мусить бути неотрицательной. «

Else m_DegreesK = new_value

End If End Property

======357

Программы, достойні цьому матеріалі, потворно порушують принцип инкапсуляции, використовуючи в класах відкриті перемінні. Не занадто хороший стиль програмування, але так зроблено за трьома причинами. По-перше, безпосереднє зміна значень даних виконується швидше, ніж виклик процедур властивостей. Більшість програм вже й так кілька втрачають в продуктивності через використання посилань на об'єкти замість застосування складнішого методу псевдоуказателей. Застосування процедур властивостей ще більше уповільнить їх роботу. По-друге, багато програм демонструють методи роботи з структурами даних. Наприклад, мережні алгоритми, достойні 12 главі, безпосередньо послуговуються даними об'єкта. Покажчики, пов’язуваних вузли у мережі друг з іншому, становлять невід'ємну частину алгоритмів. Було б безглуздо змінювати спосіб зберігання цих покажчиків. І, нарешті, завдяки використанню відкритих значень даних, код стає простіше. Це дозволяє вам сконцентруватися на алгоритми, і цього заважають зайві процедури роботи з свойствами.

Полиморфизм

Второе перевагу объектно-ориентированного програмування — це поліморфізм (polymorphism), що означає «має безліч форм». У Visual Basic це, що перший об'єкт може мати різний форми в залежно від ситуації. Наприклад, наступний код є підпрограму, яка може брати участь у ролі параметра будь-який об'єкт. Об'єкт obj то, можливо формою, елементом управління, чи об'єктом певного вами класса.

Private Sub ShowName (obj As Object)

MsgBox TypeName (obj) End Sub

Полиморфизм дозволяє створювати процедури, які можуть буквально з усіма типами об'єктів. Але за цю гнучкість доводиться платити. Якщо визначити узагальнений (generic) об'єкт, як цього прикладі, то Visual Basic зможе визначити, які типи дій зможе виконувати об'єкт, до запуску программы.

========357

Если Visual Basic наперед знає, з об'єктом якого типу він матиме справа, може виконати попередні дії у тому, щоб більш змогли ефективно використати об'єкт. Якщо використовується узагальнений (generic) об'єкт, то програма неспроможна виконати підготовки, і цього цього втратить в продуктивності. Програма Generic демонструє різницю у продуктивності між оголошенням об'єктів як які належать до певному типу чи як узагальнених об'єктів. Тест виконується однаково, крім те, що в одному з подібних випадків об'єкт визначається, як має тип Object, а чи не тип SpecificClass. У цьому установка значення даних об'єкта з допомогою узагальненого об'єкта виконується в 200 раз медленнее.

Private Sub TestSpecific () Const REPS = 1 000 000 «Виконати мільйон повторений.

Dim obj As SpecificClass Dim і As Long Dim start_time As Single Dim stop_time As Single

Set obj = New SpecificClass start_time = Timer

For і = 1 To REPS obj. Value = I

Next і stop_time = Timer

SpecificLabel. Caption = _

Format$(1000 * (stop_time — start_time) / REPS, «0. 0000 ») End Sub

Зарезервоване слово Implements

В 5-ї версії Visual Basic зарезервоване слово Implements (Реалізує) дозволяє програмі використовувати поліморфізм без використання узагальнених об'єктів. Наприклад, програма може інтерфейс Vehicle (Засіб пересування), Якщо класи Car (Автомобіль) і Truck (Вантажівка) обидва реалізують інтерфейс Vehicle, то програма може використовуватиме виконання функцій інтерфейсу Vehicle об'єкти кожного з двох класів. Створимо спочатку клас інтерфейсу, у якому визначимо відкриті перемінні, що їх буде підтримувати. У ньому також мають бути визначено прототипи відкритих процедур всім методів, що їх буде підтримувати. Наприклад, наступний код демонструє, як клас Vehicle може зміну Speed (Швидкість) і метод Drive (Вести машину):

Public Speed Long

Public Sub Drive ()

End Sub

=======358

Теперь створимо клас, який реалізує інтерфейс. Після оператора Option Explicit в секції Declares додається оператор Implements визначальний ім'я класу інтерфейсу. Цей клас повинен також визначати всі необхідні для роботи локальні перемінні. Клас Car реалізує інтерфейс Vehicle. Наступний код демонструє, як і ньому визначається інтерфейс Й затулений (private) змінна m_Speed:

Option Explicit

Implements Vehicle

Private m_Speed As Long

Когда до класу додається оператор Implements, Visual Basic зчитує інтерфейс, певний зазначеним класом, та був створює відповідні заглушки в коді класу. У цьому вся прикладі Visual Basic додасть нову секцію Vehicle в вихідний код класу Car, та визначить процедури let і get властивості Vehicle_Speed до подання перемінної Speed, певної в інтерфейсі Vehicle. Процедури let Visual Basic використовує зміну RHS, яка є скороченням від Right Hand Side (З правого боку), у якій задається нового значення перемінної. Також визначається процедура Vehicle_Drive. Щоб реалізувати функції цих процедур, потрібно написати код їм. Наступний код демонструє, як клас Car може визначати процедури Speed і Drive.

Private Property Let Vehicle_Speed (ByVal RHS As Long) m_Speed = RHS End Property

Private Property Get Vehicle_Speed () As Long

Vehicle_Speed = m_Speed End Property

Private Sub Get Vehicle_Drive ()

" Виконати якісь действия.

: End Property

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

Dim obj As Vehicle

Set obj = New Car obj. Speed = 55

Set obj = New Truck obj. Speed =45

==========359

Ссылка obj може вказувати або на об'єкт Car, або на об'єкт Truck. Так як і обох цих об'єктах реалізований інтерфейс Vehicle, то програма може оперувати властивістю obj. Speed незалежно від цього, вказує чи посилання obj на Car чи Truck. Оскільки посилання obj свідчить про об'єкт, який реалізує інтерфейс Vehicle, то Visual Basic знає, що це об'єкт має процедури, працюючі зі властивістю Speed. Це означає, що може виконувати виклики процедур властивості Speed ефективніше, ніж було у разі, якби obj була посиланням на узагальнений об'єкт. Програма Implem є доробленою версією програми описаної вище програми Generic. Вона порівнює швидкість установки значень з використанням узагальнених об'єктів, певних об'єктів та, які реалізують інтерфейс. У одному з тестів за комп’ютером з процесором Pentium з тактовою частотою 166 МГц, програмі знадобилося 0,0007 секунди для установки значень під час використання певного типу об'єкта. Для установки значень під час використання об'єкта, що реалізовуватиме інтерфейс, знадобилося 0,0028 секунди (вчетверо більше). Для установки значень при використанні узагальненого об'єкта знадобилося 0,0508 секунди (в 72 разу більше). Використання інтерфейсу не таким швидким, як використання посилання певний об'єкт, але значно швидше, ніж використання узагальнених объектов.

Наследование і повторне использование

Процедуры і функції підтримують повторне використання (reuse). Замість здобуття права постійно переписувати код наново, можна вмістили його в підпрограму, тоді замість блоку коду можна просто підставити виклик підпрограми. Аналогічно, визначення процедури у п’ятому класі робить її доступною в усій програмі. Програма може використати цю процедуру, використовуючи об'єкт, що є примірником класу. Серед програмістів, використовують объектно-ориентированный підхід, під повторним використанням зазвичай мається на увазі щось більше, саме успадкування (inheritance). У объектно-ориентированных мовами, як-от З++ чи Delphi, один клас часто породжує (derive) інший. У цьому другий клас успадковує (inherits) всю функціональність першого класу. Після цього можна додавати, змінювати чи прибирати будь-які функції з класу- спадкоємця. І це є формою використання коду, оскільки цьому програмісту непотрібно наново реалізувати функції батьківського класу, у тому, щоб використовувати в классе-наследнике. Хоча Visual Basic і підтримує успадкування безпосередньо, можна домогтися приблизно тієї ж результатів, використовуючи обмеження (containment) чи делегування (delegation). При делегуванні із одного класу містить примірник класу з іншого об'єкта, і далі передає частину власних обов’язків укладеним у ньому об'єкту. Наприклад, припустимо, що є клас Employee, що робить дані про співробітників, такі як прізвище, ідентифікаційний номер у системі соціального страхування і зарплата. Припустимо, що мені тепер потрібен клас Manager, що робить той самий, як і клас Employee, однак має ще одне властивість secretary (секретар). Для використання делегування, клас Manager має включати у собі закритий об'єкт типу Employee безпосередньо з ім'ям m_Employee. Замість прямого обчислення значень, процедури роботи з властивостями прізвища, номери соціального страхування і середньої зарплати передають відповідні виклики об'єкту m_Employee. Наступний код демонструє, як клас Manager може оперувати процедурами властивості name (фамилия):

==========360

Private m_Employee As New Employee

Property Get Name () As String

Name = m_Employee. Name End Property

Property Let Name (New_Name As String) m_Employee. Name = New_Name End Property

Класс Manager він може змінювати результат, возвращаемый делегованої функцією, чи видавати результат сама. Наприклад, наступного коді показано, як клас Employee повертає рядок тексту з цими про сотруднике.

Public Function TextValues () As String Dim txt As String

txt = m_Name & vbCrLf txt = txt & «» & m_SSN & vbCrLf txt = txt & «» & Format$(m_Salary, «Currency ») & vbCrLf

TextValues = txt End Function

Класс Manager використовує функцію TextValues об'єкта Employee, але додає перед поверненням інформацію про секретаря в рядок результата.

Public Function TextValues () As String Dim txt As String txt = m_Employee. TextValues txt = txt & «» & m_Secretary & vbCrLf

TextValues = txt End Function

Программа Inherit демонструє класи Employee і Manager. Інтерфейс програми технічно нескладне інтересу, та її код включає прості визначення класів Employee і Manager.

Парадигмы ООП

В першому розділі ми дали визначення алгоритму як «послідовності інструкцій до виконання будь-якого завдання». Безсумнівно, клас може використовувати алгоритми у процедурах і функціях. Наприклад, можна використовувати клас для упаковки до нього алгоритму. Деякі із програм, добре описані у попередніх розділах, використовують класи для инкапсуляции складних алгоритмов.

=========361

Классы також використовувати новий стиль програмування, при якому кілька об'єктів можуть спільно працювати до виконання завдання. І тут то, можливо безглуздим завдання послідовності інструкцій до виконання завдання. Більше адекватним то, можливо завдання моделей поведінки об'єктів, ніж зведення завдання до послідовності кроків. Щоб відрізняти таку поведінку від традиційних алгоритмів, ми назвемо їх «парадигмами». Наступні розділу описують деякі корисні об'єктно-орієнтовані парадигми. Чимало їх ми ведуть початок з деяких інших объектно-ориентированных мов, як-от З++ чи Smalltalk, хоча можуть також використовуватися в Visual Basic.

Управляющие объекты

Управляющие об'єкти (command) також називаються об'єктами дії (action objects), функцій (function objects) чи функторами (functors). Керуючий об'єкт представляє якесь дію. Програма може використовувати метод Execute (Виконати) до виконання об'єктом цього дії. Програмі не треба знати нічого звідси дії, вона знає лише, що об'єкт має метод Execute. Управляючі об'єкти може мати безліч цікавих застосувань. Програма може використовувати управляючий об'єкт для реалізації:. Настраиваемых елементів інтерфейсу;. Макрокоманд;. Ведення і відновлення записів;. Функцій «скасування» і «повтор». Щоб створити налагоджуваний інтерфейс, форма може містити управляючий масив кнопок. Під час виконання програми форма може завантажити написи на кнопках і створити відповідну набір управляючих об'єктів. Коли користувач натискає на кнопку, оброблювачу подій кнопки потрібно лише лише викликати метод Execute відповідного управляючого об'єкта. Деталі того що відбувається перебувають всередині класу управляючого об'єкта, а чи не в обработчике подій. Програма Command1 використовує управляючі об'єкти до створення настраиваемого інтерфейсу для кількох які пов’язані між собою функцій. При натисканні на кнопку програма викликає метод Execute відповідного управляючого об'єкта. Програма може використовувати управляючі об'єкти до створення певних користувачем макрокоманд. Користувач задає послідовність дій, які програма запам’ятовує в колекції як управляючих об'єктів. Коли потім користувач викликає макрокоманду, програма викликає методи Execute об'єктів, що у колекції. Управляючі об'єкти можуть забезпечувати ведення та своєчасне відновлення записів. Керуючий об'єкт в кожному своєму виклик записувати інформацію про собі у лог-файл. Якщо програма аварійно завершить роботи, вони можуть потім використовувати записану інформацію на відновлення управляючих об'єктів і виконання їхніх для повторення послідовності команд, яка виконувалася до збою програми. І, нарешті, програма може використовувати набір управляючих об'єктів для реалізації функцій скасування (undo) і повтору (redo). =========362

Программа використовує зміну LastCmd для відстежування останнього управляючого об'єкта в колекції. Якщо ви хоч вибираєте команду Undo (Скасувати) в меню Draw (Малювати), то програма зменшує значення перемінної LastCmd на одиницю. Коли програма потім виводить малюнок, вона викликає лише об'єкти, які стоять до об'єкта з номером LastCmd. Якщо вже ви вибираєте команду Redo (Повторити) в меню Draw, то програма збільшує значення перемінної LastCmd на одиницю. Коли програма виводить малюнок, вона виводить однією об'єкт більше, ніж раніше, тому відображається відновлений малюнок. При додаванні нової постаті програма видаляє будь-які команди із колекції, що лежать після позиції LastCmd,. потім додає нову команду малювання наприкінці і забороняє команду Redo, бо немає команд, які можна було б скасувати. На рис. 13.1 показано вікно програми Command2 після додавання нової фигуры.

Контролирующий объект

Контролирующий об'єкт (visitor object) перевіряє все елементи в складеному об'єкті (aggregate object). Процедура, реалізована в складеному класі, обходить всі об'єкти, передаючи кожен із новачків контролюючому об'єкту в ролі параметра. Наприклад, припустимо, що складовою об'єкт зберігає елементи в зв’язковому списку. Наступний код показує, як він метод Visit обходить список, передаючи кожен об'єкт як параметра методу Visit контролюючого об'єкта ListVisitor:

Public Sub Visit (obj As ListVisitor) Dim cell As ListCell

Set cell = TopCell

Do While Not (cell Is Nothing) obj. Visit cell

Set cell = cell. NextCell

Loop End Sub

@Рис. 13.1. Програма Command2

=========363

Следующий код демонструє, як клас ListVisitor може виводити на екран значення елементів з вікна Immediate (Срочно).

Public Sub Visit (cell As ListCell)

Debug. Print cell. Value End Sub

Используя парадигму контролюючого об'єкта, складовою класу виявляє порядок, у якому обходяться елементи. Складовою клас може визначати кілька методів для обходу містять його елементів. Наприклад, клас дерева може забезпечувати методи VisitPreorder (Прямий обхід), VisitPostorder (Зворотний обхід), VisitInorder (Симетричний обхід) і VisitBreadthFirst (Обхід завглибшки) для обходу елементів у різному порядке.

Итератор

Итератор забезпечує інший метод обходу елементів в складеному об'єкті. Объект-итератор звертається до складеному об'єкту для обходу його елементів, і у разі итератор визначає порядок, у якому перевіряються елементи. З складовим класом може бути порівняно кілька класів итераторов у тому, щоб виконувати різні обходи елементів складеного класу. Щоб виконувати обхід елементів, итератор має бути порядок, в якому елементи записані, щоб визначити порядок їх обходу. Якщо складовою клас є зв’язний список, то объект-итератор повинен знати, що елементи перебувають у зв’язковому списку, і повинен уміти переміщатися за списком. Оскільки итератору відомі деталі внутрішнього устрою списку, це порушує приховання даних складеного об'єкта. Замість кожен клас, яка хоче перевіряти елементи складеного класу, реалізував обхід самостійно, можна зіставити складеному класу клас итератора. Клас итератора мусить мати прості процедури MoveFirst (Переміститися на початок), MoveNext (Переміститися на наступний елемент), EndOfList (Переміститися насамкінець списку) і CurrentItem (Поточний елемент) задля забезпечення непрямого доступу до списку. Нові класи можуть містити примірник класу итератора і використати його методи для обходу елементів складеного класу. На рис. 13.2 схематично показано, як об'єкт використовує объект-итератор для зв’язку з списком. Програма IterTree, описана нижче, використовує итераторы для обходу повного двоичного дерева. Клас Traverser (Обхідник) містить посилання об'єкт- итератор. Вони використовує забезпечувані итератором процедури MoveFirst, MoveNext, CurrentCaption і EndOfTree щоб одержати списку вузлів в дереве.

@Рис. 13.2. Використання итератора для непрямої зв’язку з списком

=========364

Итераторы порушують приховання відповідних складових об'єктів, на відміну від нових класів, які містять итераторы. А, щоб позбутися потенційної плутанини, так можна трактувати итератор як надбудову над складовим об'єктом. Контролюючі об'єкти і итераторы забезпечують виконання схожих функцій, використовуючи різні підходи. Оскільки парадигма контролюючого об'єкта залишає деталі складеного об'єкта прихованими усередині нього, вона забезпечує кращу инкапсуляцию. Итераторы стануть у пригоді, якщо порядок обходу може часто змінюватися чи що вона повинен перевизначатися під час виконання програми. Наприклад, складовою об'єкт може використовувати методи що породжує класу (який описаний пізніше) до створення объекта-итератора у виконання програми. У Якому итератор клас ні знати, як створюється итератор, він лише використовує методи итератора для доступу до елементам складеного объекта.

Дружественный класс

Многие класи тісно працюють із іншими. Наприклад, клас итератора тісно взаємодіє зі складовим класом. На виконання роботи, итератор повинен порушувати приховання складеного класу. У цьому, хоча ці, пов’язані класи іноді повинні порушувати приховання даних одне одного, інші класи повинні не мати такої можливості. Дружній клас (friend class) — це клас, має спеціальне дозвіл порушувати приховання даних іншому класу. Наприклад, клас итератора є дружнім класом для відповідного складеного класу. Йому, на відміну інших класів, дозволено порушувати приховання даних для складеного класу. У 5-ї версії Visual Basic з’явилося зарезервоване слово Friend для дозволу обмежений доступ до змінним і процедурам, певним всередині модуля. Елементи, певні з допомогою зарезервованого слова Friend, доступні всередині проекту, але не інші проекти. Наприклад, припустимо, що створили класи LinkedList (Зв'язний список) і ListIterator (Итератор списку) у проекті ActiveX серверу. Програма може створити сервер зв’язкового списку керувати зв’язковими списками. Який Породжує метод класу LinkedList може створювати об'єкти типу ListIterator від використання у програмі. Клас LinkedList може забезпечувати у програмі кошти на роботи з зв’язковими списками. Цей клас оголошує свої властивості й фізичні методи відкритими, щоб їх можна було залучити до основну програму. Клас ListIterator дозволяє програмі виконувати ітерації над об'єктами, якими управляє клас LinkeList. Процедури, використовувані класом ListIterator для оперування об'єктами LinkedList, з’являються як дружні в модулі LinkedList. Якщо класи LinkedList і ListIterator створюють у тому ж проекті, то клас ListIterator може використовувати ці дружні процедури. Оскільки основна програма перебуває у іншому проекті, вона цього неспроможна. Цей досить ефективний, але досить громіздкий метод. Вона потребує створення проектів, та настанови одного серверу ActiveX. Він також працює в попередніх версіях Visual Basic. Найпростіший альтернативою було б угоду у тому, що тільки дружні класи можуть порушувати приховання даних одне одного. Якщо всі розробники дотримуватимуться цього правила, то проектом досі можна керуватиме. Проте, спокуса звернутися безпосередньо до даних класу LinkedList то, можливо сильним, і завжди існує можливість, що хтось порушить приховання даних через ліні чи з необережності. Інша можливість у тому, щоб дружній об'єкт передавав себе іншому класу як параметра. Передаючи себе як параметра, дружній клас цим показує, що якого є таким. Програма Fstacks використовує його реалізації стеков.

=======365

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

Интерфейс

В цієї парадигмі із об'єктів виступає як інтерфейсу (interface) між двома іншими. Один об'єкт може використовувати властивості і методи першого об'єкта для стосунків з другим. Інтерфейс іноді також називається адаптером (adapter), упаковщиком (wrapper), чи мостом (bridge). На рис. 13.3 схематично зображено робота інтерфейсу. Інтерфейс дозволяє двох інших об'єктах з його кінцях змінюватися незалежно. Наприклад, якщо властивості об'єкта зліва на рис. 13.3 зміняться, інтерфейс необхідно змінити, а об'єкт справа — немає. У цьому парадигмі процедури, використовувані двома об'єктами, підтримуються розробниками, які визначають ці об'єкти. Розробник, який реалізує лівий об'єкт, також реалізовує процедур інтерфейсу, які взаємодіють із лівим объектом.

Фасад

Фасад (Facade) аналогічний інтерфейсу, але забезпечує простий інтерфейс для складного об'єкта чи групи об'єктів. Фасад також інколи називається упаковщиком (wrapper). На рис. 13.4. показано схема роботи фасаду. Різниця між фасадом і інтерфейсом переважно умоглядна. Основна завдання інтерфейсу — забезпечення непрямого взаємодії між об'єктами, що вони могли розвиватися незалежно. Основне завдання фасаду — полегшення використання якихось складних речей з допомогою приховання деталей.

Порождающий объект

Порождающий об'єкт (Factory) — це об'єкт, що створює інші об'єкти. Який Породжує метод — це процедура чи функція, що створює об'єкт. Які Породжують об'єкти найбільш корисні, якщо два класу повинні тісно працювати разом. Наприклад, складовою клас може містити який породжує метод, що створює итераторы йому. Який Породжує метод може форматувати итератор в такий спосіб, щоб було готовий до роботи з примірником класу, який його создал.

@Рис. 13.3 Интерфейс

========366

@Рис. 13.4. Фасад

Программа IterTree створює повне двоичное дерево, записаний у масиві. Після натискання одну з кнопок, котрі задають напрям обходу, програма створює об'єкт Traverser (Обхідник). Вона також використовує одне із що породжують методів дерева до створення відповідного итератора. Об'єкт Traverser використовує итератор для обходу дерева та виведення списку вузлів в правильному порядку. На рис. 13.5 наведено вікно програми IterTree, що показує зворотний обхід дерева.

Единственный объект

Единственный об'єкт (singleton object) — це об'єкт, що існує в додатку в єдиному примірнику. Наприклад, в Visual Basic визначено клас Printer (Принтер). Він також єдиний об'єкт з тим самим назвою. Цей об'єкт представляє принтер, обраний у системі по вмовчанням. Позаяк у кожен час може бути обраний лише одне принтер, то можна буде визначити об'єкт Printer як об'єкт. Одне з способів створення єдиного об'єкта залежить від використанні процедури, працює зі властивостями в модулі BAS. Цю процедуру повертає посилання об'єкт, певний всередині модуля як закритий. Для інших частин програми цю процедуру виглядає як просто іще одна объект.

@Рис. 13.5. Програма IterTree, демонструючи зворотний обход

=======367 Програма WinList використовує цей підхід до створення єдиного об'єкта класу WinListerClass. Об'єкт класу WinListerClass представляє вікна в системі. Оскільки операційна система одна, то потрібен об'єкта класу WinListerClass. Модуль WinList. BAS використовує наступний код для створення єдиного об'єкта під назвою WindowLister.

Private m_WindowLister As New WindowListerClass

Property Get WindowLister () As WindowListerClass

Set WindowLister = m_WindowLister End Property

Единственный об'єкт WindowLister доступний в усьому проекті. Наступний код демонструє, як основна програма використовує властивість WindowList цього об'єкта висновку на екран списку окон.

WindowListText. Text = WindowLister. WindowList

Перетворення в послідовну форму

Многие докладання зберігають об'єкти і відновлюють їхніх пізніше. Наприклад, додаток може зберігати копію своїх об'єктів в текстовому файлі. При наступному запуску програми, вона зчитує це файл і завантажує об'єкти. Об'єкт може містити процедури, які зчитують і записують їх у файл. Загальний підхід може полягати у тому, щоб зробити процедури, що зберігають і відновлюють дані об'єкта, використовуючи рядок. Оскільки запис даних об'єкта лише у рядку перетворює об'єкт в послідовність символів, той процес іноді називається перетворенням в послідовну форму (serialization). Перетворення об'єкта в рядок забезпечує більшої гнучкості основний програми. У цьому вони можуть удається зберігати й зчитувати об'єкти, використовуючи текстові файли, базі даних чи область пам’яті. Вона може переслати представлений в такий спосіб об'єкт через мережу або його доступним на Web-сторінці. Програма чи елемент ActiveX іншому кінці може використовувати перетворення об'єкта в рядок для відтворення об'єкта. Програма він може додатково обробити рядок, наприклад, зашифрувати її після перетворення об'єкта в рядок і розшифрувати перед зворотним перетворенням. Одне з підходів перетворення об'єкта в послідовну форму у тому, щоб об'єкт записав всі свої дані в рядок заданого формату. Наприклад, припустимо, що клас Rectangle (Прямокутник) має властивості X1, Y1, X2 і Y2. Наступний код демонструє, як клас може визначати процедури властивості Serialization:

Property Get Serialization () As String

Serialization = _

Format$(X1) & «; «& Format$(Y1) & «; «& _

Format$(X2) & «; «& Format$(Y2) & «; «End Property

Property Let Serialization (txt As String) Dim pos1 As Integer Dim pos2 As Integer

pos1 = InStr (txt, «; «) X1 = CSng (Left$(txt, pos1 — 1)) pos2 = InStr (pos1 + 1, txt, «; «) Y1 = CSng (Mid$(txt, pos1 + 1, pos2 — pos1 — 1)) pos1 = InStr (pos2 + 1, txt, «; «) X2 = CSng (Mid$(txt, pos2 + 1, pos1 — pos2 — 1)) pos2 = InStr (pos1 + 1, txt, «; «) Y2 = CSng (Mid$(txt, pos1 + 1, pos2 — pos1 — 1)) End Property

Этот метод досить простий, але дуже гнучкий. З розвитком програми, зміни у структурі об'єктів можуть змусити вас перетранслировать все збережені раніше перетворені на послідовну форму об'єкти. Якщо вони самі перебувають у файлах чи базах даних, для завантаження старих даних, і запис їхніх з нового форматі може знадобитися написання программ-конверторов. Більше гнучкий підхід у тому, щоб зберігати разом із значеннями елементів даних об'єкта їх назви. Коли об'єкт зчитує дані, перетворені на послідовну форму, він використовує імена елементів для визначення значень, що необхідно встановити. Якщо пізніше у визначення елемента додасться будь-які елементи, чи віддалені з нього, то ми не доведеться перетворювати старі дані. Якщо новий об'єкт завантажить старі дані, він просто проігнорує не підтримувані більш значення. Визначаючи значення даних із вмовчанням, інколи можна зменшити розмір перетворених на послідовну форму об'єктів. Процедура get властивості Serialization зберігає лише значення, які від значень по вмовчанням. Перш ніж, як процедура let властивості почне виконання перетворення на послідовну форму, вона инициализирует все елементи об'єкта значеннями за умовчанням. Значення, нерівні значенням по вмовчанням, оновлюються принаймні обробки даних процедурою. Програма Shapes використовує цей підхід задля збереження і завантаження з диска малюнків, містять еліпси, лінії, і прямокутники. Об'єкт ShapePicture представляє весь малюнок повністю. Він має колекцію управляючих об'єктів, що репрезентують різні постаті. Наступний код демонструє процедури властивості Serialization об'єкта ShapePicture. Об'єкт ShapePicture зберігає ім'я типу кожного з типів об'єктів, потім у дужках — уявлення об'єкта у послідовній форме.

Property Get Serialization () As String Dim txt As String Dim і As Integer

For і = 1 To LastCmd txt = txt & _

TypeName (CmdObjects (i)) & «(«& _

CmdObjects (i). Serialization & «) «

Next I

Serialization = txt End Property

==========369

Процедура let властивості Serialization використовує підпрограму GetSerialization для читання імені об'єкту і списку даних в дужках. Наприклад, якщо об'єкт ShapePicture містить команду малювання прямокутника, його подання до послідовної формі буде включати рядок «RectangleCMD», яку йтимуть дані, представлені у послідовної формі. Процедура використовує підпрограму CommandFactory до створення об'єкта відповідного типу, та був змушує новий об'єкт перетворити себе з послідовної форми представления.

Property Let Serialization (txt As String) Dim pos As Integer Dim token_name As String Dim token_value As String Dim and As Object «Start a new picture. NewPicture «Read values until there are no more. GetSerialization txt, pos, token_name, token_value Do While token_name «» «Make the object and make it unserialize itself. Set and = ConiniandFactory (token_name) If Not (and Is Nothing) Then _ and. Serialization = token_value GetSerialization txt, pos, token_name, tokerL-value Loop LastCmd = CmdObjects. Count End Property

Парадигма Модель/Вид/Контроллер.

Парадигма Модель/Вид/Контроллер (МВК) (Model/View/Controller) дозволяє програмі управляти складними співвідношеннями між об'єктами, які зберігають дані, об'єктами, які відбивають їх у екрані, і об'єктами, які оперують даними. Наприклад, додаток роботи з фінансами може виводити даних про витратах як таблиці, секторної діаграми, чи графіка. Якщо користувач змінює значення в таблиці, додаток має автоматично оновити зображення на екрані. Може також знадобитися записати змінені дані на диск. Для складних систем управління взаємодією між об'єктами, які зберігають, відбивають і оперують даними, може бути досить заплутаним. Парадигма Модель/Вид/Контроллер розбиває взаємодії, тож решту можна працювати із нею окремо, у своїй використовуються три типу об'єктів: моделі, види, і контроллеры.

Модели

Модель (Model) представляє дані, забезпечуючи методи, які інші об'єкти може використати для перевірки та даних. У додатку роботи із саудівським фінансовим даними, модель містить даних про витратах. Вона забезпечує процедури для перегляду та значень витрат і нових значень. Вона він може забезпечувати функції для обчислення сумарних величин, як-от повні витрати, витрати на підрозділам, середні витрати протягом місяця, тощо Модель включає у собі набір видів, які відбивають дані. При зміні даних, модель повідомляє звідси видам, які змінюють зображення на екрані відповідним образом.

Виды

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

Контроллеры

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

Виды/Контроллеры

Многие об'єкти одночасно відбивають і змінюють дані. Наприклад, текстове полі дозволяє користувачеві вводити і переглядати дані. Форма, що містить текстове полі, є це й виглядом, і контролером. Перемикачі, поля вибору опцій, смуги прокручування, і з інші елементи користувальницького інтерфейсу дозволяють одночасно переглядати і оперувати даними. Видами/контроллерами найпростіше управляти, якщо спробувати максимально розділити функції перегляду та управління. Коли об'єкт змінює дані, він ні сам оновлювати зображення на екрані. Він може зробити це пізніше, коли модель повідомить їй як виду про який нещодавно трапився зміні. Ці методи досить громіздкі для реалізації стандартних об'єктів користувальницького інтерфейсу, як-от текстові поля. Коли користувач вводить значення в текстовому полі, воно негайно оновлюється, і виконуватися його оброблювач події Change. Цей оброблювач подій може модель про зміні. Модель потім повідомляє виду/контроллеру (оратора тепер як вид) про який нещодавно трапився зміні. Якщо за цьому об'єкт обновить текстове полі, відбудеться ще одна подія Change, про яку знову повідомлено моделі і яскрава програма ввійде у нескінченний цикл. Щоб запобігти цієї проблеми, методи, які змінюють дані в моделі, повинен мати необов’язковий параметр, який би на контролер, який викликав ці зміни. Якщо виду/контроллеру потрібно повідомити про зміні, що він викликає, він має передати значення Nothing процедурі, вносящей зміни. Якщо це непотрібен, то ролі параметра об'єкт повинен передавати себя.

=========371

@Рис. 13.6. Програма ExpMVC

Программа ExpMVC, показана на рис. 13. 6, використовує парадигму Модель/Вид/Контроллер висновку даних про витрати. На малюнку показані три виду різних типів. Вид/контроллер TableView відображає дані в таблиці, у своїй можна змінювати назви статей витрат та його значення відповідних полях. Вид/контроллер GraphView відображає дані з допомогою гистограммы, у своїй можна змінювати значення витрат, рухаючи стовпчики з допомогою миші вправо. Вигляд PieView відображає секторную діаграму. Це просто-таки вид, тому його не можна використовуватиме зміни данных.

Резюме

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

==============372

Требования до апаратному обеспечению

Для запуску та прикладів додатків вам знадобиться комп’ютер, який відповідає вимогам Visual Basic до апаратному забезпечення. Алгоритм виконуються із швидкістю за комп’ютерами різних конфігурацій. Комп’ютер з процесором Pentium Pro і 64 Мбайт пам’яті буде швидше комп’ютера з 386 процесором і 4 Мбайт пам’яті. Ви швидко дізнаєтеся обмеження вашого оборудования.

Выполнение програм примеров

Один із найбільш корисних способів виконання програм прикладів — запускати їх з допомогою вбудованих коштів налагодження Visual Basic. Використовуючи точки зупинки, перегляд значень змінних та інші властивості отладчика, ви можете спостерігати алгоритми діє. Це можливо, саме корисно для розуміння найскладніших алгоритмів, як-от алгоритми роботи з збалансованими деревами, і мережні алгоритми, представлені у 7 та дванадцяти розділах відповідно. Дехто навчається й програм прикладів створюють файли даних чи тимчасові файли. Ці програми поміщають такі файли на відповідні директорії. Наприклад, що з програм сортування, представлені у 9 главі, створюють файли даних в директорії SrcCh9/. Всі ці файли мають розширення «. DAT», тому ви можете знайти й видалити в разі потреби. Програми прикладів призначені для демонстраційних цілей, щоб допомогти вам зрозуміти певні концепції алгоритмів, і над ними не майже реалізована обробка помилок чи перевірка даних. Якщо ви хоч введете неправильне рішення, програма може аварійно завершити роботу. Якщо вже ви не знаєте, які дані припустимі, скористайтеся щоб одержати інструкцій меню Help (Допомога) программы.

========374

A

addressing indirect, 49 open, 314 adjacency matrix, 86 aggregate object, 382 ancestor, 139 array irregular, 89 sparse, 92 triangular, 86 augmenting path, 363

B

B+Tree, 12 balanced profit, 222 base case, 101 best case, 27 binary hunt and search, 294 binary search, 286 branch, 139 branch-and-bound technique, 204 bubblesort, 254 bucketsort, 275

C

cells, 47 child, 139 circular referencing problem, 58 collision resolution policy, 299 command, 380 complexity theory, 17 controller, 391 countingsort, 273 critical path, 359 cycle, 331

D

data abstraction, 372 decision tree, 203 delegation, 378 descendant, 139

E

edge, 331 encapsulation, 371 exhaustive search, 204, 282 expected case, 27

F

facade, 386 factorial, 100 factory, 386 fake pointer, 32, 65 fat node, 12, 140 Fibonacci numbers, 105 firehouse problem, 239 First-In-First-Out, 72 forward star, 12, 90, 143 friend class, 384 functors, 380

G

game tree, 204 garbage collection, 43 garbage value, 43 generic, 374 graph, 138, 331 greatest common divisor, 103 greedy algorithms, 339

H

Hamiltonian path, 237 hashing, 298 heap, 266 heapsort, 265 heuristic, 204 Hilbert curves, 108 hill-climbing, 219

I

implements, 375 incremental improvements, 225 inheritance, 378 insertionsort, 251 interface, 385 interpolation search, 288 interpolative hunt and search, 295

K

knapsack problem, 212

L

label correcting, 342 label setting, 342 Last-In-First-Out list, 69 least-cost, 220 linear probing, 314 link, 331 list circular, 56 doubly linked, 58 linked, 36 threaded, 61 unordered, 36, 43

M

mergesort, 263 minimal spanning tree, 338 minimax, 206 model, 391 Model/View/Controller, 390 Monte Carlo search, 223

N

network, 331 capacitated, 361 capacity, 361 connected, 332 directed, 331 flow, 361 residual, 362 node, 139, 331 degree, 139 internal, 139 sibling, 139

O

octtree, 172 optimum global, 230 local, 230

P

page file, 30 parent, 139 partition problem, 236 path, 331 pointers, 32 point-to-point shortest path, 352 polymorphism, 371, 374 primary clustering, 317 priority queue, 268 probe sequence, 300 pruning, 212 pseudo-random probing)., 324

Q

quadratic probing, 322 quadtree, 138, 165 queue, 72 circular, 75 multi-headed, 83 priority, 80 quicksort, 258

R

random search, 223 recursion direct, 99 indirect, 25, 99 multiple, 24 tail recursion, 121 recursive procedure, 23 redundancy, 368 reference counter, 33 rehashing, 327 relatively prime, 103 residual capacity, 362 reuse, 371, 378

S

satisfiability problem, 235 secondary clustering, 324 selectionsort, 248 sentinel, 52 serialization, 388 shortest path, 342 Sierpinski curves, 112 simulated annealing, 231 singleton object, 387 sink, 361 source, 361 spanning tree, 336 stack, 69 subtree, 139

T

tail recursion removal, 121 thrashing, 31 thread, 61 traveling salesman problem, 238 traversal breadth-first, 149 depth-first, 149 inorder, 148 postorder, 148 preorder, 148 tree, 138 AVL tree, 174 B+tree, 192 binary, 140 bottom-up B-trees, 192 B-tree, 187 complete, 147 depth, 140 left rotation, 177 left-right rotation, 178 right rotation, 176 right-left rotation, 178 symmetrically threaded, 160 ternary, 140 threaded, 138 top-down B-tree, 192 traversing, 148 tries, 138 turn penalties, 354

U

unsorting, 250

V

view, 391 virtual memory, 30 visitor object, 382

W

work assignment, 369 worst case, 27

А

Абстракция даних, 372 Адресація непряма, 49 відкрита, 314 Алгоритм поглинаючий, 339

Г

Гамильтонов шлях, 237 Граф, 138, 331

Д

Делегирование, 378 Дерева, 138 АВЛ-деревья, 174 Б+деревья, 12, 192, 193 Б-деревья, 187 гілка, 139 внутрішній вузол, 139 восьмеричні, 172 обертання, 176 двоичные, 140 дочірній вузол, 139 гри, 204 квадродеревья, 165 корінь, 139 лист, 139 спадні Б-деревья, 192 зворотний обхід, 148 обхід, 148 обхід завглибшки, 149 обхід завширшки, 149 поддерево, 139 повні, 147 порядок, 139 нащадок, 139 предок, 139 уявлення нумерацією зв’язків, 12, 143 прямий обхід, 148 рішень, 203 батько, 139 з повними вузлами, 12 з симетричними посиланнями, 160 симетричний обхід, 148 троичные, 140 вузол, 139 впорядковані, 153 Дружній клас, 384

З

Задача комівояжера, 238 про здійсненності, 235 про пожежних депо, 239 про розбивці, 236 пошуку Гамильтонова шляху, 237 розподілу роботи, 369 формування портфеля, 212 Значення «сміттєве », 43

И

Инкапсуляция, 372

К

Ключи об'єднання, 244 стиснення, 244 Колекція, 37 Найкоротший маршрут двоточковим, 352 дерево найкоротшого маршруту, 341 всім пар, 352, 353 корекція міток, 342, 348 зі штрафами за повороти, 352, 354 установка міток, 342, 344 Криві Гільберта, 108 Серпинского, 112

М

Массив нерегулярний, 89 подання до вигляді прямий зірки, 90 розріджене, 92 трикутний, 86 Матриця суміжності, 86 Метод гілок і національних кордонів, 204, 212 сходження на пагорб, 219 минимаксный, 206 Монте-Карло, 223 найменшої вартості, 220 отжига, 231 повного перебору, 204 послідовних наближень, 225 збалансованої прибутку, 222 випадкового пошуку, 223 евристичний, 204 Модель/Вид/Контроллер, 390

Н

Наибольший загальний дільник, 103 Успадкування, 378

О

Объект вид, 391 єдиний, 387 інтерфейс, 385 итератор, 383 контролюючий, 382 контролер, 391 модель, 391 який породжує, 386 перетворення на послідовну форму, 388 складовою, 382 управляючий, 380 фасад, 386 Обмеження, 378 Оптимум глобальний, 230 локальний, 230 Черга, 72 многопоточная, 83 пріоритетна, 80, 268 циклічна, 75

П

Память віртуальна, 30 пробуксовування, 31 чистка, 43 Піраміда, 265 Повторне використання, 378 Пошук двоїчний, 286 интерполяционный, 288 методом повного перебору, 282 стежить, 294 Поліморфізм, 374 Потоки, 61 Проблема циклічних посилань, 58 Процедура очищення пам’яті, 45 рекурсивна, 23 Псевдоуказатели, 32, 65

Р

Разрешение конфліктів, 299 Рекурсія висхідна, 175 непряма, 25, 99 багатократний, 24 пряма, 99 умова зупинки, 101 хвостова, 121

С

Сеть, 331 надмірність, 368 джерело, 361 найкоротший маршрут, 341 критичний шлях, 359 навантажена, 361 найменше остовное дерево, 338 орієнтована, 331 залишкова, 362 залишкова пропускну здатність, 362 остовное дерево, 336 потік, 361 пропускну здатність, 361 простіший шлях, 332 шлях, 331 яким розширено шлях, 363 ребро, 331 зв’язкова, 332 зв’язок, 331 стік, 361 вузол, 331 ціна зв’язку, 331 цикл, 331 Сигнальна мітка, 52 Системний стік, 26 Випадок найкращий, 27 найгірший, 27 очікуваний, 27 Сортування блокова, 275 швидка, 258 вставкою, 251 вибором, 248 пірамідальна, 265 підрахунком, 273 пузырьковая, 254 рандомизация, 250 злиттям, 263 Список двусвязный, 58 многопоточный, 61 неупорядоченный, 36, 43 перший вошел-первый вийшов, 72 перший вошел-последний вийшов, 69 зв’язний, 36 циклічний, 56 Стік, 69 Дивний аттрактор, 170 Лічильник посилань, 33

Т

Теория складності алгоритмів, 17 хаосу, 170 Тестова послідовність вторинна кластеризація, 324 квадратична перевірка, 321 лінійна перевірка, 314 первинна кластеризація, 317 псевдослучайная перевірка, 324

У

Указатели, 32, 36

Ф

Файл підкачування, 30 Факторіал, 100

Х

Хеширование, 298 блоки, 303 відкрита адресація, 314 дозвіл конфліктів, 299 рехеширование, 327 зв’язування, 300 тестова послідовність, 300 хеш-таблица, 298

Ч

Числа взаємно прості, 103 Фібоначчі, 105

Я

Ячейка, 47

ПоказатьСвернуть
Заполнить форму текущей работой