Реферат: VB, MS Access, VC++, Delphi, Builder C++ принципы(технология), алгоритмы программирования
Введение.................................................................................................. 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 можно разрабатывать и тестировать сложные приложения без прямого использования функций API. Избавляя программиста от проблем с API, Visual Basic позволяет сконцентрироваться на деталях приложения.
Хотя Visual Basic и облегчает разработку пользовательского интерфейса, задача написания кода для реакции на входные воздействия, обработки их, и представления результатов ложится на плечи программиста. Здесь начинается применение алгоритмов.
Алгоритмы представляют собой формальные инструкции для выполнения сложных задач на компьютере. Например, алгоритм сортировки может определять, как найти конкретную запись в базе из 10 миллионов записей. В зависимости от класса используемых алгоритмов искомые данные могут быть найдены за секунды, часы или вообще не найдены.
В этом материале обсуждаются алгоритмы на Visual Basic и содержится большое число мощных алгоритмов, полностью написанных на этом языке. В ней также анализируются методы обращения со структурами данных, такими, как списки, стеки, очереди и деревья, и алгоритмы для выполнения типичных задач, таких как сортировка, поиск и хэширование.
Для того чтобы успешно применять эти алгоритмы, недостаточно их просто скопировать в свою программу. Необходимо кроме этого понимать, как различные алгоритмы ведут себя в разных ситуациях, что в конечном итоге и будет определять выбор наиболее подходящего алгоритма.
В этом материале поведение алгоритмов в типичном и наихудшем случаях описано доступным языком. Это позволит понять, чего вы вправе ожидать от того или иного алгоритма и распознать, в каких условиях встречается наихудший случай, и в соответствии с этим переписать или поменять алгоритм. Даже самый лучший алгоритм не поможет в решении задачи, если применять его неправильно.
=============xi
Все алгоритмы также представлены в виде исходных текстов на Visual Basic, которые вы можете использовать в своих программах без каких‑либо изменений. Они демонстрируют использование алгоритмов в программах, а также важные характерные особенности работы самих алгоритмов.
Что дают вам эти знания
После ознакомления с данным материалом и примерами вы получите:
1. Понятие об алгоритмах. После прочтения данного материала и выполнения примеров программ, вы сможете применять сложные алгоритмы в своих проектах на Visual Basic и критически оценивать другие алгоритмы, написанные вами или кем‑либо еще.
2. Большую подборку исходных текстов , которые вы сможете легко добавить к вашим программам. Используя код, содержащийся в примерах, вы сможете легко добавлять мощные алгоритмы к вашим приложениям.
3. Готовые примеры программ дадут вам возможность протестировать алгоритмы. Вы можете использовать эти примеры и модифицировать их для углубленного изучения алгоритмов и понимания их работы, или использовать их как основу для разработки собственных приложений.
Целевая аудитория
В этом материале обсуждаются углубленные вопросы программирования на Visual Basic. Они не предназначена для обучения программированию на этом языке. Если вы хорошо разбираетесь в основах программирования на Visual Basic, вы сможете сконцентрировать внимание на алгоритмах вместо того, чтобы застревать на деталях языка.
В этом материале изложены важные концепции программирования, которые могут быть с успехом применены для решения новых задач. Приведенные алгоритмы используют мощные программные методы, такие как рекурсия, разбиение на части, динамическое распределение памяти и сетевые структуры данных, которые вы можете применять для решения своих конкретных задач.
Даже если вы еще не овладели в полной мере программированием на Visual Basic, вы сможете скомпилировать примеры программ и сравнить производительность различных алгоритмов. Более того, вы сможете выбрать удовлетворяющие вашим требованиям алгоритмы и добавить их к вашим проектам на Visual Basic.
Совместимость с разными версиями Visual Basic
Выбор наилучшего алгоритма определяется не особенностями версии языка программирования, а фундаментальными принципами программирования.
=================xii
Некоторые новые понятия, такие как ссылки на объекты, классы и коллекции, которые были впервые введены в 4-й версии Visual Basic, облегчают понимание, разработку и отладку некоторых алгоритмов. Классы могут заключать некоторые алгоритмы в хорошо продуманных модулях, которые легко вставить в программу. Хотя для того, чтобы применять эти алгоритмы, необязательно разбираться в новых понятиях языка, эти новые возможности предоставляют слишком большие преимущества, чтобы ими можно было пренебречь.
Поэтому примеры алгоритмов в этом материале написаны для использования в 4-й и 5-й версиях Visual. Если вы откроете их в 5-й версии Visual Basic, среда разработки предложит вам сохранить их в формате 5-й версии, но никаких изменений в код вносить не придется. Все алгоритмы были протестированы в обеих версиях.
Эти программы демонстрируют использование алгоритмов без применения объектно-ориентированного подхода. Ссылки и коллекции облегчают программирование, но их применение может приводить к некоторому замедлению работы программ по сравнению со старыми версиями.
Тем не менее, игнорирование классов, объектов и коллекций привело бы к упущению многих действительно мощных свойств. Их использование позволяет достичь нового уровня модульности, разработки и повторного использования кода. Их, безусловно, необходимо иметь в виду, по крайней мере, на начальных этапах разработки. В дальнейшем, если возникнут проблемы с производительностью, вы сможете модифицировать код, используя более быстрые низкоуровневые методы.
Языки программирования зачастую развиваются в сторону усложнения, но редко в противоположном направлении. Замечательным примером этого является наличие оператора goto в языке C. Это неудобный оператор, потенциальный источник ошибок, который почти не используется большинством программистов на C, но он по‑прежнему остается в синтаксисе языка с 1970 года. Он даже был включен в C++ и позднее в Java, хотя создание нового языка было хорошим предлогом избавиться от него.
Так и новые версии Visual Basic будут продолжать вводить новые свойства в язык, но маловероятно, что из них будут исключены строительные блоки, использованные при применении алгоритмов, описанных в данном материале. Независимо от того, что будет добавлено в 6-й, 7-й или 8-й версии Visual Basic, классы, массивы и определяемые пользователем типы данных останутся в языке. Большая часть, а может и все алгоритмы из приведенных ниже, будут выполняться без изменений в течение еще многих лет.
Обзор глав
В 1 главе рассматриваются понятия, которые вы должны понимать до того, как приступить к анализу сложных алгоритмов. В ней изложены методы, которые потребуются для теоретического анализа вычислительной сложности алгоритмов. Некоторые алгоритмы с высокой теоретической производительностью на практике дают не очень хорошие результаты, поэтому в этой главе также затрагиваются практические соображения, например обращение к файлу подкачки и сравнивается использование коллекций и массивов.
Во 2 главе показано, как образуются различные виды списков с использованием массивов, объектов, и псевдоуказателей. Эти структуры данных можно с успехом применять во многих программах, и они используются в следующих главах
В 3 главе описаны два особых типа списков: стеки и очереди. Эти структуры данных используются во многих алгоритмах, включая некоторые алгоритмы, описанные в последующих главах. В конце главы приведена модель очереди на регистрацию в аэропорту.
В 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 и 12 главах, поэтому вам следует прочитать 6 главу до того, как браться за них. Остальные главы можно читать в любом порядке.
=============xvi
В табл. 1 показаны три возможных учебных плана, которыми вы можете руководствоваться при изучении материала в зависимости от того, насколько широко вы хотите ознакомиться с алгоритмами. Первый план включает в себя освоение основных методов и структур данных, которые могут быть полезны при разработке вами собственных программ. Второй кроме этого описывает также основные алгоритмы, такие как алгоритмы сортировки и поиска, которые могут понадобиться при написании более сложных программ.
Последний план дает порядок для изучения всего материала целиком. Хотя 7 и 8 главы логически вытекают из 6 главы, они сложнее для изучения, чем следующие главы, поэтому они изучаются несколько позже.
Почему именно Visual Basic ?
Наиболее часто встречаются жалобы на медленное выполнение программ, написанных на Visual Basic. Многие другие компиляторы, такие как Delphi, Visual C++ дают более быстрый и гибкий код, и предоставляют программисту более мощные средства, чем Visual Basic. Поэтому логично задать вопрос — «Почему я должен использовать именно Visual Basic для написания сложных алгоритмов? Не лучше было бы использовать Delphi или C++ или, по крайней мере, написать алгоритмы на одном из этих языков и подключать их к программам на Visual Basic при помощи библиотек?» Написание алгоритмов на Visual Basic имеет смысл по нескольким причинам.
Во‑первых, разработка приложения на Visual C++ гораздо сложнее и проблематичнее, чем на Visual Basic. Некорректная реализация в программе всех деталей программирования под Windows может привести к сбоям в вашем приложении, среде разработки, или в самой операционной системе Windows.
Во‑вторых, разработка библиотеки на языке C++ для использования в программах на Visual Basic включает в себя много потенциальных опасностей, характерных и для приложений Windows, написанных на C++. Если библиотека будет неправильно взаимодействовать с программой на Visual Basic, она также приведет к сбоям в программе, а возможно и в среде разработки и системе.
В-третьих, многие алгоритмы достаточно эффективны и показывают неплохую производительность даже при применении не очень быстрых компиляторов, таких, как Visual Basic. Например, алгоритм сортировки подсчетом,
@Таблица 1. Планы занятий
===============xvii
описываемый в 9 главе, сортирует миллион целых чисел менее чем за 2 секунды на компьютере с процессором Pentium с тактовой частотой 233 МГц. Используя библиотеку C++, можно было бы сделать алгоритм немного быстрее, но скорости версии на Visual Basic и так хватает для большинства приложений. Скомпилированные при помощи 5‑й версией Visual Basic исполняемые файлы сводят отставание по скорости к минимуму.
В конечном счете, разработка алгоритмов на любом языке программирования позволяет больше узнать об алгоритмах вообще. По мере изучения алгоритмов, вы освоите методы, которые сможете применять в других частях своих программ. После того, как вы овладеете в совершенстве алгоритмами на Visual Basic, вам будет гораздо легче реализовать их на Delphi или C++, если это будет необходимо.
=============xviii
Глава 1. Основные понятия
В этой главе содержатся общие понятия, которые нужно усвоить перед началом серьезного изучения алгоритмов. Начинается она с вопроса «Что такое алгоритмы?». Прежде чем углубиться в детали программирования алгоритмов, стоит потратить немного времени, чтобы разобраться в том, что это такое.
Затем в этой главе дается введение в формальную теорию сложности алгоритмов (complexity theory). При помощи этой теории можно оценить теоретическую вычислительную сложность алгоритмов. Этот подход позволяет сравнивать различные алгоритмы и предсказывать их производительность в разных условиях. В главе приводится несколько примеров применения теории сложности к небольшим задачам.
Некоторые алгоритмы с высокой теоретической производительностью не слишком хорошо работают на практике, поэтому в данной главе также обсуждаются некоторые реальные предпосылки для создания программ. Слишком частое обращение к файлу подкачки или плохое использование ссылок на объекты и коллекции может значительно снизить производительность прекрасного в остальных отношениях приложения.
После знакомства с основными понятиями, вы сможете применять их к алгоритмам, изложенным в последующих главах, а также для анализа собственных программ для оценки их производительности и сможете предугадывать возможные проблемы до того, как они обернутся катастрофой.
Что такое алгоритмы?
Алгоритм – это последовательность инструкций для выполнения какого‑либо задания. Когда вы даете кому‑то инструкции о том, как отремонтировать газонокосилку, испечь торт, вы тем самым задаете алгоритм действий. Конечно, подобные бытовые алгоритмы описываются неформально, например, так:
Проверьте, находится ли машина на стоянке.
Убедитесь, что машина поставлена на ручной тормоз.
Поверните ключ.
И т.д.
==========1
При этом по умолчанию предполагается, что человек, который будет следовать этим инструкциям, сможет самостоятельно выполнить множество мелких операций, например, отпереть и открыть дверь, сесть за руль, пристегнуть ремень, найти ручной тормоз и так далее.
Если же составляется алгоритм для исполнения компьютером, вы не можете полагаться на то, что компьютер поймет что‑либо, если это не описано заранее. Словарь компьютера (язык программирования) очень ограничен и все инструкции для компьютера должны быть сформулированы на этом языке. Поэтому для написания компьютерных алгоритмов используется формализованный стиль.
Интересно попробовать написать формализованный алгоритм для обычных ежедневных задач. Например, алгоритм вождения машины мог бы выглядеть примерно так:
Если дверь закрыта:
Вставить ключ в замок
Повернуть ключ
Если дверь остается закрытой, то:
Повернуть ключ в другую сторону
Повернуть ручку двери
И т.д.
Этот фрагмент «кода» отвечает только за открывание двери; при этом даже не проверяется, какая дверь открывается. Если дверь заело или в машине установлена противоугонная система, то алгоритм открывания двери может быть достаточно сложным.
Формализацией алгоритмов занимаются уже тысячи лет. За 300 лет до н.э. Евклид написал алгоритмы деления углов пополам, проверки равенства треугольников и решения других геометрических задач. Он начал с небольшого словаря аксиом, таких как «параллельные линии не пересекаются» и построил на их основе алгоритмы для решения сложных задач.
Формализованные алгоритмы такого типа хорошо подходят для задач математики, где должна быть доказана истинность какого‑либо положения или возможность выполнения каких‑то действий, скорость же исполнения алгоритма не важна. Для выполнения реальных задач, связанных с выполнением инструкций, например задачи сортировки на компьютере записей о миллионе покупателей, эффективность выполнения становится важной частью постановки задачи.
Анализ скорости выполнения алгоритмов
Есть несколько способов оценки сложности алгоритмов. Программисты обычно сосредотачивают внимание на скорости алгоритма, но важны и другие требования, например, к размеру памяти, свободному месту на диске или другим ресурсам. От быстрого алгоритма может быть мало толку, если под него требуется больше памяти, чем установлено на компьютере.
Пространство — время [RP1]
Многие алгоритмы предоставляют выбор между скоростью выполнения и используемыми программой ресурсами. Задача может выполняться
быстрее, используя больше памяти, или наоборот, медленнее, заняв меньший объем памяти.
===========2
Хорошим примером в данном случае может служить алгоритм нахождения кратчайшего пути. Задав карту улиц города в виде сети, можно написать алгоритм, вычисляющий кратчайшее расстояние между любыми двумя точками в этой сети. Вместо того чтобы каждый раз заново пересчитывать кратчайшее расстояние между двумя заданными точками, можно заранее просчитать его для всех пар точек и сохранить результаты в таблице. Тогда, чтобы найти кратчайшее расстояние для двух заданных точек, достаточно будет просто взять готовое значение из таблицы.
При этом мы получим результат практически мгновенно, но это потребует большого объема памяти. Карта улиц для большого города, такого как Бостон или Денвер, может содержать сотни тысяч точек. Для такой сети таблица кратчайших расстояний содержала бы более 10 миллиардов записей. В этом случае выбор между временем исполнения и объемом требуемой памяти очевиден: поставив дополнительные 10 гигабайт оперативной памяти, можно заставить программу выполняться гораздо быстрее.
Из этой связи вытекает идея пространственно‑временной сложности алгоритмов. При этом подходе сложность алгоритма оценивается в терминах времени и пространства, и находится компромисс между ними.
В этом материале основное внимание уделяется временной сложности, но мы также постарались обратить внимание и на особые требования к объему памяти для некоторых алгоритмов. Например, сортировка слиянием (mergesort), обсуждаемая в 9 главе, требует больше временной памяти. Другие алгоритмы, например пирамидальная сортировка (heapsort), которая также обсуждается в 9 главе, требует обычного объема памяти.
Оценка с точностью до порядка
При сравнении различных алгоритмов важно понимать, как сложность алгоритма соотносится со сложностью решаемой задачи. При расчетах по одному алгоритму сортировка тысячи чисел может занять 1 секунду, а сортировка миллиона — 10 секунд, в то время как расчеты по другому алгоритму могут потребовать 2 и 5 секунд соответственно. В этом случае нельзя однозначно сказать, какая из двух программ лучше — это будет зависеть от исходных данных.
Различие производительности алгоритмов на задачах разной вычислительной сложности часто более важно, чем просто скорость алгоритма. В вышеприведенном случае, первый алгоритм быстрее сортирует короткие списки, а второй — длинные.
Производительность алгоритма можно оценить по порядку величины. Алгоритм имеет сложность порядка 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 равна всего 100, или менее чем 0,01 процента. Но это верно только для больших N. При N=2, разность между N3 +N =10 и N3 =8 равна 2, а это уже 20 процентов.
Постоянные множители в соотношении также игнорируются. Это позволяет легко оценить изменения в вычислительной сложности задачи. Алгоритм, время выполнения которого пропорционально 3*N2 , будет иметь порядок O(N2 ). Если увеличить N в 2 раза, то время выполнения задачи возрастет примерно в 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 <= 0 Then Exit Sub
CountDown N - 1
End Sub
===========6
Многократная рекурсия
Рекурсивный алгоритм, вызывающий себя несколько раз, является примером многократной рекурсии (multiple recursion). Процедуры с множественной рекурсией сложнее анализировать, чем просто рекурсивные алгоритмы, и они могут давать больший вклад в общую сложность алгоритма.
Нижеприведенная подпрограмма похожа на предыдущую подпрограмму CountDown, только она вызывает саму себя дважды:
Sub DoubleCountDown(N As Integer)
If N <= 0 Then Exit Sub
DoubleCountDown N - 1
DoubleCountDown N - 1
End Sub
Можно было бы предположить, что время выполнения этой процедуры будет в два раза больше, чем для подпрограммы CountDown, и оценить ее сложность порядка 2*O(N)=O(N). На самом деле ситуация немного сложнее.
Если T(N) — число раз, которое выполняется процедура DoubleCountDown с параметром N, то легко заметить, что T(0)=1. Если вызвать процедуру с параметром N равным 0, то она просто закончит свою работу после первого шага.
Для больших значений N процедура вызывает себя дважды с параметром, равным N-1, выполняясь 1+2*T(N-1) раз. В табл. 1.1 приведены некоторые значения функции T(0)=1 и T(N)=1+2*T(N-1). Если обратить внимание на эти значения, можно увидеть, что T(N)=2(N+1) -1, что дает оценку сложности процедуры порядка O(2N ). Хотя процедуры CountDown и DoubleCountDown и похожи, вторая процедура требует выполнения гораздо большего числа шагов.
@Таблица 1.1. Значения функции времени выполнения для подпрограммы DoubleCountDown
Косвенная рекурсия
Процедура также может вызывать другую процедуру, которая в свою очередь вызывает первую. Такие процедуры иногда даже сложнее анализировать, чем процедуры с множественной рекурсией. Алгоритм вычисления кривой Серпинского, который обсуждается в 5 главе, включает в себя четыре процедуры, которые используют как множественную, так и непрямую рекурсию. Каждая из этих процедур вызывает себя и другие три процедуры до четырех раз. После довольно сложных подсчетов можно показать, что этот алгоритм имеет сложность порядка O(4N ).
Требования рекурсивных алгоритмов к объему памяти
Для некоторых рекурсивных алгоритмов важен объем доступной памяти. Можно легко написать рекурсивный алгоритм, который будет запрашивать
============7
небольшой объем памяти при каждом своем вызове. Объем занятой памяти может увеличиваться в процессе последовательных рекурсивных вызовов.
Поэтому для рекурсивных алгоритмов необходимо хотя бы приблизительно оценивать требования к объему памяти, чтобы убедиться, что программа не исчерпает при выполнении всю доступную память.
Приведенная ниже подпрограмма запрашивает память при каждом вызове. После 100 или 200 рекурсивных вызовов, процедура займет всю свободную память, и программа аварийно остановится с ошибкой «Out of Memory».
Sub GobbleMemory(N As Integer)
Dim Array() As Integer
ReDim Array (1 To 32000)
GobbleMemory N + 1
End Sub
Даже если внутри процедуры память не запрашивается, система выделяет память из системного стека (system stack) для сохранения параметров при каждом вызове процедуры. После возврата из процедуры память из стека освобождается для дальнейшего использования.
Если в подпрограмме встречается длинная последовательность рекурсивных вызовов, программа может исчерпать стек, даже если выделенная программе память еще не вся использована. Если запустить на исполнение следующую подпрограмму, она быстро исчерпает всю свободную стековую память и программа аварийно прекратит работу с сообщением об ошибке «Out of stack Space». После этого вы сможете узнать значение переменной Count, чтобы узнать, сколько раз подпрограмма вызывала себя перед тем, как исчерпать стек.
Sub UseStack()
Static Count As Integer
Count = Count + 1
UseStack
End Sub
Определение локальных переменных внутри подпрограммы также может занимать память из стека. Если изменить подпрограмму UseStack из предыдущего примера так, чтобы она определяла три переменных при каждом вызове, программа исчерпает стековое пространство еще быстрее:
Sub UseStack()
Static Count As Integer
Dim I As Variant
Dim J As Variant
Dim K As Variant
Count = Count + 1
UseStack
End Sub
В 5 главе рекурсивные алгоритмы обсуждаются более подробно.
==============8
Наихудший и усредненный случай
Оценка с точностью до порядка дает верхний предел сложности алгоритма. То, что программа имеет определенный порядок сложности, не означает, что алгоритм будет действительно выполняться так долго. При определенных исходных данных, многие алгоритмы выполняются гораздо быстрее, чем можно предположить на основании их порядка сложности. Например, следующий код реализует простой алгоритм выбора элемента из списка:
Function LocateItem(target As Integer) As Integer
For I = 1 To N
If Value(I) = target Then Exit For
Next I
LocateItem = I
End Sub
Если искомый элемент находится в конце списка, придется перебрать все N элементов для того, чтобы его найти. Это займет N шагов, значит сложность алгоритма порядка O(N). В этом, так называемом наихудшем случае (worst case) время выполнения алгоритма будет наибольшим.
С другой стороны, если искомое число в начале списка, алгоритм завершит работу практически сразу, совершив всего несколько итераций. Это так называемый наилучший случай (best case) со сложностью порядка O(1). Обычно и наилучший, и наихудший случаи встречаются относительно редко, и интерес представляет оценка усредненного или ожидаемого (expected case) поведения.
Если первоначально числа в списке распределены случайно, искомый элемент может оказаться в любом месте списка. В среднем потребуется проверить N/2 элементов для того, чтобы его найти. Значит, сложность этого алгоритма в усредненном случае порядка O(N/2), или O(N), если убрать постоянный множитель.
Для некоторых алгоритмов порядок сложности для наихудшего и наилучшего вариантов различается. Например, сложность алгоритма быстрой сортировки из 9 главы в наихудшем случае порядка O(N2 ), но в среднем его сложность порядка O(N*log(N)), что намного быстрее. Иногда алгоритмы типа быстрой сортировки бывают очень длинными, чтобы наихудший случай достигался крайне редко.
Часто встречающиеся функции оценки порядка сложности
В табл. 1.2 приведены некоторые функции, которые обычно встречаются при оценке сложности алгоритмов. Функции приведены в порядке возрастания вычислительной сложности сверху вниз. Это значит, что алгоритмы со сложностью порядка функций, расположенных вверху таблицы, будут выполняться быстрее, чем те, сложность которых определяется функциями из нижней части таблицы.
==============9
@Таблица 1.2. Часто встречающиеся функции оценки порядка сложности
Сложность алгоритма, определяемая уравнением, которое представляет собой сумму функций из таблицы, будет сводиться к сложности той из функций, которая расположена в таблице ниже. Например, O(log(N)+N2 ) — это то же самое, что и O(N2 ).
Обычно алгоритмы со сложностью порядка N*log(N) и менее сложных функций выполняются очень быстро. Алгоритмы порядка NC при малых C, например N2 выполняются достаточно быстро. Вычислительная же сложность алгоритмов, порядок которых определяется функциями CN или N! очень велика и эти алгоритмы пригодны только для решения задач с небольшим N.
В качестве примера в табл. 1.3 показано, как долго компьютер, выполняющий миллион инструкций в секунду, будет выполнять некоторые медленные алгоритмы. Из таблицы видно, что при сложности порядка O(CN ) могут быть решены только небольшие задачи, и еще меньше параметр N может быть для задач со сложностью порядка O(N!). Для решения задачи порядка O(N!) при N=24 потребовалось бы время, большее, чем время существования вселенной.
Логарифмы
Перед тем, как продолжить дальше, следует остановиться на логарифмах, так как они играют важную роль в различных алгоритмах. Логарифм числа N по основанию B это степень P, в которую надо возвести основание, чтобы получить N, то есть BP =N. Например, если 23 =8, то соответственно log2 (8)=3.
==================10
@Таблица 1.3. Время выполнения сложных алгоритмов
Можно привести логарифм к другому основанию при помощи соотношения logB (N)=logC (N)/logC (B). Например, чтобы вычислить логарифм числа по основанию 10, зная его логарифм по основанию 2, можно воспользоваться формулой log10 (N)=log2 (N)/log2 (10). При этом log2 (10) — это табличная константа, примерно равная 3,32. Так как постоянные множители при оценке сложности алгоритма можно опустить, то O(log2 (N)) — это же самое, что и O(log10 (N)) или O(logB (N)) для любого B. Поскольку основание логарифма не имеет значения, часто просто пишут, что сложность алгоритма порядка O(log(N)).
В программировании часто встречаются логарифмы по основанию 2, что обусловлено применяемой в компьютерах двоичной системой исчисления. Поэтому мы для упрощения выражений будем везде писать log(N), подразумевая под этим log2 (N). Если используется другое основание алгоритма, это будет обозначено особо.
Реальные условия — насколько быстро?
Хотя при исследовании сложности алгоритма обычно полезно отбросить малые члены уравнения и постоянные множители, иногда их все‑таки необходимо учитывать, особенно если размерность данных задачи N мала, а постоянные множители достаточно велики.
Допустим, мы рассматриваем два алгоритма решения одной задачи. Один выполняется за время порядка O(N), а другой — порядка O(N2 ). Для больших N первый алгоритм, вероятно, будет работать быстрее.
Тем не менее, если взять конкретные функции оценки времени выполнения для каждого из двух алгоритмов, например, для первого f(N)=30*N+7000, а для второго f(N)=N2 , то в этом случае при N меньше 100 второй алгоритм будет выполняться быстрее. Поэтому, если известно, что размерность данных задачи не будет превышать 100, возможно будет целесообразнее применить второй алгоритм.
С другой стороны, время выполнения разных инструкций может сильно отличаться. Если первый алгоритм использует быстрые операции с памятью, а второй использует медленное обращение к диску, то первый алгоритм будет быстрее во всех случаях.
==================11
Другие факторы могут также осложнить проблему выбора оптимального алгоритма. Например, первый алгоритм может требовать большего объема памяти, чем установлено на компьютере. Реализация второго алгоритма, в свою очередь, может потребовать намного больше времени, если этот алгоритм намного сложнее, а его отладка может превратиться в настоящий кошмар. Иногда подобные практические соображения могут сделать теоретический анализ сложности алгоритма почти бессмысленным.
Тем не менее, анализ сложности алгоритма полезен для понимания особенностей алгоритма и обычно обнаруживает части программы, занимающие большую часть компьютерного времени. Уделив внимание оптимизации кода в этих частях, можно внести максимальный эффект в увеличение производительности программы в целом.
Иногда тестирование алгоритмов является наиболее подходящим способом определить наилучший алгоритм. При таком тестировании важно, чтобы тестовые данные были максимально приближены к реальным данным. Если тестовые данные сильно отличаются от реальных, результаты тестирования могут сильно отличаться от реальных.
Обращение к файлу подкачки
Важным фактором при работе в реальных условиях является частота обращения к файлу подкачки (page file). Операционная система Windows отводит часть дискового пространства под виртуальную память (virtual memory). Когда исчерпывается оперативная память, Windows сбрасывает часть ее содержимого на диск. Освободившаяся память предоставляется программе. Этот процесс называется подкачкой, поскольку страницы, сброшенные на диск, могут быть подгружены системой обратно в память при обращении к ним.
Поскольку операции с диском намного медленнее операций с памятью, слишком частое обращение к файлу подкачки может значительно снизить производительность приложения. Если программа часто обращается к большим объемам памяти, система будет часто использовать файл подкачки, что приведет к замедлению работы.
Приведенная в числе примеров программа Pager запрашивает все больше и больше памяти под создаваемые массивы до тех пор, пока программа не начнет обращаться к файлу подкачки. Введите количество памяти в мегабайтах, которое программа должна запросить, и нажмите кнопку Page (Подкачка). Если ввести небольшое значение, например 1 или 2 Мбайт, программа создаст массив в оперативной памяти, и будет выполняться быстро.
Если же вы введете значение, близкое к объему оперативной памяти вашего компьютера, то программа начнет использовать файл подкачки. Вполне вероятно, что она будет при этом обращаться к диску постоянно. Вы также заметите, что программа выполняется намного медленнее. Увеличение размера массива на 10 процентов может привести к 100‑процентному увеличению времени исполнения.
Программа Pager может использовать память одним из двух способов. Если вы нажмете кнопку Page, программа начнет последовательно обращаться к элементам массива. По мере перехода от одной части массива к другой, системе может потребоваться подгружать их с диска. После того, как часть массива оказалась в памяти, программа может продолжить работу с ней.
============12
Если же вы нажмете на кнопку Thrash (Пробуксовка), программа будет случайно обращаться к разным участкам памяти. При этом вероятность того, что нужная страница находится в этот момент на диске, намного возрастает. Это избыточное обращение к файлу подкачки называется пробуксовкой [RP2] памяти (thrashing). В табл. 1.4 приведено время исполнения программы Pager на компьютере с процессором Pentium с тактовой частотой 90 МГц и 24 Мбайт оперативной памяти. В зависимости от конфигурации вашего компьютера, скорости работы с диском, количества установленной оперативной памяти, а также наличия других запущенных параллельно приложений время выполнения программы может сильно различаться.
Вначале время выполнения теста растет почти пропорционально размеру занятой памяти. Когда начинается обращение к файлу подкачки, скорость работы программы резко падает. Заметьте, что до этого тесты с обращением к файлу подкачки и пробуксовкой ведут себя практически одинаково, то есть когда весь массив находится в оперативной памяти, последовательное и случайное обращение к элементам массива занимает одинаковое время. При подкачке элементов массива с диска случайный доступ к памяти намного менее эффективен.
Для уменьшения числа обращений к файлу подкачки есть несколько способов. Основной прием — экономное расходование памяти. При этом надо помнить, что программа обычно не может занять всю физическую память, потому что часть ее занимает система и другие программы. Компьютер, на котором были получены результаты, приведенные в табл. 1.4, начинал интенсивно обращаться к диску, когда программа занимала 20 Мбайт из 24 Мбайт физической памяти.
Иногда можно написать код так, что программа будет обращаться к блокам памяти последовательно. Алгоритм сортировки слиянием, описанный в 9 главе, манипулирует большими блоками данных. Эти блоки сортируются, а затем сливаются вместе. Упорядоченная работа с памятью уменьшает число обращений к диску.
@Таблица 1.4. Время выполнения программы Pager в секундах
===============13
Алгоритм пирамидальной сортировки, также описанный в 9 главе, произвольно переходит от одной части списка к другой. Для очень больших списков это может приводить к перегрузке памяти. С другой стороны, сортировка слиянием требует большего объема памяти, чем пирамидальная сортировка. Если список достаточно большой, это также может приводить к обращению к файлу подкачки.
Псевдоуказатели, ссылки на объекты и коллекции
В некоторых языках, например в C, C++ или Delphi, можно определять переменные, которые являются указателями (pointers) на участки памяти. В этих участках могут содержаться массивы, строки, или другие структуры данных. Часто указатель ссылается на структуру, которая содержит другой указатель и так далее. Используя структуры, содержащие указатели, можно организовывать всевозможные списки, графы, сети и деревья. В последующих главах рассматриваются некоторые из этих сложных структур.
До третьей версии Visual Basic не содержал средств для прямого создания ссылок. Тем не менее, поскольку указатель всего лишь ссылается на какой‑либо участок данных, то можно, создав массив, использовать целочисленный индекс массива в качестве указателя на его элементы. Это называется псевдоуказателем (fake pointer).
Ссылки
В 4-й версии Visual Basic были впервые введены классы. Переменная, указывающая на экземпляр класса, является ссылкой на объект. Например, в следующем фрагменте кода переменная obj — это ссылка на объект класса MyClass. Эта переменная не указывает ни на какой объект, пока она не определяется при помощи зарезервированного слова New. Во второй строке оператор New создает новый объект и записывает ссылку на него в переменную obj.
Dim obj As MyClass
Set obj = New MyClass
Ссылки в Visual Basic — это разновидность указателей.
Объекты в Visual Basic используют счетчик ссылок (reference counter) для упрощения работы с объектами. Когда создается новая ссылка на объект, счетчик ссылок увеличивается на единицу. После того, как ссылка перестает указывать на объект, счетчик ссылок соответственно уменьшается. Когда счетчик ссылок становится равным нулю, объект становится недоступным программе. В этот момент Visual Basic уничтожает объект и возвращает занятую им память.
В следующих главах более подробно обсуждаются ссылки и счетчики ссылок.
Коллекции
Кроме объектов и ссылок, в 4-й версии Visual Basic также появились коллекции. Коллекцию можно представить как разновидность массива. Они
================14
предоставляют в распоряжение программиста удобные возможности, например можно менять размер коллекции, а также осуществлять поиск объекта по ключу.
Вопросы производительности
Псевдоуказатели, ссылки и коллекции упоминаются в этой главе потому, что они могут сильно влиять на производительность программы. Ссылки и коллекции могут упрощать программирование определенных операций, но они могут потребовать дополнительных расходов памяти.
Программа Faker на диске с примерами демонстрирует взаимосвязь между псевдоуказателями, ссылками и коллекциями. Когда вы вводите число и нажимаете кнопку Create List (Создать список), программа создает список элементов одним из трех способов. Вначале она создает объекты, соответствующие отдельным элементам, и добавляет ссылки на объекты к коллекции. Затем она использует ссылки внутри самих объектов для создания связанного списка объектов. И, наконец, она создает связный список при помощи псевдоуказателей. Пока не будем останавливаться на том, как работают связные списки. Они будут подробно разбираться во 2 главе.
После нажатия на кнопку Search List (Поиск в списке), программа Faker выполняет поиск по всем элементам списка, а после нажатия на кнопку Destroy List (Уничтожить список) уничтожает все списки и освобождает память.
В табл. 1.5 приведены значения времени, которое требуется программе для выполнения этих задач на компьютере с процессором Pentium с тактовой частотой 90 МГц. Из таблицы видно, что за удобство работы с коллекциями приходится платить ценой большего времени, затрачиваемого на создание и уничтожение коллекций.
Коллекции также содержат индекс списка. Часть времени, затрачиваемого при создании коллекции, и уходит на создание индекса. При уничтожении коллекции сохраняемые в ней ссылки освобождаются. При этом система проверяет и обновляет счетчики ссылок для всех объектов. Если они равны нулю, то сам объект также уничтожается. Все это занимает дополнительное время.
При использовании псевдоуказателей создание и уничтожение списка происходит так быстро, что этим временем можно практически пренебречь. Системе при этом не надо заботиться о ссылках, счетчиках ссылок и об освобождении объектов.
С другой стороны, поиск в коллекции осуществляется гораздо быстрее, чем в двух остальных случаях, поскольку коллекция использует быстрое хеширование (hashing) построенного индекса, в то время как список ссылок и список псевдоуказателей используют медленный последовательный поиск. В 11 главе объясняется, как можно добавить хеширование к своей программе без использования коллекций.
@Таблица 1.5. Время Создания/Поиска/Уничтожения списков в секундах
==============15
Хотя применение псевдоуказателей обычно обеспечивает лучшую производительность, оно менее удобно, чем использование ссылок. Если в программе нужен лишь небольшой список, ссылки и коллекции могут работать достаточно быстро. При работе с большими списками можно получить более высокую производительность, используя псевдоуказатели.
Резюме
Анализ производительности алгоритмов позволяет сравнить разные алгоритмы. Он также помогает оценить поведение алгоритмов при различных условиях. Выделяя только части алгоритма, которые вносят наибольший вклад во время исполнения программы, анализ помогает определить, доработка каких участков кода позволяет внести максимальный вклад в улучшение производительности.
В программировании часто приходится идти на различные компромиссы, которые могут сказываться на производительности. Один алгоритм может быть быстрее, но за счет использования большого объема памяти. Другой алгоритм, использующий коллекции, может быть более медленным, но зато его проще разрабатывать и поддерживать.
После анализа доступных алгоритмов, понимания того, как они ведут себя в различных условиях и их требований к ресурсам, вы можете выбрать оптимальный алгоритм для вашей задачи.
==============16
Глава 2. Списки
Существует четыре основных способа распределения памяти в Visual Basic: объявление переменных стандартных типов (целые, с плавающей точкой и т.д.); объявление переменных типов, определенных пользователем; создание экземпляров классов при помощи оператора New и изменение размера массивов. Существует еще несколько способов, например, создание нового экземпляра формы или элемента управления, но эти способы не дают больших возможностей при создании сложных структур данных.
Используя эти методы, можно легко строить статические структуры данных, такие как большие массивы определенных пользователем типов. Вы также можете изменять размер массива при помощи оператора ReDim. Тем не менее, перераспределение данных может быть достаточно сложным. Например, для того, чтобы переместить элемент с одного конца массива на другой, потребуется переупорядочить весь массив, сдвинув все элементы на одну позицию, чтобы заполнить освободившееся пространство. Затем можно поместить элемент на его новое место.
Динамические структуры данных позволяют быстро и легко выполнять такого рода изменения. Всего за несколько шагов можно переместить любой элемент в структуре данных в любое другое положение.
В этой главе описаны методы создания динамических списков в Visual Basic. Различные типы списков обладают разными свойствами. Некоторые из них просты и обладают ограниченной функциональностью, другие же, такие как циклические списки, одно‑ или двусвязные списки, являются более сложными и поддерживают более развитые средства управления данными.
В последующих главах описанные методы используются для построения стеков, очередей, массивов, деревьев, хэш‑таблиц и сетей. Вам необходимо усвоить материал этой главы перед тем, как продолжить чтение.
Знакомство со списками
Простейшая форма списка — это группа объектов. Она включает в себя объекты и позволяет программе обращаться к ним. Если это все, что вам нужно от списка, вы можете использовать массив в качестве списка, отслеживая при помощи переменной NumInList число элементов в списке. Определив при помощи этой переменной число имеющихся элементов, программа затем может по очереди обратиться к ним в цикле For и выполнить необходимые действия.
=============17
Если вы в своей программе можете обойтись этим подходом, используйте его. Этот метод эффективен, и его легко отлаживать и поддерживать благодаря его простоте. Тем не менее, большинство программ не столь просты, и в них требуются более сложные конструкции даже для таких простых объектов, как списки. Поэтому в последующих разделах этой главы обсуждаются некоторые пути создания списков с большей функциональностью.
В первом параграфе описываются пути создания списков, которые могут расти и уменьшаться со временем. В некоторых программах нельзя заранее определить, насколько большой список понадобится. Вы можете справиться с такой ситуацией при помощи списка, который может при необходимости изменять свой размер.
В следующем параграфе обсуждаются неупорядоченные списки (unordered list), которые позволяют удалять элементы из любой части списка. Неупорядоченные списки дают больший контроль над содержимым списка, чем простые списки. Они также являются более динамичными, так как позволяют изменять содержимое в произвольный момент времени.
В последующих разделах обсуждаются связные списки (linked list), которые используют указатели для создания чрезвычайно гибких структур данных. Вы можете добавлять или удалять элементы из любой части связного списка с минимальными усилиями. В этих параграфах также описаны некоторые разновидности связных списков, такие как циклические, двухсвязные списки или списки со ссылками.
Простые списки
Если в вашей программе необходим список постоянного размера, вы можете создать его, просто используя массив. В этом случае можно при необходимости опрашивать его элементы в цикле For.
Многие программы используют списки, которые растут или уменьшаются со временем. Можно создать массив, соответствующий максимально возможному размеру списка, но такое решение не всегда будет оптимальным. Не всегда можно заранее знать, насколько большим может стать список, кроме того, вероятность, что список станет очень большим, может быть невелика, и созданный массив гигантских размеров может большую часть времени лишь понапрасну занимать память.
Коллекции
Программа может использовать коллекции Visual Basic для хранения списка переменного размера. Метод Add Item добавляет элемент в коллекцию. Метод Remove удаляет элемент. Следующий фрагмент кода демонстрирует программу, которая добавляет три элемента к коллекции и затем удаляет второй элемент.
Dim list As New Collection
Dim obj As MyClass
Dim I As Integer
‘ Создать и добавить 1 элемент.
Set obj = New MyClass
list.Add obj
‘ Добавить целое число.
i = 13
list.Add I
‘ Добавить строку.
list.Add "Работа с коллекциями"
‘ Удалить 2 элемент (целое число).
list.Remove 2
===============18
Коллекции пытаются обеспечить поддержку любых приложений, и выполняют замечательную работу. Их легко использовать, они позволяют извлекать элементы, проиндексированные по ключу, и дают приемлемую производительность, если не содержат слишком много элементов.
Тем не менее, коллекциям свойственны и определенные недостатки. Для больших списков, коллекции могут работать медленнее, чем массивы. Если в вашей программе не нужны все свойства, предоставляемые коллекцией, более быстрым может быть использование простого массива.
Схема хэширования, которую коллекции используют для управления ключами, также накладывает ряд ограничений. Во-первых, коллекции не позволяют дублировать ключи. Во-вторых, для коллекции можно определить, какой элемент имеет заданный ключ, но нельзя узнать, какой ключ соответствует данному элементу. И, наконец, коллекции не поддерживают множественных ключей. Например, может быть, вам хотелось бы, чтобы программа могла производить поиск по списку служащих, используя имя сотрудника или его идентификационный номер в системе социального страхования. Коллекция не сможет поддерживать оба метода поиска, так как она способна оперировать только одним ключом.
В последующих параграфах описываются методы построения списков, свободных от этих ограничений.
Список переменного размера
Оператор Visual Basic ReDim позволяет изменять размер массива. Вы можете использовать это свойство для построения простого списка переменного размера. Начните с объявления безразмерного массива для хранения элементов списка. Также определите переменную NumInList для отслеживания числа элементов в списке. При добавлении элементов к списку используйте оператор ReDim для увеличения размера массива, чтобы новый элемент мог поместиться в нем. При удалении элемента также используйте оператор ReDim для уменьшения массива и освобождения ненужной больше памяти.
Dim List() As String ‘ Список элементов.
Dim NumInList As Integer ‘ Число элементов в списке.
Sub AddToList(value As String)
‘ Увеличить размер массива.
NumInList = NumInList + 1
ReDim Preserve List (1 To NumInList)
‘ Добавить новый элемент к концу списка.
List(NumInList) = value
End Sub
Sub RemoveFromList()
‘ Уменьшить размер массива, освобождая память.
NumInList = NumInList – 1
ReDim Preserve List (1 To NumInList)
End Sub
==================19
Эта простая схема неплохо работает для небольших списков, но у нее есть пара недостатков. Во-первых, приходится часто менять размер массива. Для создания списка из 1000 элементов, придется 1000 раз изменять размер массива. Хуже того, при увеличении размера списка, на изменение его размера потребуется больше времени, поскольку придется каждый раз копировать растущий список в памяти.
Для уменьшения частоты изменений размера массива, можно добавлять дополнительные элементы к массиву при увеличении его размера, например, по 10 элементов вместо одного. При этом, когда вы будете добавлять новые элементы к списку в будущем, массив уже будет содержать неиспользуемые ячейки, в которые вы сможете поместить новые элементы без увеличения размера массива. Новое увеличение размера массива потребуется, только когда пустые ячейки закончатся.
Подобным же образом можно избежать изменения размера массива при каждом удалении элемента из списка. Можно подождать, пока в массиве не накопится 20 неиспользуемых ячеек, прежде чем уменьшать его размер. При этом нужно оставить 10 свободных ячеек для того, чтобы можно было добавлять новые элементы без необходимости снова увеличивать размер массива.
Заметим, что максимальное число неиспользуемых ячеек (20) должно быть больше, чем минимальное число (10). Это уменьшает число изменений размера массива при удалении или добавлении его элементов.
При такой схеме в списке обычно есть несколько свободных ячеек, тем не менее их число достаточно мало, и лишние затраты памяти невелики. Свободные ячейки гарантируют возможность добавления или удаления элементов без изменения размера массива. Фактически, если вы неоднократно добавляете к списку, а затем удаляете из него один или два элемента, вам может никогда не понадобиться изменять размер массива.
Dim List() As String ‘ Список элементов.
Dim ArraySize As Integer ‘ Размер массива.
Dim NumInList As Integer ‘ Число используемых элементов.
‘ Если массив заполнен, увеличить его размер, добавив 10 ячеек.
‘ Затем добавить новый элемент в конец списка.
Sub AddToList(value As String)
NumInList = NumInList + 1
If NumInList > ArraySize Then
ArraySize = ArraySize + 10
ReDim Preserve List(1 To ArraySize)
End If
List(NumInList) = value
End Sub
‘ Удалить последний элемент из списка. Если осталось больше
‘ 20 пустых ячеек, уменьшить список, освобождая память.
Sub RemoveFromList()
NumInList = NumInList – 1
If ArraySize – NumInList > 20 Then
ArraySize = ArraySize –10
ReDim Preserve List(1 To ArraySize)
End If
End Sub
=============20
Для очень больших массивов это решение может также оказаться не самым лучшим. Если вам нужен список, содержащий 1000 элементов, к которому обычно добавляется по 100 элементов, то все еще слишком много времени будет тратиться на изменение размера массива. Очевидной стратегией в этом случае было бы увеличение приращения размера массива с 10 до 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). Они также иногда называются «множеством элементов». [RP3]
Неупорядоченный список должен поддерживать следующие операции:
* добавление элемента к списку;
* удаление элемента из списка;
* определение наличия элемента в списке;
* выполнение каких‑либо операций (например, вывода на дисплей или принтер) для всех элементов списка.
Простую структуру, представленную в предыдущем параграфе, можно легко изменить для того, чтобы обрабатывать такие списки. Когда удаляется элемент из середины списка, остальные элементы сдвигаются на одну позицию, заполняя образовавшийся промежуток. Это показано на рис. 2.1, на котором второй элемент удаляется из списка, и третий, четвертый, и пятый элементы сдвигаются влево, заполняя свободный участок.
Удаление из массива элемента при таком подходе может занять достаточно много времени, особенно если удаляется элемент в начале списка. Чтобы удалить первый элемент из массива с 1000 элементов, потребуется сдвинуть влево на одну позицию 999 элементов. Гораздо быстрее удалять элементы можно при помощи простой схемы чистки памяти ( garbage collection)[RP4] .
Вместо удаления элементов из списка, пометьте их как неиспользуемые. Если элементы списка — данные простых типов, например целые, можно помечать элементы, используя определенное, так называемое «мусорное» значение (garbage value).
@Рисунок 2.1 Удаление элемента из середины массива
===========23
Для целых чисел можно использовать для этого значение ‑32.767. Для переменной типа Variant можно использовать значение NULL. Это значение присваивается каждому неиспользуемому элементу. Следующий фрагмент кода демонстрирует удаление элемента из подобного целочисленного списка:
Const GARBAGE_VALUE = -32767
‘ Пометить элемент как неиспользуемый.
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)) [RP5] Then ‘ Если элемент не помечен
Print Str$(List(I)) ‘ напечатать его.
End If
Next I
End Sub
После использования в течение некоторого времени схемы пометки «мусора», список может оказаться полностью им заполнен. В конце концов, подпрограммы вроде этой процедуры больше времени будут тратить на пропуск ненужных элементов, чем на обработку настоящих данных.
=============24
Для того, чтобы избежать этого, можно периодически запускать процедуру очистки памяти (garbage collection routine). Эта процедура перемещает все непомеченные записи в начало массива. После этого можно добавить их к свободным элементам в конце массива. Когда потребуется добавить к массиву дополнительные элементы, их также можно будет использовать без изменения размера массива.
После добавления помеченных элементов к другим свободным ячейкам массива, полный объем свободного пространства может стать достаточно большим, и в этом случае можно уменьшить размер массива, освобождая память:
Private Sub CollectGarbage()
Dim i As Long
Dim good As Long
good = 1 ‘ Первый используемый элемент.
For i = 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
При выполнении чистки памяти, используемые элементы перемещаются ближе к началу списка, заполняя пространство, которое занимали помеченные элементы. Значит, положение элементов в списке может измениться во время этой операции. Если другие часть программы обращаются к элементам списка по их положению в нем, необходимо модифицировать процедуру чистки памяти, с тем, чтобы она также обновляла ссылки на положение элементов в списке. В общем случае это может оказаться достаточно сложным, приводя к проблемам при сопровождении программ.
Можно выбирать разные моменты для запуска процедуры чистки памяти. Один из них — когда массив достигает определенного размера, например, когда список содержит 30000 элементов.
Этому методу присущи определенные недостатки. Во‑первых, он использует большой объем памяти. Если вы часто добавляете или удаляете элементы, «мусор» будет занимать довольно большую часть массива. При таком неэкономном расходовании памяти, программа может тратить время на свопинг, хотя список мог бы целиком помещаться в памяти при более частом переупорядочивании.
===========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 процентов и 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 i As Integer
For i = 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 i As Integer
Set ptr = top_cell
For i = 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. Двусвязный список с сигнальными метками
Теперь вы можете написать новые процедуры для вставки нового элемента до или после данного элемента, и процедуру удаления заданного элемента. Например, следующие подпрограммы добавляют и удаляют ячейки из двусвязного списка. Заметьте, что эти процедуры не нуждаются в доступе ни к одной из сигнальных меток списка. Им нужны только указатели на узел, который должен быть удален или
Работы, похожие на Реферат: VB, MS Access, VC++, Delphi, Builder C++ принципы(технология), алгоритмы программирования
|