VB, MS Access, VC++, Delphi, Builder C++ принципы(технология), алгоритмы программирования
========63
Затратив еще немного усилий, можно построить приоритетную очередь, в
которой добавление и удаление элемента потребуют порядка O(log(N)) шагов.
Для очень больших очередей, ускорение работы может стоить этих усилий. Этот
тип приоритетных очередей использует структуры данных в виде пирамиды,
которые также применяются в алгоритме пирамидальной сортировки. Пирамиды и
приоритетные очереди на их основе обсуждаются более подробно в 9 главе.
Многопоточные очереди
Интересной разновидностью очередей являются многопоточные очереди (multi-
headed queues). Элементы, как обычно, добавляются в конец очереди, но
очередь имеет несколько потоков (front end) или голов (heads). Программа
может удалять элементы из любого потока.
Примером многопоточной очереди в обычной жизни является очередь клиентов в
банке. Все клиенты находятся в одной очереди, но их обслуживает несколько
служащих. Освободившийся банковский работник обслуживает клиента, который
находится в очереди первым. Такой порядок обслуживания кажется
справедливым, поскольку клиенты обслуживаются в порядке прибытия. Он также
эффективен, так как все служащие остаются занятыми до тех пор, пока клиенты
ждут в очереди.
Сравните этот тип очереди с несколькими однопоточными очередями в
супермаркете, в которых покупатели не обязательно обслуживаются в порядке
прибытия. Покупатель в медленно движущейся очереди, может прождать дольше,
чем тот, который подошел позже, но оказался в очереди, которая продвигается
быстрее. Кассиры также могут быть не всегда заняты, так как какая-либо
очередь может оказаться пустой, тогда как в других еще будут находиться
покупатели.
В общем случае, многопоточные очереди более эффективны, чем несколько
однопоточных очередей. Последний вариант используется в супермаркетах
потому, что тележки для покупок занимают много места. При использовании
многопоточной очереди всем покупателям пришлось бы построиться в одну
очередь. Когда кассир освободится, покупателю пришлось бы переместиться с
громоздкой тележкой к кассиру. С другой стороны, в банке посетителям не
нужно двигать большие тележки для покупок, поэтому они легко могут
уместиться в одной очереди.
Очереди на регистрацию в аэропорту иногда представляют собой комбинацию
этих двух ситуаций. Хотя пассажиры имеют с собой большое количество багажа,
в аэропорту все-таки используются многопоточные очереди, при этом
приходится отводить дополнительное место, чтобы пассажиры могли выстроиться
в порядке очереди.
Многопоточную очередь просто построить, используя обычную однопоточную
очередь. Элементы, представляющие клиентов, хранятся в обычной однопоточной
очереди. Когда агент (кассир, банковский служащий и т.д.) освобождается,
первый элемент в начале очереди удаляется и передается этому агенту.
Модель очереди
Предположим, что вы отвечаете за разработку счетчика регистрации для нового
терминала в аэропорту и хотите сравнить возможности одной многопоточной
очереди или нескольких однопоточных. Вам потребуется какая-то модель
поведения пассажиров. Для этого примера можно сделать следующие
предположения:
=====63
1. регистрация каждого пассажира занимает от двух до пяти минут;
2. при использовании нескольких однопоточных очередей, прибывающие
пассажиры встают в самую короткую очередь;
3. скорость поступления пассажиров примерно неизменна.
Программа HeadedQ моделирует эту ситуацию. Вы можете менять некоторые
параметры модели, включая следующие:
4. число прибывающих в течение часа пассажиров;
5. минимальное и максимальное затрачиваемое время;
6. число свободных служащих;
7. паузу между шагами программы в миллисекундах.
При выполнении программы, модель показывает прошедшее время, среднее и
максимальное время ожидания пассажирами обслуживания, и процент времени, в
течение которого служащие заняты.
При экспериментировании с различными значениями параметров, вы заметите
несколько любопытных моментов. Во-первых, для многопоточной очереди среднее
и максимальное время ожидания будет меньше. При этом, служащие также
оказываются немного более загружены, чем в случае однопоточной очереди.
Для обоих типов очереди есть порог, при котором время ожидания пассажиров
значительно возрастает. Предположим, что на обслуживание одного пассажира
требуется от 2 до 10 минут, или в среднем 6 минут. Если поток пассажиров
составляет 60 человек в час, тогда персонал потратит около 6*60=360 минут в
час на обслуживание всех пассажиров. Разделив это значение на 60 минут в
часе, получим, что для обслуживания клиентов в этом случае потребуется 6
клерков.
Если запустить программу HeadedQ с этими параметрами, вы увидите, что
очереди движутся достаточно быстро. Для многопоточной очереди время
ожидания составит всего несколько минут. Если добавить еще одного
служащего, чтобы всего было 7 служащих, среднее и максимальное время
ожидания значительно уменьшатся. Среднее время ожидания упадет примерно до
одной десятой минуты.
С другой стороны, если уменьшить число служащих до 5, это приведет к
большому увеличению среднего и максимального времени ожидания. Эти
показатели также будут расти со временем. Чем дольше будет работать
программа, тем дольше будут задержки.
@Таблица 3.1. Время ожидания в минутах для одно- и многопоточных очередей
======64
@Рис. 3.9. Программа HeadedQ
В табл. 3.1 приведены среднее и максимальное время ожидания для 2 разных
типов очередей. Программа моделирует работу в течение 3 часов и
предполагает, что прибывает 60 пассажиров в час и на обслуживание каждого
из них уходит от 2 до 10 минут.
Многопоточная очередь также кажется более справедливой, так как пассажиры
обслуживаются в порядке прибытия. На рис. 3.9 показана программа HeadedQ
после моделирования чуть более, чем двух часов работы терминала. В
многопоточной очереди первым стоит пассажир с номером 104. Все пассажиры,
прибывшие до него, уже обслужены или обслуживаются в настоящий момент. В
однопоточной очереди, обслуживается пассажир с номером 106. Пассажиры с
номерами 100, 102, 103 и 105 все еще ждут своей очереди, хотя они и прибыли
раньше, чем пассажир с номером 106.
Резюме
Разные реализации стеков и очередей обладают различными свойствами. Стеки и
циклические очереди на основе массивов просты и эффективны, в особенности,
если заранее известно насколько большим может быть их размер. Связные
списки обеспечивают большую гибкость, если размер списка часто изменяется.
Стеки и очереди на основе коллекций Visual Basic не так эффективны, как
реализации на основе массивов, но они очень просты. Коллекции могут подойти
для небольших структур данных, если производительность не критична. После
тестирования приложения, можно переписать код для стека или очереди, если
коллекции окажутся слишком медленными.
Глава 4. Массивы
В этой главе описаны структуры данных в виде массивов. С помощью Visual
Basic вы можете легко создавать массивы данных стандартных или определенных
пользователем типов. Если определить массив без границ, затем можно
изменять его размер при помощи оператора ReDim. Эти свойства делают
применение массивов в Visual Basic очень полезным.
Некоторые программы используют особые типы массивов, которые не
поддерживаются Visual Basic непосредственно. К этим типа относятся
треугольные массивы, нерегулярные массивы и разреженные массивы. В этой
главе объясняется, как можно использовать гибкие структуры массивов,
которые могут значительно снизить объем занимаемой памяти.
Треугольные массивы
Некоторым программам требуется только половина элементов в двумерном
массиве. Предположим, что мы располагаем картой, на которой 10 городов
обозначены цифрами от 0 до 9. Можно использовать массив для создания
матрицы смежности (adjacency matrix), показывающей наличие автострады между
парами городов. Элемент A(I,J) равен True, если между городами I и J есть
автострада.
В этом случае, значения в половине матрицы будут дублировать значения в
другой ее половине, так как A(I, J)=A(J, I). Также элемент A(I, I) не имеет
смысла, так как бессмысленно строить автостраду из города I в тот же самый
город. В действительности потребуются только элементы A(I,J) из верхнего
левого угла, для которых I > J. Вместо этого можно также использовать
элементы из верхнего правого угла. Поскольку эти элементы образуют
треугольник, этот тип массивов называется треугольным массивом (triangular
array).
На рис. 4.1 показан треугольный массив. Элементы со значащими данными
обозначены буквой X, ячейки, соответствующие дублирующимся элементам,
оставлены пустыми. Незначащие элементы A(I,I) обозначены тире.
Для небольших массивов потери памяти при использовании обычных двумерных
массивов для хранения таких данных не слишком существенны. Если же на карте
много городов, потери памяти могут быть велики. Для N городов эти потери
составят N*(N-1)/2 дублирующихся элементов и N незначащих диагональных
элементов A(I,I). Если карта содержит 1000 городов, в массиве будет более
полумиллиона ненужных элементов.
====67
@Рис. 4.1. Треугольный массив
Избежать потерь памяти можно, создав одномерный массив B и упаковав в него
значащие элементы из массива A. Разместим элементы в массиве B по строкам,
как показано на рис. 4.2. Заметьте, что индексы массивов начинаются с нуля.
Это упрощает последующие уравнения.
Для того, чтобы упростить использование этого представления треугольного
массива, можно написать функции для преобразования индексов массивов A и B.
Уравнение для преобразования индекса A(I,J) в B(X) выглядит так:
X = I * (I - 1) / 2 + J ' Для I > J.
Например, для I=2 и J=1 получим X = 2 * (2 - 1) / 2 + 1 = 2. Это значит,
что A(2,1) отображается на 2 позицию в массиве B, как показано на рис. 4.2.
Помните, что массивы нумеруются с нуля.
Уравнение остается справедливым только для I > J. Значения других элементов
массива A не сохраняются в массиве B, потому что они являются избыточными
или незначащими. Если вам нужно получить значение A(I,J) при I < J, вместо
этого следует вычислять значение A(J,I).
Уравнения для обратного преобразования B(X) в A(I,J) выглядит так:
I = Int((1 + Sqr(1 + 8 * X)) / 2)
J = X - I * (I - 1) / 2
@Рис. 4.2. Упаковка треугольного массива в одномерном массиве
=====68
Подстановка в эти уравнения X=4 дает I = Int((1 + Sqr(1 + 8 * 4)) / 2) = 3
и J = 4 – 3 * (3 - 1) / 2 = 1. Это означает, что элемент B(4) отображается
на позицию A(3,1). Это также соответствует рис. 4.2.
Эти вычисления не слишком просты. Они требуют нескольких умножений и
делений, и даже вычисления квадратного корня. Если программе придется
выполнять эти функции очень часто, это внесет определенную задержку
скорости выполнения. Это пример компромисса между пространством и временем.
Упаковка треугольного массива в одномерный массив экономит память, хранение
данных в двумерном массиве требует больше памяти, но экономит время.
Используя эти уравнения, можно написать процедуры Visual Basic для
преобразования координат между двумя массивами:
Private Sub AtoB(ByVal I As Integer, ByVal J As Integer, X As Integer)
Dim tmp As Integer
If I = J Then ' Незначащий элемент.
X = -1
Exit Sub
ElseIf I < J Then ' Поменять местами I и J.
tmp = I
I = J
J = tmp
End If
X = I * (I - 1) / 2 + J
End Sub
Private Sub BtoA(ByVal X As Integer, I As Integer, J As Integer)
I = Int((1 + Sqr(1 + 8 * X)) / 2)
J = X - I * (I - 1) /2
End Sub
Программа Triang использует эти подпрограммы для работы с треугольными
массивами. Если вы нажмете на кнопку A to B (Из A в B), программа пометит
элементы в массиве A и скопирует эти метки в соответствующие элементы
массива B. Если вы нажмете на кнопку B to A (Из B в A), программа пометит
элементы в массиве B, и затем скопирует метки в массив A.
Программа Triangc использует класс TriangularArray для работы с треугольным
массивом. При старте программы, она записывает в объект TriangularArray
строки, представляющие собой элементы массива. Затем она извлекает и
выводит на экран элементы массива.
Диагональные элементы
Некоторые программы используют треугольные массивы, которые включают
диагональные элементы A(I, I). В этом случае необходимо внести только три
изменения в процедуры преобразования индексов. Процедура преобразования
AtoB не должна пропускать случаи с I=J, и должна добавлять к I единицу при
подсчете индекса массива B.
=====69
Private Sub AtoB(ByVal I As Integer, ByVal J As Integer, X As Integer)
Dim tmp As Integer
If I < J Then ' Поменять местами I и J.
tmp = I
I = J
J = tmp
End If
I = I + 1
X = I * (I - 1) / 2 + J
End Sub
Процедура преобразования BtoA должна вычитать из I единицу перед возвратом
значения.
Private Sub BtoA(ByVal X As Integer, I As Integer, J As Integer)
I = Int((1 + Sqr(1 + 8 * X)) / 2)
J = X - I * (I - 1) / 2
I = J - 1
End Sub
Программа Triang2 аналогична программе Triang, но она использует для работы
с диагональными элементами в массиве A эти новые функции. Программа
TriangC2 аналогична программе TriangC, но использует класс TriangularArray,
который включает диагональные элементы.
Нерегулярные массивы
В некоторых программах нужны массивы нестандартного размера и формы.
Двумерный массив может содержать шесть элементов в первом ряду, три — во
втором, четыре — в третьем, и т.д. Это может понадобиться, например, для
сохранения ряда многоугольников, каждый из которых состоит из разного числа
точек. Массив будет при этом выглядеть, как на рис. 4.3.
Массивы в Visual Basic не могут иметь такие неровные края. Можно было бы
использовать массив, достаточно большой для того, чтобы в нем могли
поместиться все строки, но при этом в таком массиве было бы множество
неиспользуемых ячеек. Например, массив на рис. 4.3 мог бы быть объявлен при
помощи оператора Dim Polygons(1 To 3, 1 To 6), и при этом четыре ячейки
останутся неиспользованными.
Существует несколько способов представления нерегулярных массивов.
@Рис. 4.3. Нерегулярный массив
=====70
Прямая звезда
Один из способов избежать потерь памяти заключается в том, чтобы упаковать
данные в одномерном массиве B. В отличие от треугольных массивов, для
нерегулярных массивов нельзя записать формулы для определения соответствия
элементов в разных массивах. Чтобы справиться с этой задачей, можно создать
еще один массив A со смещениями для каждой строки в одномерном массиве B.
Для упрощения определения в массиве B положения точек, соответствующих
каждой строке, в конец массива A можно добавить сигнальную метку, которая
указывает на точку сразу за последним элементом в массиве B. Тогда точки,
образующие многоугольник I, занимают в массиве B позиции с A(I) до A(I+1)-
1. Например, программа может перечислить элементы, образующие строку I,
используя следующий код:
For J = A(I) To A(I + 1) - 1
‘ Внести в список элемент I.
:
Next J
Этот метод называется прямой звездой (forward star). На рис. 4.4 показано
представление нерегулярного массива с рис. 4.3 в виде прямой звезды.
Сигнальная метка закрашена серым цветом.
Этот метод можно легко обобщить для создания многомерных нерегулярных
массивов. Для хранения набора рисунков, каждый из которых состоит из
разного числа многоугольников, можно использовать трехмерную прямую звезду.
На рис. 4.5 схематически представлена трехмерная структура данных в виде
прямой звезды. Две сигнальных метки закрашены серым цветом. Они указывают
на одну позицию позади значащих данных в массиве.
Такое представление в виде прямой звезды требует очень небольших затрат
памяти. Только память, занимаемая сигнальными метками, расходуется
«впустую».
При использовании структуры данных прямой звезды легко и быстро можно
перечислить точки, образующие многоугольник. Так же просто сохранять такие
данные на диске и загружать их обратно в память. С другой стороны,
обновлять массивы, записанные в формате прямой звезды, очень сложно.
Предположим, вы хотите добавить новую точку к первому многоугольнику на
рис. 4.4. Для этого понадобится сдвинуть все элементы справа от новой точки
на одну позицию, чтобы освободить место для нового элемента. Затем нужно
добавить по единице ко всем элементам массива A, которые идут после
первого, чтобы учесть сдвиг, вызванный добавлением точки. И, наконец, надо
вставить новый элемент. Сходные проблемы возникают при удалении точки из
первого многоугольника.
@Рис. 4.4. Представления нерегулярного массива в виде прямой звезды
=====71
@Рис. 4.5. Трехмерная прямая звезда
На рис. 4.6 показано представление в виде прямой звезды с рис. 4.4 после
добавления одной точки к первому многоугольнику. Элементы, которые были
изменены, закрашены серым цветом. Как видно из рисунка, почти все элементы
в обоих массивах были изменены.
Нерегулярные связные списки
Другим методом создания нерегулярных массивов является использование
связных списков. Каждая ячейка содержит указатель на следующую ячейку на
том же уровне иерархии, и указатель на список ячеек на более низком уровне
иерархии. Например, ячейка многоугольника может содержать указатель на
следующий многоугольник и указатель на ячейку, содержащую координаты первой
точки.
Следующий код приводит определения переменных для классов, которые можно
использовать для создания связного списка рисунков. Каждый из рисунков
содержит связный список многоугольников, каждый из которых содержит связный
список точек.
В классе PictureCell:
Dim NextPicture As PictureCell ' Следующий рисунок.
Dim FirstPolygon As PolyfonCell ' Первый многоугольник на этом рисунке.
В классе PolygonCell:
Dim NextPolygon As PolygonCell ' Следующий многоугольник.
Dim FirstPoint As PointCell ' Первая точка в этом многоугольнике.
В классе PointCell:
@Рис. 4.6. Добавление точки к прямой звезде
======72
Dim NextPoint As PointCell ' Следующая точка в этом многоугольнике.
Dim X As Single ' Координаты точки.
Dim Y As Single
Используя эти методы, можно легко добавлять и удалять рисунки,
многоугольники или точки в любом месте структуры данных.
Программа Poly на диске содержит связный список многоугольников. Каждый
многоугольник содержит связный список точек. Когда вы закрываете форму,
ссылка на список многоугольников из формы уничтожается. Это уменьшает
счетчик ссылок на верхнюю ячейку многоугольников до нуля. Она уничтожается,
поэтому ее ссылки на следующий многоугольник и его первую точку также
уничтожаются. Счетчики ссылок на эти ячейки также уменьшаются до нуля, и
они тоже уничтожаются. Уничтожение каждой ячейки многоугольника или точки
приводит к уничтожению следующей ячейки. Этот процесс продолжается до тех
пор, пока все многоугольники и точки не будут уничтожены.
Разреженные массивы
Во многих приложениях требуются большие массивы, которые содержат лишь
небольшое число ненулевых элементов. Матрица смежности для авиалиний,
например, может содержать 1 в позиции A(I, J) если есть рейс между городами
I и J. Многие авиалинии обслуживают сотни городов, но число существующих
рейсов намного меньше, чем N2 возможных комбинаций. На рис. 4.8 показана
небольшая карта рейсов авиалинии, на которой изображены только 11
существующих рейсов из 100 возможных пар сочетаний городов.
@Рис. 4.7. Программа Poly
====73
@Рис. 4.8. Карта рейсов авиалинии
Можно построить матрицу смежности для этого примера при помощи массива 10
на 10 элементов, но этот массив будет по большей части пустым. Можно
избежать потерь памяти, используя для создания разреженного массива
указатели. Каждая ячейка содержит указатели на следующий элемент в строке и
столбце массива. Это позволяет программе определить положение любого
элемента в массиве и обходить элементы в строке или столбце. В зависимости
от приложения, может оказаться полезным также добавить обратные указатели.
На рис. 4.9 показана разреженная матрица смежности, соответствующая карте
рейсов с рис. 4.8.
Чтобы построить разреженный массив в Visual Basic, создайте класс для
представления элементов массива. В этом случае, каждая ячейка представляет
наличие рейсов между двумя городами. Для представления связи, класс должен
содержать переменные с индексами городов, которые связаны между собой. Эти
индексы, в сущности, дают номера строк и столбцов ячейки. Каждая ячейка
также должна содержать указатели на следующую ячейку в строке и столбце.
Следующий код показывает объявление переменных в классе ConnectionCell:
Public FromCity As Integer ' Строка ячейки.
Public ToCity As Integer ' Столбец ячейки.
Public NextInRow As ConnectionCell
Public NextInCol As ConnectionCell
Строки и столбцы в этом массиве по существу представляют собой связные
списки. Как это часто случается со связными списками, с ними проще
работать, если они содержат сигнальные метки. Например, переменная
RowHead(I) должна содержать сигнальную метку для строки I. Для обхода
строки I в массиве можно использовать следующий код:
Private Sub PrintRow(I As Integer)
Dim cell As ConnectionCell
Set Cell = RowHead(I).Next ' Первый элемент данных.
Do While Not (cell Is Nothing)
Print Format$(cell.FromCity) & " -> " & Format$(cell.ToCity)
Set cell = cell.NextInRow
Loop
End Sub
====74
@Рис. 4.9. Разреженная матрица смежности
Индексирование массива
Нормальное индексирование массива типа A(I, J) не будет работать с такими
структурами. Можно облегчить индексирование, написав процедуры, которые
извлекают и устанавливают значения элементов массива. Если массив
представляет матрицу, могут также понадобиться процедуры для сложения,
умножения, и других матричных операций.
Специальное значение NoValue представляет пустой элемент массива.
Процедура, которая извлекает элементы массива, должна возвращать значение
NoValue при попытке получить значение элемента, не содержащегося в массиве.
Аналогично, процедура, которая устанавливает значения элементов, должна
удалять ячейку из массива, если ее значение установлено в NoValue.
Значение NoValue должно выбираться в зависимости от природы данных
приложения. Для матрицы смежности авиалинии пустые ячейки могут иметь
значение False. При этом значение A(I, J) может устанавливаться равным
True, если существует рейс между городами I и J.
Класс SparseArray определяет процедуру get для свойства Value для
возвращения значения элемента в массиве. Процедура начинает с первой ячейки
в указанной строке и затем перемещается по связному списку ячеек строки.
Как только найдется ячейка с нужным номером столбца, это и будет искомая
ячейка. Так как ячейки в списке строки расположены по порядку, процедура
может остановиться, если найдется ячейка, номер столбца которой больше
искомого.
=====75
Property Get Value(t As Integer, c As Integer) As Variant
Dim cell As SparseArrayCell
Value = NoValue ' Предположим, что мы не найдем элемент.
If r < 1 Or c < 1 Or _
r > NumRows Or c > NumCols _
Then Exit Property
Set cell = RowHead(r).NextInRow ' Пропустить метку.
Do
If cell Is Nothing Then Exit Property ' Не найден.
If cell.Col > c Then Exit Property ' Не найден.
If cell.Col = c Then Exit Do ' Найден.
Set cell = cell.NextInRow
Loop
Value = cell. Data
End Property
Процедура let свойства value присваивает ячейке новое значение. Если новое
значение равно NoValue, процедура вызывает для удаления элемента из
массива. В противном случае, она ищет требуемое положение элемента в нужной
строке. Если элемент уже существует, процедура обновляет его значение.
Иначе, она создает новый элемент и добавляет его к списку строки. Затем она
добавляет новый элемент в правильное положение в соответствующем списке
столбцов.
Property Let Value (r As Integer, c As Integer, new_value As Variant)
Dim i As Integer
Dim found_it As Boolean
Dim cell As SparseArrayCell
Dim nxt As SparseArrayCell
Dim new_cell As SparseArrayCell
' Если value = MoValue, удалить элемент из массива.
If new_value = NoValue Then
RemoveEntry r, c
Exit Property
End If
' Если нужно, добавить строки.
If r > NumRows Then
ReDim Preserve RowHead(1 To r)
' Инициализировать метку для каждой новой строки.
For i = NumRows + 1 To r
Set RowHead(i) = New SparseArrayCell
Next i
End If
' Если нужно, добавить столбцы.
If c > NumCols Then
ReDim Preserve ColHead(1 To c)
' Инициализировать метку для каждой новой строки.
For i = NumCols + 1 To c
Set ColHead(i) = New SparseArrayCell
Next i
NumCols = c
End If
' Попытка найти элемент.
Set cell = RowHead(r)
Set nxt = cell.NextInRow
Do
If nxt Is Nothing Then Exit Do
If nxt.Col >= c Then Exit Do
Set cell = nxt
Set nxt = cell.NextInRow
Loop
' Проверка, найден ли элемент.
If nxt Is Nothing Then
found_it = False
Else
found_it = (nxt.Col = c)
End If
' Если элемент не найден, создать его.
If Not found_it Then
Set new_cell = New SparseArrayCell
' Поместить элемент в список строки.
Set new_cell.NextInRow = nxt
Set cell.NextInRow = new_cell
' Поместить элемент в список столбца.
Set cell = ColHead(c)
Set nxt = cell.NextInCol
Do
If nxt Is Nothing Then Exit Do
If nxt.Col >= c Then Exit Do
Set cell = nxt
Set nxt = cell.NextInRow
Loop
Set new_cell.NextInCol = nxt
Set cell.NextInCol = new_cell
new_cell.Row = r
new_cell.Col = c
' Поместим значение в элемент nxt.
Set nxt = new_cell
End If
' Установим значение.
nxt.Data = new_value
End Property
Программа Sparse, показанная на рис. 4.10, использует классы SparseArray и
SparseArrayCell для работы с разреженным массивом. Используя программу,
можно устанавливать и извлекать элементы массива. В этой программе значение
NoValue равно нулю, поэтому если вы установите значение элемента равным
нулю, программа удалит этот элемент из массива.
Очень разреженные массивы
Некоторые массивы содержат так мало непустых элементов, что многие строки и
столбцы полностью пусты. В этом случае, лучше хранить заголовки строк и
столбцов в связных списках, а не в массивах. Это позволяет программе
полностью пропускать пустые строки и столбцы. Заголовки строки и столбцов
указывают на связные списки элементов строк и столбцов. На рис. 4.11
показан массив 100 на 100, который содержит всего 7 непустых элементов.
@Рис. 4.10. Программа Sparse
=====76-78
@Рис. 4.11. Очень разреженный массив
Для работы с массивами этого типа можно довольно просто доработать
предыдущий код. Большая часть кода остается неизменной, и для элементов
массива можно использовать тот же самый класс SparseArray.Тем не менее,
вместо хранения меток строк и столбцов в массивах, они записываются в
связных списках.
Объекты класса HeaderCell представляют связные списки строк и столбцов. В
этом классе определяются переменные, содержащие число строк и столбцов,
которые он представляет, сигнальная метка в начале связного списка
элементов строк или столбцов, и объект HeaderCell, представляющий следующий
заголовок строки или столбца.
Public Number As Integer ' Номер строки или столбца.
Public Sentinel As SparseArrayCell ' Метка для строки или
' столбца.
Public NextHeader As HeaderCell ' Следующая строка или
' столбец.
Например, чтобы обратиться к строке I, нужно вначале просмотреть связный
список заголовков HeaderCells строк, пока не найдется заголовок,
соответствующий строке I. Затем продолжается работа со строкой I.
Private Sub PrintRow(r As Integer)
Dim row As HeaderCell
Dim cell As SparseArrayCell
' Найти правильный заголовок строки.
Set row = RowHead. NextHeader ' Список первой строки.
Do
If row Is Nothing Then Exit Sub ' Такой строки нет.
If row.Number > r Then Exit Sub ' Такой строки нет.
If row.Number = r Then Exit Do ' Строка найдена.
Set row = row.NextHeader
Loop
' Вывести элементы в строке.
Set cell = row.Sentinel. NextInRow ' Первый элемент в строке.
Do While Not (cell Is Nothing)
Print Format$(cell.FromCity) & " -> " & Format$(cell.ToCity)
Set cell = cell.NextInRow
Loop
End Sub
Резюме
Некоторые программы используют массивы, содержащие только небольшое число
значащих элементов. Использование обычных массивов Visual Basic привело бы
к большим потерям памяти. Используя треугольные, нерегулярные, разреженные
и очень разреженные массивы, вы можете создавать мощные представления
массивов, которые требуют намного меньших объемов памяти.
=========80
Глава 5. Рекурсия
Рекурсия — мощный метод программирования, который позволяет разбить задачу
на части все меньшего и меньшего размера до тех пор, пока они не станут
настолько малы, что решение этих подзадач сведется к набору простых
операций.
После того, как вы приобретете опыт применения рекурсии, вы будете
обнаруживать ее повсюду. Многие программисты, недавно овладевшие рекурсией,
увлекаются, и начинают применять ее в ситуациях, когда она является
ненужной, а иногда и вредной.
В первых разделах этой главы обсуждается вычисление факториалов, чисел
Фибоначчи, и наибольшего общего делителя. Все эти алгоритмы являются
примерами плохого использования рекурсии — нерекурсивные версии этих
алгоритмов намного эффективнее. Эти примеры интересны и наглядны, поэтому
имеет смысл обсудить их.
Затем, в главе рассматривается несколько примеров, в которых применение
рекурсии более уместно. Алгоритмы построения кривых Гильберта и Серпинского
используют рекурсию правильно и эффективно.
В последних разделах этой главы объясняется, почему реализацию
алгоритмов вычисления факториалов, чисел Фибоначчи, и наибольшего общего
делителя лучше осуществлять без применения рекурсии. В этих параграфах
объясняется также, когда следует избегать рекурсии, и приводятся способы
устранения рекурсии, если это необходимо.
Что такое рекурсия?
Рекурсия происходит, если функция или подпрограмма вызывает сама себя.
Прямая рекурсия (direct recursion) выглядит примерно так:
Function Factorial(num As Long) As Long
Factorial = num * Factorial(num - 1)
End Function
В случае косвенной рекурсии (indirect recursion) рекурсивная процедура
вызывает другую процедуру, которая, в свою очередь, вызывает первую:
Private Sub Ping(num As Integer)
Pong(num - 1)
End Sub
Private Sub Pong(num As Integer)
Ping(num / 2)
End Sub
===========81
Рекурсия полезна при решении задач, которые естественным образом
разбиваются на несколько подзадач, каждая из которых является более простым
случаем исходной задачи. Можно представить дерево на рис. 5.1 в виде
«ствола», на котором находятся два дерева меньших размеров. Тогда можно
написать рекурсивную процедуру для рисования деревьев:
Private Sub DrawTree()
Нарисовать "ствол"
Нарисовать дерево меньшего размера, повернутое на -45 градусов
Нарисовать дерево меньшего размера, повернутое на 45 градусов
End Sub
Хотя рекурсия и может упростить понимание некоторых проблем, люди обычно
не мыслят рекурсивно. Они обычно стремятся разбить сложные задачи на задачи
меньшего объема, которые могут быть выполнены последовательно одна за
другой до полного завершения. Например, чтобы покрасить изгородь, можно
начать с ее левого края и продолжать двигаться вправо до завершения.
Вероятно, во время выполнения подобной задачи вы не думаете о возможности
рекурсивной окраски — вначале левой половины изгороди, а затем рекурсивно —
правой.
Для того чтобы думать рекурсивно, нужно разбить задачу на подзадачи,
которые затем можно разбить на подзадачи меньшего размера. В какой-то
момент подзадачи становятся настолько простыми, что могут быть выполнены
непосредственно. Когда завершится выполнение подзадач, большие подзадачи,
которые из них составлены, также будут выполнены. Исходная задача окажется
выполнена, когда будут все выполнены образующие ее подзадачи.
Рекурсивное вычисление факториалов
Факториал числа N записывается как N! (произносится «эн факториал»). По
определению, 0! равно 1. Остальные значения определяются формулой:
N! = N * (N - 1) * (N - 2) * ... * 2 * 1
Как уже упоминалось в 1 главе, эта функция чрезвычайно быстро растет с
увеличением N. В табл. 5.1 приведены 10 первых значений функции факториала.
Можно также определить функцию факториала рекурсивно:
0! = 1
N! = N * (N - 1)! для N > 0.
@Рис. 5.1. Дерево, составленное из двух деревьев меньшего размера
===========82
@Таблица 5.1. Значения функции факториала
Легко написать на основе этого определения рекурсивную функцию:
Public Function Factorial(num As Integer) As Integer
If num A / 2. Первым рекурсивным
вызовом функции GCD будет GCD(B Mod A, A).
Подстановка в функцию значения B Mod A и A вместо A и B дает следующий
рекурсивный вызов GCD(B Mod A, A).
Но мы предположили, что B Mod A > A / 2. Тогда B Mod A разделится на A
только один раз, с остатком A – (B Mod A). Так как B Mod A больше, чем A /
2, то A – (B Mod A) должно быть меньше, чем A / 2. Значит, первый параметр
второго рекурсивного вызова функции GCD меньше, чем A / 2, что и
требовалось доказать.
Предположим теперь, что N — это исходное значение параметра A. После
двух вызовов функции GCD, значение параметра A должно уменьшится, по
крайней мере, до N / 2. После четырех вызовов, это значение будет не
больше, чем (N / 2) / 2 = N / 4. После шести вызовов, значение не будет
превосходить (N / 4) / 2 = N / 8. В общем случае, после 2 * K вызовов
функции GCD, значение параметра A будет не больше, чем N / 2K.
Поскольку алгоритм должен остановиться, когда значение параметра A
дойдет до 1, он может продолжать работу только до тех, пока не выполняется
равенство N/2K=1. Это происходит, когда N=2K или когда K=log2(N). Так как
алгоритм выполняется за 2*K шагов это означает, что алгоритм остановится не
более, чем через 2*log2(N) шагов. С точностью до постоянного множителя, это
означает, что алгоритм выполняется за время порядка O(log(N)).
=======85
Этот алгоритм — один из множества рекурсивных алгоритмов, которые
выполняются за время порядка O(log(N)). При выполнении фиксированного числа
шагов, в данном случае 2, размер задачи уменьшается вдвое. В общем случае,
если размер задачи уменьшается, по меньшей мере, в D раз после каждых S
шагов, то задача потребует S*logD(N) шагов.
Поскольку при оценке по порядку величины можно игнорировать постоянные
множители и основания логарифмов, то любой алгоритм, который выполняется за
время S*logD(N), будет алгоритмом порядка O(log(N)). Это не обязательно
означает, что этими постоянными можно полностью пренебречь при реализации
Страницы: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
|